Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

tiko by Example

tiko is a compiled, statically typed language for building fast, self-contained programs on Windows and Linux. This site teaches it the way you learn most things in practice: by reading short, complete programs and watching what they do.

Every page is built around one example. The example is a whole program, not a fragment, and the text under it is the output that program printed. Nothing here was typed in by hand: each example on the site is compiled and run on both windows-x86_64 and linux-x86_64 before a page is published, and the output you see is the output the program produced.

How to run an example

Save the program as example.tiko and compile it:

tiko example.tiko

The program takes the source file's name: example on Linux, example.exe on Windows. Run it as ./example or example.exe. Where an example needs a compiler flag, the flag is written inside the program itself, as a #cmdline line near the top, so the file alone reproduces the result.

How this site is organised

The sections start with the smallest things you need to write a program — printing, values, variables and control flow — and move through procedures, types and objects to the standard library, concurrency and calling C. Each section opens with a short overview page, and each page ends with a few activities: small changes to try, so the example becomes yours.

If you want the longer explanation of why tiko works the way it does, read The tiko Programming Language, the book this site accompanies.

Where to look things up

An example shows one way to use a feature. The full definition of every keyword, option and library function is in the tiko wiki, github.com/PaulSquires/tiko-lang/wiki, the language's reference documentation:

  • Feature Index — every language feature and library, one line each, with a link to its reference page.
  • Function Finder — every function, grouped by what you want to do, with one line on what each one does.
  • Keyword Index — every keyword, statement and operator, A to Z.
  • Quick Start — building a toolchain and compiling your first program.

When an example uses a function you have not met, the Function Finder is the fastest way to its page.

Hello World

This is the source of the traditional first program:

'' This is a comment. The compiler ignores everything after ''.

'' print writes a line of text to standard output.
print "Hello, world!"

Output

Hello, world!

A tiko program needs no main procedure and no boilerplate around it. The statements at the top level of the file are the program, and they run from top to bottom. print writes its arguments to standard output and ends the line.

Compile it with the tiko command and run the result:

tiko hello.tiko
./hello

The program takes the source file's name, so on Windows it is hello.exe. -o names the output yourself, exactly as written: -o greet writes a file called greet, so on Windows give it the extension, -o greet.exe. tiko produces one self-contained executable: there is no runtime to install beside it.

Activities

  1. Change the text and print a second line under the first.
  2. Compile with -v added to the command line and read the steps tiko takes to turn the source into a program.

Comments

Every program needs comments, and tiko has two kinds:

  • Line comments start with '' and run to the end of the line.
  • Block comments start with /' and end with '/. They can span several lines, sit in the middle of an expression, and nest inside one another.
'' A line comment starts with two apostrophes and runs to the end
'' of the line. It is the form used for almost every comment.

/' A block comment starts with slash-apostrophe and ends with
   apostrophe-slash. It can span lines, /' and nest '/ inside. '/

dim as int32 x = 5 + /' 90 + '/ 5   '' a block comment mid-expression

print "x is"; x

Output

x is 10

The block comment inside the expression removes 90 + from the program, so x is 5 + 5. Because block comments nest, you can comment out a region of code that already contains one without breaking it.

A single ' also starts a line comment, but the doubled '' is the form tiko code uses, and the one this site uses throughout.

Activities

  1. Remove the block comment in the dim line and predict the new output.
  2. Comment out the print line with a block comment, then wrap that whole line in a second block comment.

Printing

print takes a list of values separated by ; or ,, and the separator decides the spacing:

  • ; puts the next value directly after the previous one.
  • , moves to the next print zone, a column boundary every 14 characters, which is a quick way to line up a small table.
  • A ; at the very end keeps the cursor on the same line, so the next print continues it.
'' A semicolon joins items with no gap; a comma moves to the next
'' 14-column print zone.
print "one"; "two"; "three"
print "a", "b", "c"

'' A positive number prints with a leading space where its sign
'' would go; a negative one prints its minus sign there.
print 42
print -42
print "answer:"; 42

'' A trailing semicolon keeps the cursor on the same line.
print "no newline yet... ";
print "now there is one"

'' An empty print ends the line.
print
print "done"

Output

onetwothree
a             b             c
 42
-42
answer: 42
no newline yet... now there is one

done

Numbers are printed with room for a sign: a positive number gets a leading space and a negative one its minus sign. That is why "answer:"; 42 prints answer: 42 with a space you did not write. When you want exact control over the text, build it with strings.Format instead.

Activities

  1. Print a three-column table of names and ages using , between the columns.
  2. Print the numbers 1 to 3 on one line using a trailing ; on each print.

Formatted print

print is fine for quick output, but most programs want to lay text out precisely. strings.Format, from the standard library's std/strings module, builds a ustring from a format string and a list of values:

#import "std/strings"

'' strings.Format fills {} placeholders with its arguments, in order.
print strings.Format( "{} days", 31 )
print strings.Format( "{} plus {} is {}", 2, 3, 2 + 3 )

'' A number inside the braces picks an argument by position.
print strings.Format( "{0}, this is {1}. {1}, this is {0}.", _
                      "Alice", "Bob" )

'' After a colon comes the format spec: fill, alignment, width.
print strings.Format( "[{:>8}]", "right" )
print strings.Format( "[{:<8}]", "left" )
print strings.Format( "[{:^8}]", "mid" )
print strings.Format( "[{:-^12}]", "fill" )

'' Zero padding and a sign.
print strings.Format( "{:05} {:+}", 42, 7 )

'' Other bases, with or without a prefix.
print strings.Format( "{:x} {:#x} {:o} {:b}", 255, 255, 8, 5 )

'' Precision for floating-point values.
dim as float64 pi = 3.14159265358979
print strings.Format( "pi is about {:.3f}", pi )

'' {{ and }} print literal braces.
print strings.Format( "{{}} is an empty pair" )

Output

31 days
2 plus 3 is 5
Alice, this is Bob. Bob, this is Alice.
[   right]
[left    ]
[  mid   ]
[----fill----]
00042 +7
ff 0xff 10 101
pi is about 3.142
{} is an empty pair

Each {} in the format string is replaced by the next argument. Inside the braces you can write:

PartExampleMeaning
a position{1}use argument 1 (counting from 0) instead of the next one
fill and alignment{:-^12}centre in 12 columns, padding with -
< > ^{:>8}align left, right or centre
0 and a width{:05}pad a number with zeros
+{:+}always show the sign
x o b{:#x}hexadecimal, octal, binary; # adds the prefix
.n and f / e{:.3f}fixed-point or exponent notation with n digits

A format string is checked when the program is compiled wherever it is a literal, so a placeholder that refers to an argument you did not pass is an error at build time, not a surprise at run time.

#import "std/strings" brings in the module; its procedures are then called through the module name, as strings.Format.

Activities

  1. Print pi with 2, 5 and 10 digits after the point.
  2. Print the numbers 1, 10 and 100 right-aligned in a column 6 wide.
  3. Print 255 in binary, padded with zeros to 16 digits.

Display

print and strings.Format know how to show numbers, strings and booleans. To make one of your own types printable, give it a cast to ustring: an operator cast that returns its text form.

#import "std/strings"

'' A type that can turn itself into text declares a cast to ustring.
type Point
    as float64 x, y
    declare operator cast( ) as ustring
end type

operator Point.cast( ) as ustring
    return strings.Format( "({}, {})", x, y )
end operator

dim p as Point = ( 1.5, -2 )

'' Every place that wants text now accepts a Point.
print p
print "p is " & p
print strings.Format( "p is {} and {:>14}", p, p )

Output

(1.5, -2)
p is (1.5, -2)
p is (1.5, -2) and      (1.5, -2)

Once the cast exists, a Point is accepted wherever text is expected: by print, by the & concatenation operator, and by strings.Format, where the format spec — here {:>14} — applies to the text the cast produced.

Types and operators are covered properly in Custom Types and Objects; this page only shows the hook that makes a value displayable.

Activities

  1. Add a z field to Point and include it in the text.
  2. Write a Money type holding a number of cents that prints as $12.34.

Primitives

tiko's built-in scalar types put their size in their name: int8 to int64 for signed integers, uint8 to uint64 for unsigned ones, float32 and float64 for floating point. boolean holds true or false, and ustring holds text.

#import "std/strings"

'' The scalar types say how big they are in their names.
dim as int8    tiny  = -128
dim as int16   small = 32767
dim as int32   count = 2147483647
dim as int64   huge  = 9223372036854775807
dim as uint8   octet = 255
dim as uint64  mask  = &hFFFFFFFFFFFFFFFF
dim as float32 ratio = 0.75
dim as float64 pi    = 3.141592653589793
dim as boolean ready = true

print strings.Format( "{:<8} {:>5}  {}", "type", "bytes", "value" )
print strings.Format( "{:<8} {:>5}  {}", "int8", sizeof( tiny ), tiny )
print strings.Format( "{:<8} {:>5}  {}", "int16", sizeof( small ), small )
print strings.Format( "{:<8} {:>5}  {}", "int32", sizeof( count ), count )
print strings.Format( "{:<8} {:>5}  {}", "int64", sizeof( huge ), huge )
print strings.Format( "{:<8} {:>5}  {}", "uint8", sizeof( octet ), octet )
print strings.Format( "{:<8} {:>5}  {}", "uint64", sizeof( mask ), mask )
print strings.Format( "{:<8} {:>5}  {}", "float32", sizeof( ratio ), ratio )
print strings.Format( "{:<8} {:>5}  {}", "float64", sizeof( pi ), pi )
print strings.Format( "{:<8} {:>5}  {}", "boolean", sizeof( ready ), ready )

'' Text is a ustring: a sequence of characters that grows as needed.
dim as ustring label = "primitive"
print strings.Format( "label is {}, {} characters", label, len( label ) )

Output

type     bytes  value
int8         1  -128
int16        2  32767
int32        4  2147483647
int64        8  9223372036854775807
uint8        1  255
uint64       8  18446744073709551615
float32      4  0.75
float64      8  3.141592653589793
boolean      1  true
label is primitive, 9 characters

sizeof gives the size of a value's type in bytes. Each width is the same on every target, so an int32 is four bytes on Windows and on Linux alike.

TypeBytesHolds
int8 int16 int32 int641 2 4 8signed whole numbers
uint8 uint16 uint32 uint641 2 4 8whole numbers from 0 up
float32 float644 8floating point, about 7 and 16 digits
boolean1true or false
ustringtext of any length

Two more integer types, integer and uinteger, are as wide as an address on the target; Sizes shows them. A ustring is text, not a scalar: len( label ) counts its characters, and Strings covers it properly.

The pages in this section cover how to write values (literals), what you can do with them (operators), and two ways to group them: tuples and fixed-size arrays.

Activities

  1. Add an int16 set to -32768 and a uint16 set to 65535 to the table.
  2. Add a row for integer and compare its size with int64.

Literals

A literal is a value written straight into the source. Integers can be written in four bases, floating-point numbers with a point or an exponent, and a suffix can pin a literal to a particular type.

'' Integers in four bases. The prefix names the base.
print "decimal 255        ="; 255
print "hex     &hFF       ="; &hFF
print "octal   &o377      ="; &o377
print "binary  &b11111111 ="; &b11111111

'' Floating-point literals: a point, an exponent, or both.
print "1.5     ="; 1.5
print "2.5e3   ="; 2.5e3
print "1e-4    ="; 1e-4
print ".25     ="; .25

'' A suffix fixes the literal's type; sizeof shows the width it chose.
print "sizeof( 1 )    ="; sizeof( 1 )
print "sizeof( 1L )   ="; sizeof( 1L )
print "sizeof( 1LL )  ="; sizeof( 1LL )
print "sizeof( 1UL )  ="; sizeof( 1UL )
print "sizeof( 1.5 )  ="; sizeof( 1.5 )
print "sizeof( 1.5F ) ="; sizeof( 1.5F )

'' Booleans and text.
print "true and false: "; true; " "; false
print "a ustring literal: "; "in double quotes"
print !"an escaped literal:\tafter a tab"

Output

decimal 255        = 255
hex     &hFF       = 255
octal   &o377      = 255
binary  &b11111111 = 255
1.5     = 1.5
2.5e3   = 2500
1e-4    = 0.0001
.25     = 0.25
sizeof( 1 )    = 8
sizeof( 1L )   = 4
sizeof( 1LL )  = 8
sizeof( 1UL )  = 4
sizeof( 1.5 )  = 8
sizeof( 1.5F ) = 4
true and false: true false
a ustring literal: in double quotes
an escaped literal:	after a tab
PrefixBaseExample
none10255
&h16&hFF
&o8&o377
&b2&b11111111

A literal with a point or an exponent (1.5, 2.5e3, .25) is floating point. tiko has no digit separator: 1_000_000 is error TKO0420: expected the end of the statement.

A plain whole-number literal is an integer, which is 8 bytes on the 64-bit targets; a plain floating-point literal is a float64. The suffixes change that:

SuffixType
Lint32
LLint64
Uuinteger
ULuint32
ULLuint64
Ffloat32
Dfloat64

Literal types shows how to see the type a literal gets, including one case that surprises people.

true and false are values of type boolean. Text goes in double quotes; with a ! in front, backslash escapes such as \t are read, so the last line prints a real tab. Strings has the full set.

Activities

  1. Print &h7FFFFFFF and &b10000000 and check the values by hand.
  2. Print cbool( &o17 = &b1111 ) and work out why the answer is what it is.

Operators

Most of tiko's operators are the ones you expect. A few are spelled as words (mod, and, shl), and three arithmetic ones behave differently from C-family languages: /, \ and ^.

'' Arithmetic. / always gives a float; \ is integer division.
print "7 + 2   ="; 7 + 2
print "7 - 2   ="; 7 - 2
print "7 * 2   ="; 7 * 2
print "7 / 2   ="; 7 / 2
print "7 \ 2   ="; 7 \ 2
print "7 mod 2 ="; 7 mod 2
print "7 ^ 2   ="; 7 ^ 2
print "-7 \ 2  ="; -7 \ 2; "   -7 mod 2 ="; -7 mod 2
print

'' Comparisons give -1 for true and 0 for false.
print "3 < 5  ="; 3 < 5; "   3 = 5 ="; 3 = 5; "   3 <> 5 ="; 3 <> 5
print "as a boolean: "; cbool( 3 < 5 ); " "; cbool( 3 = 5 )
print "text compares too: "; cbool( "apple" < "banana" )
print

'' Bitwise operators work on every bit of an integer.
print "12 and 10 ="; 12 and 10
print "12 or 10  ="; 12 or 10
print "12 xor 10 ="; 12 xor 10
print "not 12    ="; not 12
print "1 shl 4   ="; 1 shl 4
print "256 shr 2 ="; 256 shr 2
print "bits of 12 and 10: "; bin( 12 ); " "; bin( 10 ); " -> "; _
      bin( 12 and 10 )
print

'' Each binary operator has a compound assignment form.
dim as int32 total = 10
total += 5
total *= 2
total \= 4
total shl= 1
print "total ="; total

Output

7 + 2   = 9
7 - 2   = 5
7 * 2   = 14
7 / 2   = 3.5
7 \ 2   = 3
7 mod 2 = 1
7 ^ 2   = 49
-7 \ 2  =-3   -7 mod 2 =-1

3 < 5  =-1   3 = 5 = 0   3 <> 5 =-1
as a boolean: true false
text compares too: true

12 and 10 = 8
12 or 10  = 14
12 xor 10 = 6
not 12    =-13
1 shl 4   = 16
256 shr 2 = 64
bits of 12 and 10: 1100 1010 -> 1000

total = 14

Arithmetic. / always divides in floating point, so 7 / 2 is 3.5 even though both operands are whole numbers. Integer division is its own operator, \, and it truncates towards zero: -7 \ 2 is -3. mod is the remainder that goes with it, so it takes the sign of the left operand: -7 mod 2 is -1. ^ raises to a power and always produces a float64.

Comparison. =, <>, <, <=, >, >= compare numbers, and also text, alphabetically. Their result is an integer: -1 for true and 0 for false. That is what print shows, so wrap a comparison in cbool( ... ) when you want to see true or false. In a statement such as x = 3, the first = assigns; Expressions shows what a second one does.

Bits. and, or, xor and not work on every bit of an integer, and shl / shr shift left and right. not 12 is -13 because every bit flips, including the sign bit. bin( ) shows the bits, which makes masks easy to check. The same words also combine conditions; the short-circuit page explains when to use andalso and orelse instead.

Compound assignment. Every binary operator has an assigning form: +=, -=, *=, /=, \=, ^=, mod=, and=, or=, xor=, shl=, shr= and &= for text.

Dividing by a constant zero is caught when the program is compiled: 5 \ 0 is error TKO0671: division by a constant zero.

The order in which operators apply is on the precedence page.

Activities

  1. Print -7 / 2, -7 \ 2 and fix( -7 / 2 ). Which two agree?
  2. Use and with a mask to print the low four bits of 1234.
  3. Start with dim as uint8 flags = 0 and set bits 0 and 3 using or= and shl.

Tuples

A tuple groups two or more values into one, without declaring a type for them first. It is the quick way to keep a pair together, or to give a function more than one result.

'' A tuple groups values without declaring a type first.
var pair = ( 7, 2.5 )
print "pair:"; pair._1; pair._2

'' A tuple type can name its elements. _1, _2 ... always work too.
dim as ( x as float64, y as float64 ) pt = ( 1.5, -2 )
print "point: x ="; pt.x; ", y ="; pt.y; ", _1 ="; pt._1
pt.y = 4
print "after pt.y = 4, y ="; pt.y

'' A function can return a tuple, which gives it two results.
function DivMod( byval a as int32, byval b as int32 ) _
        as ( quotient as int32, remainder as int32 )
    return ( a \ b, a mod b )
end function

var r = DivMod( 17, 5 )
print "17 / 5 is"; r.quotient; " remainder"; r.remainder

'' Tuples of the same element types compare element by element.
dim as ( int32, int32 ) a = ( 1, 2 ), b = ( 1, 2 )
print "a = b: "; cbool( a = b )
b._2 = 3
print "a = b: "; cbool( a = b )

Output

pair: 7 2.5
point: x = 1.5, y =-2, _1 = 1.5
after pt.y = 4, y = 4
17 / 5 is 3 remainder 2
a = b: true
a = b: false

( 7, 2.5 ) is a tuple value. Its elements are reached by position as _1, _2 and so on; var gives pair the tuple's type, here an integer and a float64.

A tuple type is written in parentheses too: ( x as float64, y as float64 ) has two elements called x and y. The names are for reading; the positional names _1 and _2 still work, as the _1 on the second line shows. Elements can be assigned to one at a time.

DivMod returns ( quotient as int32, remainder as int32 ). The return builds the tuple, and the caller reads the parts by the names the function gave them. A tuple returned this way replaces the extra byref parameter a function would otherwise need for its second result.

Two tuples of the same element types compare with = and <>, element by element. They have no ordering: a < b is error TKO0720: no operator < compares this type. Nor can print take a whole tuple; for a pair of integers it reports TKO0871: a '(integer, integer)' has no printable form. Print the elements instead.

Taking a tuple apart into separate variables in one line, var ( q, r ) = DivMod( 17, 5 ), is covered in Tuples and Iterators.

Activities

  1. Write a function that returns the smallest and largest of three int32 values as a tuple with named elements.
  2. Store three tuples of ( ustring, int32 ) in variables and print the one with the largest second element.

Arrays

A fixed-size array holds a set number of values of one type. Its bounds are written in the declaration, and a brace list can fill it there too.

'' A fixed-size array: its bounds are part of the declaration.
dim as int32 primes( 0 to 4 ) = { 2, 3, 5, 7, 11 }
print "primes go from"; lbound( primes ); " to"; ubound( primes )
print "the third prime is"; primes( 2 )

'' The lower bound can be anything; "..." lets the list set the upper.
dim as ustring days( 1 to ... ) = { "Mon", "Tue", "Wed" }
print "days 1 to"; ubound( days ); ": "; days( 1 ); " "; days( 3 )

'' A single number is the upper bound, so this has 4 elements, 0 to 3.
dim as float64 readings( 3 )
readings( 3 ) = 9.5
print "readings:"; readings( 0 ); readings( 1 ); readings( 2 ); readings( 3 )

'' One set of braces per dimension.
dim as int32 grid( 1 to 2, 1 to 3 ) = { { 1, 2, 3 }, { 4, 5, 6 } }
print "grid is"; ubound( grid, 1 ); " by"; ubound( grid, 2 ); _
      ", grid( 2, 3 ) ="; grid( 2, 3 )

'' Walking an array by index.
dim as int32 total = 0
for i as int32 = lbound( primes ) to ubound( primes )
    total += primes( i )
next
print "sum of primes:"; total

Output

primes go from 0 to 4
the third prime is 5
days 1 to 3: Mon Wed
readings: 0 0 0 9.5
grid is 2 by 3, grid( 2, 3 ) = 6
sum of primes: 28

primes( 0 to 4 ) has five elements, numbered 0 to 4. An element is reached with parentheses, primes( 2 ), and lbound and ubound give the lowest and highest index, so a loop over them visits every element whatever the bounds are.

The lower bound can be any integer, days( 1 to ... ) starts at 1, and ... as the upper bound takes the size from the list. A single number in the declaration is the upper bound with 0 as the lower, so readings( 3 ) has four elements. Elements that no initialiser fills start at zero.

An array can have more than one dimension. Each dimension gets its own pair of bounds and its own level of braces, and ubound( grid, 2 ) asks about the second dimension.

Arrays that grow and shrink, passing arrays to procedures, and bounds checks are in the Arrays section.

Activities

  1. Declare dim as int32 squares( 1 to 10 ), fill it in a loop, and print the last element.
  2. Change days to hold all seven days and print the number of elements as ubound( days ) - lbound( days ) + 1.

Custom Types

The built-in types describe single values. Most data comes in groups and in categories, and tiko has a declaration for each: a type (a record) groups named fields, an enum names a set of related values, and a const names a value that never changes.

'' Three kinds of custom type, working together.

'' A constant: a name for a fixed value.
const ReorderLevel = 5

'' An enum: a closed set of named values.
enum Category
    Tools
    Parts
    Supplies
end enum

'' A record: named fields grouped into one value.
type StockItem
    as ustring label
    as Category kind
    as int32 onHand
end type

sub Report( byref item as StockItem )
    with item
        print .label; " (category"; .kind; "):"; .onHand; " on hand";
        if .onHand <= ReorderLevel then print " -- reorder";
        print
    end with
end sub

dim as StockItem hammer = ( "hammer", Tools, 12 )
dim as StockItem bolt = ( "bolt", Parts, 3 )
dim as StockItem tape = ( "tape", Supplies, 5 )

Report( hammer )
Report( bolt )
Report( tape )

Output

hammer (category 0): 12 on hand
bolt (category 1): 3 on hand -- reorder
tape (category 2): 5 on hand -- reorder

StockItem keeps a label, a category and a count together, so Report takes one argument instead of three. Its kind field has the enum type Category, whose members Tools, Parts and Supplies print as 0, 1 and 2. ReorderLevel gives the number 5 a name, so the test reads as what it means.

The pages in this section take each part in turn:

  • Records: declaring fields, filling them, nesting one type in another, and copying.
  • with: naming a record once and reaching its fields with a dot.
  • Enums: named integer constants.
  • Unions: several views of the same bytes.
  • Constants: const, with and without a type.

Methods, constructors and operators on your own types are in Objects.

Activities

  1. Add a fourth item with a count of 0 and check that it is flagged.
  2. Add a Consumables member to Category and use it for tape.

Records

A type declares a record: a set of named fields that travel together as one value. Each field has a name and a type, and that type can itself be a record.

'' A type groups named fields into one value.
type Point
    as int32 x, y
end type

'' Fields can be other types, and can declare a starting value.
type Frame
    as Point origin
    as Point size = ( 640, 480 )
    as ustring title = "untitled"
    as boolean visible = true
end type

'' A list in parentheses fills the fields in declaration order.
dim as Point corner = ( 3, 4 )
print "corner:"; corner.x; corner.y

'' Fields you do not set start at their declared value, or zero.
dim as Frame w
print "fresh frame: "; w.title; ","; w.size.x; " x"; w.size.y; _
      ", origin"; w.origin.x; w.origin.y; ", visible "; w.visible

'' A dot reaches a field; a second dot reaches into a nested type.
w.title = "editor"
w.origin = corner
w.size.x = 800
print "edited:      "; w.title; ","; w.size.x; " x"; w.size.y; _
      ", origin"; w.origin.x; w.origin.y

'' Assignment copies every field. The copy is independent.
dim as Frame twin = w
twin.title = "twin"
twin.origin.x = 99
print "original: "; w.title; w.origin.x
print "twin:     "; twin.title; twin.origin.x

Output

corner: 3 4
fresh frame: untitled, 640 x 480, origin 0 0, visible true
edited:      editor, 800 x 480, origin 3 4
original: editor 3
twin:     twin 99

as int32 x, y declares two fields of one type on one line, the same way dim declares variables. A field may carry a starting value, as size, title and visible do; every new Frame starts with those values, and the fields without one start at zero, or "" for text.

A list in parentheses, ( 3, 4 ), fills the fields of a record in the order they are declared. A record nested inside another takes its own list, as in size as Point = ( 640, 480 ).

A dot reaches a field, and a chain of dots reaches into nested records: w.size.x. A whole record can be assigned at once, as w.origin = corner does.

Assignment copies every field, text included. Changing twin afterwards leaves w as it was: two record variables never share their contents.

Records cannot be compared with = until you define what equality means for them: a = b on two Points is error TKO0720: no operator = compares this type. Objects shows how to define it. Field layout and padding are on the Sizes page.

Activities

  1. Add a Rect type holding two Points, and print its width and height.
  2. Write a function that takes a Point and returns a new one moved by 10 in each direction.

with

Filling in a record with many fields repeats its name on every line. with ... end with names it once: inside the block, a field written with a leading dot belongs to that record.

type Settings
    as int32 columns, rows
    as ustring title
    as boolean resizable
end type

dim as Settings s

'' Inside with ... end with, a leading dot means "a field of s".
with s
    .columns = 80
    .rows = 25
    .title = "Report"
    .resizable = false
end with

print s.title; ":"; s.columns; " x"; s.rows; ", resizable "; s.resizable

'' with works on a nested field too, and code inside it can use
'' anything else in scope.
type Document
    as ustring path
    as Settings layout
end type

dim as Document doc
doc.path = "notes.txt"
with doc.layout
    .columns = s.columns * 2
    .rows = s.rows
    .title = doc.path
end with
print doc.layout.title; ":"; doc.layout.columns; " x"; doc.layout.rows

Output

Report: 80 x 25, resizable false
notes.txt: 160 x 25

.columns = 80 inside with s means s.columns = 80. The block can name any record expression, including a nested field such as doc.layout, and the code inside it can still use every other name in scope: s.columns * 2 reads a different record entirely.

with reads best for short blocks that set or read many fields of one record. In a long block, a reader has to remember which record the dots belong to, so the plain record.field form is often clearer.

Activities

  1. Use a with block to print every field of s on one line.
  2. Inside a with doc block, open with .layout and try to set .path from the inner block. Read the error: which record does the dot refer to?

Enums

An enum gives names to a set of related integer constants. Code that says Blue instead of 10 explains itself, and the enum's name doubles as a type for variables and parameters that hold one of the values.

'' An enum names a set of related integer constants. Values count up
'' from 0, or on from the last value given.
enum Colour
    Red
    Green
    Blue = 10
    Violet
end enum

print "Red ="; Red; ", Green ="; Green; ", Blue ="; Blue; _
      ", Violet ="; Violet

'' A variable of the enum's type holds one of its values. The members
'' can be written plainly or through the enum's name.
dim as Colour wall = Colour.Blue
print "wall ="; wall
wall = Green
print "wall ="; wall

'' select case reads well with enum members.
function Describe( byval c as Colour ) as ustring
    select case c
    case Red, Violet
        return "warm"
    case Green, Blue
        return "cool"
    case else
        return "unknown"
    end select
end function
print "Violet is "; Describe( Violet )

'' An explicit enum must always be qualified, so its short member
'' names cannot clash with anything else.
enum Level explicit
    Low = 1
    Medium
    High
end enum
dim as Level volume = Level.High
print "volume ="; volume; ", and High - Low ="; Level.High - Level.Low

'' An enum value converts to an integer freely.
dim as int32 n = Level.Medium
print "Medium as an int32:"; n

'' The other direction is not checked: any integer fits.
wall = 42
print "wall ="; wall; " is "; Describe( wall )

Output

Red = 0, Green = 1, Blue = 10, Violet = 11
wall = 10
wall = 1
Violet is warm
volume = 3, and High - Low = 2
Medium as an int32: 2
wall = 42 is unknown

The first member is 0 unless it is given a value, and each member after it is one more than the one before. Blue = 10 restarts the count, so Violet is 11.

A member can be written on its own, Green, or qualified with the enum's name, Colour.Blue. Plain names are shorter, but when two enums share a member name only the qualified form says which one you mean. An explicit enum must always be qualified: in the Level example, a bare High is error TKO0743: 'high' is not declared. That makes short member names safe.

Enum values are integers. They print as numbers, compare with = and <, take part in arithmetic, and convert to an int32 without a cast. select case with one case per member is the usual way to act on one.

Pitfall

An enum variable accepts any integer. wall = 42 compiles even though no member of Colour is 42. That is why Describe has a case else: a value that arrived from a file or a calculation may not be a member at all.

Activities

  1. Add Orange between Red and Green and predict every value printed.
  2. Write a function that returns the Level one step above its argument, staying at High at the top.

Unions

A union is declared like a record, but all its fields start at the same address. They are different ways of reading one block of memory, and the union is as large as its largest field.

'' A union's fields all start at the same address: they are different
'' views of one block of memory, as large as the largest field.
union Bits32
    as float32 asFloat
    as uint32 asInt
    as uint8 bytes( 0 to 3 )
end union

dim as Bits32 u
u.asFloat = 1.0
print "sizeof( Bits32 ) ="; sizeof( Bits32 )
print "1.0 as a float32 is the bit pattern &h"; hex( u.asInt, 8 )
print "its bytes, lowest address first:";
for i as int32 = 0 to 3
    print " "; hex( u.bytes( i ), 2 );
next
print

'' A union inside a type gives one slot two meanings. A field beside
'' it must record which meaning is current: the union cannot.
enum ValueKind
    KindInt
    KindFloat
end enum

type Value
    as ValueKind kind
    union
        as int64 i
        as float64 f
    end union
end type

sub Show( byref v as Value )
    if v.kind = KindInt then
        print "an int64:"; v.i
    else
        print "a float64:"; v.f
    end if
end sub

dim as Value a, b
a.kind = KindInt : a.i = 42
b.kind = KindFloat : b.f = 2.5
Show( a )
Show( b )
print "sizeof( Value ) ="; sizeof( Value ); ", i and f both at offset"; _
      offsetof( Value, i ); offsetof( Value, f )

Output

sizeof( Bits32 ) = 4
1.0 as a float32 is the bit pattern &h3F800000
its bytes, lowest address first: 00 00 80 3F
an int64: 42
a float64: 2.5
sizeof( Value ) = 16, i and f both at offset 8 8

Bits32 is four bytes, whichever field you use. Writing asFloat and reading asInt shows the bit pattern of the float32 value 1.0, and the bytes array shows the same four bytes one at a time. The lowest address holds the lowest byte, because both targets are little-endian.

A union ... end union without a name, inside a type, gives one slot of the record two meanings: Value stores either an int64 or a float64 in the same eight bytes, which is why offsetof reports both i and f at offset 8. The kind field beside it says which one is current, and Show reads it before touching the union.

Pitfall

A union does not know which field was written last. Reading v.f after writing v.i compiles and runs, and gives you the integer's bits read as a float. Keep a field beside the union that records which member is live, and check it every time.

Activities

  1. Store -1.0 in u.asFloat and work out which bit changed from 1.0.
  2. Add a KindBool member and a boolean field to Value's union, and extend Show to print it.

Constants

const names a value that is fixed when the program is compiled. A name such as MaxUsers says what a number is for, and changing it means editing one line rather than hunting for every 100.

'' const names a value that is fixed when the program is compiled.
const MaxUsers = 100
const Pi = 3.141592653589793
const AppName = "inventory"

'' Without a type, a constant takes the literal's type. With one,
'' the value is converted to it.
const Small as uint8 = 200
const Ratio as float32 = 0.5

print "MaxUsers = " & MaxUsers & ", " & sizeof( MaxUsers ) & " bytes"
print "Pi = " & Pi & ", " & sizeof( Pi ) & " bytes"
print "AppName = " & AppName
print "Small = " & Small & ", " & sizeof( Small ) & " byte"
print "Ratio = " & Ratio & ", " & sizeof( Ratio ) & " bytes"

'' A constant can be built from other constants, and can size an array.
const Rows = 3
const Cols = Rows * 2
const Cells = Rows * Cols
dim as int32 board( 1 to Cells )
print "board has"; ubound( board ); " cells"

'' Constants declared inside a block belong to that block.
scope
    const Greeting = "hello from inside"
    print Greeting
end scope

Output

MaxUsers = 100, 8 bytes
Pi = 3.141592653589793, 8 bytes
AppName = inventory
Small = 200, 1 byte
Ratio = 0.5, 4 bytes
board has 18 cells
hello from inside

A const without a type takes the type of its value, by the same rules as a literal: MaxUsers is an 8-byte integer and Pi a float64. With as T the value is converted to that type, so Small is a one-byte uint8 and Ratio a float32.

A constant's value can be an expression over other constants, worked out by the compiler. That makes constants usable where the compiler needs a fixed number, such as the bounds of a fixed-size array: board( 1 to Cells ).

A constant declared inside a block, as Greeting is inside scope, exists only in that block; using it after end scope is error TKO0743: 'greeting' is not declared. Assigning to any constant is refused, as Mutability shows, which also covers dim as const for values computed while the program runs.

Activities

  1. Add const Area = Rows * Cols and use it to declare a second array.
  2. Declare const Label as ustring = AppName & " v2" and print it.

Variable Bindings

A variable is a name for a value of one type. tiko has two ways to declare one: dim, which states the type, and var, which takes the type from the value it starts with.

'' dim declares a variable. The type can come before the names...
dim as int32 columns = 80, rows = 25

'' ...or after each name, which lets one line mix types.
dim title as ustring = "console", ratio as float64 = 3.2

'' var takes the type from the value, so the value is required.
var count = 3
var area = columns * rows

'' For text, state the type: a ustring.
dim greeting as ustring = "hello"

print "columns ="; columns; ", rows ="; rows
print "title = "; title; ", ratio ="; ratio
print "count ="; count; ", greeting = "; greeting; ", area ="; area

'' A variable can be assigned a new value of its type at any time.
count = count + 1
greeting = greeting & ", world"
print "count ="; count; ", greeting = "; greeting

'' Declarations can appear anywhere a statement can, so a variable can
'' be declared where it is first needed.
dim as float64 average = area / count
print "average ="; average

Output

columns = 80, rows = 25
title = console, ratio = 3.2
count = 3, greeting = hello, area = 2000
count = 4, greeting = hello, world
average = 500

dim has two spellings. dim as int32 columns = 80, rows = 25 puts the type first and applies it to every name on the line. dim title as ustring = ... puts a type after each name, so one line can declare variables of different types.

var needs a value, because the value is where the type comes from: count is an integer, from the literal 3, and area has the type of columns * rows. Use var when the type is obvious from the line, and dim when it is not. Text is declared with dim ... as ustring, as greeting is; Inference explains why.

A variable keeps its type for its whole life. It can be given a new value of that type at any time, but count = "three" is error TKO0702: a string and a number do not convert into each other.

A declaration is a statement like any other, so it can go where the variable is first needed, as average does.

Activities

  1. Declare three float64 variables on one dim line and print their sum.
  2. Write var empty with no value and read the compiler's message.

Mutability

Variables in tiko can be changed unless you say otherwise. Saying otherwise is worth doing: a value that cannot change is one less thing to keep track of when you read the code later.

'' An ordinary variable can be changed as often as you like.
dim as int32 score = 10
score += 5
print "score ="; score

'' const fixes a name to a value the compiler knows.
const MaxScore = 100
print "MaxScore ="; MaxScore

'' dim as const fixes a variable after its first value, which may be
'' computed while the program runs.
function StartingLives( ) as int32
    return 3
end function

dim as const int32 lives = StartingLives( )
dim as const int32 snapshot = score
score = 0
print "lives ="; lives; ", snapshot ="; snapshot; ", score ="; score

'' A const parameter promises the procedure will not change what it is
'' given, even though it receives the caller's variable by reference.
sub Show( byref value as const int32 )
    print "showing"; value
end sub

'' Without const, a byref parameter can change the caller's variable.
sub Bump( byref value as int32 )
    value += 1
end sub

Show( score )
Bump( score )
Bump( score )
print "after two bumps, score ="; score

Output

score = 15
MaxScore = 100
lives = 3, snapshot = 15, score = 0
showing 0
after two bumps, score = 2

There are two ways to fix a value:

DeclarationValue knownUse it for
const MaxScore = 100when compilingfixed settings, array bounds
dim as const int32 lives = StartingLives( )when the line runsa result that must not change afterwards

snapshot shows the second kind copying a variable at one moment: score changes afterwards, and snapshot keeps the old value.

A parameter declared byref value as const int32 receives the caller's own variable, without a copy, but the procedure may only read it. Without const, a byref parameter writes straight through to the caller, which is how Bump changes score. Writing to a const parameter is refused in the same way as writing to a const variable.

Any attempt to change a constant is a compile error:

dim as const int32 lives = 3

lives -= 1      '' refused: lives is const
print lives
assignconst.tiko(3,1): error TKO0617: cannot modify 'lives': it is a constant
     |
   3 | lives -= 1      '' refused: lives is const
     | ^

Activities

  1. Add dim as const ustring player = "Ada" and try to append to it.
  2. Write a sub that takes a byref ... as const record and prints a field. Then try to change the field inside it.

Scope and Shadowing

Every variable belongs to the block it is declared in, and lives until that block ends. A block inside another may declare a name that already exists outside; the inner one then hides, or shadows, the outer one until the block ends.

dim as ustring message = "outer"
print "1: "; message

'' scope ... end scope opens a block. A name declared inside it lives
'' only until end scope, and may hide a name from outside.
scope
    dim as ustring message = "inner"
    dim as int32 temporary = 42
    print "2: "; message; ", temporary ="; temporary

    '' Blocks nest, and the innermost declaration wins.
    scope
        dim as float64 message = 2.5
        print "3:"; message
    end scope

    print "4: "; message
end scope

'' Back outside, the outer variable was never touched.
print "5: "; message

'' Every block works this way: if, for, do and select bodies too.
for i as int32 = 1 to 2
    dim as int32 doubled = i * 2
    print "loop"; i; ":"; doubled
next

'' Assigning, rather than declaring, inside a block changes the outer
'' variable: there is only one message here.
if len( message ) > 0 then
    message = "changed in an if"
end if
print "6: "; message

Output

1: outer
2: inner, temporary = 42
3: 2.5
4: inner
5: outer
loop 1: 2
loop 2: 4
6: changed in an if

scope ... end scope opens a block whose only job is to limit names. Inside it, message is a new ustring holding "inner", and the innermost block declares a third message, this time a float64. Each one disappears at its end scope, and the outer message is untouched throughout: line 5 still prints outer.

The body of every if, for, do and select case is a block in the same way, so doubled exists only inside the loop. An assignment inside a block, as in the if, has no dim and so declares nothing: it changes the variable already in scope.

A name used after its block has ended is no longer declared:

scope
    dim as int32 temporary = 42
end scope

print temporary     '' refused: temporary ended with its scope
outofscope.tiko(5,7): error TKO0743: 'temporary' is not declared
     |
   5 | print temporary     '' refused: temporary ended with its scope
     |       ^

Shadowing needs a new block. Declaring the same name twice in one block is an error, and the note points at the first declaration:

dim as int32 total = 1

dim as int32 total = 2      '' refused: same name, same scope
print total
redeclare.tiko(3,14): error TKO0600: 'total' is already declared in this scope
     |
   3 | dim as int32 total = 2      '' refused: same name, same scope
     |              ^
redeclare.tiko(1,14): note: declared here
     |
   1 | dim as int32 total = 1
     |              ^

Activities

  1. Inside the first scope, assign to message without dim before the inner declaration, and see which variable changes.
  2. Move dim as int32 doubled above the for loop. What does the loop print, and what is doubled after it?

Declare First

Every variable must be declared before it is used, but it does not need a value at that point. A variable declared without one starts at zero, so a program can declare it first and decide its value later.

'' A variable declared without a value starts at zero: 0 for numbers,
'' false for booleans, "" for text.
dim as int32 count
dim as float64 total
dim as boolean done
dim as ustring label
print "count ="; count; ", total ="; total; ", done = "; done; _
      ", label = """; label; """"

'' So a variable can be declared first and given its value later.
dim as ustring verdict
if count = 0 then
    verdict = "empty"
else
    verdict = "has items"
end if
print "verdict: "; verdict

'' Records and arrays start at zero in every field and element.
type Tally
    as int32 hits, misses
end type
dim as Tally t
dim as int32 slots( 1 to 4 )
print "t ="; t.hits; t.misses; ", slots ="; slots( 1 ); slots( 4 )

'' A declaration inside a loop runs on every pass, so it starts at
'' zero each time.
for i as int32 = 1 to 3
    dim as int32 n
    n += i
    print "pass"; i; ": n ="; n
next

'' = any skips the zeroing. The variable holds whatever was in that
'' memory, so it must be written before it is read.
dim as int32 scratch = any
scratch = 7
print "scratch ="; scratch

Output

count = 0, total = 0, done = false, label = ""
verdict: empty
t = 0 0, slots = 0 0
pass 1: n = 1
pass 2: n = 2
pass 3: n = 3
scratch = 7

"Zero" depends on the type: 0 for numbers, false for a boolean, "" for a ustring. Records start with every field at zero, unless the field declares a value of its own, and arrays start with every element at zero. verdict shows the usual reason to declare first: the value depends on a test, and each branch of the if assigns it.

A declaration runs each time the program reaches it. Inside a loop, n is declared, and zeroed, on every pass, which is why n += i gives 1, 2 and 3 rather than a running total.

= any skips the zeroing, for the rare case where a large variable is about to be overwritten anyway. The variable then holds whatever was in that memory, so write it before you read it.

Using a name that was never declared is an error, which turns a typing mistake into a message instead of a second, silent variable:

dim as int32 total = 0

totl = total + 1      '' a typo: there is no variable called totl
print total
undeclared.tiko(3,1): error TKO0743: 'totl' is not declared
     |
   3 | totl = total + 1      '' a typo: there is no variable called totl
     | ^

The same error appears for a variable used on a line above its declaration.

Activities

  1. Declare a dim as float64 readings( 1 to 5 ) and print the sum of its elements before assigning any of them.
  2. Move dim as int32 n above the for loop and predict the output.

Types

Every value in a tiko program has a type, and every variable keeps the type it was declared with. The compiler knows all of them before the program runs, so it can check that each operation makes sense and convert between number types where one is needed.

'' Every variable has one type, fixed when the program is compiled.
dim as int32 items = 3
dim as float64 unitPrice = 2.5
dim as ustring currency = "EUR"

'' Mixing numeric types converts to the wider type before the
'' operation, so nothing is lost.
var cost = items * unitPrice
print "cost ="; cost; " (" & sizeof( cost ) & " bytes, a float64)"

'' Assigning to a narrower type converts on the way in. A float
'' assigned to an integer is rounded.
dim as int32 rounded = cost
print "rounded ="; rounded

'' Text and numbers meet only through an explicit conversion, or the
'' & operator, which converts its operands to text.
print "total: " & cost & " " & currency
print "total: " & str( rounded ) & " " & currency

Output

cost = 7.5 (8 bytes, a float64)
rounded = 8
total: 7.5 EUR
total: 8 EUR

items * unitPrice multiplies an int32 by a float64. The int32 is converted to float64 first, so cost is a float64 and keeps the half. Going the other way, from cost into the int32 called rounded, the value is converted on assignment; a floating-point value assigned to an integer is rounded, and 7.5 becomes 8.

Numbers and text do not convert into each other by themselves:

dim as int32 items = 3
dim as ustring label

label = items      '' refused: a number is not text
print label
mixing.tiko(4,1): error TKO0702: a string and a number do not convert into each other
     |
   4 | label = items      '' refused: a number is not text
     | ^

str( ) makes the conversion explicit, and & makes it for you: it turns each operand into text and joins them.

The rest of this section covers casting from one type to another, the type each literal has, letting the compiler infer a type, giving a type another name with an alias, and the sizes of types in memory. Conversion goes further into numbers and text.

Activities

  1. Change items to a float32 and print sizeof( cost ).
  2. Assign 6.5 to rounded and print it. Is it 6 or 7?

Casting

A cast converts a value to a type you name. tiko converts between number types on assignment anyway, so a cast is for the places where the type would otherwise be wrong: forcing floating-point arithmetic, choosing an integer width, or making a narrowing conversion visible.

'' cast( T, value ) converts a value to the type you name.
dim as float64 price = 7.0
print "cast( float64, 7 ) / 2 ="; cast( float64, 7 ) / 2
print "cast( int32, price ) \ 2 ="; cast( int32, price ) \ 2

'' The c... functions are shorthand for the common targets.
print "cint( 2.6 )    ="; cint( 2.6 )      '' integer
print "clng( 2.6 )    ="; clng( 2.6 )      '' int32
print "clngint( 2.6 ) ="; clngint( 2.6 )   '' int64
print "cdbl( 1 ) / 3  ="; cdbl( 1 ) / 3    '' float64
print "csng( 1 ) / csng( 3 ) ="; csng( 1 ) / csng( 3 )   '' float32
print

'' Narrowing an integer keeps the low bits, so it wraps silently.
print "cast( uint8, 300 )  = "; cast( uint8, 300 )
print "cast( int8, 200 )   = "; cast( int8, 200 )
print "cast( uint16, -1 )  = "; cast( uint16, -1 )
print "cubyte( 260 )       = "; cubyte( 260 )

'' Unsigned arithmetic wraps the same way.
dim as uint32 counter = 0
counter -= 1
print "0 - 1 as a uint32   = "; counter
dim as uint8 small = 255
small += 1
print "255 + 1 as a uint8  = "; small
print

'' Booleans: zero is false, anything else true; true is -1.
print "cbool( 0 ) = "; cbool( 0 ); ", cbool( 5 ) = "; cbool( 5 )
print "cint( true ) ="; cint( true ); ", cint( false ) ="; cint( false )

Output

cast( float64, 7 ) / 2 = 3.5
cast( int32, price ) \ 2 = 3
cint( 2.6 )    = 3
clng( 2.6 )    = 3
clngint( 2.6 ) = 3
cdbl( 1 ) / 3  = 0.3333333333333333
csng( 1 ) / csng( 3 ) = 0.3333333

cast( uint8, 300 )  = 44
cast( int8, 200 )   = -56
cast( uint16, -1 )  = 65535
cubyte( 260 )       = 4
0 - 1 as a uint32   = 4294967295
255 + 1 as a uint8  = 0

cbool( 0 ) = false, cbool( 5 ) = true
cint( true ) =-1, cint( false ) = 0

cast( T, value ) works for any target type. For the common ones there is a shorter function:

FunctionResult type
cintinteger
clngint32
clngintint64
cubyte, cbyteuint8, int8
cushort, cshortuint16, int16
culnguint32
csngfloat32
cdblfloat64
cboolboolean

A floating-point value converted to an integer is rounded, not cut off: cint( 2.6 ) is 3. Rounding has the exact rule and the functions that round other ways.

Converting to a narrower integer keeps only the low bits, so a value that does not fit wraps around with no error: 300 in eight bits is 44, and 200 as a signed int8 is -56. Unsigned arithmetic wraps the same way, which is why 0 - 1 in a uint32 is 4294967295.

cbool maps 0 to false and every other number to true. In the other direction true is -1, the value a comparison produces.

Activities

  1. Print cast( int16, 40000 ) and cast( uint16, 40000 ). Why does one change and not the other?
  2. Print cdbl( 7 ) / 2, 7 \ 2 and cint( 7 / 2 ).

Literal Types

A literal has a type too, chosen from how it is written. Usually you never notice, because a literal converts to whatever type it is assigned to. It matters when the literal is used on its own: in arithmetic, as an argument to an overloaded procedure, or as the value a var copies.

'' One overload per type: the compiler picks the one that matches the
'' argument's type exactly, so TypeOf_ names the type a literal has.
function TypeOf_ overload( byval v as int32 ) as ustring
    return "int32"
end function
function TypeOf_ overload( byval v as int64 ) as ustring
    return "int64"
end function
function TypeOf_ overload( byval v as integer ) as ustring
    return "integer"
end function
function TypeOf_ overload( byval v as uint32 ) as ustring
    return "uint32"
end function
function TypeOf_ overload( byval v as uint64 ) as ustring
    return "uint64"
end function
function TypeOf_ overload( byval v as uinteger ) as ustring
    return "uinteger"
end function
function TypeOf_ overload( byval v as float32 ) as ustring
    return "float32"
end function
function TypeOf_ overload( byval v as float64 ) as ustring
    return "float64"
end function

print "42         is "; TypeOf_( 42 )
print "&hFF       is "; TypeOf_( &hFF )
print "2.5        is "; TypeOf_( 2.5 )
print "1e6        is "; TypeOf_( 1e6 )
print

'' A suffix asks for a type.
print "42L        is "; TypeOf_( 42L )
print "42LL       is "; TypeOf_( 42LL )
print "42U        is "; TypeOf_( 42U )
print "42UL       is "; TypeOf_( 42UL )
print "42ULL      is "; TypeOf_( 42ULL )
print "2.5F       is "; TypeOf_( 2.5F )
print "2.5D       is "; TypeOf_( 2.5D )
print

'' A decimal literal too big for 32 signed bits but small enough for
'' 32 unsigned ones is unsigned.
print "3000000000 is "; TypeOf_( 3000000000 )
print "3000000000 - 3000000001 = "; 3000000000 - 3000000001
print "3000000000LL - 3000000001LL = "; 3000000000LL - 3000000001LL

Output

42         is integer
&hFF       is integer
2.5        is float64
1e6        is float64

42L        is int32
42LL       is int64
42U        is uinteger
42UL       is uint32
42ULL      is uint64
2.5F       is float32
2.5D       is float64

3000000000 is uinteger
3000000000 - 3000000001 = 18446744073709551615
3000000000LL - 3000000001LL = -1

TypeOf_ has one overload per type. The compiler calls the overload whose parameter type matches the argument exactly, so the text it returns names the literal's type.

A plain whole number is an integer, as wide as an address: 8 bytes on both 64-bit targets. A number with a point or an exponent is a float64. A suffix asks for a different type; the Literals page lists them.

Pitfall

A decimal literal from 2147483648 to 4294967295 is unsigned. It does not fit in 32 signed bits, but does fit in 32 unsigned ones, so it gets the type uinteger. Arithmetic with an unsigned operand is unsigned, so 3000000000 - 3000000001 wraps round to 18446744073709551615 instead of giving -1. A suffix fixes it: with LL on both literals the subtraction is done in int64. Literals above 4294967295 are integer again.

Activities

  1. Add an int16 overload and call it with cshort( 1 ). Which overload does a plain 1 pick now?
  2. Print TypeOf_( 2147483647 ) and TypeOf_( 2147483648 ).

Inference

var declares a variable without naming its type: the compiler works the type out from the starting value. typeof goes the other way and gives you the type of an expression, to use wherever a type is expected.

'' var gives a variable the type of its first value.
var count = 10          '' integer, from the literal
var price = 4.25        '' float64
var ready = true        '' boolean
var tiny = cast( uint8, 7 )

print "count:"; sizeof( count ); " bytes, price:"; sizeof( price ); _
      " bytes, ready:"; sizeof( ready ); " byte, tiny:"; sizeof( tiny ); _
      " byte"

'' The value can be any expression, including a call.
function Average( byval a as float64, byval b as float64 ) as float64
    return ( a + b ) / 2
end function
var mean = Average( 3, 4 )
var total = count * price       '' integer times float64 is float64
print "mean ="; mean; ", total ="; total; ","; sizeof( total ); " bytes"

'' Arithmetic on narrow integers is done at integer width.
dim as int16 a = 30000, b = 30000
var sum = a + b
print "int16 + int16 ="; sum; " in"; sizeof( sum ); " bytes"

'' typeof( x ) is x's type, usable wherever a type is expected.
dim as typeof( tiny ) other = 200
dim as typeof( price ) another = 1.5
print "other = "; other; " in"; sizeof( other ); " byte, another ="; another

'' The type is fixed once inferred: tiny is still a uint8.
tiny = 255
tiny += 1
print "tiny after 255 + 1 = "; tiny

Output

count: 8 bytes, price: 8 bytes, ready: 1 byte, tiny: 1 byte
mean = 3.5, total = 42.5, 8 bytes
int16 + int16 = 60000 in 8 bytes
other = 200 in 1 byte, another = 1.5
tiny after 255 + 1 = 0

var copies the type of the value exactly. count is an integer because a plain whole-number literal is one, and tiny is a uint8 because the cast says so. The value can be any expression: mean takes the float64 that Average returns, and total the float64 that an integer times a float64 produces.

Arithmetic on integers narrower than integer is carried out at integer width, so a + b on two int16s is an 8-byte result, and 60000 does not overflow. Assign it back to an int16 and it would.

typeof( tiny ) stands for uint8 in the declaration of other. That is useful when a variable must match another one whose type might change later.

Inference happens once. After var tiny = cast( uint8, 7 ), tiny is a uint8 for good, and 255 + 1 wraps to 0 in it.

Note

For text, write the type: dim as ustring title = "report". A string literal on its own is the 8-bit legacy string type, and var copies that, so var title = "report" does not give you a ustring.

Activities

  1. Declare var half = 1 / 2 and print sizeof( half ) and half.
  2. Declare dim as typeof( mean ) spread = 0 and check its size.

Aliasing

type Name as T gives an existing type a second name. The alias is the same type under another name, so it costs nothing and converts nothing; what it adds is meaning for the reader, or a shorter way to write a long type.

'' type Name as T gives an existing type a second name.
type Celsius as float64
type UserId as uint32

dim as Celsius outside = 21.5
dim as UserId owner = 1042
print "outside:"; outside; ", owner: "; owner

'' An alias is the same type, not a new one: values mix freely.
dim as float64 plain = outside
dim as Celsius fromPlain = plain + 0.5
print "plain:"; plain; ", fromPlain:"; fromPlain
print "sizeof( Celsius ) ="; sizeof( Celsius ); _
      ", sizeof( UserId ) ="; sizeof( UserId )

'' Aliases shorten long type names, such as a tuple type.
type Span as ( first as int32, last as int32 )

function Widen( byval s as Span, byval by_ as int32 ) as Span
    return ( s.first - by_, s.last + by_ )
end function

dim as Span hours = ( 9, 17 )
var longer = Widen( hours, 1 )
print "hours"; hours.first; " to"; hours.last; _
      ", widened"; longer.first; " to"; longer.last

'' They also name a record type a second way.
type Point2D
    as float64 x, y
end type
type Vec2 as Point2D
dim as Vec2 v = ( 3, 4 )
dim as Point2D p = v
print "p ="; p.x; p.y

Output

outside: 21.5, owner: 1042
plain: 21.5, fromPlain: 22
sizeof( Celsius ) = 8, sizeof( UserId ) = 4
hours 9 to 17, widened 8 to 18
p = 3 4

Celsius is a float64 and UserId a uint32; sizeof confirms it. Because an alias is not a new type, a Celsius and a float64 assign to each other freely, as plain and fromPlain show. An alias documents intent, but it will not stop you passing a UserId where a temperature was wanted. When you need that protection, declare a record with one field instead: two different record types never convert into each other.

Aliases pay for themselves with long types. Span names a tuple type with two named elements, and from then on parameters, results and variables can all say Span.

An alias can name a record type too: Vec2 and Point2D are one type, so a Vec2 initialises a Point2D directly.

Activities

  1. Add type Fahrenheit as float64 and assign a Celsius to it. Does the compiler object?
  2. Write a function returning a Span that covers two other Spans.

Sizes

Every type occupies a fixed number of bytes, and sizeof reports it. Most of the time you do not need the number, but it matters when you read binary data, call C code, or keep a large array of records in memory.

'' sizeof( T ) is the size of a type in bytes; sizeof( x ) is the size
'' of a value's type. len( T ) means the same for a type.
print "sizeof( int16 ) ="; sizeof( int16 ); ", len( int16 ) ="; len( int16 )
print "sizeof( float64 ) ="; sizeof( float64 )

'' integer and pointers are as wide as an address on the target.
print "sizeof( integer ) ="; sizeof( integer ); _
      ", sizeof( any ptr ) ="; sizeof( any ptr )

'' For text, len( s ) is the number of characters; sizeof( s ) is the
'' fixed size of the ustring value itself, whatever it holds.
dim as ustring shortText = "hi"
dim as ustring longText = "a much longer piece of text"
print "len:"; len( shortText ); len( longText ); _
      ", sizeof:"; sizeof( shortText ); sizeof( longText )

'' A record is at least the sum of its fields. Each field sits at a
'' multiple of its own size, so the order of the fields matters.
type Loose
    as uint8 flag
    as int32 value
    as uint8 mark
end type
type Tight
    as int32 value
    as uint8 flag
    as uint8 mark
end type
print "Loose: sizeof"; sizeof( Loose ); ", value at offset"; _
      offsetof( Loose, value ); ", mark at"; offsetof( Loose, mark )
print "Tight: sizeof"; sizeof( Tight ); ", value at offset"; _
      offsetof( Tight, value ); ", mark at"; offsetof( Tight, mark )

Output

sizeof( int16 ) = 2, len( int16 ) = 2
sizeof( float64 ) = 8
sizeof( integer ) = 8, sizeof( any ptr ) = 8
len: 2 27, sizeof: 24 24
Loose: sizeof 12, value at offset 4, mark at 8
Tight: sizeof 8, value at offset 0, mark at 5

sizeof takes a type or a value. len given a type means the same thing; given text, it means the number of characters.

integer and pointers have the width of an address: 8 bytes on both 64-bit targets. Use the fixed-width names (int32, int64) when the width matters and integer when a value has to match the machine's word, such as an index into something very large.

A ustring variable is a fixed-size value that manages its text elsewhere, so sizeof is the same for short and long text. len is the one that changes.

A record's fields are laid out in order, each at an offset that is a multiple of its own size, and the whole record is rounded up to a multiple of its largest field. Loose puts a 1-byte field before a 4-byte one, so three bytes of padding go in between and the record takes 12 bytes. Tight has the same fields largest first and takes 8. offsetof( T, field ) shows where each field starts.

Activities

  1. Add an int64 field to the end of Tight and predict its sizeof before you run it.
  2. Print sizeof( Loose ) * 1000000 and sizeof( Tight ) * 1000000: the memory an array of a million of each would take.

Conversion

Programs move values between forms all the time: text read from a user or a file becomes a number, numbers of one type meet numbers of another, and results become text again for display. Each of those steps is a conversion, and tiko has a tool for each.

'' A small order: the quantity and price arrive as text.
dim as ustring quantityText = "12"
dim as ustring priceText = "7.29"

'' Text to number: val reads a float64, valint a whole number.
var quantity = valint( quantityText )
var price = val( priceText )

'' Number to number: arithmetic converts to the wider type, and
'' cint converts back to a whole number, rounding.
var total = quantity * price
var wholeTotal = cint( total )
var perItem = cast( float32, total / quantity )

'' Number to text: str, or the & operator.
dim as ustring report = "total " & str( total ) & ", about " & _
                         wholeTotal & ", or " & perItem & " each"
print report
print "the report is"; len( report ); " characters of text"

Output

total 87.48, about 87, or 7.29 each
the report is 35 characters of text
FromToWith
textnumberval, valint, vallng, or numeric.TryParse... to reject bad input
numbernumberassignment, cast( T, x ), cint, cdbl and the other c... functions
numbertextstr( x ), the & operator, strings.Format
your typeanother typea constructor and an operator cast

The example uses one of each. valint and val read the two strings, quantity * price converts the integer to float64 before multiplying, cint rounds the total back to a whole number, and & turns every operand into text as it joins them.

The pages that follow take the steps one at a time: numbers to text, text to numbers, conversions for your own types, and the rules for rounding when a fraction has to go.

Activities

  1. Change quantityText to "12 boxes". What does valint make of it?
  2. Change priceText to "seven" and explain the total.

Numbers to Text

print shows a number without being asked, but to keep the text, join it with other text or lay it out in columns, you need the text as a ustring. There are three ways to get it, from the quickest to the most controlled.

#import "std/strings"

dim as int32 count = 42
dim as float64 ratio = 2.71828
dim as boolean ok = true

'' str( x ) is the text of a value, with no leading space.
print "["; str( count ); "] ["; str( -count ); "] ["; str( ratio ); _
      "] ["; str( ok ); "]"

'' & converts each side to text and joins them.
dim as ustring line_ = "count=" & count & ", ratio=" & ratio & ", ok=" & ok
print line_

'' hex, oct and bin show an integer in another base; a second
'' argument pads to that many digits.
print "hex: "; hex( 255 ); " "; hex( 255, 4 ); "  oct: "; oct( 8 ); _
      "  bin: "; bin( 5, 8 )

'' strings.Format controls width, alignment, precision and base.
print strings.Format( "[{:>6}] [{:<6}] [{:06}]", count, count, count )
print strings.Format( "[{:.2f}] [{:.3e}] [{:+}]", ratio, ratio, count )
print strings.Format( "[{:#x}] [{:#b}] [{:>8}]", count, 5, ok )

Output

[42] [-42] [2.71828] [true]
count=42, ratio=2.71828, ok=true
hex: FF 00FF  oct: 10  bin: 00000101
[    42] [42    ] [000042]
[2.72] [2.718e+00] [+42]
[0x2a] [0b101] [    true]

str( x ) gives the text of a number or a boolean. Unlike print, it adds no space in front of a positive number. The & operator calls the same conversion on each side, so "count=" & count needs no str at all.

hex, oct and bin write an integer in base 16, 8 or 2, and an optional second argument pads the result with zeros to that many digits.

strings.Format from std/strings does the rest: width and alignment ({:>6}), zero padding ({:06}), a sign ({:+}), digits after the point ({:.2f}), exponent notation ({:.3e}) and other bases with a prefix ({:#x}). The Formatted print page lists the parts of a format spec.

Activities

  1. Print ratio with 0, 2 and 4 digits after the point.
  2. Print the numbers 1 to 12 in hexadecimal, two digits each, on one line.

Text to Numbers

Text from a user, a file or the command line has to become a number before you can calculate with it. tiko has two kinds of function for that: the forgiving val family, and strict parsers in std/numeric that tell you when the text is not a number.

#import "std/numeric"

'' val reads a number from the front of the text. It skips leading
'' spaces, stops at the first character that cannot continue the
'' number, and gives 0 when there is no number at all.
print "val( ""3.75"" )     ="; val( "3.75" )
print "val( ""  12 cm"" )  ="; val( "  12 cm" )
print "val( ""1e3"" )      ="; val( "1e3" )
print "val( ""&hFF"" )     ="; val( "&hFF" )
print "val( ""none"" )     ="; val( "none" )
print

'' valint, vallng and valuint read whole numbers into integer types.
print "valint( ""99.9"" )  ="; valint( "99.9" )
print "vallng( ""9000000000"" ) ="; vallng( "9000000000" )
print

'' val cannot tell "0" from rubbish. numeric.TryParseLong and
'' TryParseDouble take the whole text or nothing, and say which.
dim as ustring inputs( 1 to 4 ) = { "42", "42abc", "", "-7" }
for i as int32 = 1 to 4
    var n = numeric.TryParseLong( inputs( i ) )
    if n.HasValue( ) then
        print """" & inputs( i ) & """ is the int32 " & n.Value( )
    else
        print """" & inputs( i ) & """ is not a number"
    end if
next

var d = numeric.TryParseDouble( "2.5e-3" )
print "TryParseDouble( ""2.5e-3"" ) ="; d.ValueOr( 0 )

Output

val( "3.75" )     = 3.75
val( "  12 cm" )  = 12
val( "1e3" )      = 1000
val( "&hFF" )     = 255
val( "none" )     = 0

valint( "99.9" )  = 99
vallng( "9000000000" ) = 9000000000

"42" is the int32 42
"42abc" is not a number
"" is not a number
"-7" is the int32 -7
TryParseDouble( "2.5e-3" ) = 0.0025

val reads as much of a number as it can from the front of the text and ignores the rest, so " 12 cm" gives 12. It understands exponents and the &h prefix. When the text does not start with a number, the answer is 0. valint and vallng work the same way but produce whole numbers, as int32 and int64; valuint and valulng are the unsigned forms.

That forgiveness is a problem when you need to know whether the input was valid, because val( "0" ) and val( "none" ) give the same answer. numeric.TryParseLong accepts only text that is entirely a number: it returns an optional value, and HasValue( ) says whether there is one. Value( ) reads it, and ValueOr( d ) reads it or gives d when there is none. TryParseLongInt, TryParseDouble and TryParseSingle cover the other types.

Error Handling covers optional values in general.

Activities

  1. Pass " 7", with a leading space, to TryParseLong and to valint.
  2. Read "9999999999" with TryParseLong and then with TryParseLongInt.

From and Into

Your own types can take part in conversions too. A constructor that takes another type says how to build your type from it, and an operator cast says how to turn your type into another one. Once they exist, the compiler calls them wherever the conversion is needed.

#import "std/strings"

type Fahrenheit
    as float64 degrees
end type

'' A constructor taking another type says how to build a Celsius
'' FROM it. An operator cast says how to turn a Celsius INTO another
'' type.
type Celsius
    as float64 degrees
    declare constructor( byval d as float64 )
    declare constructor( byref f as Fahrenheit )
    declare operator let( byref f as Fahrenheit )
    declare operator cast( ) as float64
    declare operator cast( ) as ustring
end type

constructor Celsius( byval d as float64 )
    degrees = d
end constructor

function ToCelsius( byref f as Fahrenheit ) as float64
    return ( f.degrees - 32 ) * 5 / 9
end function

constructor Celsius( byref f as Fahrenheit )
    degrees = ToCelsius( f )
end constructor

operator Celsius.let( byref f as Fahrenheit )
    degrees = ToCelsius( f )
end operator

operator Celsius.cast( ) as float64
    return degrees
end operator

operator Celsius.cast( ) as ustring
    return strings.Format( "{:.1f} C", degrees )
end operator

'' From: the constructors run wherever a Celsius is built from a value.
dim as Fahrenheit boiling = ( 212 )
dim as Celsius a = boiling
dim as Celsius b = 37.5
print "a = "; a; ", b = "; b

'' Assigning to a Celsius that already exists needs an operator let.
dim as Fahrenheit freezing = ( 32 )
b = freezing
print "b = "; b

'' Into: the casts run wherever a float64 or a ustring is expected.
dim as float64 raw = a
print "raw ="; raw
print "a + 1 ="; cast( float64, a ) + 1
dim as ustring text = b
print "text = " & text

Output

a = 100.0 C, b = 37.5 C
b = 0.0 C
raw = 100
a + 1 = 101
text = 0.0 C

Celsius has three ways in and two ways out:

DeclaredRuns when
constructor( byval d as float64 )a Celsius is declared from a number: dim as Celsius b = 37.5
constructor( byref f as Fahrenheit )a Celsius is declared from a Fahrenheit
operator let( byref f as Fahrenheit )a Fahrenheit is assigned to an existing Celsius
operator cast( ) as float64a float64 is expected, or cast( float64, ... ) asks
operator cast( ) as ustringtext is expected: print, &, a ustring variable

The constructors only run when a value is being created. Assigning to a variable that already exists is a different operation, and needs an operator let; without it, b = freezing is error TKO0738: no conversion between these two types.

print picks the ustring cast, which is why a prints as 100.0 C. cast( float64, a ) asks for one particular cast by naming its type, which keeps the reader from having to work out which of the two arithmetic will use.

Constructors and operators are covered properly in Objects; Display uses the same ustring cast to make a type printable.

Activities

  1. Add a constructor that builds a Fahrenheit from a Celsius, and convert a back.
  2. Remove the operator let and read the error for b = freezing.

Rounding

When a value with a fraction becomes a whole number, something has to decide what happens to the fraction. tiko's built-in conversions round to the nearest integer, and send an exact half to the even neighbour. Other rules are a function call away.

#import "std/numeric"

'' Right-align a number in a column 6 wide.
function Cell( byval v as float64 ) as ustring
    var s = str( v )
    return space( 6 - len( s ) ) & s
end function

dim as float64 samples( 1 to 8 ) = _
    { 0.5, 1.5, 2.5, 2.6, -0.5, -1.5, -2.5, -2.6 }

print "     x  cint   int   fix  Round  HalfEven"
for i as int32 = 1 to 8
    var x = samples( i )
    print Cell( x ); Cell( cint( x ) ); Cell( int( x ) ); Cell( fix( x ) ); _
          Cell( numeric.Round( x ) ); "  "; Cell( numeric.RoundHalfEven( x ) )
next
print

'' Every float-to-integer conversion rounds the way cint does.
dim as int32 assigned = 2.5
print "dim as int32 = 2.5 gives"; assigned
print "cast( int32, 3.5 ) gives"; cast( int32, 3.5 )
print

'' Floor and Ceil round towards minus and plus infinity.
print "Floor( -1.5 ) ="; numeric.Floor( -1.5 ); _
      ", Ceil( -1.5 ) ="; numeric.Ceil( -1.5 )
print "Round( 3.14159, 2 ) ="; numeric.Round( 3.14159, 2 )

Output

     x  cint   int   fix  Round  HalfEven
   0.5     0     0     0     1       0
   1.5     2     1     1     2       2
   2.5     2     2     2     3       2
   2.6     3     2     2     3       3
  -0.5     0    -1    -0    -1       0
  -1.5    -2    -2    -1    -2      -2
  -2.5    -2    -3    -2    -3      -2
  -2.6    -3    -3    -2    -3      -3

dim as int32 = 2.5 gives 2
cast( int32, 3.5 ) gives 4

Floor( -1.5 ) =-2, Ceil( -1.5 ) =-1
Round( 3.14159, 2 ) = 3.14
FunctionRule2.5-2.52.6
cint, cast, assignmentnearest, half to even2-23
intdown, towards minus infinity2-32
fixtowards zero, dropping the fraction2-22
numeric.Roundnearest, half away from zero3-33
numeric.RoundHalfEvennearest, half to even2-23

Pitfall

Converting a float to an integer rounds; it does not truncate. cint( 2.6 ) is 3, and so is dim as int32 n = 2.6. And half goes to the even neighbour, so cint( 2.5 ) is 2 while cint( 3.5 ) is 4. When you want the fraction dropped, use fix; when you want halves rounded up, use numeric.Round.

int and fix give a float64 when they are given one, not an integer, so fix( -0.5 ) is negative zero and prints as -0. Assign the result to an integer variable when you need one.

numeric.Floor and numeric.Ceil round down and up, and numeric.Round( x, n ) rounds to n digits after the point. Numbers has the rest of std/numeric.

Activities

  1. Add 3.5 and -3.5 to samples and check each column.
  2. Cut 2.71828 to two places with fix( x * 100 ) / 100, and compare the result with numeric.Round( 2.71828, 2 ).

Expressions

A tiko program is a list of statements: declarations, assignments, calls to subs, if blocks, loops. Inside them are expressions, which compute a value of some type. Knowing which is which explains a few rules that otherwise look arbitrary.

'' A statement does something; an expression has a value and a type.
dim as int32 x = 5                   '' a statement, holding an expression
dim as int32 y = x * x + 2 * x + 1   '' an expression can be any size
print "y ="; y

'' A call to a function is an expression; a call to a sub is not.
function Square( byval n as int32 ) as int32
    return n * n
end function
sub Announce( byval n as int32 )
    print "announcing"; n
end sub

Announce( Square( 3 ) + 1 )

'' In a statement that starts with a variable, the first = assigns
'' and any later = compares. So this stores the comparison's value.
dim as int32 flag = 0
flag = y = 36
print "flag ="; flag
flag = ( y = 37 )
print "flag ="; flag

'' if is a statement. To pick a value inside an expression, use iif.
dim as ustring parity
if y mod 2 = 0 then
    parity = "even"
else
    parity = "odd"
end if
print "y is "; parity; " and x is "; iif( x mod 2 = 0, "even", "odd" )

'' A computation that needs several steps goes in a function, whose
'' call is then a single expression.
function Hypotenuse( byval a as float64, byval b as float64 ) as float64
    var squares = a * a + b * b
    return sqr( squares )
end function
print "hypotenuse of 3 and 4:"; Hypotenuse( 3, 4 )

Output

y = 36
announcing 10
flag =-1
flag = 0
y is even and x is odd
hypotenuse of 3 and 4: 5

An expression can be as large as you like, and it can contain calls to functions, because a function call has a value. A call to a sub has none, so it can only stand alone as a statement, as Announce( ... ) does; print Announce( 3 ) is error TKO0873: 'announce' is a sub and has no value.

Assignment is a statement, not an expression. In flag = y = 36 the first = assigns and the second compares, so flag receives -1, the value of the comparison y = 36. Writing the comparison in parentheses, as on the next line, makes that easier to read.

if is also a statement: it chooses what to do. To choose a value inside an expression, use iif. And when a value takes several steps to compute, put the steps in a function: the call is then a single expression, as Hypotenuse( 3, 4 ) is.

This section covers iif, the short-circuit operators andalso and orelse, and the precedence that decides how an expression without parentheses is grouped.

Activities

  1. Print y = 36 directly, then cbool( y = 36 ).
  2. Write a function Clamp( x, lo, hi ) and use it inside a print.

iif

iif( condition, a, b ) is the expression form of if: its value is a when the condition is true and b when it is false. It is the way to choose between two values in the middle of a print, an argument list or an initialiser.

'' iif( condition, a, b ) is a for a true condition and b otherwise.
'' Unlike if, it is an expression, so it can sit inside another one.
dim as int32 stock = 0
print "in stock: "; iif( stock > 0, "yes", "no" )

dim as int32 n = 7
print n; " is "; iif( n mod 2 = 0, "even", "odd" )

'' The two values must have compatible types. Numbers of different
'' types meet at the wider one.
var limit = iif( n > 5, 10, 2.5 )
limit += 0.25          '' limit is a float64, so it can hold this
print "limit ="; limit

'' Only the chosen value is evaluated, so the other may be one that
'' would fail.
function Report( byval tag as ustring ) as ustring
    print "  (evaluated "; tag; ")"
    return tag
end function

print iif( n > 5, Report( "large" ), Report( "small" ) )

dim as int32 divisor = 0
print "safe quotient:"; iif( divisor <> 0, 100 \ divisor, 0 )

'' iif nests, though a select case often reads better.
for score as int32 = 40 to 100 step 30
    print score; " -> "; _
          iif( score >= 90, "A", iif( score >= 60, "pass", "fail" ) )
next

Output

in stock: no
 7 is odd
limit = 10.25
  (evaluated large)
large
safe quotient: 0
 40 -> fail
 70 -> pass
 100 -> A

The two values must be of compatible types: two pieces of text, or two numbers. A text and a number are error TKO0655: the two arms of 'iif' have incompatible types. Numbers of different types meet at the wider one, so iif( n > 5, 10, 2.5 ) is a float64 even though the 10 was chosen, and limit can hold 10.25 afterwards.

Only the chosen value is evaluated. Report( "small" ) never runs, which the output shows, and 100 \ divisor is never computed while divisor is zero. That makes iif safe for a value that is only valid under its condition.

iif calls nest, as the grade line shows, but more than two levels read badly. A select case in a small function is usually clearer.

Activities

  1. Print "1 item" or "n items" correctly for n from 0 to 3.
  2. Rewrite the grade line as a function using select case.

Short-Circuit Logic

tiko has two sets of logical operators. andalso and orelse stop evaluating as soon as the answer is known. and, or, xor and not are bitwise operators that also work on conditions, and they always evaluate both sides.

#cmdline "-exx"

'' A condition with a side effect shows which operands run.
function Check( byval tag as ustring, byval result as boolean ) as boolean
    print " "; tag;
    return result
end function

'' andalso and orelse stop as soon as the answer is known.
print "false andalso ...:";
if Check( "left", false ) andalso Check( "right", true ) then print " yes";
print
print "true orelse ...:  ";
if Check( "left", true ) orelse Check( "right", false ) then print " yes";
print

'' and and or always evaluate both sides.
print "false and ...:    ";
if Check( "left", false ) and Check( "right", true ) then print " yes";
print
print "true or ...:      ";
if Check( "left", true ) or Check( "right", false ) then print " yes";
print

'' That makes andalso the safe way to guard a second test.
dim as int32 values( 1 to 3 ) = { 4, 0, 9 }
for i as int32 = 0 to 4
    if i >= lbound( values ) andalso i <= ubound( values ) _
            andalso values( i ) > 3 then
        print "values("; i; " ) is big"
    end if
next

'' On integers, and/or/xor/not work bit by bit. andalso and orelse
'' treat any non-zero value as true.
print "6 and 3 ="; 6 and 3; ", 6 andalso 3 = "; cbool( 6 andalso 3 )

Output

false andalso ...: left
true orelse ...:   left yes
false and ...:     left right
true or ...:       left right yes
values( 1 ) is big
values( 3 ) is big
6 and 3 = 2, 6 andalso 3 = true

Check prints its tag when it runs, so each line of output lists the operands that were evaluated. When the left side of andalso is false, the whole condition is false and the right side is skipped; when the left side of orelse is true, the whole condition is true and the right side is skipped. and and or run both, every time.

That difference is what makes a guard work. In the loop, the bounds tests come first, and values( i ) is only read once they have passed. The program is compiled with -exx, which checks every array index while the program runs, and the indexes 0 and 4 never reach the array. Written with and, the same condition would read values( 0 ) and stop the program with an out-of-bounds error.

OperatorEvaluates the right sideWorks on
andalsoonly if the left is trueconditions
orelseonly if the left is falseconditions
and, or, xoralwaysconditions and the bits of integers
not(one operand)conditions and the bits of integers

On integers the two sets give different answers: 6 and 3 is 2, the bits the two numbers share, while 6 andalso 3 is true, because both are non-zero. Use andalso and orelse for conditions, and keep and, or, xor and not for bits.

Activities

  1. Change the guard in the loop to use and and run the program.
  2. Write if s <> "" andalso left( s, 1 ) = "#" then for a ustring called s, and test it with an empty string.

Precedence

When an expression has no parentheses, precedence decides which operator applies first: 2 + 3 * 4 is 14 because * binds tighter than +. Most of tiko's order is the one you know from arithmetic, but a few places differ from other languages, and those are where bugs hide.

'' Each line shows an expression and its value, then the same
'' expression with the grouping tiko gives it written out.
sub Show( byval e1 as ustring, byval v1 as float64, _
          byval e2 as ustring, byval v2 as float64 )
    print left( e1 & space( 14 ), 14 ); "= "; left( v1 & space( 6 ), 6 ); _
          left( e2 & space( 18 ), 18 ); "= " & v2
end sub

'' ^ binds tighter than unary minus, and groups to the left.
Show( "-2 ^ 2", -2 ^ 2, "-(2 ^ 2)", -( 2 ^ 2 ) )
Show( "2 ^ 3 ^ 2", 2 ^ 3 ^ 2, "(2 ^ 3) ^ 2", ( 2 ^ 3 ) ^ 2 )

'' * and / come before \, which comes before mod.
Show( "7 \ 2 * 2", 7 \ 2 * 2, "7 \ (2 * 2)", 7 \ ( 2 * 2 ) )
Show( "9 mod 5 \ 2", 9 mod 5 \ 2, "9 mod (5 \ 2)", 9 mod ( 5 \ 2 ) )

'' Shifts bind tighter than + and -.
Show( "1 shl 2 + 3", 1 shl 2 + 3, "(1 shl 2) + 3", ( 1 shl 2 ) + 3 )

'' Comparisons bind tighter than and, or and not.
dim as int32 n = 6, a = 3, b = 0
Show( "n and 1 <> 0", n and 1 <> 0, "n and (1 <> 0)", n and ( 1 <> 0 ) )
Show( "not a = b", not a = b, "not (a = b)", not ( a = b ) )
print

'' & joins after the arithmetic, and comparisons come after &.
print """x"" & 1 + 2 = ""x3"" is "; cbool( "x" & 1 + 2 = "x3" )

'' and binds tighter than or.
print "true or false and false   = "; cbool( true or false and false )
print "true or (false and false) = "; cbool( true or ( false and false ) )

'' andalso and orelse share one level and group left to right.
print "true orelse false andalso false   = "; _
      cbool( true orelse false andalso false )
print "(true orelse false) andalso false = "; _
      cbool( ( true orelse false ) andalso false )

Output

-2 ^ 2        = -4    -(2 ^ 2)          = -4
2 ^ 3 ^ 2     = 64    (2 ^ 3) ^ 2       = 64
7 \ 2 * 2     = 1     7 \ (2 * 2)       = 1
9 mod 5 \ 2   = 1     9 mod (5 \ 2)     = 1
1 shl 2 + 3   = 7     (1 shl 2) + 3     = 7
n and 1 <> 0  = 6     n and (1 <> 0)    = 6
not a = b     = -1    not (a = b)       = -1

"x" & 1 + 2 = "x3" is true
true or false and false   = true
true or (false and false) = true
true orelse false andalso false   = false
(true orelse false) andalso false = false

From tightest to loosest:

LevelOperators
1^
2unary -
3* /
4\
5mod
6shl shr
7+ -
8&
9= <> < <= > >=
10not
11and
12or
13xor
14andalso orelse

Operators on one level group from left to right.

The places that catch people out, all shown in the example:

  • -2 ^ 2 is -4. ^ binds tighter than the minus sign, so it means -(2 ^ 2). And 2 ^ 3 ^ 2 groups to the left, giving 64, not 512.
  • \ and mod sit below *. 7 \ 2 * 2 is 7 \ 4, which is 1, not 6. 9 mod 5 \ 2 is 9 mod 2, which is 1, not 2.
  • Shifts bind tighter than +. 1 shl 2 + 3 shifts first, giving 7.
  • Comparisons bind tighter than and. n and 1 <> 0 compares first and gives 6, not the bit test it looks like. Write ( n and 1 ) <> 0.
  • not is looser than comparisons. not a = b means not ( a = b ).
  • andalso and orelse share a level. They group left to right, so true orelse false andalso false is ( true orelse false ) andalso false, which is false.

Pitfall

In most languages and binds tighter than or, and that is true here for and and or. It is not true for andalso and orelse. Put parentheses round every condition that mixes the two.

Activities

  1. Predict 2 + 3 * 4 ^ 2 \ 5 and check it.
  2. Print cbool( ( n and 1 ) <> 0 ) for n = 6 and n = 7, then the same without the parentheses, and explain the difference.

Flow of Control

A program runs its statements from top to bottom until something changes that order. tiko has two families of statement that do: ones that choose which block runs (if, select case) and ones that repeat a block (for, do ... loop, for each). This program uses one of each:

'' Choosing and repeating: the statements most programs are made of.

'' for counts; if chooses; continue for skips to the next pass.
dim as int32 total = 0
for n as int32 = 1 to 10
    if n mod 2 = 0 then continue for     '' leave out the even numbers
    total += n
next
print "sum of the odd numbers 1 to 10:"; total

'' do loop repeats while a condition holds.
dim as int32 side = 1
do while side * side < 50
    side += 1
loop
print "first square over 50:"; side * side

'' select case compares one value against several cases.
select case total
case is < 10
    print "the sum is small"
case 10 to 30
    print "the sum is medium"
case else
    print "the sum is large"
end select

Output

sum of the odd numbers 1 to 10: 25
first square over 50: 64
the sum is medium

Every block statement ends with a matching keyword: end if, end select, next for a for, loop for a do. Conditions need no parentheses, and the body of a block is not limited to one statement.

StatementUse it toPage
if / elseif / elsechoose between a few conditionsif/else
select casecompare one value against many casesselect case
for ... nextcount through a rangefor
do ... looprepeat while (or until) a condition holdsdo loop
exit / continueleave a loop, or skip to its next passexit and continue
for eachvisit every element of an arrayfor each
deferrun a statement when a block is leftdefer

Activities

  1. Change the first loop so that it adds up the even numbers instead.
  2. Make the do loop find the first cube over 500.

if/else

if runs a block when a condition is true. elseif adds more conditions, tested in order until one holds, and else catches everything left over. Only one branch ever runs.

'' if ... then ... end if, with any number of elseif branches.
dim as int32 temperature = 18

if temperature >= 25 then
    print "hot"
elseif temperature >= 15 then
    print "mild"
else
    print "cold"
end if

'' A whole if on one line needs no end if, and may have an else.
if temperature > 0 then print "above freezing"
if temperature > 30 then print "a heatwave" else print "no heatwave"

'' The condition is a number: zero is false, anything else is true.
dim as int32 items = 3
if items then print items; " items to pack"

'' andalso and orelse join conditions.
if temperature > 10 andalso temperature < 20 then print "jacket weather"

'' On a one-line if, everything after then belongs to the if --
'' including the statements after a colon.
if temperature > 30 then print "stay inside" : print "drink water"
print "done"

Output

mild
above freezing
no heatwave
 3 items to pack
jacket weather
done

A temperature of 18 fails the first test and passes the second, so the output starts with mild; the else branch is never reached.

When the statement after then is on the same line, the if is complete on that line and takes no end if. A one-line if may also have an else. Keep these short: they suit a guard or a single action, and a multi-line if reads better as soon as there is more to do.

The condition can be any number, and zero counts as false. That is why if items then works: items is 3. To join conditions, use andalso and orelse, which stop as soon as the answer is known; the Expressions section covers them.

Pitfall

On a one-line if, a colon does not end the if. Every statement after then on that line belongs to it:

if temperature > 30 then print "stay inside" : print "drink water"

Neither print runs, because the condition is false. If the second statement should always run, put it on a line of its own.

Activities

  1. Set temperature to 30, then to 31, and predict each output before running it.
  2. Rewrite the if / elseif chain so that it tests for cold first.

select case

When every branch asks a question about the same value, select case states the value once and lists the answers. Each case can name single values, a list, a range or an open comparison:

'' select case tests one value against each case in turn. The first
'' case that matches runs, and nothing falls through to the next.
sub Classify( byval n as int32 )
    print n; ": ";
    select case n
    case 0
        print "zero"
    case 1, 2, 3
        print "one, two or three"
    case 4 to 9
        print "a digit above three"
    case is < 0
        print "negative"
    case 10 to 99, 1000
        print "two digits, or exactly 1000"
    case else
        print "something else"
    end select
end sub

dim as int32 samples( 0 to 6 ) = { 0, 2, 7, -5, 42, 1000, 500 }
for each n in samples
    Classify( n )
next

'' A ustring works too, and a range compares alphabetically.
dim as ustring words( 0 to 2 ) = { "pear", "kiwi", "zebra" }
for each w in words
    select case w
    case "apple", "pear"
        print w; " is a fruit we stock"
    case "a" to "m"
        print w; " is in the first half of the alphabet"
    case else
        print w; " is in the second half"
    end select
next

Output

 0: zero
 2: one, two or three
 7: a digit above three
-5: negative
 42: two digits, or exactly 1000
 1000: two digits, or exactly 1000
 500: something else
pear is a fruit we stock
kiwi is in the first half of the alphabet
zebra is in the second half
CaseMatches
case 0one value
case 1, 2, 3any value in the list
case 4 to 9a range, both ends included
case is < 0any value the comparison accepts (is >, is <=, is <> ...)
case 10 to 99, 1000a list that mixes the forms
case elseanything no earlier case matched

The cases are tried from the top, and the first one that matches is the only one that runs. There is no fall-through into the next case and nothing to break out of. If no case matches and there is no case else, nothing runs.

The value can be a ustring. Then "a" to "m" is an alphabetical range: "kiwi" is inside it and "zebra" is not.

Note

select case as const builds a jump table, which is faster for a dense set of integer cases. It takes only constant values, so an open comparison inside one is refused: TKO0419: 'is' is not allowed in a 'select case as const'.

A common use of select case is to put true at the top and a condition in each case, so that the first true condition wins:

'' select case true runs the first case whose condition is true.
dim as boolean raining = false, windy = true

select case true
case raining
    print "take an umbrella"
case windy
    print "take a coat"
case else
    print "take nothing"
end select

Output

take a coat

Status in this version

This program prints take nothing: a select case whose value is the literal true (or a constant equal to it) matches no case, not even case true. A boolean variable at the top works as expected. Until this is fixed, write a chain of conditions as if / elseif.

Activities

  1. Add a case for the numbers 100 to 999 and check which samples move into it.
  2. Move case is < 0 to the top. Does any output change? Why not?

for

for ... next repeats a block a counted number of times. The counter starts at the first value, and the loop runs while it has not passed the second:

'' for counts from a start value to an end value, both included.
'' Declaring the counter in the for keeps it inside the loop.
for i as int32 = 1 to 5
    print i;
next
print

'' step sets the stride; a negative step counts down.
for i as int32 = 0 to 20 step 5
    print i;
next
print
for i as int32 = 10 to 1 step -3
    print i;
next
print

'' A range that is already finished runs zero times.
for i as int32 = 5 to 1
    print "never printed"
next
print "the 5 to 1 loop ran zero times"

'' The end value is worked out once, before the first pass.
dim as int32 last = 3
for i as int32 = 1 to last
    last = 10                    '' too late: the loop still stops at 3
    print i;
next
print

'' The counter may be a floating-point number.
for x as float64 = 0 to 1 step 0.25
    print x;
next
print

'' A counter declared before the loop is still there after it.
dim as int32 k
for k = 1 to 3
next
print "k after the loop:"; k

Output

 1 2 3 4 5
 0 5 10 15 20
 10 7 4 1
the 5 to 1 loop ran zero times
 1 2 3
 0 0.25 0.5 0.75 1
k after the loop: 4

Both ends of the range are included: 1 to 5 runs five times. step sets how much the counter changes after each pass, and a negative step counts down; 10 to 1 step -3 stops at 1 because the next value, -2, is past the end. A range that is already finished, such as 5 to 1 with the default step of 1, runs the body zero times.

The end value is worked out once, when the loop starts. Changing last inside the body does not stretch the loop.

for i as int32 = ... declares the counter as part of the loop, so it exists only inside it; using i after next is TKO0743: 'i' is not declared. That is usually what you want. A counter declared before the loop, like k, is an ordinary variable that keeps its value afterwards, and that value is the one that ended the loop: 4, not 3.

To leave a for early or skip a pass, see exit and continue. To walk the elements of an array without a counter, see for each.

Activities

  1. Print the multiples of 7 from 7 to 70 with one for and a step.
  2. Count down from 1 to 0 in steps of -0.1 and look closely at the last values printed.

do loop

do ... loop repeats a block for as long as a condition says so. The condition can be written at the top, after do, or at the bottom, after loop, and it can be a while or an until:

'' do ... loop repeats a block. The test can go at either end.
dim as int32 n

'' do while tests first, so the body may run zero times.
n = 0
do while n < 3
    print "while"; n
    n += 1
loop

'' do until is the same test with the sense reversed.
n = 0
do until n = 3
    print "until"; n
    n += 1
loop

'' With the test after loop, the body always runs at least once.
n = 10
do
    print "ran once, with n ="; n
    n += 1
loop while n < 3

n = 0
do
    n += 1
loop until n * n > 50
print "first n whose square is over 50:"; n

'' A do with no test at all runs until something leaves it.
n = 0
do
    n += 1
    if n = 4 then exit do
loop
print "left the loop at"; n

Output

while 0
while 1
while 2
until 0
until 1
until 2
ran once, with n = 10
first n whose square is over 50: 8
left the loop at 4
FormTestsBody runs
do while c ... loopbefore each pass; continues while c is truezero or more times
do until c ... loopbefore each pass; continues until c is truezero or more times
do ... loop while cafter each passat least once
do ... loop until cafter each passat least once
do ... loopneveruntil exit do

The third block shows why the position matters. n starts at 10, so n < 3 is false from the beginning, yet the body runs once because the test comes after it. Put the test at the top when the body might not need to run at all, and at the bottom when the body produces the value the test looks at, as the square search does.

A do with no condition runs until something inside it leaves: exit do here, or exit sub, return, or an exit for aimed at an enclosing loop. exit and continue covers those.

Activities

  1. Rewrite the do until n = 3 loop as a do while loop with the same output.
  2. Change loop until n * n > 50 to loop while n * n <= 50. Is the result the same?

exit and continue

exit leaves a block straight away; continue abandons the current pass of a loop and starts the next one. Both name the kind of block they act on: exit for, exit do, continue for, continue do.

'' continue for skips to the next pass; exit for leaves the loop.
for i as int32 = 1 to 10
    if i mod 3 = 0 then continue for
    if i > 7 then exit for
    print i;
next
print

'' Naming the loop kind twice leaves two loops at once.
dim as int32 grid( 1 to 3, 1 to 3 ) = _
    { { 5, 20, 31 }, { 12, 64, 7 }, { 99, 1, 2 } }
dim as int32 foundRow = 0, foundCol = 0
for r as int32 = 1 to 3
    for c as int32 = 1 to 3
        if grid( r, c ) > 50 then
            foundRow = r
            foundCol = c
            exit for, for
        end if
    next
next
print "first value over 50: row"; foundRow; ", column"; foundCol

'' continue for, for goes straight to the next pass of the outer loop.
'' Each row is printed up to its first zero.
dim as int32 rows( 1 to 3, 1 to 4 ) = _
    { { 1, 2, 0, 4 }, { 5, 0, 7, 8 }, { 9, 10, 11, 12 } }
for r as int32 = 1 to 3
    print "row"; r; ":";
    for c as int32 = 1 to 4
        if rows( r, c ) = 0 then
            print " (stopped at a zero)"
            continue for, for
        end if
        print rows( r, c );
    next
    print
next

'' exit names the kind of block, so it can reach past a different kind:
'' this exit for leaves the for from inside a do.
for i as int32 = 1 to 3
    do
        if i = 2 then exit for
        print "pass"; i
        exit do
    loop
next
print "done"

Output

 1 2 4 5 7
first value over 50: row 2, column 2
row 1: 1 2 (stopped at a zero)
row 2: 5 (stopped at a zero)
row 3: 9 10 11 12
pass 1
done

In the first loop, continue for skips 3 and 6 without printing them, and exit for ends the loop when it reaches 8. A continue for still moves the counter on, exactly as reaching next would.

Leaving several loops. A plain exit for leaves only the innermost for. Repeating the kind, exit for, for, leaves two of them, which is how the grid search stops at the first value over 50 without a flag variable. continue for, for works the same way: it gives up on the rest of the inner loop and the rest of the outer loop's current pass, so each row stops at its first zero. exit do, do and continue do, do exist too.

Different kinds. Because the statement names a kind, it finds the nearest enclosing block of that kind and passes through any other blocks on the way. The last loop leaves the for from inside a do.

The same word leaves a procedure: exit sub and exit function, covered in Procedures.

Activities

  1. Change the grid search to find the first value under 5 instead.
  2. In the row printer, replace continue for, for with exit for. What else do you have to change to keep the output the same?

for each

Most loops over an array do not need the index; they need each element. for each hands you the elements in order and keeps the bounds out of sight:

'' for each visits every element of an array, in index order.
dim as ustring planets( 1 to 4 ) = { "Mercury", "Venus", "Earth", "Mars" }
for each p as ustring in planets
    print p
next

'' Leave out the type and the loop variable takes the element type.
dim as int32 primes( 0 to 4 ) = { 2, 3, 5, 7, 11 }
dim as int32 total = 0
for each n in primes
    total += n
next
print "sum of the primes:"; total

'' exit for and continue for work as they do in a counting loop.
for each n in primes
    if n = 2 then continue for
    if n > 7 then exit for
    print "odd prime"; n
next

'' A dynamic array with nothing in it runs the body zero times.
dim as int32 nothing_( )
for each n in nothing_
    print "never printed"
next
print "the empty array had no elements"

'' A ustring is walked by position instead, with for and mid.
dim as ustring word = "loop"
for i as int32 = 1 to len( word )
    print "["; mid( word, i, 1 ); "]";
next
print

Output

Mercury
Venus
Earth
Mars
sum of the primes: 28
odd prime 3
odd prime 5
odd prime 7
the empty array had no elements
[l][o][o][p]

for each p as ustring in planets declares p, which exists only inside the loop. With the type left out, as in for each n in primes, n takes the element type of the array. The loop works the same whatever the array's bounds, and an empty dynamic array runs the body zero times.

exit for and continue for behave as they do in a counting for.

The loop variable is a copy of the element. Writing to it does not change the array; for each over arrays shows for each byref, which does. for each also walks the containers of the standard library, which the Containers section covers.

A ustring cannot be walked this way:

'' for each does not walk a ustring. Does not compile.
dim as ustring word = "loop"
for each letter in word
    print letter
next
foreachtext.tiko(3,1): error TKO0688: 'for each' cannot walk a ustring; index it with 'for' and 'mid' instead

The last loop in the first example does what the message suggests: it counts from 1 to len( word ) and takes one character at a time with mid. The Strings section has more on working with text.

Activities

  1. Print the planets in a single line, separated by commas.
  2. Find the largest prime in primes with for each, without using an index.

defer

Code that acquires something has to release it on every path out, and early exits make that easy to get wrong. defer takes one statement and runs it when the enclosing block is left, however it is left. You write the release next to the acquire, and no later exit can skip it:

'' defer runs a statement when the enclosing block is left.
sub Copy( byval fail as boolean )
    print "  open the source"
    defer print "  close the source"

    print "  open the target"
    defer print "  close the target"

    if fail then
        print "  the write failed"
        exit sub                       '' both defers still run
    end if
    print "  copied"
end sub

print "a copy that works:"
Copy( false )
print "a copy that fails:"
Copy( true )

'' A function's result is settled before its defers run.
function Ticket( ) as int32
    dim as int32 n = 1
    defer n = 100                      '' too late to change the result
    defer print "  leaving Ticket, n ="; n
    return n
end function

dim as int32 t = Ticket( )
print "Ticket( ) returned"; t

Output

a copy that works:
  open the source
  open the target
  copied
  close the target
  close the source
a copy that fails:
  open the source
  open the target
  the write failed
  close the target
  close the source
  leaving Ticket, n = 1
Ticket( ) returned 1

Deferred statements run in reverse order: the target is closed before the source, the opposite of the order they were opened. They run on the normal path and on the early exit sub alike.

In Ticket the result is settled by return n before the defers run, so the function returns 1 even though a defer then sets n to 100.

A defer belongs to its block

The block a defer belongs to is the nearest enclosing one, and an if, a loop body and a scope are all blocks:

'' A defer belongs to the nearest enclosing block, not the procedure.
sub InAnIf( )
    print "  start"
    if true then
        print "    acquired"
        defer print "    released"     '' runs at end if
        print "    used"
    end if
    print "  after the if: already released"
end sub

'' A loop body is a block, so a defer there runs at the end of every
'' pass -- including a pass cut short by continue or exit.
sub InALoop( )
    for i as int32 = 1 to 5
        defer print "    end of pass"; i
        if i = 2 then continue for
        if i = 3 then exit for
        print "  pass"; i
    next
end sub

'' The deferred statement reads its variables when it runs.
sub LateRead( )
    dim as int32 v = 7
    defer print "  v when the defer ran:"; v
    v = 9
end sub

print "a defer inside an if:"
InAnIf( )
print "a defer inside a loop:"
InALoop( )
print "what a defer reads:"
LateRead( )

Output

a defer inside an if:
  start
    acquired
    used
    released
  after the if: already released
a defer inside a loop:
  pass 1
    end of pass 1
    end of pass 2
    end of pass 3
what a defer reads:
  v when the defer ran: 9

Pitfall

A defer inside an if runs at end if, while the procedure carries on. Writing acquire inside an if, then defer the release releases the thing at once and leaves the rest of the procedure using something already closed. Test for the failure, leave early, and put the acquire and its defer in the procedure's own body.

In a loop body the same rule is what you want: the deferred statement runs at the end of every pass, including the pass that continue for cut short and the one that exit for ended.

The deferred statement is not evaluated when the defer line is reached. It runs as written when the block is left, and reads its variables then, so LateRead prints 9. Copy a value into a local first if you need it as it was.

A defer directly at the top level of a program has no block to leave and is refused with TKO0814: 'defer' is not allowed at module level; put it in a procedure or a block. The Memory and Resources section shows defer alongside destructors.

Activities

  1. Add a third resource to Copy and check the order the three are closed in.
  2. Move the defer in InAnIf above the if and predict the new output.

Procedures

A procedure gives a piece of work a name. tiko has two kinds: a sub does something and returns nothing, and a function computes a value and hands it back.

'' A sub does something. A function answers something.

sub Greet( byref who as ustring )
    print "Hello, " & who & "!"
end sub

function Square( byval n as int32 ) as int32
    return n * n
end function

'' function = sets the result and carries on; return leaves at once.
function Sign( byval n as int32 ) as ustring
    function = "positive"
    if n < 0 then return "negative"
    if n = 0 then function = "zero"
end function

Greet( "world" )
print "Square( 12 ) ="; Square( 12 )
print Sign( -4 ) & " " & Sign( 0 ) & " " & Sign( 9 )

'' A function's answer may be ignored; the call still happens.
function Report( ) as int32
    print "Report ran"
    return 1
end function

Report( )

Output

Hello, world!
Square( 12 ) = 144
negative zero positive
Report ran

Parameters go in parentheses after the name, each with its type and a word saying how it is passed (see Parameters). A function names its result type after the parameter list, as in as int32.

A function has two ways to set its result:

  • return value sets the result and leaves the function at once.
  • function = value sets the result and carries on. Sign uses it to set a default answer first and change it further down.

A function that reaches end function without setting a result returns zero (or an empty string), with no warning, so make sure every path sets one.

A function can also be called as a statement, as Report( ) is: the call runs and the result is thrown away.

Procedures are written at the top level of a file, and statements at the top level run in order around them. A procedure sees only its parameters and its own variables, not the variables of the code that calls it.

Activities

  1. Write a function Cube and print the cubes of 1 to 5.
  2. Remove the if n = 0 line from Sign and predict what Sign( 0 ) returns.

Parameters

Each parameter is passed one of two ways. byval gives the procedure its own copy, so changes stay inside. byref gives it the caller's variable, so changes are seen by the caller. That is also how a procedure reports more than one result.

'' byval passes a copy; byref passes the caller's own variable.
sub AddTax( byval price as float64, byref total as float64 )
    price *= 1.25            '' changes the copy only
    total += price           '' changes the caller's variable
end sub

dim as float64 price = 8, total = 0
AddTax( price, total )
AddTax( price, total )
print "price"; price; ", total"; total

'' With no keyword, what you get depends on the parameter's type.
type Point
    as int32 x, y
end type

sub NoKeywordInt( n as int32 )
    n = 99
end sub

sub NoKeywordText( s as ustring )
    s = "changed"
end sub

sub NoKeywordPoint( p as Point )
    p.x = 99
end sub

dim as int32 n = 1
dim as ustring s = "kept"
dim as Point p = ( 1, 2 )
NoKeywordInt( n )
NoKeywordText( s )
NoKeywordPoint( p )
print "int32:"; n; "  ustring: " & s & "  Point.x:"; p.x

'' byref as const passes without copying and refuses writes.
function Initials( byref fullName as const ustring ) as ustring
    dim as int32 gap = instr( fullName, " " )
    return left( fullName, 1 ) & mid( fullName, gap + 1, 1 )
end function

print "initials: " & Initials( "Ada Lovelace" )

Output

price 8, total 20
int32: 1  ustring: changed  Point.x: 99
initials: AL

AddTax scales its copy of price, so the caller's price is still 8, and adds into the caller's total twice, which ends at 20.

A parameter written with neither keyword gets a default, and the default depends on the type:

Parameter typePassed, when no keyword is written
numbers, boolean, enums, pointersbyval: a copy
ustringbyref: the caller's variable
your own types and tuplesbyref: the caller's variable
arraysalways by reference (see Passing arrays)

Pitfall

The three NoKeyword subs are written the same way and behave differently: the int32 is copied, but the ustring and the Point are the caller's own, so the caller sees changed and 99. A procedure that uses a ustring parameter as scratch space quietly changes its caller's text. Write byval or byref on every parameter, as the other examples in this section do.

A byref parameter still accepts a literal or an expression in place of a variable. tiko passes a temporary that holds the value, and the procedure's writes go to the temporary.

byref ... as const passes the caller's variable without copying it, and refuses any write to it. It suits a large value or a ustring that the procedure only reads, like Initials. A write is a compile error:

'' A byref as const parameter cannot be written. Does not compile.
sub Shout( byref text as const ustring )
    text = ucase( text )
    print text
end sub

Shout( "hello" )
constparam.tiko(3,5): error TKO0617: cannot modify 'text': it is a constant

Activities

  1. Write sub Swap2( byref a as int32, byref b as int32 ) that exchanges two values, then change one parameter to byval and watch it stop working.
  2. Change NoKeywordText to take byval s as ustring and check the output.

Optional parameters

A parameter with = value after its type has a default. A call may leave the argument out, and the parameter then takes the default:

#import "std/strings"

'' A parameter with "= value" may be left out of the call.
sub Rule( byval length as int32 = 10, byref mark as ustring = "-", _
          byref title as ustring = "" )
    print title & strings.Repeat( length, mark )
end sub

Rule( )                       '' every default
Rule( 4 )                     '' length given
Rule( 6, "=" )                '' length and mark
Rule( , "*" )                 '' an empty slot keeps length's default
Rule( 3, , "total " )         '' and so does one in the middle

'' A default can be an expression; it is worked out at each call.
function Scaled( byval value as float64, _
                 byval factor as float64 = 1 / 4 ) as float64
    return value * factor
end function

print Scaled( 10 ); Scaled( 10, 2 )

Output

----------
----
======
**********
total ---
 2.5 20

Arguments are matched by position. Leaving out the last ones is the usual case: Rule( 4 ) supplies length and takes the defaults for mark and title. To skip an argument that has others after it, leave its slot empty, with only the comma: Rule( , "*" ) and Rule( 3, , "total " ).

An empty slot is easy to miss when reading a call. One is fine; when a call needs several, the parameters are probably in the wrong order, and the ones callers change most often should come first.

A default does not have to be a literal. It is an expression, worked out at each call that leaves the argument out; 1 / 4 here is the simplest case.

Activities

  1. Give Scaled a default value as well and call it with no arguments.
  2. Call Rule so that it prints == heading followed by eight = signs.

Overloading

Several procedures may share one name when their parameter lists differ. Mark each one overload, and at each call the compiler picks the version whose parameters fit the arguments best:

'' overload lets one name have several parameter lists. The compiler
'' picks the version whose parameters fit the arguments best.
sub Describe overload ( byval n as int32 )
    print "a whole number:"; n
end sub

sub Describe overload ( byval x as float64 )
    print "a fraction:"; x
end sub

sub Describe overload ( byref text as ustring )
    print "some text: " & text
end sub

sub Describe overload ( byval x as int32, byval y as int32 )
    print "a pair:"; x; ","; y
end sub

Describe( 42 )
Describe( 2.5 )
Describe( "tiko" )
Describe( 3, 4 )

'' An int16 has no version of its own; it converts to int32.
dim as int16 small = 7
Describe( small )

Output

a whole number: 42
a fraction: 2.5
some text: tiko
a pair: 3, 4
a whole number: 7

The versions can differ in the number of parameters, as the two-argument Describe does, or in their types. An argument that matches no version exactly is converted: small is an int16, and the int32 version is the cheapest conversion, so that one runs.

overload is what makes the shared name legal. Two procedures of the same name with overload on neither are refused with TKO0600: 'describe' is already declared in this scope, the same error as any duplicate name, because most repeated names are mistakes.

When two versions fit equally well, the compiler refuses to guess:

'' Two versions that fit equally well. Does not compile.
sub Show overload ( byval a as int32, byval b as float64 )
    print "int32, float64"
end sub

sub Show overload ( byval a as float64, byval b as int32 )
    print "float64, int32"
end sub

Show( 1, 2 )
ambiguous.tiko(10,5): error TKO0660: the call to 'show' matches more than one overload

Show( 1, 2 ) needs one int32 converted to float64 whichever version is chosen. Passing 1, 2.0 would pick the first version.

Activities

  1. Add a Describe for boolean and call it with true.
  2. Change one argument in ambiguous.tiko so that the call picks the second version.

Recursion

A procedure may call itself. Each call gets its own parameters and locals, so the calls in progress do not disturb one another. A recursive procedure needs a base case that answers without calling itself, and every other call must move towards it:

'' A procedure may call itself. Each call has its own parameters.
function Factorial( byval n as int32 ) as int64
    if n <= 1 then return 1
    return n * Factorial( n - 1 )
end function

for n as int32 = 1 to 5
    print n; "! ="; Factorial( n )
next

'' Moving towards the base case one step at a time.
sub Countdown( byval n as int32 )
    if n = 0 then
        print " liftoff"
        exit sub
    end if
    print n;
    Countdown( n - 1 )
end sub

Countdown( 5 )

'' Two procedures that call each other: declare the second one first.
declare function IsOdd( byval n as int32 ) as boolean

function IsEven( byval n as int32 ) as boolean
    if n = 0 then return true
    return IsOdd( n - 1 )
end function

function IsOdd( byval n as int32 ) as boolean
    if n = 0 then return false
    return IsEven( n - 1 )
end function

print "7 is even: "; IsEven( 7 ); ", 10 is even: "; IsEven( 10 )

Output

 1! = 1
 2! = 2
 3! = 6
 4! = 24
 5! = 120
 5 4 3 2 1 liftoff
7 is even: false, 10 is even: true

Factorial( 5 ) calls Factorial( 4 ), and so on down to Factorial( 1 ), which returns 1; the multiplications then happen as the calls return. Countdown prints on the way down and stops at 0.

IsEven and IsOdd call each other. The compiler reads a file from top to bottom, so when it reaches IsEven it has not yet seen IsOdd. The declare line states IsOdd's signature in advance; without it the call is TKO0743: 'isodd' is not declared. The body can then come later.

Every call uses some stack, and a recursion that is very deep, or that never reaches its base case, runs out of it. For a simple count, a loop does the same work with none of that cost.

Activities

  1. Write a recursive Fib( n ) and print the first ten Fibonacci numbers.
  2. Change Countdown to print liftoff first and the numbers after it, by moving one line.

Variadic procedures

Some procedures make sense with any number of arguments: a total, a join, a maximum. A typed variadic parameter takes them all. Write ... after its type, and the caller writes a plain list:

'' "..." after the type makes the last parameter take any number of
'' arguments. Inside, it is an ordinary array.
function Total( values( ) as const int32 ... ) as int64
    dim as int64 sum = 0
    for each v in values
        sum += v
    next
    return sum
end function

print Total( 1, 2, 3 ); Total( 10 ); Total( )

'' Fixed parameters come first; the variadic one is always last.
function Join( byref sep as const ustring, _
               parts( ) as const ustring ... ) as ustring
    dim as ustring result
    for i as int32 = lbound( parts ) to ubound( parts )
        if i > lbound( parts ) then result &= sep
        result &= parts( i )
    next
    return result
end function

print Join( ", ", "red", "green", "blue" )
print "[" & Join( ", " ) & "]"

'' An array of the element type can be passed as the whole list.
dim as int32 week( 0 to 6 ) = { 3, 5, 2, 8, 6, 4, 7 }
print "week total:"; Total( week( ) )

'' Every argument is converted to the element type, as for any int32.
dim as int16 small = 4
print Total( small, 5.0, 6 )

Output

 6 10 0
red, green, blue
[]
week total: 35
 15

Inside the procedure, values is an ordinary array: for each walks it, and lbound and ubound give its bounds. With no arguments the array is empty, so Total( ) is 0 and Join( ", " ) is an empty string without any special case in the code.

The variadic parameter must be the last one; anything else is TKO0973: a typed variadic parameter ('...') must be the last parameter. Fixed parameters, like Join's separator, come before it.

Each argument is converted to the element type exactly as it would be for an ordinary parameter, so small (an int16) and 5.0 are accepted where int32 is wanted. An argument that does not convert is a compile error at that argument.

An array of the element type can stand for the whole list, as week( ) does. It is passed as it is, without copying.

Pitfall

Declare the parameter as const unless the procedure is meant to write to it. A list of arguments is packed into a temporary array, but an array passed as the whole list is the caller's own, so a procedure that writes to its variadic parameter changes the caller's array in one case and nothing in the other.

Status in this version

as const on a variadic parameter is not yet enforced: an assignment to one of its elements compiles, and, when a whole array was passed, changes the caller's array. Passing arrays has the same issue for ordinary array parameters.

Activities

  1. Write function Largest( values( ) as const int32 ... ) as int32. Decide what it should return when called with no arguments.
  2. Call Join with a separator of " and " and four words.

Procedure pointers

A procedure has an address, and a variable can hold it. That lets a program decide which procedure to call while it runs, and lets one procedure take another as a parameter:

'' A procedure has an address, and a variable can hold it.
function Twice( byval x as float64 ) as float64
    return x * 2
end function

function Square( byval x as float64 ) as float64
    return x * x
end function

'' The type of such a variable is the procedure's signature, unnamed.
dim as function( byval as float64 ) as float64 op = @Twice
print "op( 5 ) ="; op( 5 )
op = procptr( Square )             '' procptr( ) is the same as @
print "op( 5 ) ="; op( 5 )

'' A type alias gives the signature a name.
type Operation as function( byval x as float64 ) as float64

'' A parameter of that type takes any procedure of that shape.
sub Tabulate( byref title as ustring, byval f as Operation )
    print title & ":";
    for i as int32 = 1 to 4
        print f( i );
    next
    print
end sub

Tabulate( "twice", @Twice )
Tabulate( "square", @Square )

'' A function can return one, chosen while the program runs.
function Pick( byref opName as ustring ) as Operation
    if opName = "square" then return @Square
    return @Twice
end function

var chosen = Pick( "square" )
print "chosen( 7 ) ="; chosen( 7 )

'' A sub's address works the same way.
sub Announce( byref text as ustring )
    print "** " & text & " **"
end sub

dim as sub( byref as ustring ) callback = @Announce
callback( "called through a pointer" )

Output

op( 5 ) = 10
op( 5 ) = 25
twice: 2 4 6 8
square: 1 4 9 16
chosen( 7 ) = 49
** called through a pointer **

The type of a procedure pointer is the procedure's signature without its name: function( byval as float64 ) as float64 holds any function that takes one float64 by value and returns a float64. Parameter names are optional in the type. A sub pointer is written the same way with sub, as callback is.

@Twice and procptr( Twice ) both take the address. For an overloaded name, procptr also takes the signature you mean, as in procptr( Describe, sub( byval as float64 ) ).

A pointer is called with ordinary call syntax: op( 5 ), f( i ). A type alias such as Operation gives the signature a name, which keeps parameter lists like Tabulate's readable and lets a function such as Pick return one.

A procedure pointer is one address and nothing more, so it cannot carry any data along with the code. Closures can.

Activities

  1. Add a Negate function and pass it to Tabulate without changing Tabulate.
  2. Make Pick return @Negate for the name "negate" and call the result.

Lambdas

A small procedure that is used in one place reads better written in that place. A lambda is a procedure without a name, written as an expression: function( parameters ) as Type ... end function, or sub( parameters ) ... end sub when nothing is returned.

'' A lambda is a procedure written as an expression, where it is used.
type Transform as function( byval x as int32 ) as int32

sub ApplyAll( values( ) as int32, byval f as Transform )
    for i as int32 = lbound( values ) to ubound( values )
        values( i ) = f( values( i ) )
    next
end sub

'' Stored in a variable of a matching procedure type...
dim as Transform double_ = function( byval x as int32 ) as int32
    return x * 2
end function
print double_( 21 )

'' ...or in a var, which takes its type from the lambda. A short body
'' fits on one line, with colons between the statements.
var cube = function( byval x as int32 ) as int32 : return x * x * x : _
    end function
print cube( 3 )

'' ...or passed straight to the procedure that wants it.
dim as int32 readings( 0 to 3 ) = { 1, 2, 3, 4 }
ApplyAll( readings( ), function( byval x as int32 ) as int32
    return x + 100
end function )
for each r in readings
    print r;
next
print

'' A lambda with nothing to return is a sub.
var greet = sub( byref who as ustring )
    print "hello, " & who
end sub
greet( "lambda" )

Output

 42
 27
 101 102 103 104
hello, lambda

A lambda can go wherever an expression can:

  • into a variable of a matching procedure pointer type, like double_;
  • into a var, which takes its type from the lambda, like cube and greet;
  • straight into an argument, like the one ApplyAll receives.

The body is ordinary statements. Because : separates statements, a short body fits on one line, as cube's does. A lambda is called like any other procedure: cube( 3 ).

A lambda that uses only its own parameters and locals is compiled to an ordinary procedure, and its value is that procedure's address. That is why it can be stored in a Transform variable and passed where a Transform is wanted. A lambda that uses variables from the code around it is a closure, which is a different kind of value.

Activities

  1. Pass ApplyAll a lambda that replaces every value with its square.
  2. Store a lambda that returns the larger of two int32 values in a var, and call it.

Closures

A lambda that needs a variable from the code around it lists the variable in square brackets after sub or function. That captures it, and the lambda becomes a closure: the code plus the captured variables. Every capture states how it is held:

'' A lambda that uses variables from around it lists them in square
'' brackets, and says for each one how it is held.
dim as int32 calls = 0
dim as int32 step_ = 5

'' byref: the lambda works on the caller's variable itself.
var countCall = sub[ byref calls ]( )
    calls += 1
end sub
countCall( )
countCall( )
print "calls:"; calls

'' byval: the lambda keeps its own copy, taken when the lambda is made.
var addStep = function[ byval step_ ]( byval x as int32 ) as int32
    return x + step_
end function
var addStepNow = function[ byref step_ ]( byval x as int32 ) as int32
    return x + step_
end function

step_ = 1000
print "byval copy:"; addStep( 1 )        '' still adds 5
print "byref view:"; addStepNow( 1 )     '' adds the current 1000

'' Modes can be mixed in one list. step_ is 1000 by now, and that is
'' the value this lambda copies.
dim as int32 total = 0
var addScaled = sub[ byref total, byval step_ ]( byval x as int32 )
    total += x * step_
end sub
addScaled( 2 )
addScaled( 3 )
print "total:"; total

Output

calls: 2
byval copy: 6
byref view: 1001
total: 5000
CaptureThe closure holdsWrites inside the lambda
byref callsthe caller's variablereach the caller
byval step_its own copy, taken when the lambda expression runsstay in the copy

countCall increments the caller's calls. addStep copied step_ when its var line ran, while it was 5, so it still adds 5 after step_ becomes 1000; addStepNow holds the variable and sees 1000. addScaled mixes the two modes and copies step_ at the point it is made, by which time it is 1000.

Pitfall

A byval capture is a snapshot taken when the lambda is made, not when it is called. A closure written early and called late, such as a handler or a comparer kept for later, sees the value from when it was written. Capture byref when the lambda must see the variable as it is at each call.

There is no default mode. A capture without byval or byref is refused:

'' Every capture must say byval or byref. Does not compile.
dim as int32 count = 0
var bump = sub[ count ]( )
    count += 1
end sub
bump( )
capturemode.tiko(3,17): error TKO0413: every lambda capture needs an explicit 'byval' or 'byref'

A closure is not a procedure pointer, because a pointer has no room for the captured variables. Assigning one to a procedure-pointer type is TKO0738: no conversion between these two types. To pass a closure to a procedure, the parameter is generic, as Higher-order procedures shows.

Activities

  1. Write a closure that appends each word it is given to a ustring captured byref, and call it three times.
  2. Change addStep to capture byref and predict its output before running.

Higher-order procedures

A procedure that takes another procedure as an argument is higher-order. It decides when the work happens; the caller decides what the work is. To accept plain lambdas and capturing closures alike, the parameter is a generic type:

#import "std/containers"

'' A procedure that takes another procedure is "higher-order". Taking it
'' as a generic F accepts plain lambdas and capturing closures alike.
sub Repeat( of F )( byval times as int32, byref action as F )
    for i as int32 = 1 to times
        action( i )
    next
end sub

dim as int32 sumOfSquares = 0
Repeat( 4, sub[ byref sumOfSquares ]( byval i as int32 )
    sumOfSquares += i * i
end sub )
print "1 + 4 + 9 + 16 ="; sumOfSquares

'' The standard containers take callables the same way.
dim scores as containers.Array( of int32 ) = { 72, 95, 41, 88, 60 }

'' SortBy asks the comparer one question: does a come before b?
containers.SortBy( scores, function( byref a as int32, _
                                     byref b as int32 ) as boolean
    return a > b
end function )

'' ForEach hands each element to an action; this one counts passes.
dim as int32 passMark = 60, passed = 0
containers.ForEach( scores, _
    sub[ byval passMark, byref passed ]( byref s as int32 )
        print s;
        if s >= passMark then passed += 1
    end sub )
print
print passed; " of"; scores.Count( ); " scores passed"

Output

1 + 4 + 9 + 16 = 30
 95 88 72 60 41
 4 of 5 scores passed

Repeat( of F ) takes its action as byref action as F. F is whatever type the argument has: here, the type of a closure that captures sumOfSquares. Inside, action( i ) calls it like any procedure. Generics covers of in full.

The standard library's containers take callables the same way:

  • containers.SortBy takes a comparer that answers one question, "does a come before b?". Answering a > b sorts from highest to lowest.
  • containers.ForEach hands each element to an action. This one captures passMark by value, because it only reads it, and passed by reference, because it updates it.

A parameter typed as a procedure pointer, like Tabulate's on the procedure pointers page, takes only lambdas that capture nothing; a closure passed to one is TKO0698: this argument does not convert to the parameter's type. The generic form takes both. Containers lists the other procedures that take a callable.

Activities

  1. Write function CountIf( of F )( values( ) as int32, byref test as F ) as int32 and use it with a lambda that tests for even numbers.
  2. Sort scores from lowest to highest, then print only the three best.

Static locals

An ordinary local variable is created fresh on every call. A static local is created once and keeps its value from one call to the next, which gives a procedure a memory without a variable outside it:

'' A static local is created once and keeps its value between calls.
function NextTicket( ) as int32
    static as int32 counter = 100      '' set once, before the first call
    counter += 1
    return counter
end function

print NextTicket( ); NextTicket( ); NextTicket( )

'' An ordinary local starts again on every call.
function Fresh( ) as int32
    dim as int32 counter = 100
    counter += 1
    return counter
end function

print Fresh( ); Fresh( ); Fresh( )

'' A static array works too: this one remembers the last three values.
sub Remember( byval value as int32 )
    static as int32 recent( 0 to 2 )
    static as int32 slot = 0
    recent( slot ) = value
    slot = ( slot + 1 ) mod 3
    print "recent:"; recent( 0 ); recent( 1 ); recent( 2 )
end sub

Remember( 7 )
Remember( 8 )
Remember( 9 )
Remember( 10 )

Output

 101 102 103
 101 101 101
recent: 7 0 0
recent: 7 8 0
recent: 7 8 9
recent: 10 8 9

counter in NextTicket is set to 100 once, before the first call, and each call moves it on. Fresh declares the same variable with dim, so every call starts again from 100.

An array can be static too. Remember keeps a ring of the last three values and a slot that wraps round, so the fourth value overwrites the first.

A static belongs to the procedure, not to a call, so every call shares it: the calls of a recursion, and calls from two threads at once. Reset it yourself when its job is done, and give a procedure that several threads call no static it writes to; Concurrency has the tools for shared state.

Activities

  1. Make NextTicket start at 1 and print five tickets.
  2. Change Remember to keep the last five values.

Returning tuples

A function that has two answers, such as a quotient and a remainder, can return both at once as a tuple: a group of values written in parentheses. The result type lists the element types, and return lists the values:

'' A function can return several values at once as a tuple.
function Divide( byval a as int32, byval b as int32 ) as ( int32, int32 )
    return ( a \ b, a mod b )
end function

'' var ( a, b ) = takes the tuple apart into new variables.
var ( quotient, remainder ) = Divide( 17, 5 )
print "17 / 5 ="; quotient; " remainder"; remainder

'' The elements can have names, which the caller can use.
function MinMax( values( ) as int32 ) as ( lo as int32, hi as int32 )
    dim as int32 lo = values( lbound( values ) ), hi = lo
    for each v in values
        if v < lo then lo = v
        if v > hi then hi = v
    next
    return ( lo, hi )
end function

dim as int32 readings( 0 to 4 ) = { 14, 9, 21, 17, 3 }
var range = MinMax( readings( ) )
print "lowest"; range.lo; ", highest"; range.hi

'' A tuple can also be assigned to variables that already exist.
dim as int32 q, r
( q, r ) = Divide( 23, 4 )
print "23 / 4 ="; q; " remainder"; r

Output

17 / 5 = 3 remainder 2
lowest 3, highest 21
23 / 4 = 5 remainder 3

var ( quotient, remainder ) = Divide( 17, 5 ) declares two variables and fills them from the tuple in order. To fill variables that already exist, leave out var, as the last call does with ( q, r ).

Elements can be named in the result type: ( lo as int32, hi as int32 ). A caller that keeps the whole tuple in one variable, like range, then reads range.lo and range.hi. The names are for reading only; the tuple is the same type as an unnamed ( int32, int32 ).

A tuple is the light-weight choice when the values belong together only for the moment of the return. When they are passed around together, a named record type says more. Tuples in general, and taking them apart in for each, are in Tuples and Iterators.

Activities

  1. Write function Stats( values( ) as int32 ) as ( count as int32, total as int64 ) and print both parts.
  2. Swap two variables with ( a, b ) = ( b, a ).

Arrays

An array holds many values of one type under one name, and you reach each one by its index. tiko arrays carry their bounds with them, a first and a last index, rather than a length. There are two kinds:

  • a fixed array, whose bounds are written in its declaration and never change;
  • a dynamic array, which starts empty and is sized while the program runs.
'' An array is many values of one type under one name, reached by index.

'' A fixed array: its bounds are written in the declaration.
dim as int32 days( 1 to 12 ) = { 31, 28, 31, 30, 31, 30, _
                                 31, 31, 30, 31, 30, 31 }
print "months"; lbound( days ); " to"; ubound( days )
print "days in June:"; days( 6 )

dim as int32 total = 0
for m as int32 = lbound( days ) to ubound( days )
    total += days( m )
next
print "days in the year:"; total

'' Elements start at zero (an empty string for text) until assigned.
dim as float64 rainfall( 0 to 2 )
rainfall( 1 ) = 12.5
print "rainfall:"; rainfall( 0 ); rainfall( 1 ); rainfall( 2 )

'' A dynamic array has no size until redim gives it one.
dim as ustring names( )
print "names starts with"; ubound( names ) - lbound( names ) + 1; " elements"
redim names( 1 to 3 )
names( 1 ) = "Ada" : names( 2 ) = "Grace" : names( 3 ) = "Edsger"
print "now"; ubound( names ) - lbound( names ) + 1; ": " & names( 2 )

Output

months 1 to 12
days in June: 30
days in the year: 365
rainfall: 0 12.5 0
names starts with 0 elements
now 3: Grace

lbound( a ) and ubound( a ) are the first and last valid index. A loop written between them works for any array, whatever its bounds, so it never needs a count remembered somewhere else. The number of elements is ubound( a ) - lbound( a ) + 1.

days( 1 to 12 ) is indexed by month, so days( 6 ) is June with no arithmetic. A brace list fills the elements in index order. Elements that are never assigned are zero, or an empty string for text; there is no such thing as an uninitialised element.

Pitfall

A single number in the declaration is the upper bound, and the lower bound is 0: dim as int32 a( 10 ) has eleven elements, 0 to 10. Write both bounds, a( 0 to 9 ), and the declaration cannot be misread.

dim as ustring names( ), with empty parentheses, declares a dynamic array. An empty array's ubound is below its lbound, so its count is 0 and a loop over it runs zero times. Dynamic arrays covers redim and the rest.

Activities

  1. Print every month with 31 days by its number.
  2. Declare dim as int32 a( 5 ) and print its lbound, ubound and count.

Dynamic arrays

When the number of elements is not known until the program runs, declare the array without bounds and give it some later with redim:

'' A dynamic array is sized, resized and freed while the program runs.
sub Show( byref label as ustring, values( ) as int32 )
    print label & " (" & lbound( values ) & " to " & ubound( values ) & "):";
    for each v in values
        print v;
    next
    print
end sub

dim values( any ) as int32         '' "any": the bounds come later
Show( "declared", values( ) )

redim values( 1 to 3 )
values( 1 ) = 10 : values( 2 ) = 20 : values( 3 ) = 30
Show( "redim", values( ) )

'' redim preserve keeps what fits; new elements start at zero.
redim preserve values( 1 to 5 )
Show( "grown", values( ) )
redim preserve values( 1 to 2 )
Show( "shrunk", values( ) )

'' Plain redim starts over: every element is zero again.
redim values( 1 to 4 )
Show( "redim again", values( ) )

'' erase frees the storage and leaves the array empty.
erase values
Show( "erased", values( ) )

'' Growing one element at a time builds a list of unknown length.
for n as int32 = 1 to 3
    redim preserve values( lbound( values ) to ubound( values ) + 1 )
    values( ubound( values ) ) = n * 100
next
Show( "appended", values( ) )

Output

declared (0 to -1):
redim (1 to 3): 10 20 30
grown (1 to 5): 10 20 30 0 0
shrunk (1 to 2): 10 20
redim again (1 to 4): 0 0 0 0
erased (0 to -1):
appended (0 to 2): 100 200 300

dim values( any ) as int32 and dim as int32 values( ) both declare an empty dynamic array. any stands for bounds that come later, and with ( any, any ) it says how many dimensions they will have.

StatementEffect
redim a( 1 to 3 )new bounds; every element is zero again
redim preserve a( 1 to 5 )new bounds; elements that still fit keep their values
erase afrees the elements; the array is empty again

Pitfall

redim without preserve is not a resize. It makes a fresh array under the same name, so redim values( 1 to 4 ) turns 10 20 into 0 0 0 0. Use redim preserve unless you mean to start over.

The last loop grows the array one element at a time, starting from empty: ubound( values ) + 1 is 0 the first time, so the first element lands at index 0. That builds a list of any length, but each redim preserve may copy every element, so for long lists the containers of the standard library are the better tool.

erase on a fixed array cannot free anything. It sets the elements back to zero and the bounds stay as they were.

Activities

  1. Read numbers into a dynamic array until one is negative, with the values coming from a fixed array instead of the keyboard.
  2. Change redim preserve values( 1 to 2 ) to plain redim and predict the line that follows.

Multi-dimensional arrays

A table, a board or an image has rows and columns. An array declared with two ranges has two subscripts, one per dimension:

'' An array can have more than one dimension: one subscript for each.
'' Brace lists nest, one level per dimension, row by row.
dim as int32 grid( 1 to 2, 1 to 3 ) = { { 1, 2, 3 }, { 4, 5, 6 } }

print "rows"; lbound( grid, 1 ); " to"; ubound( grid, 1 ); _
      ", columns"; lbound( grid, 2 ); " to"; ubound( grid, 2 )

for r as int32 = lbound( grid, 1 ) to ubound( grid, 1 )
    for c as int32 = lbound( grid, 2 ) to ubound( grid, 2 )
        print grid( r, c );
    next
    print
next

'' Column totals: the same loops in the other order.
print "column totals:";
for c as int32 = lbound( grid, 2 ) to ubound( grid, 2 )
    dim as int32 sum = 0
    for r as int32 = lbound( grid, 1 ) to ubound( grid, 1 )
        sum += grid( r, c )
    next
    print sum;
next
print

'' A dynamic array can have several dimensions too.
dim as ustring board( any, any )
redim board( 0 to 2, 0 to 2 )
board( 1, 1 ) = "X" : board( 0, 2 ) = "O"
for r as int32 = 0 to 2
    for c as int32 = 0 to 2
        if board( r, c ) = "" then print "."; else print board( r, c );
    next
    print
next

Output

rows 1 to 2, columns 1 to 3
 1 2 3
 4 5 6
column totals: 5 7 9
..O
.X.
...

The dimensions are separated by commas, both in the declaration and at each use: grid( r, c ). lbound and ubound take the dimension number as a second argument, so ubound( grid, 2 ) is the last column. ubound( grid, 0 ) is the number of dimensions.

A brace list has one level of braces per dimension. The first inner list is row 1, the second row 2.

Which loop goes outside is your choice and decides the order of the work: rows outside to print the grid, columns outside to total each column. The elements are stored row by row, with the last subscript changing fastest, so the loop over the last subscript is the fastest one to put innermost on a large array.

A dynamic array can have several dimensions too, declared with ( any, any ) and sized by redim. The memory order matters there:

'' redim preserve on a grid keeps the elements in memory order, which
'' is row by row. Changing the number of columns moves them.
sub ShowGrid( g( , ) as int32 )
    for r as int32 = lbound( g, 1 ) to ubound( g, 1 )
        print " ";
        for c as int32 = lbound( g, 2 ) to ubound( g, 2 )
            print g( r, c );
        next
        print
    next
end sub

dim as int32 g( )
redim g( 1 to 2, 1 to 3 )
for r as int32 = 1 to 2
    for c as int32 = 1 to 3
        g( r, c ) = r * 10 + c
    next
next
print "before:"
ShowGrid( g( ) )

redim preserve g( 1 to 3, 1 to 3 )       '' one more row: safe
print "a row added:"
ShowGrid( g( ) )

redim preserve g( 1 to 3, 1 to 4 )       '' one more column: moves
print "a column added:"
ShowGrid( g( ) )

Output

before:
  11 12 13
  21 22 23
a row added:
  11 12 13
  21 22 23
  0 0 0
a column added:
  11 12 13 21
  22 23 0 0
  0 0 0 0

Pitfall

redim preserve keeps the elements in their memory order, not at their indexes. Adding a row keeps every element where it was. Adding a column does not: the old values run on into the new column, so 21 ends up at row 1, column 4. To widen a grid, make a new one and copy the elements across by index.

Activities

  1. Build a 10 by 10 multiplication table and print it.
  2. Add a row-totals column to the first example's output.

Passing arrays

A procedure that takes an array writes the parameter as name( ) as Type, and the caller passes name( ). The empty parentheses are required at both ends:

'' An array parameter is written name( ) as Type, and so is the
'' argument. The procedure works on the caller's array, not a copy.
sub Fill( values( ) as int32, byval start as int32 )
    for i as int32 = lbound( values ) to ubound( values )
        values( i ) = start
        start += 1
    next
end sub

function Sum( values( ) as const int32 ) as int64
    dim as int64 total = 0
    for each v in values
        total += v
    next
    return total
end function

'' The bounds travel with the array.
dim as int32 small( 1 to 3 )
dim as int32 odd( -2 to 2 )
Fill( small( ), 10 )
Fill( odd( ), 1 )
print "small:"; small( 1 ); small( 2 ); small( 3 ); ", sum"; Sum( small( ) )
print "odd: " & lbound( odd ) & " to " & ubound( odd ) & ", sum"; Sum( odd( ) )

'' A dynamic array can be resized by the procedure it is passed to.
sub Grow( values( ) as int32, byval extra as int32 )
    redim preserve values( lbound( values ) to ubound( values ) + extra )
end sub

dim as int32 list( )
redim list( 1 to 2 )
Grow( list( ), 3 )
print "list now " & lbound( list ) & " to " & ubound( list )

'' A parameter's dimensions are written with commas: ( , ) is two.
sub Describe( m( , ) as int32 )
    print "a grid of"; ubound( m, 1 ) - lbound( m, 1 ) + 1; " rows and"; _
          ubound( m, 2 ) - lbound( m, 2 ) + 1; " columns"
end sub

dim as int32 table( 1 to 4, 0 to 5 )
Describe( table( ) )

Output

small: 10 11 12, sum 33
odd: -2 to 2, sum 15
list now 1 to 5
a grid of 4 rows and 6 columns

An array is always passed by reference: Fill writes straight into the caller's array, and nothing is copied however large the array is. To promise that a procedure only reads, write the parameter as const, as Sum does.

The bounds travel with the array. odd runs from -2 to 2 and Fill sees exactly that, which is why a procedure written between lbound and ubound works on any array of the right type without being told its size.

A dynamic array can be resized by the procedure it was passed to: Grow adds three elements to the caller's list.

A const array parameter is meant to refuse writes, so this should not compile:

'' An array parameter declared as const cannot be written. Does not
'' compile.
sub Reset_( values( ) as const int32 )
    values( lbound( values ) ) = 0
end sub

dim as int32 counts( 1 to 3 ) = { 4, 5, 6 }
Reset_( counts( ) )
print counts( 1 )

Status in this version

The program above compiles and prints 0: an element of a const array parameter can be assigned, and the array can be resized with redim, without an error. for each byref over one is refused, as it should be. Until this is fixed, as const on an array parameter documents the intent but does not enforce it.

The parameter states the number of dimensions with commas and nothing else: m( , ) as int32 takes any two-dimensional int32 array, whatever its bounds.

Activities

  1. Write sub Reverse( values( ) as int32 ) that reverses an array in place, and test it on small and on odd.
  2. Write function Largest( m( , ) as const int32 ) as int32.

Returning arrays

A function can hand back a whole array: write ( ) after the result type, as in as int32( ). The result is an ordinary dynamic array.

'' A function returns an array when its result type ends in ( ).

'' A brace list: the array is sized to the list, indexed from 0.
function Vowels( ) as ustring( )
    return { "a", "e", "i", "o", "u" }
end function

'' A local array, built in a loop. It is moved to the caller, not
'' copied, and keeps the bounds the function gave it.
function Squares( byval n as int32 ) as int32( )
    dim as int32 result( )
    redim result( 1 to n )
    for i as int32 = 1 to n
        result( i ) = i * i
    next
    return result
end function

'' var takes the result with its type and bounds.
var v = Vowels( )
print "vowels " & lbound( v ) & " to " & ubound( v ) & ", last: " & v( 4 )

var sq = Squares( 5 )
print "squares " & lbound( sq ) & " to " & ubound( sq ) & ", sq( 5 ) ="; sq( 5 )

'' A result can be walked directly; it is freed after the loop.
for each n in Squares( 3 )
    print n;
next
print

'' Assigning a result to an existing dynamic array replaces its contents.
dim as int32 table( )
table = Squares( 2 )
print "table " & lbound( table ) & " to " & ubound( table ) & ":"; _
      table( 1 ); table( 2 )

Output

vowels 0 to 4, last: u
squares 1 to 5, sq( 5 ) = 25
 1 4 9
table 1 to 2: 1 4

return takes one of three things:

  • a brace list, as in Vowels. The array is sized to the list and indexed from 0.
  • a local array, as in Squares. It is moved to the caller rather than copied, and keeps its bounds, so sq runs from 1 to 5.
  • a call to another function that returns an array of the same type.

The caller owns the result. var sq = Squares( 5 ) takes it with its type and bounds. Assigning to an existing dynamic array, table = Squares( 2 ), frees what table held and takes the new array. A result used in place, like the one for each walks, is freed when the statement ends.

Only an array that belongs to the function can be moved out. Returning a parameter is refused with TKO0840: 'a' is not a local array of this procedure, so returning it would need a copy, which is not supported yet; return a local array or a brace list. To return a changed copy of a parameter, build a local array from it and return that.

dim r( ) as int32 = Squares( 3 ) is refused too (TKO0612: var-len array 'r' cannot be initialized): use var, or declare the array first and assign to it.

Activities

  1. Write function Evens( byval n as int32 ) as int32( ) returning the first n even numbers, indexed from 1.
  2. Write function Doubled( values( ) as const int32 ) as int32( ) that returns a new array with every element doubled and the same bounds.

Bounds checking

An index outside an array's bounds reads or writes memory that belongs to something else. By default nothing stops it. Compiled with -exx, the program checks every index and stops at the first bad one:

'' -exx makes the program check every index against the array's bounds.
#cmdline "-exx"

dim as int32 scores( 1 to 3 ) = { 70, 85, 92 }
dim as int32 i = 4                 '' one past the end

scores( i ) = 100                  '' stops here with runtime error 6
print "not reached"

Output


Aborting due to runtime error 6 (out of bounds array access) at line 7 of bounds.tiko::(), 
'SCORES' accessed with invalid index = 4, must be between 1 and 3

#cmdline "-exx" puts the flag in the source, so the file builds the same way wherever it is compiled. The program stops at the bad write with runtime error 6, prints which array was touched, with which index, and what the bounds are, and exits with code 6. print "not reached" never runs. The message goes to standard error.

The check works for dynamic arrays too. It reads the bounds the array has at that moment, so after redim preserve a( 1 to 2 ), a( 3 ) is caught.

-exx costs a comparison on every access. Use it while writing and testing a program. Where an index comes from outside the program, such as input or a calculation you are not sure of, check it yourself as well, because that check is there in every build:

'' Checking an index yourself works in every build, with or without -exx.
function ScoreAt( scores( ) as const int32, byval i as int32 ) as int32
    if i < lbound( scores ) orelse i > ubound( scores ) then return -1
    return scores( i )
end function

dim as int32 scores( 1 to 3 ) = { 70, 85, 92 }
for i as int32 = 0 to 4
    print "ScoreAt("; i; " ) ="; ScoreAt( scores( ), i )
next

Output

ScoreAt( 0 ) =-1
ScoreAt( 1 ) = 70
ScoreAt( 2 ) = 85
ScoreAt( 3 ) = 92
ScoreAt( 4 ) =-1

Comparing against lbound and ubound works on any array. ScoreAt returns -1 for an index outside them; Error Handling shows ways to report a failure that cannot be confused with a real value.

Activities

  1. Change i to 0 in the first example and read the new message.
  2. Remove #cmdline "-exx" and compile the program again. Do not rely on what it prints.

for each over arrays

for each walks an array in index order without an index. It has two forms, and the difference is whether you can change the elements:

'' for each gives a copy of each element; for each byref gives the
'' element itself.
dim as int32 prices( 1 to 4 ) = { 10, 20, 30, 40 }

for each p in prices
    p = 0                          '' changes the copy only
next
print "after writing to copies:";
for each p in prices : print p; : next
print

for each byref p in prices
    p += p \ 10                    '' changes the array
next
print "after for each byref:";
for each p in prices : print p; : next
print

'' The same works for an array of your own type.
type Item
    as ustring label
    as int32 count
end type

dim as Item stock( 0 to 2 )
stock( 0 ).label = "nuts" : stock( 1 ).label = "bolts"
stock( 2 ).label = "washers"
for each byref it in stock
    it.count = len( it.label )
next
for each it in stock
    print it.label; it.count
next

'' A grid is walked one row at a time, with the order written out.
dim as int32 grid( 1 to 2, 1 to 3 ) = { { 1, 2, 3 }, { 4, 5, 6 } }
dim as int32 total = 0
for r as int32 = lbound( grid, 1 ) to ubound( grid, 1 )
    for c as int32 = lbound( grid, 2 ) to ubound( grid, 2 )
        total += grid( r, c )
    next
next
print "grid total:"; total

Output

after writing to copies: 10 20 30 40
after for each byref: 11 22 33 44
nuts 4
bolts 5
washers 7
grid total: 21
  • for each p in prices hands you a copy of each element. Writing p = 0 changes the copy, and the array is untouched.
  • for each byref p in prices hands you the element itself, so p += p \ 10 changes the array.

byref also avoids copying each element, which matters when the elements are large, as records with text fields are. The stock loop sets a field of each Item through the reference.

for each does not walk an array of more than one dimension:

'' for each refuses a grid: the order is not defined. Does not compile.
dim as int32 grid( 1 to 2, 1 to 3 ) = { { 1, 2, 3 }, { 4, 5, 6 } }

for each v in grid
    print v;
next
foreach2d.tiko(4,1): error TKO0608: 'for each' over a multi-dimensional array has no defined order

Rows first or columns first is a decision the program should state, so the compiler does not choose one. Write the two counting loops, as the last part of the first example does, in the order you want.

Activities

  1. Use for each byref to round every price up to the next multiple of 25.
  2. Find the Item with the largest count using for each and no index.

Strings

Text in tiko is a ustring. It grows as you add to it, it holds any Unicode text, and it is stored the same way on every target, so a program that handles text on one machine handles it identically on another.

'' ustring is tiko's text type: it grows as needed and holds Unicode.
dim as ustring greeting = "Hello"
dim as ustring who = "world"

'' & joins two strings into a new one.
dim as ustring message = greeting & ", " & who & "!"
print message
print "length:"; len( message )

'' & also turns numbers into text on the way.
dim as int32 apples = 3
print "I have " & apples & " apples and " & 2.5 & " pears"

'' &= appends to the string you already have.
message &= " Again."
print message

'' Strings compare by their characters, case included.
print cbool( "apple" < "banana" ); " "; cbool( "Tiko" = "tiko" )

'' A new ustring starts out empty.
dim as ustring blank
print "empty:"; len( blank ); " "; cbool( blank = "" )

'' Assigning a string copies it: the two are independent afterwards.
dim as ustring twin = greeting
twin &= "!!!"
print greeting; " / "; twin

Output

Hello, world!
length: 13
I have 3 apples and 2.5 pears
Hello, world! Again.
true false
empty: 0 true
Hello / Hello!!!

len gives the length, and it costs the same however long the string is: the length is stored, not counted.

& joins two values into a new string, converting a number to text when one side is a number, so "I have " & apples & " apples" needs no conversion call. &= appends to a string that already exists.

Strings compare character by character, so "apple" < "banana", and the comparison is case-sensitive: "Tiko" and "tiko" are different strings. A ustring you have not assigned is empty, not missing; it equals "".

Assignment copies. Changing twin afterwards leaves greeting alone, so you can pass and store strings without worrying about who else holds them.

The pages in this section:

Laying out values with strings.Format is covered in Formatted print, and making your own types printable in Display.

Activities

  1. Print your name and its length on one line.
  2. Compare "Zebra" and "apple" with <. Then compare them again after passing both through lcase( ), from the next page.

Built-in functions

The everyday operations on text are built into the language: no #import is needed for any of them, and they all work on ustring.

'' The string functions built into the language. Positions count
'' from 1, and a search that finds nothing returns 0.
dim as ustring s = "The quick brown fox"

print "["; left( s, 3 ); "] ["; right( s, 3 ); "]"
print "["; mid( s, 5, 5 ); "] ["; mid( s, 11 ); "]"

print "brown is at"; instr( s, "brown" )
print "from 14 on, o is at"; instr( 14, s, "o" )
print "last o is at"; instrrev( s, "o" )
print "zebra is at"; instr( s, "zebra" )

print ucase( s )
print lcase( s )

dim as ustring padded = "   spaced out   "
print "["; trim( padded ); "]"
print "["; ltrim( padded ); "] ["; rtrim( padded ); "]"
print "["; trim( "--core--", any "-" ); "]"

print "["; space( 4 ); "] ["; string( 6, "=" ); "]"
print "chr( 65 ) is " & chr( 65 ) & ", asc( ""B"" ) is " & asc( "B" )
print "the third character's code is " & asc( s, 3 )

'' mid on the left of = overwrites characters in place.
mid( s, 5, 5 ) = "QUICK"
print s

'' str and val convert between numbers and text.
dim as ustring n = str( 6 * 7 )
print "["; n; "]"; val( "2.5" ) * 2

Output

[The] [fox]
[quick] [brown fox]
brown is at 11
from 14 on, o is at 18
last o is at 18
zebra is at 0
THE QUICK BROWN FOX
the quick brown fox
[spaced out]
[spaced out   ] [   spaced out]
[core]
[    ] [======]
chr( 65 ) is A, asc( "B" ) is 66
the third character's code is 101
The QUICK brown fox
[42] 5

Two conventions run through every one of them:

  • Positions start at 1. mid( s, 5, 5 ) is five characters starting at the fifth.
  • Not found is 0. instr returns 0 when the text is absent, which is never a valid position, so if instr( s, x ) > 0 is the test.
FunctionGives
left( s, n ), right( s, n )the first or last n characters
mid( s, start, n )n characters from start; leave out n for the rest
instr( s, t ), instr( start, s, t )where t first appears, searching from start
instrrev( s, t )where t last appears
ucase( s ), lcase( s )the text in upper or lower case
trim, ltrim, rtrimthe text without surrounding spaces; any "chars" strips those characters instead
space( n ), string( n, c )n spaces, or n copies of a character
chr( n ), asc( s, i )a character from its code, and the code of the character at i
str( n ), val( s )a number as text, and text as a number

None of these change their argument: each returns a new string. The exception is mid written on the left of =, the mid statement, which overwrites characters inside an existing string. It never changes the string's length, so it is also how Building strings fills a buffer without reallocating.

Converting between numbers and text in more depth, including what val does with text that is not a number, is in Conversion.

Activities

  1. Use instr in a do loop to count how many times "o" appears in s, passing the position after each match as the start of the next search.
  2. Print s with its first letter in lower case and the rest in upper case, using left, mid, lcase and ucase.

The strings module

The built-in functions find and cut text. The standard library's std/strings module adds the operations most programs end up writing for themselves: prefix and suffix tests, counting, replacing, padding, and asking whether text is a number.

#import "std/strings"

dim as ustring path = "reports/2026/summary.tiko"

'' Tests: each returns a boolean.
print "starts with reports: "; strings.StartsWith( path, "reports" )
print "ends with .TIKO:     "; strings.EndsWith( path, ".TIKO" )
print "  ... ignoring case: "; strings.EndsWith( path, ".TIKO", true )
print "contains /2026/:     "; strings.Contains( path, "/2026/" )
print "slashes:"; strings.Tally( path, "/" )

'' Pieces: before a match, after it, and between two.
print strings.Extract( 1, path, "/" ); " | "; _
      strings.Remain( path, "/" )
print strings.Between( "total=[42]", "[", "]" )

'' Changes: each returns a new string and leaves its input alone.
print strings.Replace( path, "/", "\" )
print strings.Remove( "1,234,567", "," )
print strings.Shrink( "  far    too   many  spaces " ); "|"
print strings.MCase( "the QUICK brown fox" )
print strings.Reverse( "stressed" ); " "; strings.Repeat( 3, "ab" )

'' Padding to a width, with any fill character.
print "["; strings.PadLeft( "42", 6, "0" ); "] ["; _
      strings.PadRight( "id", 6, "." ); "] ["; _
      strings.PadCenter( "mid", 7 ); "]"

'' Is it a number? val( ) cannot tell you; IsNumeric can.
print strings.IsNumeric( "-1.5e3" ); " "; strings.IsNumeric( "12abc" )
print strings.IsBlank( "   " ); " "; strings.IsBlank( " x " )

Output

starts with reports: true
ends with .TIKO:     false
  ... ignoring case: true
contains /2026/:     true
slashes: 2
reports | 2026/summary.tiko
42
reports\2026\summary.tiko
1234567
far too many spaces|
The Quick Brown Fox
desserts ababab
[000042] [id....] [  mid  ]
true false
true false

#import "std/strings" brings the module in, and its procedures are called through the module name: strings.Replace. Write using strings after the import if you would rather call them bare.

GroupProcedures
TestsStartsWith, EndsWith, Contains, IsNumeric, IsBlank
CountingTally (non-overlapping matches)
PiecesExtract (text before a match), Remain (text after it), Between
ChangesReplace, Remove, Shrink, MCase, Reverse, Repeat
LayoutPadLeft, PadRight, PadCenter

Every search takes an optional last argument, ignoreCase. EndsWith( path, ".TIKO" ) is false; with true added it matches. Case folding covers letters beyond ASCII too, the same way ucase does.

Like the built-in functions, these return a new string; the one passed in is not changed. Positions are counted from 1 and 0 means "not found", as with instr.

Shrink collapses each run of spaces to one and removes the spaces at the ends, which is what you want for text a person typed.

IsNumeric answers a question val cannot: val( "12abc" ) returns 12, but IsNumeric( "12abc" ) is false, because the whole text is not a number.

Cutting a string into a list of fields, and joining one back together, have a page of their own: Split and Join.

Activities

  1. Use Replace with ignoreCase to change every cat in "Cat, cat and CAT" to dog.
  2. Print a three-row table where the first column is padded to 10 characters with PadRight and the second right-aligned in 6 with PadLeft.

Split and Join

A line of comma-separated values, a path, a list of words: text that is really a list of fields. strings.Split turns it into a list you can walk, and strings.Join turns a list back into text.

#import "std/strings"

'' Split cuts a string at every delimiter and returns the pieces
'' in an Array( of ustring ).
var fields = strings.Split( "red,green,,blue", "," )
print "fields:"; fields.Count( )
for each f in fields
    print "  ["; f; "]"
next

'' Indexing counts from 0, like every container.
print "first is "; fields[ 0 ]; ", last is "; fields[ fields.Count( ) - 1 ]

'' Join is the reverse: pieces in, one string out.
print strings.Join( fields, " + " )

'' Splitting and joining with the same delimiter gives back the
'' original, empty fields and all.
dim as ustring row = ",a,,b,"
print cbool( strings.Join( strings.Split( row, "," ), "," ) = row ); _
      " (" & strings.Split( row, "," ).Count( ) & " fields)"

'' SplitChars treats every character it is given as a delimiter.
var words = strings.SplitChars( "one two;three,four", " ;," )
print strings.Join( words, "|" )

Output

fields: 4
  [red]
  [green]
  []
  [blue]
first is red, last is blue
red + green +  + blue
true (5 fields)
one|two|three|four

Split returns an Array( of ustring ), the standard library's growable array from Containers. Here it is asked only for .Count( ), for an element with [ i ], and to be walked with for each. Its indexes count from 0, unlike positions inside a string, which count from 1.

Split never drops a field. Two delimiters in a row make an empty field between them, and a delimiter at either end makes an empty field there, so ",a,,b," is five fields and n delimiters always give n + 1 fields. That rule is what makes the round trip work: Join( Split( s, d ), d ) is s again, for every s.

When the delimiter is a choice of characters rather than one string, SplitChars takes them all at once: " ;," splits at a space, a semicolon or a comma.

Note

A comma-separated file is not a Split problem: a quoted field such as "London, UK" contains the delimiter. strings.SplitCsvRow and strings.SplitCsv read quoting correctly, and Writing CSV shows the other direction.

Activities

  1. Split "1969-07-20" on "-" and print the three parts as numbers with val.
  2. Split a sentence into words with SplitChars, then Join them back in reverse order.

Slices

mid, left and Split hand back new strings: each one allocates and copies. A slice names part of a string where it already is, so taking text apart costs nothing until you decide to keep a piece.

#import "std/strings"

dim as ustring s = "red,green,blue"

'' s[lo to hi] names units lo to hi of s, counted from 0 with both
'' ends included. Nothing is copied: the slice points into s.
dim as uslice first = s[0 to 2]
print "first: ["; first; "] len"; len( first )

'' A slice of a slice is a smaller view of the same text.
print "s[4 to 8][1 to 3] = "; s[4 to 8][1 to 3]

'' Indexing a slice gives a code unit, as indexing a ustring does.
print "first[0] = "; first[0]

'' A slice compares, prints and joins like a string.
print cbool( first = "red" ); " [" & first & "]"

'' Writing through a slice writes into s.
first[0] = asc( "R" )
print s

'' Assigning to a ustring is how a slice becomes text you keep.
dim as ustring kept = first
kept &= "!"
print kept; " "; s

'' std/strings works on slices, with positions counted from 0.
print "comma at"; strings.Find( s[0 to len( s ) - 1], "," )
for each part as const uslice in _
        strings.SplitSlices( s[0 to len( s ) - 1], "," )
    print "  " & part & " (" & len( part ) & ")"
next
print "trimmed: ["; strings.TrimSpace( "  padded  " ); "]"

Output

first: [red] len 3
s[4 to 8][1 to 3] = ree
first[0] = 114
true [red]
Red,green,blue
Red! Red,green,blue
comma at 3
  Red (3)
  green (5)
  blue (4)
trimmed: [padded]

s[lo to hi] counts from 0 and includes both ends, the same counting as s[i]. s[0 to 2] is the first three units; mid( s, 1, 3 ) is the same text counted from 1, and copied. s[3 to 2], whose end is one before its start, is empty, so s[0 to len( s ) - 1] is the whole string even when s is empty. A range outside the string is runtime error 6, checked every time a slice is made.

There is one slice type for each width of text:

TypeSlicesUnit
usliceustringa UTF-16 code unit
slicestring, zstring, a zstring ptra byte
wslicewstring, a wstring ptra wide character

A slice is as writable as its source. first[0] = asc( "R" ) changed s. A slice of a constant, a literal or a temporary is const, and writing through it is refused.

Assigning a slice to a ustring copies it. That is the moment to do it: when the text has to outlive the string it came from.

std/strings has functions that take and return slices: Find, Equals, StartsWith, EndsWith, TrimStart, TrimEnd, TrimSpace, and SplitSlices, which hands the parts to for each one at a time instead of building an array. Their positions count from 0, and Find answers -1 when there is no match. Any string can be passed where they want a slice.

Tip

Pass s[0 to len( s ) - 1] rather than s when a ustring meets a literal in a call such as SplitSlices( s, "," ). With a ustring and a literal the compiler has two equally good ways to convert, and refuses to guess.

The rules that keep a slice from outliving its text are on the next page, Slices and lifetime.

Activities

  1. Print every word of "the quick brown fox" using SplitSlices, then print only those that StartsWith "b".
  2. Use Find to print the part of "key=value" after the =, without calling mid.

Slices and lifetime

A slice points into memory it does not own. If the string it came from goes away, or moves its text somewhere else, the slice would read whatever is there now. The compiler keeps track of where each slice came from and refuses the programs where that could happen.

'' A function may return a slice of text its caller owns: here s is
'' byref, so the text outlives the call.
function Before( byref s as const ustring, byval sep as int32 ) _
        as const uslice
    for i as int32 = 0 to len( s ) - 1
        if s[i] = sep then return s[0 to i - 1]
    next
    return s[0 to len( s ) - 1]
end function

dim as ustring path = "docs/guide/intro.md"
dim as const uslice top = Before( path, asc( "/" ) )
print "top folder: "; top

'' A type that holds a slice is declared "as view". It follows the
'' same rules as a slice: no shared, no arrays, no new.
type Cursor as view
    text as const uslice
    pos as int32
end type

function Take( byref c as Cursor, byval n as int32 ) as const uslice
    dim as int32 start = c.pos
    c.pos += n
    if c.pos > len( c.text ) then c.pos = len( c.text )
    return c.text[start to c.pos - 1]
end function

dim as Cursor c
c.text = path[0 to len( path ) - 1]
print Take( c, 4 ); " | "; Take( c, 6 ); " | "; Take( c, 99 )

Output

top folder: docs
docs | /guide | /intro.md

A function may return a slice of its caller's text. Before takes s byref, so the text belongs to the caller and is still there after the call. With byval s, s is the call's own copy, and returning a slice of it is error TKO1059.

A slice lives in a local, a parameter or a result, nowhere else. It cannot be shared or static, an array element, a container element or allocated with new (TKO1054), and it cannot be kept past the end of the block, or the statement, its text belongs to (TKO1057, TKO1058).

A type that holds a slice is declared as view, and follows the same rules: Cursor can be a local, a parameter or a result, and an ordinary type cannot hold one (TKO1055).

While a slice is in use, the string it came from cannot change size:

dim as ustring name_ = "Ada"
dim as const uslice initial = name_[0 to 0]

'' Appending may move the text to a bigger block of memory, which
'' would leave initial pointing at the old one.
name_ &= " Lovelace"
print initial
slicelock.tiko(6,1): error TKO1062: 'name_' cannot be changed while slice 'initial' borrows from it (last used at line 7)
     |
   6 | name_ &= " Lovelace"
     | ^

Assigning, appending, swap, input and passing the string byref to something that may change it all count, up to the last line that reads the slice. Move the print above the &= and it compiles. Writing one unit, name_[0] = x, or the mid statement never moves the text, and is allowed.

The compiler sees one procedure at a time. When another procedure changes the string, -exx catches it when the slice is next read:

#cmdline "-exx"

dim shared as ustring buffer

sub AddText( byref more as const ustring )
    buffer &= more & more & more & more
end sub

sub Show( )
    buffer = "abc"
    dim as const uslice view_ = buffer[0 to 2]
    '' The compiler cannot see that AddText changes buffer.
    AddText( "a line long enough to need a new block of memory" )
    '' -exx checks view_ is still inside buffer: it is not.
    print view_
end sub

Show( )

Aborting due to runtime error 6 (out of bounds array access) at line 15 of slicecheck.tiko::SHOW()

Pitfall

Without -exx the same program reads freed memory and may well print abc, because nothing has reused it yet. Build with -exx while you develop. A slice of a zstring ptr has nothing to check against: the pointer is trusted, as it is everywhere else.

Activities

  1. Change Before to take byval s as ustring and read the error.
  2. Move print initial above the &= in slicelock.tiko and run it.

Block strings

An ordinary string literal ends with its line. A query, a usage message or a template is several lines of text, and gluing it together from one literal per line buries the text under punctuation. A block string holds it the way it reads.

'' A block string opens with """ at the end of a line and closes at
'' the first line that starts with """.
dim as ustring query = """
    select id, name
      from users
     where name = "Ada"
    """
print query

'' The closing line's indentation is the margin, removed from every
'' line. The line breaks next to the quotes are not part of the text.
print "length:"; len( query ); ", ends with: "; right( query, 5 )

'' An empty line before the closing """ ends the text with a line feed.
dim as ustring list = """
    - eggs
    - milk

    """
print list; "(end of list)"

'' !""" reads escape sequences; a backslash at the end of a line joins
'' it to the next.
dim as ustring row = !"""
    name\tvalue
    one long line, \
    continued
    """
print row

'' A block string is an expression like any other literal.
print ucase( """
    shout
    """ ); "!"

Output

select id, name
  from users
 where name = "Ada"
length: 48, ends with: "Ada"
- eggs
- milk
(end of list)
name	value
one long line, continued
SHOUT!

""" at the end of a line opens a block string, and the first line that starts with """ closes it. Only spaces may follow the opening quotes. After the closing quotes the line carries on as ordinary code, which is how the last example passes a block string straight to ucase( ... ).

The closing line's indentation is the margin. It is removed from every line, so the text can be indented along with the code around it, while indentation that belongs to the text, such as the extra spaces before from, is kept.

The line breaks next to the quotes are not part of the text. query is 48 characters and ends with "Ada", not with a line feed. To end the text with one, leave an empty line before the closing """, as list does. Every line break inside is a single line feed, chr( 10 ), however the source file was saved.

Nothing inside needs escaping: a " is an ordinary character. Write !""" to read escape sequences inside the block, where a backslash at the very end of a line also joins it to the next.

A line that starts to the left of the margin is an error, because there is no way to remove a margin it does not have:

'' Every line must start at or right of the closing line's margin.
dim as ustring note = """
    first
  second
    """
print note
blockmargin.tiko(4,1): error TKO0208: block string line does not start with the closing line's indentation
     |
   4 |   second
     | ^~~~~~~~~

Blank lines are always allowed, whatever their indentation.

Pitfall

The closing line sets the margin, not the first line. Put the closing """ at the left edge and nothing is removed: the spaces that only indented the code become part of the text.

Activities

  1. Move the closing """ of query to the left edge and see where the spaces go.
  2. Write a usage message for a command-line program as one block string and print it.

Escape sequences

In an ordinary string literal a backslash is an ordinary character, which is what you want for Windows paths and regular expressions. When you need a tab, a line feed or a character you cannot type, put ! in front of the literal and it reads escape sequences.

'' In an ordinary literal a backslash is just a backslash, and a
'' doubled quote stands for one quote character.
print "C:\tools\new [""quoted""]"

'' Put ! in front and the literal reads escape sequences instead.
print !"tab:[\t] quote:[\"] backslash:[\\]"
print !"two lines:\n  this is the second"

'' Codes in hexadecimal or decimal, and Unicode code points.
print !"\x41\x42 \67 \&h44"
dim as ustring cafe = !"caf\u00E9"
print cafe; " is"; len( cafe ); " characters"

'' \U takes eight hex digits, for code points above U+FFFF.
dim as ustring face = !"\U0001F600"
print "U+1F600 is"; len( face ); " code units"

'' $ in front marks a literal that never reads escapes.
print $"raw: \t \n"

'' A program can build the characters as well as spell them.
print cbool( !"a\tb" = "a" & chr( 9 ) & "b" )

Output

C:\tools\new ["quoted"]
tab:[	] quote:["] backslash:[\]
two lines:
  this is the second
AB C D
café is 4 characters
U+1F600 is 2 code units
raw: \t \n
true

Without a prefix, the only special thing inside quotes is the quote itself: write it twice, "", for one " in the text.

With !, a backslash starts an escape:

EscapeCharacter
\ttab
\nline feed
\rcarriage return
\\a backslash
\"a quote
\xHH, \&hHHthe character with that hexadecimal code
\NNNthe character with that decimal code
\uXXXXthe Unicode code point U+XXXX
\UXXXXXXXXany code point, including those above U+FFFF

A $ prefix marks a literal that never reads escapes. It says the same as no prefix at all, but states it, which is useful in code that also uses ! literals nearby.

\u and \U give you any Unicode character while the source itself stays plain ASCII. A code point above U+FFFF, such as U+1F600, becomes two UTF-16 code units in a ustring, which Unicode explains.

The same escapes work in a block string opened with !""".

Activities

  1. Print a three-column table using \t between the columns.
  2. Spell the word "naive" with an i-diaeresis, written \u00EF, print it and its len.

Unicode

A ustring stores text as UTF-16 code units, 16-bit numbers. Almost every character in use, from Latin and Cyrillic to Chinese, is one code unit. The characters above U+FFFF, which include emoji and many historic scripts, need two. Most of the time you can ignore the difference; this page shows when you cannot.

'' Count characters, treating a surrogate pair as one.
function CharCount( byref s as ustring ) as int32
    dim as int32 count = 0
    for i as int32 = 1 to len( s )
        dim as int32 unit = asc( s, i )
        '' A low surrogate is the second half of a pair: skip it.
        if unit < &hDC00 orelse unit > &hDFFF then count += 1
    next
    return count
end function

'' wchr builds a ustring from code points, so the source stays ASCII.
dim as ustring cafe = "caf" & wchr( 233 )             '' U+00E9
dim as ustring face = "a" & wchr( &h1F600 ) & "b"     '' U+1F600

print cafe; ": len"; len( cafe ); ", characters"; CharCount( cafe )
print "face: len"; len( face ); ", characters"; CharCount( face )

'' len counts UTF-16 code units. U+1F600 is above U+FFFF, so it is
'' stored as two units, a surrogate pair.
print "units of face:";
for i as int32 = 1 to len( face )
    print " " & hex( asc( face, i ) );
next
print

'' Cutting by position can split the pair and leave half a character.
dim as ustring half = mid( face, 2, 1 )
print "mid( face, 2, 1 ) is a lone unit: "; hex( asc( half ) )

'' Converting to the byte type string encodes UTF-8, and back again.
dim as string bytes = face
dim as ustring back = bytes
print "UTF-8 bytes:"; len( bytes ); ", round trip equal: "; _
      cbool( back = face )

'' ucase and lcase know letters beyond ASCII.
dim as ustring alpha = wchr( &h3B1 )                    '' Greek alpha
print ucase( cafe ); ", U+3B1 in upper case is U+"; _
      hex( asc( ucase( alpha ) ) )

Output

café: len 4, characters 4
face: len 4, characters 3
units of face: 61 D83D DE00 62
mid( face, 2, 1 ) is a lone unit: D83D
UTF-8 bytes: 6, round trip equal: true
CAFÉ, U+3B1 in upper case is U+391

wchr( ... ) builds a string from code points, which keeps the source file plain ASCII and makes it obvious which character you mean. It takes several code points at once, too: wchr( 72, 105 ) is "Hi".

café is four characters and four code units. face, which is a, U+1F600 and b, is three characters but four code units: U+1F600 is stored as the surrogate pair D83D DE00. len, asc( s, i ), mid and every other position in the string functions count code units, not characters.

Pitfall

A position you compute, such as "the first 20 characters", can land in the middle of a pair, and mid then returns half a character: D83D on its own is not text. Positions that come from a search (instr, Split, a delimiter you put there) are safe, because a search only matches whole characters.

CharCount shows the way to walk characters rather than units: the second unit of a pair is always in the range DC00 to DFFF, so skipping those counts each character once.

Assigning a ustring to the byte type string encodes it as UTF-8, where U+1F600 takes four bytes, and assigning it back decodes it again; the round trip loses nothing. That is the conversion to use when text leaves the program as bytes.

ucase and lcase know the case of letters well beyond ASCII: é becomes É and Greek alpha U+3B1 becomes U+391.

Status in this version

wchr itself returns a wstring, not a ustring. Printing that directly, as in print wchr( 233 ), writes raw wide characters instead of UTF-8 when the output is redirected to a file or a pipe. Store the result in a ustring first, as the example does, and it prints correctly everywhere.

Activities

  1. Write CharAt( s, n ), which returns the nth character of a string as a ustring, keeping a surrogate pair together.
  2. Print the UTF-8 length of café, then of a plain ASCII word of the same length. When do len of the string and of the ustring agree?

Building strings

Reports, generated code and log lines are built from many small pieces. How you add the pieces decides whether the work grows with the length of the result or with its square, so it is worth knowing which forms are cheap.

#import "std/strings"
#import "std/containers"
using containers

'' Three ways to build "1;2;3;...;1000;" from many small pieces.
const PieceCount = 1000

'' 1. Append one piece at a time. A ustring keeps spare room at its
''    end, so each &= copies only the new piece.
dim as ustring appended
for i as int32 = 1 to PieceCount
    appended &= strings.Format( "{};", i )
next

'' 2. Collect the pieces, then Join them in one pass.
dim as Array( of ustring ) pieces
for i as int32 = 1 to PieceCount
    pieces.Push( str( i ) )
next
dim as ustring joined = strings.Join( pieces, ";" ) & ";"

'' 3. When the final size is known, make room once and overwrite it
''    with the mid statement, which never reallocates.
dim as ustring filled = space( len( appended ) )
dim as int32 at = 1
for i as int32 = 1 to PieceCount
    dim as ustring piece = i & ";"
    mid( filled, at ) = piece
    at += len( piece )
next

print "length:"; len( appended )
print left( appended, 20 ); " ... "; right( appended, 15 )
print "all three equal: "; cbool( appended = joined ) andalso _
                           cbool( appended = filled )

Output

length: 3893
1;2;3;4;5;6;7;8;9;10 ... 7;998;999;1000;
all three equal: true

All three produce the same 3893 characters:

  1. &= one piece at a time. A ustring keeps spare room at its end and grows it in large steps, so appending a piece copies only that piece. For most programs this is the whole answer.
  2. Collect, then strings.Join. Useful when the pieces are a list anyway, or when the separator should go between them and not after the last one. Join measures the result first and copies each piece once.
  3. Make room once, then overwrite. When you know the final size, space( n ) allocates it in one go and the mid statement writes into it without ever changing the length.

What is not cheap is anything that rebuilds the whole string each time: s = piece & s puts the new text first, so every pass copies everything built so far, and a loop of them slows down with the square of the length.

Status in this version

&= takes the fast path only when its right-hand side is a single ustring, such as a variable, a literal or the result of strings.Format. With a concatenation on the right,

s &= "item" & i & ";"

the whole of s is copied on every pass. Measured, 40,000 such appends took several seconds, where the same loop appending strings.Format( "item{};", i ) took a few hundredths of one, and each doubling of the count made the gap wider. The same happens for the byte-string result of str( i ). Build the piece first, with strings.Format or in a ustring variable, and append that, as the example does.

There is no separate string builder type in the standard library: a ustring and &= already do that job.

Activities

  1. Change the first loop to put a comma between the numbers but not after the last one. Which of the three ways makes that easiest?
  2. Build the numbers in reverse order, 1000 down to 1, with &= rather than by putting each new piece in front.

Base64, hex and URLs

Bytes that are not text — a token, a hash, a key — have to be spelled in characters before they can go into JSON, an e-mail or a URL. std/codec has the usual spellings, and percent-encoding for text inside URLs.

#import "std/codec"

'' Base64 and hex carry bytes through places that only take text.
dim token as ustring = "user:secret"
print "base64: "; codec.Base64Encode( token )
print "hex:    "; codec.HexEncode( token )
print "back:   "; codec.Base64DecodeText( "dXNlcjpzZWNyZXQ=" ).Value( )

'' Percent-encoding makes text safe inside a URL.
dim search as ustring = "rock & roll/live"
print "query:  ?q="; codec.PercentEncode( search, codec.PCT_QUERY )
print "path:   /"; codec.PercentEncode( search )

'' Decoding is strict: it says what is wrong, and where.
var bad = codec.Base64DecodeText( "dXNl!jpz" )
if bad.IsOk( ) = false then print "error:  "; bad.Failure( ).Message( )

Output

base64: dXNlcjpzZWNyZXQ=
hex:    757365723a736563726574
back:   user:secret
query:  ?q=rock+%26+roll%2Flive
path:   /rock%20%26%20roll%2Flive
error:  Base64DecodeText: invalid character at 5
ProcedureGives
codec.Base64Encode( x )base64; codec.B64_URL for the URL-safe alphabet
codec.Base32Encode( x )capitals and digits only, for codes people type
codec.HexEncode( x )two digits per byte; pass true for upper case
codec.PercentEncode( s, mode )text safe inside a URL

x can be a ustring (encoded as its UTF-8 bytes) or a containers.Array( of ubyte ). Each encoding has a …Decode that fills an array of bytes and a …DecodeText that gives back a ustring; both return a Result.

Decoding is strict: a character outside the alphabet, missing or misplaced = padding, or whitespace is refused, and the error says where. Pass codec.DECODE_IGNORE_WHITESPACE or codec.DECODE_ALLOW_MISSING_PAD when the input is known to be loose.

PercentEncode keeps letters, digits and - . _ ~. PCT_PATH also keeps /; PCT_QUERY is what an HTML form sends, with a space as +.

Activities

  1. Encode the same token with codec.B64_URL and no padding, as a JSON Web Token does, and decode it back with codec.DECODE_ALLOW_MISSING_PAD.
  2. Decode "dXNlcjpzZWNyZXQ" without the option and read the error.

Writing CSV

strings.SplitCsv reads comma-separated text (see Split and Join). Writing it has the opposite problem: a field holding a comma or a quote has to be quoted, or the reader splits it in two.

#import "std/strings"
#import "std/containers"
#import "std/fs"
using containers

dim fields as Array( of ustring ) = { "plain", "a,b", !"5\" pipe", " pad" }
print strings.JoinCsvRow( fields )
print strings.JoinCsvRow( fields, ";" )

'' CsvWriter writes rows to any stream; this one is in memory.
dim buffer as fs.MemoryStream
dim w as strings.CsvWriter = strings.CsvWriter( buffer, ",", false )
w.WriteRow( fields )
w.WriteRow( fields )
print "ok? "; w.Finish( ).IsOk( )

'' Reading it back gives the same fields.
var rows = strings.SplitCsv( buffer.ToUtf8Text( ) )
print "rows: "; rows.Count( ); ", third field: "; rows[1].fields[2]

Output

plain,"a,b","5"" pipe"," pad"
plain;a,b;"5"" pipe";" pad"
ok? true
rows:  2, third field: 5" pipe

JoinCsvRow( fields [, separator] ) writes one record. A field is quoted only when it holds the separator, a double quote or a line break, or starts or ends with a space; a quote inside a field is doubled. With ";" as the separator, a,b no longer needs quotes.

CsvWriter writes records to any fs.Stream — a File, a pipe, or a MemoryStream as here. Records end with CR LF, the standard CSV line ending, unless the third argument is false. If a write fails, the writer stops writing and Finish( ) returns the failure.

Whatever JoinCsvRow writes, SplitCsv reads back as the same fields.

Activities

  1. Add a field containing a line break and check that it survives the round trip.
  2. Write the rows to a file with fs.File and Create, then open it in a spreadsheet.

Objects

A record from Custom Types holds data and leaves every rule about that data to the code around it. An object is a type that also carries procedures of its own, its methods, so the code that looks after the data lives with the data.

'' A type can carry procedures of its own: methods.
type Counter
    declare sub Increment( )
    declare sub Add( byval amount as int32 )
    declare function Value( ) as int32
    count as int32
end type

'' A method is defined outside the type and named Type.Method.
sub Counter.Increment( )
    count += 1                      '' a field of the object
end sub

sub Counter.Add( byval amount as int32 )
    this.count += amount            '' "this." spells it out
end sub

function Counter.Value( ) as int32
    return count
end function

dim as Counter clicks, errors
clicks.Increment( )
clicks.Increment( )
clicks.Add( 10 )
errors.Increment( )

'' Each object has its own fields.
print "clicks:"; clicks.Value( )
print "errors:"; errors.Value( )

'' Methods add nothing to the size of an object.
print "size of a Counter:"; sizeof( Counter )

Output

clicks: 12
errors: 1
size of a Counter: 4

A method is declared inside the type with declare and defined outside it, under the name Type.Method. It is called on an object with a dot: clicks.Increment( ).

Inside a method, the object it was called on is this. A field can be named on its own, as Increment does with count, or as this.count, which you need when a parameter or local has the same name as the field.

Methods cost nothing per object: sizeof( Counter ) is 4, the size of its one int32 field. A method call is an ordinary procedure call with the object passed in by reference.

The rest of this section:

PageCovers
Constructorshow objects are created
Destructorswhat runs when they go away
Propertiesmethods that read and write like fields
Access controlpublic: and private:
Static membersfields and methods that belong to the type
Const methodsmethods that promise not to change the object
Operator overloading+, =, <, let, += on your own types
Indexingoperator []
Copyingcopy constructors and operator let

Types that build on other types are in Inheritance.

Activities

  1. Add a Reset method to Counter that sets the count back to 0.
  2. Add a method Add that takes another Counter and adds its count, then call clicks.Add( errors ). Two methods may share a name when their parameters differ.

Constructors

A constructor runs when an object is created, so an object can never be seen half-built. A type may have several, told apart by their parameters like overloaded procedures.

type Temperature
    declare constructor( )                          '' the default
    declare constructor( byval celsius as float64 )
    declare constructor( byref text as ustring )
    celsius as float64
end type

constructor Temperature( )
    celsius = 20
    print "  default: room temperature"
end constructor

constructor Temperature( byval celsius as float64 )
    this.celsius = celsius
    print "  from a number"
end constructor

'' One constructor can hand its work to another.
constructor Temperature( byref text as ustring )
    this.constructor( val( text ) )
    print "  ... by way of the text"
end constructor

sub Show( byref t as Temperature )
    print "  Show got"; t.celsius
end sub

print "dim as Temperature a"
dim as Temperature a

print "var b = Temperature( 5.5 )"
var b = Temperature( 5.5 )

print "dim as Temperature c = Temperature( ""31"" )"
dim as Temperature c = Temperature( "31" )

'' A constructor with one parameter is also a conversion.
print "dim as Temperature d = -4"
dim as Temperature d = -4
print "Show( 12 )"
Show( 12 )

'' An array of objects runs the default constructor for each element.
print "dim as Temperature week( 1 to 2 )"
dim as Temperature week( 1 to 2 )

print "celsius: " & a.celsius & ", " & b.celsius & ", " & c.celsius & _
      ", " & d.celsius

Output

dim as Temperature a
  default: room temperature
var b = Temperature( 5.5 )
  from a number
dim as Temperature c = Temperature( "31" )
  from a number
  ... by way of the text
dim as Temperature d = -4
  from a number
Show( 12 )
  from a number
  Show got 12
dim as Temperature week( 1 to 2 )
  default: room temperature
  default: room temperature
celsius: 20, 5.5, 31, -4

The constructor with no parameters is the default constructor. A plain dim as Temperature a uses it, and so does every element of an array of objects.

The others are chosen by their arguments, wherever an object is made:

FormCalls
dim as Temperature athe default constructor
var b = Temperature( 5.5 )the float64 constructor
dim as Temperature c = Temperature( "31" )the ustring constructor
dim as Temperature d = -4the float64 constructor, as a conversion

One constructor can delegate to another with this.constructor( ... ). The text constructor converts its argument and hands the rest of the work to the number constructor, so the setup lives in one place.

A type that declares constructors but no default one cannot be created with a plain dim: the compiler refuses it with TKO0657: the type of 'a' has no constructor that takes no argument. Default parameter values, such as declare constructor( byval n as int32 = 0 ), give one constructor that serves both ways.

Pitfall

A constructor with one parameter is also an implicit conversion. Because Temperature can be built from a float64, the compiler builds one on its own whenever a number appears where a Temperature is wanted: that is what dim as Temperature d = -4 and Show( 12 ) did. There is no keyword to turn this off (declare explicit constructor is refused with TKO0936). If a silent conversion would be wrong for your type, give the constructor a second parameter, or create objects through a named function.

Activities

  1. Add a constructor that takes a temperature in Fahrenheit. What has to be different about its parameter list so the compiler can tell it from the Celsius one?
  2. Remove the default constructor and read the error for dim as Temperature a. Then give the float64 parameter a default value and try again.

Destructors

A destructor runs when an object goes away. It is where an object gives back what it holds: memory, a file, a lock. Because the compiler calls it for you, the clean-up cannot be forgotten on any path out of a scope.

type Tracer
    declare constructor( byref label as ustring )
    declare destructor( )
    label as ustring
end type

constructor Tracer( byref label as ustring )
    this.label = label
    print "  + "; label
end constructor

'' The destructor runs when the object goes away.
destructor Tracer( )
    print "  - "; label
end destructor

sub Work( )
    dim as Tracer t = Tracer( "local to Work" )
    print "  working"
end sub                             '' t is destroyed here

print "start"
dim as Tracer whole = Tracer( "program-wide" )

print "a scope:"
scope
    dim as Tracer first = Tracer( "first" )
    dim as Tracer second = Tracer( "second" )
end scope                           '' last created, first destroyed

print "a procedure:"
Work( )

print "a loop:"
for i as int32 = 1 to 2
    dim as Tracer lap = Tracer( "pass " & i )
next                                '' once per pass

print "a temporary:"
var size = len( Tracer( "temporary" ).label )
print "  its label had"; size; " characters"

print "end"

Output

start
  + program-wide
a scope:
  + first
  + second
  - second
  - first
a procedure:
  + local to Work
  working
  - local to Work
a loop:
  + pass 1
  - pass 1
  + pass 2
  - pass 2
a temporary:
  + temporary
  - temporary
  its label had 9 characters
end
  - program-wide

An object is destroyed when the block that declared it ends:

Declared inDestroyed
a scope or other blockat its end
a procedurewhen the procedure returns
a loop bodyat the end of every pass
an expression, as a temporarywhen the statement finishes
the program's top levelwhen the program ends

Objects in one block are destroyed in reverse order of creation: second before first. Something created later may depend on something created earlier, never the other way round, so this order is always safe.

A type has one destructor, with no parameters. If you do not write one, the compiler still destroys every field: a field that is itself an object gets its destructor, and a ustring field frees its text, without your help.

Objects made with new live until you delete them; they are covered in Memory and Resources. When a type is meant to be destroyed through a pointer to its base type, its destructor must be virtual: see Virtual destructors.

Activities

  1. Add a third Tracer to the scope and predict the order of the lines.
  2. Put exit for in the loop on the first pass. Does lap still get its destructor?

Properties

A property is a pair of methods that reads and writes like a field. The caller writes t.Celsius = 21.5 and print t.Celsius, and the type runs code on each side: that is where it checks the value, converts it, or computes it from other fields.

type Thermostat
    declare property Celsius( ) as float64                '' get
    declare property Celsius( byval value as float64 )    '' set
    declare property Fahrenheit( ) as float64             '' get only
    declare property Preset( byval slot as int32 ) as float64
    declare property Preset( byval slot as int32, byval value as float64 )
  private:
    celsius_ as float64
    presets_( 1 to 3 ) as float64
end type

property Thermostat.Celsius( ) as float64
    return celsius_
end property

'' The setter is where the type keeps its own rules.
property Thermostat.Celsius( byval value as float64 )
    if value < -273.15 then value = -273.15
    celsius_ = value
end property

'' Computed on demand, so it can never disagree with Celsius.
property Thermostat.Fahrenheit( ) as float64
    return celsius_ * 9 / 5 + 32
end property

'' A property can take an index, like an array the type controls.
property Thermostat.Preset( byval slot as int32 ) as float64
    return presets_( slot )
end property

property Thermostat.Preset( byval slot as int32, byval value as float64 )
    presets_( slot ) = value
end property

dim as Thermostat t
t.Celsius = 21.5                    '' calls the setter
print "celsius"; t.Celsius; ", fahrenheit"; t.Fahrenheit

t.Celsius += 2.5                    '' get, add, then set
print "warmer:"; t.Celsius

t.Celsius = -500
print "asked for -500, got "; t.Celsius


t.Preset( 2 ) = 18
print "preset 2 is"; t.Preset( 2 )

Output

celsius 21.5, fahrenheit 70.7
warmer: 24
asked for -500, got -273.15
preset 2 is 18

A property is declared with property instead of sub or function:

  • The getter takes no value and returns one: property Celsius( ) as float64. It runs wherever the property is read.
  • The setter takes the new value and returns nothing. It runs wherever the property is assigned, and it is the place the type defends its rules: asking for -500 degrees gives absolute zero.
  • A property with only a getter is read-only. Fahrenheit is computed from celsius_ every time, so it can never disagree with it.
  • A property can take an index, as Preset( slot ) does. It then reads like an array whose elements the type controls.

A property with only a setter can be assigned but never read:

'' A property with only a setter can be assigned but never read.
type Door
    declare property Code( byval value as int32 )
  private:
    code_ as int32
end type

property Door.Code( byval value as int32 )
    code_ = value
end property

dim as Door d
d.Code = 1234
print d.Code                        '' refused: there is no getter
writeonly.tiko(14,8): error TKO0934: 'code' is a property with only a setter: it can be assigned, not read
     |
  14 | print d.Code                        '' refused: there is no getter
     |        ^

The fields behind the properties are private:, which is what makes the setter's rule hold: nothing outside the type can write celsius_ directly. Access control explains the sections.

When a property only stores a value, an auto-property writes both accessors for you, and a property block keeps a getter and setter together in one place.

Activities

  1. Add a read-write Kelvin property that stores into the same celsius_ field.
  2. Make the Preset setter ignore a slot outside 1 to 3 instead of writing past the array.

Auto-properties and property blocks

Most properties only store a value. An auto-property says so in one line: the declaration ends in its accessors, and the compiler writes both of them over a hidden field.

type Person
    declare property Name as ustring get, set           '' read and written like a field
    declare property Age as int32 = 18 get, private set '' anyone reads, only Person writes
    declare property Id as int32 get, init              '' set once, by the constructor
    declare constructor( byval newId as int32 )
    declare sub Birthday( )
end type

constructor Person( byval newId as int32 )
    Id = newId
end constructor

sub Person.Birthday( )
    Age += 1
end sub

dim as Person p = Person( 42 )
p.Name = "Ann"
p.Birthday( )
print p.Name; ", age"; p.Age; ", id"; p.Id

Output

Ann, age 19, id 42
  • get, set reads and writes like a plain field.
  • get, private set can be read anywhere but written only by the type's own code, as Birthday does. The setter may be less visible than the getter, never more.
  • get, init (or get alone) can be read anywhere and assigned only by a constructor.
  • = 18 gives the hidden field its first value.

Writing a private setter from outside is refused:

type Person
    declare property Age as int32 = 18 get, private set
end type

dim as Person p
p.Age = 30                          '' refused: the setter is private
autopropserror.tiko(6,2): error TKO0625: 'age' is not accessible here

An auto-property has no code of its own. When the setter has a rule to keep, write an ordinary property over a field of your own.

Property blocks

A property block defines the getter and setter together, between property and end property. The type declares them as usual.

type Thermostat
    declare property Celsius( ) as float64
    declare property Celsius( byval value as float64 )
    declare property Fahrenheit( ) as float64
  private:
    celsius_ as float64
end type

'' Both accessors in one block. In set, value is what was assigned;
'' in get, value is what will be returned.
property Thermostat.Celsius as float64
    get
        value = celsius_
    end get
    set
        if value < -273.15 then value = -273.15
        celsius_ = value
    end set
end property

property Thermostat.Fahrenheit as float64
    get
        return celsius_ * 9 / 5 + 32
    end get
end property

dim as Thermostat t
t.Celsius = 21.5
t.Celsius += 2.5                    '' get, add, set
print "celsius"; t.Celsius; ", fahrenheit"; t.Fahrenheit
t.Celsius = -500
print "asked for -500, got "; t.Celsius

Output

celsius 24, fahrenheit 75.2
asked for -500, got -273.15

Inside both sections value is the value crossing the boundary:

  • In set, it is the value being assigned. It can be changed before it is stored, as the absolute-zero check does.
  • In get, it is the result. It starts at the type's default, and whatever it holds at end get is returned. return works too.

Either section can be left out: Fahrenheit has only a get. get and set open a section only when they stand alone on their line.

Static, indexed and namespace properties

type Grid
    declare property Cell( byval r as int32, byval c as int32 ) as int32
    declare property Cell( byval r as int32, byval c as int32, byval value as int32 )
    declare static property Created as int32 get, private set
    declare constructor( )
  private:
    cells_( 0 to 2, 0 to 2 ) as int32
end type

constructor Grid( )
    Created += 1                    '' one count for the whole type
end constructor

property Grid.Cell( byval r as int32, byval c as int32 ) as int32
    return cells_( r, c )
end property

property Grid.Cell( byval r as int32, byval c as int32, byval value as int32 )
    cells_( r, c ) = value
end property

namespace Settings
    dim shared as int32 level_
    declare property Level( ) as int32
    declare property Level( byval value as int32 )
end namespace

property Settings.Level( ) as int32
    return level_
end property

property Settings.Level( byval value as int32 )
    level_ = iif( value > 10, 10, value )
end property

sub Bump( byref n as int32 )
    n += 5
end sub

dim as Grid a, b
a.Cell( 1, 2 ) = 7
Bump( a.Cell( 1, 2 ) )              '' written back through the setter
print "cell"; a.Cell( 1, 2 ); ", grids"; Grid.Created
Settings.Level = 50
print "level"; Settings.Level

Output

cell 12, grids 2
level 10
  • A property can take any number of indices. The setter's value comes last.
  • A static property belongs to the type, not to an object: Grid.Created. It can be an auto-property too.
  • A property can live in a namespace, with no object at all.
  • A property passed to a byref parameter is written back through its setter when the call returns, so Bump changes the cell.
  • Compound assignment (+=, &=, ...) and swap call the getter and then the setter.

Activities

  1. Give Person an Email auto-property that only Person may change, and a SetEmail method that refuses a value with no @.
  2. Turn Settings.Level into a property block over level_.

Access control

A type that keeps rules about its data, such as "a balance never goes negative", can only keep them if nothing else can write that data. Access sections let a type decide which of its members the rest of the program may touch.

type Account
  public:                           '' anyone may use these
    declare constructor( byval opening as int64 )
    declare sub Deposit( byval amount as int64 )
    declare function Withdraw( byval amount as int64 ) as boolean
    declare function Balance( ) as int64
  private:                          '' only Account's own methods
    declare sub Record( byref what as ustring )
    balance_ as int64
    entries_ as int32
end type

constructor Account( byval opening as int64 )
    balance_ = iif( opening < 0, 0, opening )
    Record( "opened" )
end constructor

sub Account.Deposit( byval amount as int64 )
    if amount <= 0 then exit sub    '' the type keeps its own rules
    balance_ += amount
    Record( "deposit" )
end sub

function Account.Withdraw( byval amount as int64 ) as boolean
    if amount <= 0 orelse amount > balance_ then return false
    balance_ -= amount
    Record( "withdrawal" )
    return true
end function

function Account.Balance( ) as int64
    return balance_
end function

'' A private method is a helper the outside world never sees.
sub Account.Record( byref what as ustring )
    entries_ += 1
    print "  entry"; entries_; ": "; what
end sub

dim as Account acct = Account( 100 )
acct.Deposit( 50 )
acct.Deposit( -1000 )               '' ignored
var tooMuch = acct.Withdraw( 500 )
var enough = acct.Withdraw( 30 )
print "withdraw 500: "; tooMuch; ", withdraw 30: "; enough
print "balance:"; acct.Balance( )

Output

  entry 1: opened
  entry 2: deposit
  entry 3: withdrawal
withdraw 500: false, withdraw 30: true
balance: 120

Inside a type, a label starts a section that runs to the next label:

LabelMembers reachable from
public:anywhere; this is the default before any label
private:the type's own methods, constructors and operators only
protected:the type's own methods and those of types derived from it

The private section of Account holds the balance, an entry counter and Record, a helper method. Every change to the balance goes through Deposit or Withdraw, which refuse what the account does not allow, so the balance cannot go wrong from outside. Reaching for the field directly is a compile error:

type Account
    declare function Balance( ) as int64
  private:
    balance_ as int64
end type

function Account.Balance( ) as int64
    return balance_
end function

dim as Account acct
acct.balance_ = 1000000             '' refused: balance_ is private
accesserror.tiko(12,5): error TKO0625: 'balance_' is not accessible here
     |
  12 | acct.balance_ = 1000000             '' refused: balance_ is private
     |     ^

A global operator, such as an operator = written for the type, is not a method, so it sees only the public members too; build it from the type's public methods. protected: matters once types extend one another, in Inheritance.

The trailing underscore on balance_ is a naming habit, not syntax. It marks the data the methods look after and leaves the plain name free for a method or property that exposes it.

Status in this version

A type with a private constructor and a public static function that creates it is the usual way to insist that objects are made only through that function. In this version var acct = Account.Open( 100 ) is refused with TKO0633 as soon as the type has any private constructor, so that pattern cannot be used yet.

Activities

  1. Add a public Entries( ) function that returns how many entries Record has made.
  2. Move Deposit into the private section and read the error for acct.Deposit( 50 ).

Static members

Some data belongs to the type rather than to any one object: a count of how many have been made, a shared table, a setting. A static member exists once, however many objects there are, including none.

type Ticket
    declare constructor( )
    declare static function Issued( ) as int32
    declare static function Reset( ) as int32
    declare function Number( ) as int32
  private:
    static issued_ as int32         '' one copy, shared by every Ticket
    number_ as int32                '' one per Ticket
end type

'' A definition outside the type gives the static field its starting
'' value. Without one, it starts at 0.
dim as int32 Ticket.issued_ = 100

constructor Ticket( )
    issued_ += 1
    number_ = issued_
end constructor

'' A static method has no object: it is called on the type itself.
static function Ticket.Issued( ) as int32
    return issued_
end function

static function Ticket.Reset( ) as int32
    dim as int32 before = issued_
    issued_ = 0
    return before
end function

function Ticket.Number( ) as int32
    return number_
end function

print "counter before any ticket:"; Ticket.Issued( )
dim as Ticket a, b, c
print "counter:"; Ticket.Issued( ); ", b is number"; b.Number( )

print "reset after"; Ticket.Reset( )
dim as Ticket d
print "d is number"; d.Number( ); ", c is still"; c.Number( )

Output

counter before any ticket: 100
counter: 103, b is number 102
reset after 103
d is number 1, c is still 103

A static field is declared in the type with static and shared by every Ticket. Each constructor call increments the same issued_, while each ticket keeps its own number_. The line outside the type, dim as int32 Ticket.issued_ = 100, gives the shared field its starting value; without it, the field starts at 0.

A static method has no object, so it has no this and cannot use number_ or any other per-object field; it is called on the type, Ticket.Issued( ). That is why it can answer before any ticket exists. Reaching for a per-object field from one is refused with TKO0937: 'number_' needs an object, and 'issued' has none: it is a static member.

Static members follow the same access rules as any other: issued_ is private, so only Ticket's own code can change it, and the outside world reads it through Issued( ).

A static field takes no room in an object: it is stored once, apart from them all.

Activities

  1. Add a static Peak( ) that reports the highest number ever issued, even after a Reset.
  2. Try to print Ticket.issued_ from outside the type, then move it to the public section and try again.

Const methods

A procedure that takes an object byref as const promises not to change it. That promise is only useful if the compiler can tell which of the object's methods keep it, and a const method is one that does.

type Rect
    declare const function Area( ) as float64     '' promises not to
    declare const function Describe( ) as ustring '' change the object
    declare sub Scale( byval k as float64 )
    w as float64
    h as float64
end type

const function Rect.Area( ) as float64
    return w * h
end function

'' A const method may call other const methods.
const function Rect.Describe( ) as ustring
    return w & " x " & h & ", area " & Area( )
end function

sub Rect.Scale( byval k as float64 )
    w *= k
    h *= k
end sub

'' A const parameter can be read, and its const methods called.
sub Report( byref r as const Rect )
    print "  "; r.Describe( )
end sub

dim as Rect r = ( 2, 3 )
Report( r )
r.Scale( 2 )                        '' an ordinary object can change
Report( r )

Output

  2 x 3, area 6
  4 x 6, area 24

declare const function Area( ) marks Area as a method that reads the object but never writes it, and the definition repeats the word: const function Rect.Area( ). Inside, the compiler holds the method to its promise: assigning a field w is refused with TKO0664: 'w' cannot be written from a const method, and calling Scale from it with TKO0665: a const method cannot call the non-const method 'scale'.

Report takes its rectangle byref as const, so it receives the caller's object without a copy and may not change it. It may call Describe and Area, which are const. Calling Scale is refused:

type Rect
    declare const function Area( ) as float64
    declare sub Scale( byval k as float64 )
    w as float64
    h as float64
end type

const function Rect.Area( ) as float64
    return w * h
end function

sub Rect.Scale( byval k as float64 )
    w *= k
    h *= k
end sub

sub Report( byref r as const Rect )
    print r.Area( )                 '' fine: Area is const
    r.Scale( 2 )                    '' refused: Scale is not
end sub
constcall.tiko(19,6): error TKO0935: 'scale' is not a const method, so a const object cannot call it
     |
  19 |     r.Scale( 2 )                    '' refused: Scale is not
     |      ^

Mark every method that only reads as const. A method left unmarked cannot be called on a const object at all, even though it changes nothing, and every procedure that takes the type byref as const loses it.

Activities

  1. Remove const from Describe and read the error. Which call does the compiler point at?
  2. Add a const function IsSquare( ) as boolean and call it from Report.

Operator overloading

A type that behaves like a number reads best when it is written like one: half + third rather than half.Plus( third ). An operator overload is a procedure the compiler calls when it sees an operator applied to your type.

function Gcd( byval a as int64, byval b as int64 ) as int64
    a = abs( a ) : b = abs( b )
    do while b <> 0
        dim as int64 t = a mod b
        a = b : b = t
    loop
    return a
end function

type Fraction
    declare constructor( byval n as int64 = 0, byval d as int64 = 1 )
    declare operator let( byval whole as int64 )  '' members: they change
    declare operator +=( byref o as Fraction )    '' or convert this
    declare operator cast( ) as ustring           '' object
    num as int64
    den as int64
end type

constructor Fraction( byval n as int64, byval d as int64 )
    dim as int64 g = Gcd( n, d )
    if d < 0 then g = -g             '' keep the sign on top
    num = n \ g : den = d \ g
end constructor

operator Fraction.let( byval whole as int64 )
    num = whole : den = 1
end operator

operator Fraction.cast( ) as ustring
    if den = 1 then return str( num )
    return num & "/" & den
end operator

'' Binary operators are global: neither side is privileged.
operator + ( byref a as Fraction, byref b as Fraction ) as Fraction
    return Fraction( a.num * b.den + b.num * a.den, a.den * b.den )
end operator

operator * ( byref a as Fraction, byref b as Fraction ) as Fraction
    return Fraction( a.num * b.num, a.den * b.den )
end operator

operator - ( byref a as Fraction ) as Fraction    '' unary minus
    return Fraction( -a.num, a.den )
end operator

operator = ( byref a as Fraction, byref b as Fraction ) as boolean
    return a.num = b.num andalso a.den = b.den
end operator

operator <> ( byref a as Fraction, byref b as Fraction ) as boolean
    return not ( a = b )
end operator

operator < ( byref a as Fraction, byref b as Fraction ) as boolean
    return a.num * b.den < b.num * a.den
end operator

operator Fraction.+=( byref o as Fraction )
    this = Fraction( num * o.den + o.num * den, den * o.den )
end operator

dim as Fraction half = Fraction( 1, 2 ), third = Fraction( 1, 3 )

print "1/2 + 1/3 = "; half + third
print "1/2 * 1/3 = "; half * third
print "-(1/2)    = "; -half
print "2/4 = 1/2:  "; Fraction( 2, 4 ) = half
print "1/3 < 1/2:  "; third < half; ", 1/3 <> 1/2: "; third <> half
print "1/2 + 1/3 * 1/2 = "; half + third * half; "  (* first)"

dim as Fraction total
total = 3                           '' operator let, from an int64
total += half
total += Fraction( 6, -4 )
print "3 + 1/2 - 3/2 = "; total

Output

1/2 + 1/3 = 5/6
1/2 * 1/3 = 1/6
-(1/2)    = -1/2
2/4 = 1/2:  true
1/3 < 1/2:  true, 1/3 <> 1/2: true
1/2 + 1/3 * 1/2 = 2/3  (* first)
3 + 1/2 - 3/2 = 2

An operator is defined like a function whose name is the operator: operator + ( byref a as Fraction, byref b as Fraction ) as Fraction. From then on, a + b on two fractions is a call to it.

Where an operator is declared depends on what it acts on:

OperatorsDeclaredWhy
+ - * /, =, <>, <, unary -, ...outside the typeneither operand is privileged
let, += and the other compound assignments, castinside the typethey change, or convert from, this object
[]inside the typesee Indexing

Declaring a binary operator inside the type is refused with TKO0790: operator '+' cannot be a member of a type.

A few things in the example are deliberate:

  • Each comparison is its own operator. Defining = does not define <>, so <> is written as not ( a = b ) to keep the two in agreement.
  • Precedence does not change. half + third * half multiplies first, because the expression is parsed before any operator procedure is chosen.
  • operator let( byval whole as int64 ) makes total = 3 work: it is assignment from another type. Assignment from another Fraction is the ordinary copy, covered in Copying.
  • operator cast( ) as ustring is what lets print show a fraction, as on the Display page.

A global operator sees only the type's public members, like any other code outside the type. That keeps operators from breaking the type's rules.

Status in this version

Inside a method, this cannot yet be an operand of an overloaded operator: this = this + o is refused with TKO0820: no operator '+' takes these operand types. Build the result from the fields, as += does here, or copy this into a local first.

Activities

  1. Add operator - for two fractions, and operator >.
  2. Add operator cast( ) as float64 and print cast( float64, third ).

Indexing

A type that holds a sequence of values can let callers reach them with square brackets, r[ i ], while deciding for itself what an index means. operator [] is the procedure behind the brackets.

'' The last four readings, oldest first. Index 0 is always the oldest,
'' wherever it happens to sit in the storage.
type Recent
    declare sub Push( byval value as int32 )
    declare function Count( ) as int32
    declare operator []( byval i as int32 ) byref as int32
  private:
    slots_( 0 to 3 ) as int32
    start_ as int32                 '' where the oldest reading is
    count_ as int32
end type

sub Recent.Push( byval value as int32 )
    if count_ < 4 then
        slots_( ( start_ + count_ ) mod 4 ) = value
        count_ += 1
    else                            '' full: overwrite the oldest
        slots_( start_ ) = value
        start_ = ( start_ + 1 ) mod 4
    end if
end sub

function Recent.Count( ) as int32
    return count_
end function

'' Returning byref hands back the element itself, so r[ i ] can be
'' read, assigned and updated in place.
operator Recent.[]( byval i as int32 ) byref as int32
    return slots_( ( start_ + i ) mod 4 )
end operator

sub Show( byref r as Recent )
    for i as int32 = 0 to r.Count( ) - 1
        print r[ i ];
    next
    print
end sub

dim as Recent r
for reading as int32 = 1 to 6
    r.Push( reading * 10 )
next
Show( r )                           '' 10 and 20 have been pushed out

r[ 0 ] = 99                         '' assign through the index
r[ 3 ] += 1                         '' or update in place
Show( r )

Output

 30 40 50 60
 99 40 50 61

Recent keeps the last four readings in a fixed array that it reuses in a circle. Where the oldest reading sits in that array changes as readings arrive, but callers never see it: r[ 0 ] is always the oldest, because the operator turns the index into a position.

byref as int32 is the important part. The operator returns the element itself rather than a copy of its value, so r[ i ] can stand on the left of =, and r[ 3 ] += 1 updates the element in place.

operator [] is declared inside the type and takes exactly one index, of any type. A string index works as well as a number, which is how a dictionary-like type offers table[ "key" ]. A second index is refused with TKO0789: operator '[]' takes 1 parameter(s) here; for a grid, take a row and column through a method or an indexed property instead.

Status in this version

An operator [] that returns by value, without byref, should give a read-only index. Reading through it works, but assigning to it stops the build with TKO0994: internal compiler error rather than a proper diagnostic. Return byref whenever the elements may be written.

The standard containers, such as Array( of T ) and Map( of K, V ), use this same operator: see Containers.

Activities

  1. Make the capacity a constant and change Recent to keep the last eight readings.
  2. Make [] refuse an index outside 0 to Count( ) - 1 by stopping the program with an assert.

Copying

dim as Contact b = a makes b a copy of a. For most types that needs no thought: the compiler copies every field, and each field knows how to copy itself. A type that owns something through a pointer is different, and must say what a copy means.

'' Fields that copy themselves need nothing more: the default copy
'' copies every field, and a ustring copies its text.
type Contact
    fullName as ustring
    phone as ustring
end type

dim as Contact a = ( "Ada", "555-0100" )
dim as Contact b = a                '' copy
b.phone = "555-0199"
print a.fullName; " "; a.phone; " / "; b.fullName; " "; b.phone

'' A type that owns memory through a pointer must say what a copy
'' means, or two objects would share, and both free, one block.
type Samples
    declare constructor( byval n as int32 )
    declare constructor( byref other as Samples )     '' copy
    declare operator let( byref other as Samples )    '' assign
    declare destructor( )
    items as int32 ptr
    size as int32
end type

constructor Samples( byval n as int32 )
    size = n
    items = callocate( n, sizeof( int32 ) )
end constructor

'' A new object made from an existing one: nothing to free first.
constructor Samples( byref other as Samples )
    size = other.size
    items = allocate( size * sizeof( int32 ) )
    for i as int32 = 0 to size - 1 : items[ i ] = other.items[ i ] : next
    print "  copy constructor"
end constructor

'' Assigning into an object that already owns a block: free it first,
'' unless the object is being assigned to itself.
operator Samples.let( byref other as Samples )
    if @this = @other then exit operator
    deallocate( items )
    size = other.size
    items = allocate( size * sizeof( int32 ) )
    for i as int32 = 0 to size - 1 : items[ i ] = other.items[ i ] : next
    print "  operator let"
end operator

destructor Samples( )
    deallocate( items )
end destructor

sub Inspect( byval s as Samples )      '' byval makes a copy, too
    print "  Inspect sees"; s.items[ 0 ]
end sub

dim as Samples x = Samples( 3 )
x.items[ 0 ] = 7

print "dim as Samples y = x"
dim as Samples y = x
y.items[ 0 ] = 8

print "z = x"
dim as Samples z = Samples( 1 )
z = x
z = z                               '' to itself: nothing happens

print "Inspect( x )"
Inspect( x )
print "x, y, z hold"; x.items[ 0 ]; y.items[ 0 ]; z.items[ 0 ]

Output

Ada 555-0100 / Ada 555-0199
dim as Samples y = x
  copy constructor
z = x
  operator let
Inspect( x )
  copy constructor
  Inspect sees 7
x, y, z hold 7 8 7

The default copy copies each field. Contact is two ustrings, and a ustring copies its text, so changing b.phone leaves a alone. A type built only from fields like these, numbers, strings and other well-behaved objects, needs no copying code at all.

A pointer field is copied as a pointer. Without its own copy code, Samples would give y the same items block as x: a write through one would show in the other, and both destructors would free the same memory. Two procedures prevent that:

ProcedureRuns forMust
copy constructor, constructor( byref other as Samples )dim as Samples y = x, and every byval argumentmake a new block and copy the values
operator let( byref other as Samples )z = x, on an object that already existsfree the old block first, and do nothing when assigned to itself

The self-assignment check in operator let matters: without it, z = z would free z's block and then copy from the memory it had freed.

The output shows the costs as well. Inspect( x ) takes its parameter byval, so a whole copy was made to call it. Pass an object byref, or byref as const when the procedure only reads it, and no copy is made.

Pitfall

A type that needs a destructor almost always needs a copy constructor and an operator let too: the destructor is there because the type owns something, and the other two decide what happens to that something when the object is duplicated. Write all three or none.

Activities

  1. Remove the if @this = @other line, run z = z, and print z.items[ 0 ]. What did you get, and why is it not reliable?
  2. Change Inspect to take byref as const Samples and check that the copy constructor no longer runs for it.

Inheritance

When one type is a more specific kind of another, a bicycle being a kind of vehicle, extends lets it start as the general type and add to it. The derived type gets every field and method of its base without repeating them, and it can be handed to any code written for the base.

type Vehicle
    declare constructor( byval wheels as int32 = 4 )
    declare function Describe( ) as ustring
    wheels as int32
    speed as int32
end type

constructor Vehicle( byval wheels as int32 )
    this.wheels = wheels
end constructor

function Vehicle.Describe( ) as ustring
    return wheels & " wheels at " & speed & " km/h"
end function

'' A Bicycle is a Vehicle with more fields.
type Bicycle extends Vehicle
    declare constructor( byval gears as int32 )
    gears as int32
end type

constructor Bicycle( byval gears as int32 )
    base( 2 )                       '' build the Vehicle part first
    this.gears = gears
end constructor

'' Anything written for a Vehicle accepts a Bicycle.
sub Report( byref v as Vehicle )
    print "  "; v.Describe( )
end sub

dim as Bicycle bike = Bicycle( 21 )
bike.speed = 25                     '' a field it inherited
print "bike has"; bike.gears; " gears"
Report( bike )

dim as Vehicle car
car.speed = 90
Report( car )

'' The base's fields come first, laid out exactly as in a Vehicle.
print "sizes:"; sizeof( Vehicle ); sizeof( Bicycle )
print "offsets:"; offsetof( Bicycle, wheels ); offsetof( Bicycle, speed ); _
      offsetof( Bicycle, gears )

Output

bike has 21 gears
  2 wheels at 25 km/h
  4 wheels at 90 km/h
sizes: 8 12
offsets: 0 4 8

type Bicycle extends Vehicle makes Bicycle a Vehicle with one more field. bike.speed and bike.Describe( ) work although Bicycle declares neither; they are inherited.

The base part is built first. base( 2 ), the first statement of the Bicycle constructor, chooses which Vehicle constructor builds the Vehicle part and with what. Leave it out and the base's default constructor runs instead. Calling the base covers this further.

The layout is the base's, then the new fields. The offsets show wheels and speed exactly where they are in a Vehicle, with gears after them. A Bicycle starts with a complete Vehicle, which is why Report, written for a Vehicle, accepts bike as it is.

A type extends exactly one base. To be several things at once, it implements interfaces instead: any number of them, beside its one base.

The pages in this section:

PageCovers
Virtual methodsletting the object, not the variable, choose the method
Overridingoverride, and when an override can be overridden again
Abstract methodsmethods every derived type must supply
Calling the basebase( ... ) and base.Method( )
Runtime type checksis
Interfacesinterface, implements and class: a contract any type can take on
Several interfacesone type, many contracts; renamed and private mappings; trycast
Virtual destructorsdeleting through a base pointer
Slicingwhat passing by value does to a derived object

Activities

  1. Add a Truck that extends Vehicle with a load field and a constructor passing 6 to base( ... ), and pass one to Report.
  2. Print offsetof( Truck, load ) and explain the number.

Virtual methods

A derived type can declare a method with the same name as one in its base. Whether code written for the base then reaches the derived version depends on one word, virtual. Without it, the type of the variable decides:

'' Without virtual, the type the compiler sees picks the method.
type Animal
    declare function Sound( ) as ustring
    legs as int32
end type

function Animal.Sound( ) as ustring
    return "..."
end function

type Dog extends Animal
    declare function Sound( ) as ustring    '' hides Animal.Sound
end type

function Dog.Sound( ) as ustring
    return "woof"
end function

sub Speak( byref a as Animal )
    print "  through an Animal: "; a.Sound( )
end sub

dim as Dog rex
print "directly: "; rex.Sound( )
Speak( rex )

Output

directly: woof
  through an Animal: ...

Dog.Sound hides Animal.Sound: on a Dog, Sound means the dog's. But inside Speak the parameter is an Animal, so a.Sound( ) is compiled as a call to Animal.Sound, whatever object arrives. That is the right behaviour for most methods, and it costs nothing at run time.

When code that sees "an animal" must reach each object's own method, make the method virtual:

'' extends Object gives the type a table of virtual methods.
type Animal extends Object
    declare virtual function Sound( ) as ustring
    declare function Introduce( ) as ustring
end type

function Animal.Sound( ) as ustring
    return "..."
end function

'' Not virtual itself, but the call inside it is.
function Animal.Introduce( ) as ustring
    return "I say " & Sound( )
end function

type Dog extends Animal
    declare function Sound( ) as ustring override
end type

function Dog.Sound( ) as ustring
    return "woof"
end function

type Cat extends Animal
    declare function Sound( ) as ustring override
end type

function Cat.Sound( ) as ustring
    return "meow"
end function

sub Speak( byref a as Animal )
    print "  through an Animal: "; a.Sound( )
end sub

dim as Dog rex
dim as Cat tom
dim as Animal someone
Speak( rex )
Speak( tom )
Speak( someone )

'' One loop, many types: each call reaches the object's own method.
dim as Animal ptr zoo( 0 to 2 ) = { @rex, @tom, @someone }
for i as int32 = 0 to 2
    print "  "; zoo( i )->Introduce( )
next

Output

  through an Animal: woof
  through an Animal: meow
  through an Animal: ...
  I say woof
  I say meow
  I say ...

Three things changed:

  • extends Object. Object is the built-in root type. Extending it gives every object a hidden pointer to its type's table of virtual methods, which is how a call finds the right code at run time.
  • virtual on Animal.Sound says derived types may replace it. A call to it goes through the table, so it reaches the version that belongs to the object really there.
  • override on Dog.Sound and Cat.Sound says each replaces a virtual method. The compiler checks there is one to replace; see Overriding.

Introduce is not virtual, yet its answer differs for every animal, because the call to Sound inside it is virtual. The base writes the procedure once; the derived types supply the part that differs. The array of Animal ptr shows the payoff: one loop over many types, with no select case on what each one is.

A virtual call is an indirect call through the table, a few instructions more than a direct one, and the hidden pointer adds 8 bytes to every object (see Slicing). Make a method virtual because a derived type needs to replace it.

Activities

  1. Add a Cow to virtual.tiko and put it in the zoo array. What did you have to change outside Cow itself?
  2. Remove virtual from Animal.Sound and read the compiler's message for the override lines.

Overriding

override marks a method that replaces a virtual one from the base, and asks the compiler to check that there is one to replace. Whether types further down can replace it again is decided by virtual.

type Animal extends Object
    declare virtual function Sound( ) as ustring
end type

function Animal.Sound( ) as ustring
    return "..."
end function

'' override alone replaces Animal's Sound, and closes it there.
type Cat extends Animal
    declare function Sound( ) as ustring override
end type

function Cat.Sound( ) as ustring
    return "meow"
end function

type Kitten extends Cat
    declare function Sound( ) as ustring    '' hides; cannot override
end type

function Kitten.Sound( ) as ustring
    return "mew"
end function

'' virtual and override together replace it and keep it open.
type Dog extends Animal
    declare virtual function Sound( ) as ustring override
end type

function Dog.Sound( ) as ustring
    return "woof"
end function

type Puppy extends Dog
    declare function Sound( ) as ustring override
end type

function Puppy.Sound( ) as ustring
    return "yip"
end function

sub Speak( byref a as Animal )
    print "  through an Animal: "; a.Sound( )
end sub

dim as Kitten kit
dim as Puppy pup
print "kitten, directly: "; kit.Sound( )
Speak( kit )
print "puppy, directly: "; pup.Sound( )
Speak( pup )

Output

kitten, directly: mew
  through an Animal: meow
puppy, directly: yip
  through an Animal: yip

An override is not virtual unless it says so. Cat.Sound is declared with override alone. It replaces Animal.Sound, and that is the end of the line: Kitten.Sound can only hide it. Called on a Kitten directly it answers mew, but through an Animal the call reaches Cat.Sound.

Dog.Sound is declared virtual ... override. It replaces Animal.Sound and keeps the method open, so Puppy.Sound overrides it in turn and is reached through an Animal as well.

Declared asReplaces an open virtual method of the baseOpen to the next level down
virtualyes, if there is oneyes
overrideyes, and it is an error if there is noneno
virtual ... overrideyes, and it is an error if there is noneyes
neitheryes, if there is oneno

A method with the same name and parameters as an open virtual method replaces it whether or not it says override. What the keyword adds is the check: writing override where nothing virtual is left to replace is an error, so a misspelt name, or a base method that is not virtual, is caught when the program compiles instead of quietly hiding.

type Animal extends Object
    declare virtual function Sound( ) as ustring
end type

function Animal.Sound( ) as ustring
    return "..."
end function

type Cat extends Animal
    declare function Sound( ) as ustring override
end type

function Cat.Sound( ) as ustring
    return "meow"
end function

'' Cat.Sound is an override but not virtual: nothing left to replace.
type Kitten extends Cat
    declare function Sound( ) as ustring override
end type

function Kitten.Sound( ) as ustring
    return "mew"
end function
overrideerror.tiko(19,13): error TKO0621: 'sound' is marked 'override' but overrides no virtual method
     |
  19 |     declare function Sound( ) as ustring override
     |             ^

The fix is one word in Cat: declare virtual function Sound( ) as ustring override. When you write a type that others will extend, write virtual with every override that they may need to replace.

Activities

  1. Change Cat.Sound to virtual ... override and check what Speak( kit ) prints now.
  2. Misspell Sound as Sund in Puppy, keeping override, and read the error.

Abstract methods

Some base types cannot say how to do something, only that every derived type can. A Shape has no area of its own, yet every shape has one. An abstract method is a virtual method with no body: the base declares it, and each derived type must supply it.

'' Shape says every shape has an area and a name, without saying how.
type Shape extends Object
    declare abstract function Area( ) as float64
    declare abstract function Name( ) as ustring
    declare function Describe( ) as ustring
end type

function Shape.Describe( ) as ustring
    return Name( ) & " with area " & Area( )
end function

type Rect extends Shape
    declare constructor( byval w as float64, byval h as float64 )
    declare function Area( ) as float64 override
    declare function Name( ) as ustring override
    w as float64
    h as float64
end type

constructor Rect( byval w as float64, byval h as float64 )
    this.w = w : this.h = h
end constructor

function Rect.Area( ) as float64
    return w * h
end function

function Rect.Name( ) as ustring
    return "a " & w & " by " & h & " rectangle"
end function

type Circle extends Shape
    declare constructor( byval r as float64 )
    declare function Area( ) as float64 override
    declare function Name( ) as ustring override
    r as float64
end type

constructor Circle( byval r as float64 )
    this.r = r
end constructor

function Circle.Area( ) as float64
    return 3.14159 * r * r
end function

function Circle.Name( ) as ustring
    return "a circle of radius " & r
end function

dim as Rect door = Rect( 1, 2 )
dim as Circle coin = Circle( 2 )
dim as Shape ptr shapes( 0 to 1 ) = { @door, @coin }

dim as float64 total = 0
for i as int32 = 0 to 1
    print shapes( i )->Describe( )
    total += shapes( i )->Area( )
next
print "total area:"; total

Output

a 1 by 2 rectangle with area 2
a circle of radius 2 with area 12.56636
total area: 14.56636

declare abstract function Area( ) as float64 declares the method and nothing else; there is no Shape.Area body anywhere. Rect and Circle each supply both abstract methods with override, and Shape.Describe, written once in the base, calls whichever versions the object really has.

A type with an abstract method is incomplete on purpose, so no object of it can exist; there would be nothing to run if someone asked for its area:

type Shape extends Object
    declare abstract function Area( ) as float64
end type

dim as Shape s                      '' refused: Area has no body
print s.Area( )
abstracterror.tiko(5,14): error TKO0620: 's' has an abstract type, which cannot be instantiated
     |
   5 | dim as Shape s                      '' refused: Area has no body
     |              ^

A derived type that leaves any abstract method unwritten is abstract too, and refused with the same error. What can exist is a Shape ptr, or a byref parameter of type Shape, referring to an object of a complete type, as the shapes array does.

Like virtual, abstract needs a type that extends Object: on any other type it is refused with TKO0645: 'area' is virtual, but its type does not extend 'object'.

Activities

  1. Add a Triangle with a base and a height, and put one in the array. Which existing lines did you have to change?
  2. Leave Name out of Triangle and read the error.

Calling the base

A derived type usually adds to what its base does rather than replacing it. base reaches the base's version of the work: base( ... ) in a constructor builds the base part, and base.Method( ) calls the base's method from inside the derived one.

type Account extends Object
    '' Defaults give Account a constructor that needs no argument, which
    '' every type derived from it needs to be copyable.
    declare constructor( byref owner as ustring = "", _
                         byval opening as int64 = 0 )
    declare virtual function Describe( ) as ustring
    owner as ustring
    balance as int64
end type

constructor Account( byref owner as ustring, byval opening as int64 )
    this.owner = owner
    balance = opening
    print "  Account constructor"
end constructor

function Account.Describe( ) as ustring
    return owner & ": " & balance
end function

type Savings extends Account
    declare constructor( byref owner as ustring, byval opening as int64, _
                         byval rate as float64 )
    declare function Describe( ) as ustring override
    rate as float64
end type

constructor Savings( byref owner as ustring, byval opening as int64, _
                     byval rate as float64 )
    '' base( ... ) picks the base constructor; it runs before the rest.
    base( owner, opening )
    this.rate = rate
    print "  Savings constructor"
end constructor

'' base.Describe( ) calls Account's version, not this one, so the
'' derived text extends the base text instead of repeating it.
function Savings.Describe( ) as ustring
    return base.Describe( ) & " at " & rate & "%"
end function

dim as Savings s = Savings( "Ada", 500, 2.5 )
dim as Account ptr p = @s
print p->Describe( )

Output

  Account constructor
  Savings constructor
Ada: 500 at 2.5%

base( owner, opening ) chooses the base constructor. It is the first statement of the Savings constructor, and the Account part is built before anything else runs, as the output shows. Its arguments choose among the base's constructors like any overloaded call. Without a base( ... ) line, the base's default constructor runs.

base.Describe( ) calls Account.Describe, even though Describe is virtual and this is a Savings. A plain Describe( ) there would call Savings.Describe again, forever. Building on the base's text means the two descriptions cannot drift apart.

The comment on Account's constructor matters. A derived type is given a copy constructor without writing one, and that free one starts by building the base part with the base's default constructor. If the base has none, the derived type cannot be copied, and the compiler says so at its declaration:

'' A base with no constructor that takes no argument.
type Account
    declare constructor( byval opening as int64 )
    balance as int64
end type

constructor Account( byval opening as int64 )
    balance = opening
end constructor

type Savings extends Account
    declare constructor( byval opening as int64 )
    rate as float64
end type

constructor Savings( byval opening as int64 )
    base( opening )
end constructor

dim as Savings s = Savings( 100 )
print s.balance
nodefault.tiko(11,1): error TKO0926: 'savings' needs a copy constructor: its base has no constructor that takes no argument, so none can be made for it
     |
  11 | type Savings extends Account
     | ^

There are two ways out: give the base constructor's parameters default values, as base.tiko does, or write the copy constructor yourself, declare constructor( byref other as const Savings ), and call base( ... ) from it.

Status in this version

base( ... ) belongs on the first line of the constructor, but the compiler does not yet insist on it. Written later, it is accepted without a word, and whatever the constructor assigned to its own fields before it is lost: this.rate = rate followed by base( owner, opening ) leaves rate at 0. Keep base( ... ) first.

Activities

  1. Add a Checking type that extends Account with an overdraft field, and give it a Describe that builds on base.Describe( ).
  2. Fix nodefault.tiko the second way, with a copy constructor of its own.

Runtime type checks

Code holding a base type sometimes needs something only one derived type has. is asks, while the program runs, what an object really is.

type Message extends Object
    declare function Summary( ) as ustring
    sender as ustring
end type

function Message.Summary( ) as ustring
    return "from " & sender
end function

type TextMessage extends Message
    text as ustring
end type

type Reply extends TextMessage
    inReplyTo as int32
end type

type Picture extends Message
    w as int32
    h as int32
end type

sub Show( byref m as Message )
    print m.Summary( );
    '' is asks what the object really is, at run time. It is true for
    '' the type named and for anything derived from it.
    if m is TextMessage then
        '' Safe now: the object really is a TextMessage.
        dim as TextMessage ptr t = cast( TextMessage ptr, @m )
        print ": "; t->text;
    end if
    if m is Reply then
        print " (reply to #" & cast( Reply ptr, @m )->inReplyTo & ")";
    end if
    if m is Picture then print ": a picture";
    print
end sub

dim as TextMessage hello
hello.sender = "Ada" : hello.text = "hello"
dim as Reply answer
answer.sender = "Grace" : answer.text = "hi!" : answer.inReplyTo = 1
dim as Picture photo
photo.sender = "Alan" : photo.w = 640 : photo.h = 480

Show( hello )
Show( answer )
Show( photo )

Output

from Ada: hello
from Grace: hi! (reply to #1)
from Alan: a picture

m is TextMessage is true when the object is a TextMessage or anything derived from one, which is why the reply passes both the TextMessage and the Reply tests. Once it is true, cast( TextMessage ptr, @m ) views the same object as a TextMessage, and its text field can be read.

The cast itself checks nothing. Cast to the wrong type and the program reads whatever memory follows the base part, so always guard a cast like this with is.

is reads the answer from the hidden table pointer, so the type must extend Object, and it asks only about types derived from the variable's own type. Both are checked when the program compiles. An interface value is the exception: v is Note and v is ISize ask about the object behind v, whatever its interface (Several interfaces).

AskingRefused with
p is More, where p's type does not extend ObjectTKO0799: 'is' needs run-time type information, and 'plain' has none: it does not extend 'object'
t is TextMessage, where t is already a TextMessageTKO0798: 'is textmessage' needs a type derived from 'textmessage', and 'textmessage' is not one

Pitfall

A chain of if m is TextMessage ... elseif m is Picture ... is a select case on the type, and it has to be edited every time a new kind of message appears. When the question is "what should this object do", the answer is a virtual method, which each new type supplies for itself. Keep is for the few places that truly need a derived type's own data.

Activities

  1. Add a Voice message with a seconds field and make Show print its length.
  2. Replace the is tests with a virtual Detail( ) method that each message type overrides. Which version is easier to extend?

Interfaces

An interface is a promise that a type can do certain things, with nothing said about how. It lists methods and properties, and has no fields and no code. Any type can take one on with an implements line, and a type can take on as many as it needs without giving up its one base.

'' A contract: what an encoder can do, with no data and no code. Any type
'' can take it on with an 'implements' line.
interface IEncoder
    function Name( ) as ustring
    function Encode( byref key as ustring, byval value as int32 ) as ustring
end interface

'' A plain type: no base, no vtable.
type IniEncoder
    implements IEncoder
    declare function Name( ) as ustring implements IEncoder.Name
    declare function Encode( byref key as ustring, _
                             byval value as int32 ) as ustring _
                             implements IEncoder.Encode
    sep as ustring = "="
end type

function IniEncoder.Name( ) as ustring
    return "ini"
end function

function IniEncoder.Encode( byref key as ustring, _
                            byval value as int32 ) as ustring
    return key & sep & value
end function

'' A class: a type that always extends Object.
class JsonEncoder
    implements IEncoder
    declare function Name( ) as ustring implements IEncoder.Name
    declare function Encode( byref key as ustring, _
                             byval value as int32 ) as ustring _
                             implements IEncoder.Encode
end class

function JsonEncoder.Name( ) as ustring
    return "json"
end function

function JsonEncoder.Encode( byref key as ustring, _
                             byval value as int32 ) as ustring
    return """" & key & """: " & value
end function

'' Written once, against the contract only.
sub Save( byval enc as IEncoder )
    print "as "; enc.Name( ); ":"
    print "  "; enc.Encode( "width", 640 )
    print "  "; enc.Encode( "height", 480 )
end sub

dim as IniEncoder ini
dim as JsonEncoder json
Save( ini )
Save( json )
print "an IEncoder value is"; sizeof( IEncoder ); " bytes"

Output

as ini:
  width=640
  height=480
as json:
  "width": 640
  "height": 480
an IEncoder value is 16 bytes

IEncoder is all contract: two members, every one public and abstract, so nothing else is written on them. Save is written against IEncoder alone. It runs unchanged for every encoder there is, including ones written after it.

implements IEncoder at the top of a type's body takes the contract on. Then each method says which member it provides: implements IEncoder.Name. IniEncoder is a plain type, with no base and no table of virtual methods; JsonEncoder is a class, a type that always extends Object. Both can implement an interface, and Save cannot tell them apart.

An interface value is a reference: the object's address and the table that type uses for this interface, 16 bytes. Save( ini ) passes a reference to ini, not a copy of it, and enc.Name( ) is called with .. Because it only refers to the object, the object must outlive it: a temporary, such as a function's result, cannot be converted to an interface (TKO1012), and delete through an interface is refused (TKO1013).

The mapping is explicit. A public Name with the right signature does not implement IEncoder.Name until its declaration says so:

interface IEncoder
    function Name( ) as ustring
end interface

'' Name is public and has the right signature, but nothing maps it.
type IniEncoder
    implements IEncoder
    declare function Name( ) as ustring
    sep as ustring = "="
end type

function IniEncoder.Name( ) as ustring
    return "ini"
end function
unmapped.tiko(7,16): error TKO1000: 'iniencoder' does not implement 'iencoder.name'; add 'implements iencoder.name' to one of its methods
     |
   7 |     implements IEncoder
     |                ^
unmapped.tiko(2,5): note: 'name' is required by interface 'iencoder', declared here
     |
   2 |     function Name( ) as ustring
     |     ^

Being explicit is what lets one type follow two contracts that use the same member name for different things: see Several interfaces.

An abstract type can serve as a contract too, and that is the right choice when the base should also supply code that derived types share. But a type extends exactly one base, so a contract written that way uses the base up.

Activities

  1. Add a CsvEncoder that writes width,640, and pass one to Save.
  2. Give IEncoder a third member, function Footer( ) as ustring, and read what the compiler says about each encoder until both provide it.
  3. Rename IniEncoder.Name to Label, keeping implements IEncoder.Name. What changes in the output?

Several interfaces

A type has one base, but it can implement any number of interfaces. That is how one type becomes several things at once: a shape that is also a document, a document that is also a resource to close.

'' One type, three contracts. Two of them both have a member called Name,
'' and they mean different things.
interface IShape
    function Area( ) as float64
    function Name( ) as ustring            '' what the shape is called
end interface

interface IDocument
    function Name( ) as ustring            '' the file it is saved in
    sub Save( )
    sub Close( )
end interface

interface IResource
    sub Close( )
end interface

class Drawing
    implements IShape, IDocument, IResource
    declare function Area( ) as float64 implements IShape.Area
    declare function Title( ) as ustring implements IShape.Name
    declare function FileName( ) as ustring implements IDocument.Name
    declare sub Close( ) implements IDocument.Close, IResource.Close
    w as float64 = 4
    h as float64 = 2
private:
    declare sub WriteOut( ) implements IDocument.Save
end class

function Drawing.Area( ) as float64
    return w * h
end function

function Drawing.Title( ) as ustring
    return "floor plan"
end function

function Drawing.FileName( ) as ustring
    return "plan.txt"
end function

sub Drawing.Close( )
    print "closed"
end sub

sub Drawing.WriteOut( )
    print "saving "; FileName( )
end sub

dim as Drawing d
dim as IShape s = d
dim as IDocument doc = d
dim as IResource res = d
print s.Name( ); " / "; doc.Name( ); ", area"; s.Area( )
doc.Save( )                    '' reaches the private WriteOut
res.Close( )

'' The same object, asked through one interface about another.
dim as IResource r2 = trycast( IResource, s )
print "the shape is a resource: "; cbool( r2 <> nullptr )
print "the shape is a Drawing: "; cbool( s is Drawing )

Output

floor plan / plan.txt, area 8
saving plan.txt
closed
the shape is a resource: true
the shape is a Drawing: true

Drawing lists three interfaces on one implements line; it could equally write one line per interface. Each member is then mapped on its own:

  • Two interfaces have a Name, and they mean different things: a shape's title, a document's file. They are different members, so each gets its own method. A method's name does not have to match the member's: Title implements IShape.Name.
  • One method can provide several members. Close implements IDocument.Close and IResource.Close at once.
  • A private method can provide a member. WriteOut cannot be called on a Drawing from outside (TKO0625), yet doc.Save( ) reaches it.

trycast( IResource, s ) asks the object behind the IShape whether it also implements IResource, and gives an IResource for the same object if it does, or an empty one, equal to nullptr, if it does not. s is Drawing asks what the object really is. Both work with an interface on either side; see Runtime type checks.

With abstract base types, none of this has a spelling: the type has room for one base, so it can follow one contract.

type Shape extends Object
    declare abstract function Area( ) as float64
end type

type Saveable extends Object
    declare abstract sub Save( )
end type

'' Two bases: a type has one.
type Drawing extends Shape, Saveable
end type
twobases.tiko(10,27): error TKO0427: a type extends one base type
     |
  10 | type Drawing extends Shape, Saveable
     |                           ^

Activities

  1. Add an IPrintable interface with sub Show( ) to Drawing, mapped to a private method, and call it through an IPrintable.
  2. Make Close print which interface it was called through. Why can it not tell?
  3. Write a Label type that implements only IShape, and check with trycast that it is not an IDocument.

Virtual destructors

An object created with new and deleted through a pointer to its base type is destroyed by whichever destructor that pointer's type names, unless the destructor is virtual. When the derived type is the one holding the resource, that is the difference between closing it and leaking it.

'' Connection has a virtual destructor; Plain does not.
type Plain
    declare destructor( )
    id as int32
end type

destructor Plain( )
    print "  ~Plain"
end destructor

type PlainSocket extends Plain
    declare destructor( )
end type

destructor PlainSocket( )
    print "  ~PlainSocket: socket closed"
end destructor

type Connection extends Object
    declare virtual destructor( )
end type

destructor Connection( )
    print "  ~Connection"
end destructor

type Socket extends Connection
    declare virtual destructor( )    '' virtual at this level too
end type

destructor Socket( )
    print "  ~Socket: socket closed"
end destructor

type TlsSocket extends Socket
    declare destructor( )
end type

destructor TlsSocket( )
    print "  ~TlsSocket: session ended"
end destructor

print "delete through a Plain ptr:"
dim as Plain ptr p = new PlainSocket
delete p                            '' only ~Plain runs

print "delete through a Connection ptr:"
dim as Connection ptr c = new TlsSocket
delete c                            '' the whole chain runs

Output

delete through a Plain ptr:
  ~Plain
delete through a Connection ptr:
  ~TlsSocket: session ended
  ~Socket: socket closed
  ~Connection

delete p ran ~Plain and nothing else. PlainSocket's destructor, the one that closes the socket, never ran, and nothing reported it: the program compiled and ran cleanly.

Connection declares virtual destructor( ), so delete c looks the destructor up in the object's table. It finds TlsSocket's, runs it, and then runs each base's in turn, from the most derived to Connection itself.

Pitfall

virtual on a destructor is not inherited by a level that does not say it. Socket repeats declare virtual destructor( ). Declared as a plain destructor( ) there instead, delete c runs only ~Socket and ~Connection, and ~TlsSocket is skipped. Leaving Socket with no destructor at all is no better: the one the compiler writes for it is not virtual either, and only ~Connection runs. Every level that may be deleted through a base pointer declares virtual destructor( ), even an empty one.

Objects in a scope, or passed byref, are destroyed by their own type and are not affected: the rule matters only for delete through a pointer to a base. new and delete themselves are covered in Memory and Resources.

Activities

  1. Change Socket's destructor to a plain declare destructor( ) and check the output against the pitfall above.
  2. Make Plain's destructor virtual. What else has to change for that to compile?

Slicing

A derived object can be passed wherever its base type is wanted. How it is passed decides whether the procedure gets the object or only part of it.

type Animal extends Object
    declare virtual function Sound( ) as ustring
    legs as int32
end type

function Animal.Sound( ) as ustring
    return "..."
end function

type Dog extends Animal
    declare function Sound( ) as ustring override
    tricks as int32
end type

function Dog.Sound( ) as ustring
    return "woof"
end function

sub ByReference( byref a as Animal )
    print "byref:  "; a.Sound( ); ", a Dog: "; cbool( a is Dog )
end sub

'' byval makes a new Animal from the Animal part of the argument.
sub ByValue( byval a as Animal )
    print "byval:  "; a.Sound( ); ", a Dog: "; cbool( a is Dog )
end sub

dim as Dog rex
rex.tricks = 3
ByReference( rex )
ByValue( rex )

'' Assignment to an Animal does the same.
dim as Animal pet = rex
print "copied: "; pet.Sound( ); ", a Dog: "; cbool( pet is Dog )

'' A pointer keeps the whole object.
dim as Animal ptr p = @rex
print "ptr:    "; p->Sound( ); ", a Dog: "; cbool( *p is Dog )

'' The hidden table pointer is the whole of an Object.
print "sizes:"; sizeof( Object ); sizeof( Animal ); sizeof( Dog )

Output

byref:  woof, a Dog: true
byval:  ..., a Dog: false
copied: ..., a Dog: false
ptr:    woof, a Dog: true
sizes: 8 16 24

ByReference receives rex itself, so the virtual call reaches Dog.Sound and is Dog is true.

ByValue receives a new Animal, built by copying the Animal part of rex. That copy is an Animal through and through: it says ..., it is not a Dog, and tricks is not part of it at all. The derived part has been sliced off. Assigning a Dog to an Animal variable does the same. A pointer, like a byref parameter, refers to the whole object.

The last line shows the hidden field behind all of this. Object is 8 bytes: the pointer to the type's table of virtual methods, and nothing else. Animal adds legs and rounds up to 16, and Dog adds tricks and rounds up to 24.

Pitfall

Nothing warns about slicing, because from the compiler's side nothing is wrong: an Animal was asked for and an Animal was made. Pass objects of a type with virtual methods byref or by pointer, never byval, and keep them in Animal ptr variables rather than Animal ones.

Activities

  1. Change ByValue to take its parameter byref and compare the output.
  2. Put rex in a dim as Animal pets( 0 to 0 ) array and call Sound on the element. Then try an array of Animal ptr.

Generics

A procedure that finds the larger of two values is the same code for integers, floating-point numbers and strings. A generic lets you write it once, with a placeholder for the type, and the compiler builds a separate, fully checked version for each type you use it with.

'' One definition of Larger serves every type that has a '>'.
function Larger( of T )( byval a as T, byval b as T ) as T
    if a > b then return a
    return b
end function

print Larger( 3, 9 )            '' T is an integer here...
print Larger( 2.5, -1.0 )       '' ...a float64 here...

dim as ustring early = "apple", late = "pear"
print Larger( early, late )     '' ...and a ustring here

Output

 9
 2.5
pear

( of T ) after the procedure's name declares a type parameter, T, and from then on T can appear anywhere a type can: in the parameters, the result and the body. Each call above passes two values of one type, so the compiler works out T from the arguments and builds Larger for that type.

The result is as fast and as strictly typed as a version you wrote by hand for each type: there is no conversion to a common type and no check at run time. What you pay is code size, since each distinct T is a separate copy.

This section covers:

Activities

  1. Write Smaller( of T ) and call it with three different types.
  2. Call Larger with a ustring variable and a string literal, and read the error. What does it tell you about how T is chosen?

Generic types

A type can take type parameters too. A stack of integers and a stack of names need the same fields and the same methods; only the element type differs.

'' A stack of anything: T stands for the element type.
type Stack( of T )
    declare sub Push( byval item as T )
    declare function Pop( ) as T
    declare property Count( ) as int32
private:
    as T items( any )
    as int32 used
end type

'' Members written outside the type repeat its ( of T ).
sub Stack( of T ).Push( byval item as T )
    if used > ubound( items ) then redim preserve items( used * 2 + 3 )
    items( used ) = item
    used += 1
end sub

function Stack( of T ).Pop( ) as T
    used -= 1
    return items( used )
end function

property Stack( of T ).Count( ) as int32
    return used
end property

dim numbers as Stack( of int32 )
for i as int32 = 1 to 4
    numbers.Push( i * i )
next

dim words as Stack( of ustring )
words.Push( "first" )
words.Push( "second" )

print "numbers:"; numbers.Count; ", words:"; words.Count
do while numbers.Count > 0
    print numbers.Pop( );
loop
print
print words.Pop( )
print words.Pop( )

Output

numbers: 4, words: 2
 16 9 4 1
second
first

type Stack( of T ) declares the parameter, and T is then used for the element array and in the method signatures. A member written outside the type repeats the parameter list: sub Stack( of T ).Push( ... ), with the same parameter names as the declaration.

The name Stack on its own is not a type. You always write it with its arguments, Stack( of int32 ), and each distinct argument list is a distinct type: Stack( of int32 ) and Stack( of ustring ) are as unrelated as two types you wrote separately, and one does not assign to the other. Writing Stack( of int32 ) twice gives the same type both times.

A type can take more than one parameter, as Pair( of K, V ) does on the next page, and a type argument can itself be an instantiation: Stack( of Stack( of int32 ) ) is a stack of stacks.

Activities

  1. Add a Peek( ) method that returns the top item without removing it.
  2. Try to assign numbers to words and read the error.
  3. Write a Queue( of T ), first in, first out, with Enqueue and Dequeue.

Explicit type arguments

The compiler infers type arguments from a call's arguments, and from nothing else. When that is not enough, you write them yourself, after the name: Larger( of float64 )( ... ).

'' Writing the type arguments yourself.
type Pair( of K, V )
    as K first
    as V second
end type

'' A name for an instantiation you use often.
type Score as Pair( of ustring, int32 )

function Larger( of T )( byval a as T, byval b as T ) as T
    if a > b then return a
    return b
end function

'' T appears only in the result, so no call could infer it.
function Blank( of T )( ) as T
    dim value as T
    return value
end function

'' Mixed arguments: name T, and both arguments convert to it.
print Larger( of float64 )( 2, 2.5 )
print Larger( of int32 )( 7, 3 )

print Blank( of int32 )( ); " ["; Blank( of ustring )( ); "]"

dim best as Score
best.first = "ada"
best.second = 36
dim same as Pair( of ustring, int32 ) = best    '' one type, two names
print same.first; same.second

Output

 2.5
 7
 0 []
ada 36

There are two situations where you must write them:

  • The arguments disagree. 2 is an integer and 2.5 is a float64, so the arguments would bind T to two different types, and inference does not pick a winner. With ( of float64 ) written, both arguments convert to float64.
  • T is only in the result. Blank( of T )( ) has no parameters, so there is nothing to infer from. A new int32 is 0 and a new ustring is empty.

Leave the type out where the arguments disagree and the program does not build:

function Larger( of T )( byval a as T, byval b as T ) as T
    if a > b then return a
    return b
end function

print Larger( 2, 2.5 )          '' an integer and a float64: which T?
noinfer.tiko(6,13): error TKO0809: cannot infer the type arguments of 'larger'; write them, as 'larger( of ... )( ... )'
     |
   6 | print Larger( 2, 2.5 )          '' an integer and a float64: which T?
     |             ^

Naming an instantiation

type Score as Pair( of ustring, int32 ) gives an instantiation a short name. It is an alias, not a new type: best is a Score, same is a Pair( of ustring, int32 ), and one assigns to the other. An alias is the usual way to keep a long generic type readable when it appears in several signatures; Result uses one for that reason.

Activities

  1. Call Larger( of int32 )( 2, 2.5 ). What does it print, and why?
  2. Add type Point as Pair( of float64, float64 ) and use it.

What a type parameter asks for

( of T ) does not say what T can do. The compiler therefore checks a generic's body once for each type it is used with, against that real type. If the body uses an operator the type does not have, that instantiation fails.

'' Largest( of T ) uses '>' on T, but nothing in its header says so.
function Largest( of T )( items() as T ) as T
    var best = items( lbound( items ) )
    for each x in items
        if x > best then best = x
    next
    return best
end function

type Version
    as int32 major, minor
end type

dim releases( 0 to 2 ) as Version = { ( 1, 4 ), ( 2, 0 ), ( 1, 9 ) }
var newest = Largest( releases() )      '' Version has no '>'
print newest.major; newest.minor

Version has no >, so this does not build:

needsgt.tiko(5,14): error TKO0720: no operator > compares this type
     |
   5 |         if x > best then best = x
     |              ^
needsgt.tiko(15,21): note: in instantiation of 'largest( of version )'
     |
  15 | var newest = Largest( releases() )      '' Version has no '>'
     |                     ^

The error points at the generic's body, which is correct code. The cause is in the note: under it: line 15 asked for Largest( of Version ). Read an error from a generic to the end before you edit anything.

The fix is on the type's side: give Version the operator the generic uses.

'' Largest( of T ) needs '>' on T.
function Largest( of T )( items() as T ) as T
    var best = items( lbound( items ) )
    for each x in items
        if x > best then best = x
    next
    return best
end function

type Version
    as int32 major, minor
end type

'' Supply the operator the generic asks for.
operator > ( byref a as Version, byref b as Version ) as boolean
    if a.major <> b.major then return a.major > b.major
    return a.minor > b.minor
end operator

dim releases( 0 to 2 ) as Version = { ( 1, 4 ), ( 2, 0 ), ( 1, 9 ) }
var newest = Largest( releases() )
print "newest:"; newest.major; "."; str( newest.minor )

dim scores( 0 to 3 ) as int32 = { 70, 85, 90, 62 }
print "best score:"; Largest( scores() )

Output

newest: 2.0
best score: 90

Largest did not change. The same generic serves int32, which has > built in, and Version, which now has one of its own. Because a generic's demands are not written in its header, state them in a comment above it, as this one does.

Activities

  1. Write a generic Contains( of T )( items() as T, byval x as T ) and use it with Version. Which operator does it need?
  2. Remove the > operator and add a < instead. Does the error change?

Library demands: Hashable keys

The standard containers are generics, so they make demands of the types you put in them. A containers.Map( of K, V ) needs two things of its key type: that two keys can be compared with =, and that a key can produce a hash. Built-in types such as int32 and ustring have both. Your own type supplies them like this:

#import "std/containers"

'' A Map key must hash itself and compare with '='.
type Cell extends containers.Hashable
    as int32 row, col
    declare constructor( byval row as int32 = 0, byval col as int32 = 0 )
    declare function HashOf( ) as ulongint override
end type

constructor Cell( byval row as int32, byval col as int32 )
    this.row = row : this.col = col
end constructor

function Cell.HashOf( ) as ulongint
    return containers.HashOf( row ) * 31 + containers.HashOf( col )
end function

operator = ( byref a as Cell, byref b as Cell ) as boolean
    return ( a.row = b.row ) andalso ( a.col = b.col )
end operator

dim board as containers.Map( of Cell, ustring )
board[ Cell( 1, 1 ) ] = "X"
board[ Cell( 0, 2 ) ] = "O"
board[ Cell( 1, 1 ) ] = "X again"

print "cells used:"; board.Count
print "centre: "; board[ Cell( 1, 1 ) ]
print "has (2, 2): "; board.Contains( Cell( 2, 2 ) )

Output

cells used: 2
centre: X again
has (2, 2): false

Cell extends containers.Hashable and overrides its HashOf( ). The map's code lives in the std/containers module and asks the key for its hash; extending Hashable is how your type's hash becomes visible there. Inside the override, containers.HashOf( ... ) hashes each field.

operator = is not optional. The hash leads the map to the right bucket, and equality decides which entry there is yours. Writing Cell( 1, 1 ) a second time finds the same entry, so the map still holds two cells.

A key type with = but no hash does not build:

#import "std/containers"

'' A key type with '=' but no hash.
type Cell
    as int32 row, col
end type

operator = ( byref a as Cell, byref b as Cell ) as boolean
    return ( a.row = b.row ) andalso ( a.col = b.col )
end operator

dim board as containers.Map( of Cell, ustring )
dim centre as Cell = ( 1, 1 )
board[ centre ] = "X"
print board.Count
map.tiko(150,35): error TKO0632: no overload of 'hashof' matches these arguments
     |
 150 | dim as long i = cast( long, HashOf( k ) and cast( ulongint, mask ) )
     |                                   ^

As on the previous page, the error is reported inside the generic, here in the container module's own source.

Status in this version

When the generic lives in a module, the error does not yet carry the note: in instantiation of line that names your program's line, and it is repeated for each place in the map's code that needs the hash. Look for the containers.Map( of ... ) your program declares.

Key types for every container are covered in Custom keys.

Activities

  1. Add a containers.Set( of Cell ) and add the same cell twice. What is its count?
  2. Make HashOf( ) return 0 for every cell. Does the program still work? What would that cost with a million cells?

Generics and lambdas

A procedure pointer has one fixed type, and a lambda that captures variables cannot become one. A type parameter has no such limit: a generic with a parameter ( of F ) accepts any value it can call, whether a procedure, a lambda or a closure.

'' F can be any callable: a procedure, a lambda, or a closure.
function CountIf( of T, F )( items() as T, byref test as F ) as int32
    dim as int32 n = 0
    for each x in items
        if test( x ) then n += 1
    next
    return n
end function

sub Transform( of T, F )( items() as T, byref change as F )
    for each byref x in items
        x = change( x )
    next
end sub

dim nums( 0 to 5 ) as int32 = { 3, 8, 1, 9, 4, 7 }

'' A lambda that captures nothing, written at the call.
print "over 5:"; CountIf( nums(), function( byval n as int32 ) as boolean
    return n > 5
end function )

'' A closure: it carries its own copy of limit.
dim as int32 limit = 3
var overLimit = function[ byval limit ]( byval n as int32 ) as boolean
    return n > limit
end function
print "over"; limit; ":"; CountIf( nums(), overLimit )

'' The same generic, with a lambda that returns a new value.
var tenfold = function( byval n as int32 ) as int32
    return n * 10
end function
Transform( nums(), tenfold )
for each n in nums
    print n;
next
print

'' T is now ustring; the lambda's parameter matches it.
dim words( 0 to 2 ) as ustring = { "fig", "banana", "kiwi" }
var fourLetters = function( byref w as ustring ) as boolean
    return len( w ) = 4
end function
print "four letters:"; CountIf( words(), fourLetters )

Output

over 5: 3
over 3: 4
 30 80 10 90 40 70
four letters: 1

CountIf has two type parameters. T is inferred from the array, and F from whatever is passed as test. The only demand the body makes of F is that test( x ) compiles and gives something if can test.

  • The first call passes a lambda written in place. It captures nothing.
  • overLimit captures limit with [ byval limit ], so it is a closure: a small object holding its own copy of limit. It can still be passed, because F becomes the closure's type.
  • Transform takes a lambda that returns a new value, and for each byref writes it back into the array.
  • With words, T is ustring, and the lambda's parameter is a ustring to match.

F is passed byref, so a closure is not copied on the way in. The standard library's containers.ForEach is declared the same way. Lambdas themselves are covered in Procedures.

Activities

  1. Write Sum( of T, F ) that adds up f( x ) for every element, and use it to total the squares of nums.
  2. Capture limit with byref instead, change limit after creating overLimit, and compare the counts.

Tuples and Iterators

Two small problems come up in almost every program. A function has two answers to give, a minimum and a maximum, and a function returns one value. And a loop over a collection needs an index it does not care about, with bounds to get wrong. A tuple solves the first, and for each with the iterator protocol solves the second.

'' One function, two answers: a tuple carries both.
function MinMax( values() as int32 ) as (lo as int32, hi as int32)
    var lo = values( lbound( values ) ), hi = lo
    for each v in values            '' for each needs no index
        if v < lo then lo = v
        if v > hi then hi = v
    next
    return (lo, hi)
end function

dim temps( 0 to 4 ) as int32 = { 12, -3, 25, 7, 0 }

'' Take the tuple apart into two new variables...
var (coldest, warmest) = MinMax( temps() )
print "coldest"; coldest; ", warmest"; warmest

'' ...or keep it whole and use its element names.
var range_ = MinMax( temps() )
print "spread:"; range_.hi - range_.lo

Output

coldest-3, warmest 25
spread: 28

MinMax returns (lo as int32, hi as int32), a tuple type, and return (lo, hi) builds a value of it. The caller can take it apart with var (coldest, warmest) = ..., which declares two variables, or keep it whole and reach the elements by the names the function's declaration gave them. Tuple types and literals themselves are introduced in Tuples.

Inside MinMax, for each v in values visits every element without an index. It walks arrays, the library containers, and any type of your own that follows the iterator protocol.

This section covers:

Activities

  1. Make MinMax return a third element, the number of values, and print it.
  2. Call MinMax on an array with a single element. What does it return?

Destructuring and swapping

A list of places in parentheses on the left of = assigns a tuple's elements to them, one each, in order. The right-hand side is evaluated completely before anything is written, which makes (a, b) = (b, a) a swap rather than two copies of the same value.

'' The right-hand side is read completely before anything is written.
dim as int32 a = 1, b = 2
(a, b) = (b, a)
print "swapped:"; a; b

dim as int32 x = 1, y = 2, z = 3
(x, y, z) = (z, x, y)
print "rotated:"; x; y; z

'' The targets can be any places: array elements, fields...
dim cards( 0 to 2 ) as ustring = { "ace", "king", "queen" }
(cards( 0 ), cards( 2 )) = (cards( 2 ), cards( 0 ))
print cards( 0 ); " "; cards( 1 ); " "; cards( 2 )

'' ...or existing variables taking a function's two results.
function DivMod( byval n as int32, byval d as int32 ) _
    as (quotient as int32, remainder as int32)
    return (n \ d, n mod d)
end function

dim as int32 q, r
(q, r) = DivMod( 17, 5 )
print "17 / 5 ="; q; " remainder"; r

'' One step of Fibonacci, without a temporary.
dim as int64 older = 0, newer = 1
for i as int32 = 1 to 50
    (older, newer) = (newer, older + newer)
next
print "fib(50) ="; older

Output

swapped: 2 1
rotated: 3 1 2
queen king ace
17 / 5 = 3 remainder 2
fib(50) = 12586269025

The targets can be any place you could assign to: variables, array elements, fields. Each step of the Fibonacci loop moves newer into older and the sum into newer in one statement, with no temporary variable.

There are two forms:

FormDoes
var (q, r) = DivMod( ... )declares q and r, each with its element's type
(q, r) = DivMod( ... )assigns to q and r, which already exist

The number of places must match the number of elements exactly:

function DivMod( byval n as int32, byval d as int32 ) _
    as (quotient as int32, remainder as int32)
    return (n \ d, n mod d)
end function

var (q, r, extra) = DivMod( 17, 5 )     '' three names, two elements
print q; r; extra
toomany.tiko(6,1): error TKO0825: the tuple has 2 elements, and 3 are written
     |
   6 | var (q, r, extra) = DivMod( 17, 5 )     '' three names, two elements
     | ^

Activities

  1. Reverse a five-element array using only tuple assignments.
  2. Write (a, a) = (1, 2) and print a. Which element won?

Named elements

A tuple's elements can be given names where its type is written, and the names then reach the elements of any value seen through that declaration. The positional names _1 to _N always work as well.

'' Element names come from the declaration a value is reached through.
type Size as (w as int32, h as int32)

function ScreenSize( ) as Size
    return (1920, 1080)
end function

var s = ScreenSize( )
print "by name:"; s.w; s.h
print "by position:"; s._1; s._2       '' _1 .. _N always work

'' A call's result carries the names of the function's declaration.
print "height alone:"; ScreenSize( ).h

'' Elements nest, and so do their names.
dim box as (topLeft as Size, bottomRight as Size) = ((0, 0), (40, 30))
print "corner:"; box.bottomRight.w; box.bottomRight.h

'' Names are not part of the type: these are all (int32, int32).
dim plain as (int32, int32) = s
dim span as (lo as int32, hi as int32) = plain
print "span:"; span.lo; span.hi; ", equal: "; cbool( span = s )

Output

by name: 1920 1080
by position: 1920 1080
height alone: 1080
corner: 40 30
span: 1920 1080, equal: true

type Size as (w as int32, h as int32) names a tuple type, so s.w and s.h work on anything declared as a Size. A call's result takes the names from the function's declaration, which is why ScreenSize( ).h compiles. Names nest along with the tuples: box.bottomRight.w.

The names are not part of the type. (int32, int32), (lo as int32, hi as int32) and Size are one type, so they assign to each other freely and compare with =. Each variable is read with the names of its own declaration.

Pitfall

Because names are not checked, a tuple will put a value into an element with the wrong name without a word:

function ScreenSize( ) as (w as int32, h as int32)
    return (1920, 1080)
end function

'' Declared (h, w): the copy is by position, so the names lie.
dim s as (h as int32, w as int32) = ScreenSize( )
print "width"; s.w; ", height"; s.h

'' Destructuring is by position too: these are just new names.
var (h, w) = ScreenSize( )
print "width"; w; ", height"; h
width 1080, height 1920
width 1080, height 1920

ScreenSize returns (w, h) and s is declared (h, w). Both are (int32, int32), so the copy goes by position and the width lands in the element s calls h. Destructuring is by position too: var (h, w) declares two new variables and does not look elements up by name.

When the meaning of each element matters, declare a type with fields. Two different types never convert into each other.

Activities

  1. Rewrite wrongnames.tiko with type Size holding fields w and h. Which line now refuses to compile?
  2. Print s._2 for the s in wrongnames.tiko. Which number is it?

for each over containers

for each works the same way over the standard library's containers as over an array: it declares the loop variable, visits every element, and ends with a plain next. The basic forms over arrays are on the for each page; this page shows what each container yields.

#import "std/containers"

'' Every library container can be walked with for each.
dim primes as containers.Array( of int32 ) = { 2, 3, 5, 7 }
print "primes:";
for each p in primes
    print p;
next
print

'' byref binds the element itself, so the loop can change it.
for each byref p in primes
    p *= 10
next
print "times ten:"; primes[0]; primes[3]

'' A set yields its members; an ordered one, in sorted order.
dim tags as containers.OrderedSet( of ustring )
tags.Add( "red" ) : tags.Add( "blue" ) : tags.Add( "red" )
print "tags:";
for each tag in tags
    print " "; tag;
next
print

'' A map yields key/value records...
dim ages as containers.OrderedMap( of ustring, int32 )
ages.Put( "grace", 85 )
ages.Put( "ada", 36 )
ages.Put( "edsger", 72 )
for each entry in ages
    print entry.key; " is"; entry.value
next

'' ...and Pairs( ) yields tuples, which for each takes apart.
for each (who, age) in ages.Pairs( )
    print who; "="; str( age ); " ";
next
print

Output

primes: 2 3 5 7
times ten: 20 70
tags: blue red
ada is 36
edsger is 72
grace is 85
ada=36 edsger=72 grace=85 
Containerfor each yields
Array( of T )each T, in index order
OrderedSet( of T )each member, in sorted order, each once
OrderedMap( of K, V )a record with fields key and value, in key order
.Pairs( ) of a mapa (key, value) tuple, ready to destructure

for each byref p binds the element itself, so p *= 10 changes the array. Without byref the loop variable is a copy. A container whose iterator hands out copies refuses byref: over an OrderedMap it is TKO0818: 'for each byref' needs a byref Value( ).

for each (who, age) in ages.Pairs( ) declares one variable per element of each tuple. The names are copies, and the list form takes neither as nor byref.

The plain Map and Set are walked the same way, but in an order you must not depend on. Use the ordered versions when the order is part of the output. Containers covers each one.

Activities

  1. Walk ages.Keys( ), an Array of the keys, and print each key.
  2. Add 1 to every age. for each byref over the map is refused, so walk ages.Keys( ) and write through ages[ k ].

for each and strings

Text can be walked in three units, and each needs a different loop: words, characters and bytes.

#import "std/strings"

'' To walk the words of a text, split it into an Array first.
dim as ustring csv = "red,green,blue"
for each colour in strings.Split( csv, "," )
    print "["; colour; "]";
next
print

'' A ustring is walked by index, one character at a time.
dim as ustring word = "caf" & wchr( 233 )        '' U+00E9, e-acute
for i as int32 = 1 to len( word )
    print mid( word, i, 1 ); " ";
next
print "-"; len( word ); " characters"

'' A string is a sequence of bytes, and for each yields those.
dim as string bytes_ = word                      '' encoded as UTF-8
var count = 0
for each b in bytes_
    print b; " ";
    count += 1
next
print "-"; count; " bytes"

Output

[red][green][blue]
c a f é - 4 characters
99 97 102 195 169 - 5 bytes
  • Words. strings.Split( csv, "," ) returns a containers.Array of ustring, and for each walks that like any array.
  • Characters. A ustring is indexed with for and mid. Each mid( word, i, 1 ) is one character, so café has four.
  • Bytes. A string is a sequence of bytes, and for each over it yields each byte as a number. Converted to a string, é is encoded as UTF-8, two bytes, 195 and 169, so the same word is five bytes long.

A ustring itself cannot be walked by for each:

dim as ustring word = "tiko"
for each ch in word                 '' a ustring cannot be walked
    print ch
next
walkustring.tiko(2,1): error TKO0688: 'for each' cannot walk a ustring; index it with 'for' and 'mid' instead
     |
   2 | for each ch in word                 '' a ustring cannot be walked
     | ^

Pitfall

for each over a string compiles and runs for any text, and for ASCII it looks right: one step per letter. The first accented letter makes the count wrong without a diagnostic. Use it to parse bytes, and use a ustring with mid for anything a reader would call a character.

Strings covers ustring and std/strings in full.

Activities

  1. Split "one two three" on " ". How many elements are there, and why?
  2. Count the vowels in a ustring with a for loop and mid.

Making a type iterable

for each walks your own types too, and there is nothing to inherit. A type is iterable when it has a GetIterator( ) method, and what that returns is an iterator when it has three methods: IsValid( ), Value( ) and MoveNext( ). The compiler looks for those names.

'' The iterator: where the walk is, and how to move on.
type RangeIterator
    as int32 at, last, stride
    declare function IsValid( ) as boolean
    declare function Value( ) as int32
    declare sub MoveNext( )
end type

function RangeIterator.IsValid( ) as boolean    '' is there an element?
    if stride > 0 then return at <= last
    return at >= last
end function

function RangeIterator.Value( ) as int32        '' the current element
    return at
end function

sub RangeIterator.MoveNext( )                   '' step to the next one
    at += stride
end sub

'' The collection only has to hand out an iterator.
type Range
    as int32 first, last, stride
    declare function GetIterator( ) as RangeIterator
end type

function Range.GetIterator( ) as RangeIterator
    print "(GetIterator)"
    return type( first, last, stride )          '' on the first element
end function

dim evens as Range = ( 0, 10, 2 )
for each n in evens
    print n;
next
print

dim countdown as Range = ( 5, 1, -1 )
for each n in countdown
    print n;
next
print " lift-off"

dim backwards as Range = ( 1, 5, -1 )           '' steps away: empty
for each n in backwards
    print "never printed"
next
print "an empty range walks zero times"

Output

(GetIterator)
 0 2 4 6 8 10
(GetIterator)
 5 4 3 2 1 lift-off
(GetIterator)
an empty range walks zero times
MemberOnJob
GetIterator( ) as Ithe collectionhand out a new iterator; called once per loop
IsValid( ) as booleanthe iteratoris there a current element?
Value( ) as Ethe iteratorthe current element; E is the loop variable's type
MoveNext( )the iteratoradvance to the next element

The loop calls GetIterator( ) once, as the (GetIterator) lines show. Then, for as long as IsValid( ) is true, it binds Value( ) to n, runs the body and calls MoveNext( ).

Three rules make an iterator behave:

  • A new iterator is already on the first element. There is no initial MoveNext( ). For an empty collection IsValid( ) is false at once, as it is for backwards, and the body never runs.
  • IsValid( ) is a question. Asking twice without a MoveNext( ) in between gives the same answer.
  • Value( ) decides what byref can do. Declared byref as E, it hands out the element itself, and for each byref can write to it; the next page does that. Declared by value, as here, it hands out a copy.

The calls are direct: no interface and no virtual dispatch.

A misspelt member does not match, and the error names the one that is missing:

type TickIterator
    as int32 left
    declare function IsValid( ) as boolean
    declare function Value( ) as int32
    declare sub MoveNxt( )                      '' misspelt
end type

function TickIterator.IsValid( ) as boolean
    return left > 0
end function

function TickIterator.Value( ) as int32
    return left
end function

sub TickIterator.MoveNxt( )
    left -= 1
end sub

type Ticks
    as int32 count
    declare function GetIterator( ) as TickIterator
end type

function Ticks.GetIterator( ) as TickIterator
    return type( count )
end function

dim three as Ticks
three.count = 3
for each t in three
    print t
next
misspelt.tiko(31,1): error TKO0607: 'tickiterator' is not an iterator: it has no 'MoveNext( )'
     |
  31 | for each t in three
     | ^

A type with no GetIterator( ) at all gets TKO0606: ... cannot be walked by 'for each': it has no GetIterator( ).

Activities

  1. Give Range a Count( ) function and use it to print how many elements a range has before walking it.
  2. Make Value( ) return the square of at. What changes in the loops?

An iterable generic collection

With generics, an iterator is written once for every element type. A ring buffer keeps the newest N values and forgets the oldest, and for each walks what it holds, oldest first:

'' A ring keeps the newest N values. Its iterator walks them oldest
'' first, and Value( ) is byref, so a byref loop can change them.
type RingIterator( of T )
    as T ptr slots
    as int32 size, at, left
    declare function IsValid( ) as boolean
    declare function Value( ) byref as T
    declare sub MoveNext( )
end type

function RingIterator( of T ).IsValid( ) as boolean
    return left > 0
end function

function RingIterator( of T ).Value( ) byref as T
    return slots[at]
end function

sub RingIterator( of T ).MoveNext( )
    at = ( at + 1 ) mod size
    left -= 1
end sub

type Ring( of T )
    declare constructor( byval size as int32 )
    declare sub Add( byval item as T )
    declare function GetIterator( ) as RingIterator( of T )
private:
    as T items( any )
    as int32 nextSlot, used
end type

constructor Ring( of T )( byval size as int32 )
    redim items( 0 to size - 1 )
end constructor

sub Ring( of T ).Add( byval item as T )
    var size = ubound( items ) + 1
    items( nextSlot ) = item
    nextSlot = ( nextSlot + 1 ) mod size
    if used < size then used += 1
end sub

function Ring( of T ).GetIterator( ) as RingIterator( of T )
    dim it as RingIterator( of T )
    it.size = ubound( items ) + 1
    it.slots = @items( 0 )
    it.at = ( nextSlot - used + it.size ) mod it.size
    it.left = used
    return it
end function

var recent = Ring( of ustring )( 3 )
dim words( 0 to 4 ) as ustring = { "one", "two", "three", "four", "five" }
for each w in words
    recent.Add( w )
next
for each w in recent
    print w; " ";
next
print

var readings = Ring( of float64 )( 4 )
readings.Add( 1.5 ) : readings.Add( 2.25 )
for each byref x in readings            '' writes into the ring
    x *= 2
next
for each x in readings
    print x;
next
print

Output

three four five 
 3 4.5

Ring( of T ).GetIterator( ) returns a RingIterator( of T ), an instantiation of a different generic. That is the usual shape, and the one the standard containers use. The iterator holds a pointer to the storage, where to start and how many elements are left; it knows nothing about how they got there.

Because Value( ) is declared byref as T, it hands out the stored element itself, so for each byref x in readings doubles the numbers inside the ring. An iterator whose Value( ) returns by value would refuse that loop with TKO0818.

After five words go into a ring of three, only the newest three are left. readings has room for four but holds two, and the walk visits exactly two: left starts at the number in use, so the empty slots are never seen.

Activities

  1. Add a Count( ) method to Ring and print it after each Add.
  2. Walk a Ring( of int32 )( 2 ) that has never had anything added. How many times does the body run?

Lazy sequences

An iterator does not have to walk anything stored. Value( ) can return a number it computed, and MoveNext( ) can compute the next one. The sequence then exists only as far as a loop asks for it.

'' A sequence computed as it is walked: nothing is stored.
type CollatzIterator
    as int64 n
    as boolean done
    declare function IsValid( ) as boolean
    declare function Value( ) as int64
    declare sub MoveNext( )
end type

function CollatzIterator.IsValid( ) as boolean
    return not done
end function

function CollatzIterator.Value( ) as int64
    return n
end function

sub CollatzIterator.MoveNext( )
    if n = 1 then
        done = true                 '' 1 was the last element
    elseif n mod 2 = 0 then
        n \= 2
    else
        n = 3 * n + 1
    end if
end sub

type Collatz
    as int64 start
    declare function GetIterator( ) as CollatzIterator
end type

function Collatz.GetIterator( ) as CollatzIterator
    return type( start, false )
end function

dim six as Collatz = ( 6 )
for each n in six
    print n;
next
print

'' A long walk: only the running totals are kept.
dim twentySeven as Collatz = ( 27 )
dim as int64 steps = 0, highest = 0
for each n in twentySeven
    steps += 1
    if n > highest then highest = n
next
print "27 takes"; steps - 1; " steps and climbs to"; highest

'' exit for stops the walk: the rest is never computed.
dim huge as Collatz = ( 989345275647 )
dim as int32 shown = 0
for each n in huge
    print n;
    shown += 1
    if shown = 4 then exit for
next
print " ..."

Output

 6 3 10 5 16 8 4 2 1
27 takes 111 steps and climbs to 9232
 989345275647 2968035826942 1484017913471 4452053740414 ...

The Collatz sequence halves an even number and turns an odd one into 3 * n + 1, until it reaches 1. CollatzIterator holds one number and a flag: MoveNext( ) applies the rule, and on reaching 1 marks the walk as done after that element has been seen.

  • The walk from 6 prints every element.
  • The walk from 27 visits 112 numbers, and the loop keeps only a count and the highest value. Nothing else is stored.
  • The walk from 989345275647 stops at exit for after four elements. The rest of the sequence is never computed.

The cost of laziness is that nothing is remembered: walking twentySeven a second time computes every element again.

Activities

  1. Write a Squares type whose iterator yields 1, 4, 9, ... for ever, and print the first square above 1000 with exit for.
  2. Change the program to find which start below 100 takes the most steps.

Changing a collection while walking it

A loop and the collection it walks share the collection's storage. Changing the collection's shape, adding or removing elements, while the loop runs breaks that sharing in ways that differ from one collection to the next.

#import "std/containers"

'' An array's bounds are read once, before the first pass.
dim queue( any ) as int32
redim queue( 0 to 2 )
queue( 0 ) = 1 : queue( 1 ) = 2 : queue( 2 ) = 3
for each q in queue
    print q;
    if q = 2 then
        redim preserve queue( 0 to 3 )      '' a fourth element...
        queue( 3 ) = 4
    end if
next
print " -"; ubound( queue ) + 1; " elements, the last never visited"

'' To change a collection's shape, collect the changes during the
'' walk and apply them after it.
dim stock as containers.OrderedMap( of ustring, int32 )
stock.Put( "apples", 3 )
stock.Put( "pears", 0 )
stock.Put( "plums", 0 )
stock.Put( "figs", 7 )

dim soldOut as containers.Array( of ustring )
for each item in stock
    if item.value = 0 then soldOut.Push( item.key )
next
for each gone in soldOut
    stock.Remove( gone )
next

for each item in stock
    print item.key; ":"; item.value; " ";
next
print

Output

 1 2 3 - 4 elements, the last never visited
apples: 3 figs: 7 

Over an array, for each reads the bounds once, before the first pass, like the counted loop it compiles to. Growing the array inside the loop does not extend the walk: the fourth element exists afterwards and is never visited. Shrinking it inside the loop does not shorten the walk either: the loop goes on to the bound it read, past the end of what is left, and -exx does not catch it.

A library container's iterator holds a position in the container's storage, and adding or removing elements can move that storage. Do not change a container's shape while for each walks it. Changing an element's value through a byref loop variable is fine; that is what byref is for.

The second half shows the safe pattern: the loop over stock only collects the keys to remove in soldOut, and a second loop removes them once the first has finished.

Pitfall

A byref loop variable bound to an array element points into the array's storage. redim preserve can move that storage, and after it the variable refers to where the element used to be.

Activities

  1. Use the same pattern to double the stock of every item with fewer than 5, adding a new "<item> (reorder)" entry for each.
  2. Rewrite the first loop so that it does visit the fourth element: a do loop that tests ubound( queue ) on every pass.

Error Handling

Every program meets things that go wrong: input that does not parse, a value that is not there, an index one past the end. The question is where the program finds out, and whether the code that finds out can do something about it.

The oldest answer is a special return value that means "no answer". It is only safe while that value can never be a real answer:

'' The average of the readings, or 0 when there are none.
function Average( readings() as float64 ) as float64
    var count = ubound( readings ) - lbound( readings ) + 1
    if count = 0 then return 0                  '' "no answer"
    dim as float64 total = 0
    for each r in readings
        total += r
    next
    return total / count
end function

dim nothing( any ) as float64
dim balanced( 0 to 3 ) as float64 = { -2, 2, -1.5, 1.5 }

'' Both print 0. Only one of them is an average.
print "no readings:"; Average( nothing() )
print "balanced:   "; Average( balanced() )

Output

no readings: 0
balanced:    0

Both calls return 0. One means "there were no readings", the other is the true average of readings that cancel out, and the caller has no way to tell them apart. The fix is a return type with room for "no answer" beside every possible answer.

tiko gives you several tools, each for a different kind of failure:

ToolForPage
-exx runtime checksbugs: a bad index, a null pointerRuntime errors
assert, assertwarnconditions that must hold if the program is rightassert
outcome.Optional( of T )a value that may be absent, with no reason neededOptional
outcome.Result( of T, E )a value, or the reason there is noneResult
returning a Result upstopping at the first failure in a chain of callsPropagating errors
error n, on error gotonumbered runtime errors and their handlersError numbers and handlers
#assertfacts about constants, checked while compilingStatic assertions

The rule of thumb: a failure the caller is expected to handle belongs in the return type, as an Optional or a Result. A failure that means the program itself is wrong is an assertion or a runtime check.

Activities

  1. Change Average to return an outcome.Optional( of float64 ) after reading the Optional page.
  2. Think of a function in your own code that returns -1, 0 or "" to mean "nothing". Could that value ever be a real answer?

Runtime errors

Some mistakes can only be found while the program runs: an array index out of range, a pointer that is null. Compiled with -exx, a program checks for them, and stops with a numbered runtime error that says what went wrong and where, instead of reading memory it does not own.

#cmdline "-exx"

'' -exx checks every array index while the program runs.
dim scores( 0 to 2 ) as int32 = { 70, 85, 90 }
dim as int32 total = 0
for i as int32 = 0 to 3                 '' one too far
    total += scores( i )
next
print total                             '' never reached

Output


Aborting due to runtime error 6 (out of bounds array access) at line 7 of bounds.tiko::(), 
'SCORES' accessed with invalid index = 3, must be between 0 and 2

The loop's last pass asks for scores( 3 ). The check stops the program before the read: runtime error 6, with the line, the array's name, the index and the bounds it should have been within. The program's exit status is the error number, 6, so a script that runs it can tell what happened.

#cmdline "-exx" puts the option in the source, so the file alone reproduces the result. On the command line it is tiko -exx bounds.tiko.

-exx adds the array-bounds and null-pointer checks, and records the procedure each runtime error happens in, so the message can name it.

The null-pointer check is shown in nullptr. Without -exx neither check is made: the read past the end returns whatever is in memory there, and nothing reports it. The message itself goes to standard error, not standard output.

Checks cost speed: a comparison on every array access and a test on every pointer dereference. Use -exx while developing and testing, and decide deliberately whether a release build keeps it.

Activities

  1. Remove #cmdline "-exx" and run the program again. What does it print?
  2. Change the loop to for i as int32 = -1 to 2. Which index does the message report?

assert

An assertion states something that must be true if the program is correct. assert( condition ) stops the program when the condition is false; assertwarn( condition ) reports it and carries on.

#cmdline "-g"

'' assert states what must be true. assertwarn reports and goes on;
'' assert reports and stops the program.
function Average( byval total as int32, byval count as int32 ) as int32
    assertwarn( count < 1000 )          '' unusual, not wrong
    assert( count > 0 )                 '' a caller's mistake
    return total \ count
end function

dim as int32 big = Average( 50000, 5000 )
dim as int32 bad = Average( 10, 0 )

Output

assert.tiko(6): assertion failed at AVERAGE: count < 1000
assert.tiko(7): assertion failed at AVERAGE: count > 0

The first call passes a count of 5000. That is allowed but unusual, so assertwarn reports it and Average goes on. The second call passes 0, which the function cannot work with: assert reports it and ends the program with exit status 1. Each report names the file, the line, the procedure and the condition as written.

Both report on standard error, not standard output, so the messages stay out of a program's normal output even when that is redirected to a file.

Assertions are compiled in only when asked for: -g, the debugging option, turns them on, and so does -eassert on its own. In a build without either, assert and assertwarn compile to nothing at all, so they cost nothing in a release and can stay in the code.

Pitfall

Because an assertion may not be compiled in, it must never be the only thing standing between the program and a crash. Without -g, Average( 10, 0 ) skips the check and divides by zero. An assertion documents and checks a promise the program makes; input from outside it is checked with ordinary code and reported with a Result.

Activities

  1. Compile without #cmdline "-g". What happens to the two messages, and to the program?
  2. Add assert( total >= 0 ) and call Average( -5, 1 ).

Optional

outcome.Optional( of T ) is a T that might not be there. Returning one puts the possibility of "no answer" into the function's type, so a caller cannot get the value out without deciding what to do when there is none. It lives in the standard library's std/outcome module.

#import "std/outcome"

'' Where needle is in items, if it is anywhere.
function IndexOf( items() as ustring, byref needle as ustring ) _
    as outcome.Optional( of int32 )
    for i as int32 = lbound( items ) to ubound( items )
        if items( i ) = needle then return outcome.Some( i )
    next
    return outcome.None( of int32 )( )
end function

dim planets( 0 to 3 ) as ustring = { "mercury", "venus", "earth", "mars" }

'' Ask first, then take the value.
var found = IndexOf( planets(), "earth" )
if found.HasValue( ) then print "earth is at"; found.Value( )

var missing = IndexOf( planets(), "pluto" )
print "pluto has a value: "; missing.HasValue( )

'' ValueOr supplies a fallback and never fails.
print "pluto is at"; missing.ValueOr( -1 )

'' A new Optional is empty; Clear( ) empties one again.
dim later as outcome.Optional( of ustring )
print "a new Optional has a value: "; later.HasValue( )
later = outcome.Some( planets( 1 ) )
print "now it holds "; later.Value( )
later.Clear( )
print "after Clear: "; later.HasValue( )

Output

earth is at 2
pluto has a value: false
pluto is at-1
a new Optional has a value: false
now it holds venus
after Clear: false
Procedure or memberDoes
outcome.Some( v )an Optional holding v
outcome.None( of T )( )an Optional holding nothing
o.HasValue( )whether there is a value
o.Value( )the value; a runtime error if there is none
o.ValueOr( d )the value, or d when there is none; never fails
o.Clear( )drop the value and become empty

IndexOf can now say "earth is at 2" and "pluto is not here" without inventing an index that means nothing.

A few details matter in practice:

  • A new Optional is empty. dim later as outcome.Optional( of ustring ) holds no value; there is no third, uninitialised state.
  • Some takes its type from its argument. outcome.Some( i ) with an int32 i is an Optional( of int32 ). A bare literal is an integer, so outcome.Some( 42 ) does not convert to an Optional( of int32 ) (TKO0738); write outcome.Some( of int32 )( 42 ). None has no argument to infer from, so its type is always written.
  • Value( ) on an empty Optional is a bug at the call site, and raises runtime error 1, "illegal function call". Error numbers and handlers shows it being caught. Ask HasValue( ) first, or use ValueOr( ).

When the caller also needs to know why there is no value, use a Result instead.

Activities

  1. Write FirstNegative( a() as int32 ) returning an Optional( of int32 ), and use it with ValueOr.
  2. Rewrite Average from the section overview to return an Optional( of float64 ).

Result

An Optional says that there is no answer. Often the caller also needs to know why, and that is outcome.Result( of T, E ): a T on success, or an E that explains the failure.

#import "std/outcome"

'' A port number, or the reason the text is not one.
type PortResult as outcome.Result( of int32, ustring )

function ParsePort( byref text as ustring ) as PortResult
    if len( text ) = 0 then
        return outcome.Fail( of int32, ustring )( "the port is empty" )
    end if
    if str( valint( text ) ) <> text then
        var why = "'" & text & "' is not a number"
        return outcome.Fail( of int32, ustring )( why )
    end if
    var port = valint( text )
    if port < 1 orelse port > 65535 then
        var why = text & " is outside 1 to 65535"
        return outcome.Fail( of int32, ustring )( why )
    end if
    return outcome.Ok( of int32, ustring )( port )
end function

dim inputs( 0 to 3 ) as ustring = { "8080", "http", "70000", "" }
for each text in inputs
    var r = ParsePort( text )
    if r.IsOk( ) then
        print "port"; r.Value( )
    else
        print "refused: "; r.Failure( )
    end if
next

'' ValueOr picks a default; a new Result is a failure.
print "or the default:"; ParsePort( "x" ).ValueOr( 80 )
dim unset as PortResult
print "a new Result is ok: "; unset.IsOk( )

Output

port 8080
refused: 'http' is not a number
refused: 70000 is outside 1 to 65535
refused: the port is empty
or the default: 80
a new Result is ok: false
Procedure or memberDoes
outcome.Ok( of T, E )( v )a success holding v
outcome.Fail( of T, E )( why )a failure holding why
r.IsOk( )whether it succeeded
r.Value( )the value; a runtime error if it failed
r.Failure( )the reason; a runtime error if it succeeded
r.ValueOr( d )the value, or d on failure

Ok and Fail each take only one of T and E as a parameter, so the compiler cannot infer the other, and both type arguments are always written. That is why the result type is named once, with type PortResult as outcome.Result( of int32, ustring ), and the long spelling appears only where a value is built.

A new Result is a failure. dim unset as PortResult reports IsOk( ) false. Code that forgets to set a Result cannot claim success by accident. Because both slots always exist, T and E must each be a type that can be created with no arguments.

Within that rule E can be any type. A ustring is enough to tell a person what went wrong; a type with a code field lets a program branch on the reason.

Activities

  1. Give ParsePort a fourth failure: a port below 1024 is "reserved".
  2. Replace the ustring failure with a type PortError holding an enum code and a message, and print a different line for each code.

Propagating errors

A function that calls another fallible function usually wants to stop at the first failure and hand it to its own caller, adding what it knows on the way. With a Result that is an early return.

#import "std/outcome"

type Endpoint
    as ustring host
    as int32 port
end type

type PortResult as outcome.Result( of int32, ustring )
type EndpointResult as outcome.Result( of Endpoint, ustring )

'' Level 3: one number.
function ParsePort( byref text as ustring ) as PortResult
    var port = valint( text )
    if str( port ) <> text orelse port < 1 orelse port > 65535 then
        var why = "bad port '" & text & "'"
        return outcome.Fail( of int32, ustring )( why )
    end if
    return outcome.Ok( of int32, ustring )( port )
end function

'' Level 2: "host:port". A failure below is passed up unchanged.
function ParseEndpoint( byref text as ustring ) as EndpointResult
    var colon = instr( text, ":" )
    if colon = 0 then
        var why = "no ':' in '" & text & "'"
        return outcome.Fail( of Endpoint, ustring )( why )
    end if
    var port = ParsePort( mid( text, colon + 1 ) )
    if port.IsOk( ) = false then
        return outcome.Fail( of Endpoint, ustring )( port.Failure( ) )
    end if
    dim ep as Endpoint
    ep.host = left( text, colon - 1 )
    ep.port = port.Value( )
    return outcome.Ok( of Endpoint, ustring )( ep )
end function

'' Level 1: a named setting. A failure gains context on the way up.
function LoadSetting( byref key as ustring, byref text as ustring ) _
    as EndpointResult
    var ep = ParseEndpoint( text )
    if ep.IsOk( ) = false then
        var why = key & ": " & ep.Failure( )
        return outcome.Fail( of Endpoint, ustring )( why )
    end if
    return ep
end function

dim keys( 0 to 2 ) as ustring = { "db", "cache", "mail" }
dim texts( 0 to 2 ) as ustring = { "db.local:5432", "cache.local", _
                                   "mail.local:25x" }
for i as int32 = 0 to 2
    var r = LoadSetting( keys( i ), texts( i ) )
    if r.IsOk( ) then
        print keys( i ); " -> "; r.Value( ).host; " port"; r.Value( ).port
    else
        print "error: "; r.Failure( )
    end if
next

Output

db -> db.local port 5432
error: cache: no ':' in 'cache.local'
error: mail: bad port '25x'

Three levels of calls, each either handling a failure or passing it on:

  • ParsePort checks one number and fails with the reason.
  • ParseEndpoint can fail on its own, with no :, and calls ParsePort. When that fails it returns at once, building its own failure from port.Failure( ). A Result( of int32, ... ) and a Result( of Endpoint, ... ) are different types, so the failure is re-wrapped rather than returned as it is.
  • LoadSetting adds the setting's name to any failure from below. On success ep already has the right type and is returned unchanged.

Nothing below the top level prints anything. The loop at the bottom is the one place that decides what a failure means to the user, and it sees exactly which setting failed and why.

The cost is one branch per call, paid where the decision is made. Compare a runtime error from error n, which jumps to a handler and, on the way out of a procedure, skips its cleanup; see Error numbers and handlers.

Activities

  1. Make ParseEndpoint refuse an empty host, and check that LoadSetting adds its context to the new failure without any change of its own.
  2. Change the loop to stop at the first failing setting, as a configuration loader that must not run with a half-read file would.

Error numbers and handlers

A runtime error is a numbered failure that stops the normal flow of the program. The runtime raises them itself, for a bad index under -exx or an empty Optional, and your code can raise one with error n. Unhandled, it ends the program with that number as the exit status. on local error goto installs a handler that catches it instead.

#cmdline "-exx"
#import "std/outcome"

'' error n raises runtime error n.
function Ratio( byval a as int32, byval b as int32 ) as int32
    if b = 0 then error 1               '' 1: illegal function call
    return a \ b
end function

sub Report( byval a as int32, byval b as int32 )
    on local error goto failed
    dim as int32 r = Ratio( a, b )
    print a; " /"; b; " ="; r
    exit sub
failed:
    '' Copy the error out first: the next runtime call may clear it.
    dim as int32 code = err, lineNo = erl
    print "error"; code; " at line"; lineNo; " in "; *erfn( )
end sub

'' The runtime raises numbered errors of its own.
sub ShowDay( byval i as int32 )
    on local error goto failed
    dim days( 0 to 6 ) as ustring = _
        { "mon", "tue", "wed", "thu", "fri", "sat", "sun" }
    dim as ustring day = days( i )      '' -exx checks the index
    print "day"; i; " is "; day
    exit sub
failed:
    dim as int32 code = err
    print "day"; i; ": runtime error"; code
end sub

sub ShowValue( byref o as outcome.Optional( of int32 ) )
    on local error goto failed
    dim as int32 v = o.Value( )         '' raises when empty
    print "value:"; v
    exit sub
failed:
    dim as int32 code = err
    print "no value: runtime error"; code
end sub

Report( 12, 4 )
Report( 1, 0 )
ShowDay( 2 )
ShowDay( 9 )
var holding = outcome.Some( of int32 )( 42 )
var vacant = outcome.None( of int32 )( )
ShowValue( holding )
ShowValue( vacant )
print "the program carried on"

Output

 12 / 4 = 3
error 1 at line 6 in RATIO
day 2 is wed
day 9: runtime error 6
value: 42
no value: runtime error 1
the program carried on

on local error goto failed installs a handler for the rest of the procedure. When anything it calls raises an error, control jumps to the failed: label, and these describe what happened:

FunctionGives
errthe error number
erlthe line it was raised on
erfn( )the procedure it was raised in, as a zstring ptr
ermn( )the source file it was raised in, as a zstring ptr

Copy err before doing anything else. The next call into the runtime, such as a print, resets it to 0. erfn( ) needs the location information that -exx compiles in; without it, it gives an empty name.

Some of the numbers the runtime itself raises:

NumberMeaning
1illegal function call: Value( ) of an empty Optional, for one
6an array index out of bounds (-exx)
7a null pointer followed (-exx)
11a floating-point error signal; under -exx that includes an integer division by zero

A handler can inspect the error, clean up and leave the procedure. It cannot go back into the code that failed: tiko has no resume.

on error goto handler
error 5
end

handler:
print "handled"
resume next                 '' tiko has no resume
noresume.tiko(7,8): error TKO0409: 'RESUME' is not available in the fb dialect
     |
   7 | resume next                 '' tiko has no resume
     |        ^

Pitfall

When an error jumps out of a procedure to a handler further out, that procedure's destructors and defer statements do not run.

type Connection
    as int32 id
    declare constructor( )
    declare destructor( )
end type

constructor Connection( )
    print "  connection opened"
end constructor

destructor Connection( )
    print "  connection closed"
end destructor

sub Transfer( )
    dim link as Connection
    defer print "  transfer finished"
    error 5                             '' leaves for the outer handler
end sub

on error goto failed
print "transfer:"
Transfer( )
end

failed:
dim as int32 code = err
print "handler: error"; code
transfer:
  connection opened
handler: error 5

Transfer opens a Connection and schedules a defer, and neither the destructor nor the deferred print runs. The handlers in the first example are different: each sits in the procedure that raised the error and leaves it with exit sub, so its locals are cleaned up as usual. Keep handlers next to the resources they protect, or report failures with a Result, which returns like any other value.

Activities

  1. Move the on error goto and the handler into Transfer. What does escape.tiko print now?
  2. Print *ermn( ) in Report's handler.

Static assertions

Some facts about a program are known before it runs: the value of a constant, the size of a type. #assert checks one of them while the program is being compiled, and refuses to build it if the fact is false.

'' #assert is checked while the program is compiled.
const SLOTS = 12
const SLOT_BYTES = 64

type Header
    as uint32 magic
    as uint16 version, flags
end type

#assert SLOTS mod 4 = 0                 '' the table is read in fours
#assert SLOTS * SLOT_BYTES <= 1024      '' it fits in one page
#assert sizeof( Header ) = 8            '' the file format says so

print "slots:"; SLOTS; ", bytes:"; SLOTS * SLOT_BYTES

Output

slots: 12, bytes: 768

Each #assert takes an expression the compiler can evaluate: constants, arithmetic on them, and sizeof. All three hold here, so the program builds, and the checks leave nothing behind in it: they cost nothing at run time.

Change a constant so that one no longer holds, and the build stops at that line:

const SLOTS = 20
const SLOT_BYTES = 64

#assert SLOTS mod 4 = 0                 '' holds
#assert SLOTS * SLOT_BYTES <= 1024      '' 1280: refused

print "built"
staticfail.tiko(5,1): error TKO0604: #assert condition failed
     |
   5 | #assert SLOTS * SLOT_BYTES <= 1024      '' 1280: refused
     | ^

The message does not say why the condition matters, so put that in a comment on the same line, as these do: the comment is what the reader sees next to the error.

Use #assert for decisions made in the source that other code depends on, such as a table size that must be a multiple of four or a record that must match a file format byte for byte. Facts known only at run time are checked with assert.

Activities

  1. Add a uint8 field to Header. Which assertion fails?
  2. Assert that SLOTS is a power of two, using and.

Memory and Resources

Most values in a tiko program live where the compiler puts them: in the frame of the procedure that declares them, created when the declaration runs and destroyed when the block ends. That is the stack, and it needs no help from you. When a value has to outlive the block that made it, or its size is known only at run time, you ask for memory on the heap, and giving it back becomes your job.

type Counter
    as int32 value
    declare constructor( byval start as int32 )
    declare destructor( )
end type

constructor Counter( byval start as int32 )
    value = start
    print "  made"; value
end constructor

destructor Counter( )
    print "  gone"; value
end destructor

function MakeOnHeap( byval n as int32 ) as Counter ptr
    dim onStack as Counter = Counter( n )   '' lives until end function
    return new Counter( n * 10 )            '' lives until delete
end function

print "calling"
dim as Counter ptr kept = MakeOnHeap( 4 )
print "back, and the heap one is still here:"; kept->value
delete kept
print "done"

Output

calling
  made 4
  made 40
  gone 4
back, and the heap one is still here: 40
  gone 40
done

MakeOnHeap makes two Counters. onStack is a local: its destructor runs at end function, which is why "gone 4" appears before the caller prints anything. The one made with new lives on the heap, so it survives the return, and the caller destroys it with delete when it has finished.

StackHeap
made bydim, var, parametersnew, allocate
destroyedat the end of its block, automaticallyby delete or deallocate
reached throughits namea pointer
a mistake looks likerarea leak, or a use after free

The pages in this section, from raw to managed:

Activities

  1. Remove the delete kept line. Which line of output disappears?
  2. Make MakeOnHeap return @onStack instead. Why is that wrong, even if it seems to work?

Pointers

A pointer holds the address of a value. Through it you can read and write the value from somewhere that does not know its name, which is how heap memory is reached and how linked structures are built.

'' An address, and what lives at it.
dim as int32 n = 10
dim as int32 ptr p = @n                 '' @ takes the address
print "*p ="; *p                        '' * reads what p points at
*p = 20                                 '' ...or writes it
print "n is now"; n

type Point
    as int32 x, y
end type

dim as Point pt = ( 3, 4 )
dim as Point ptr pp = @pt
pp->x = 30                              '' -> reaches a field
print "pt is"; pt.x; pt.y

'' Arithmetic counts elements, not bytes.
dim as int32 a( 0 to 4 ) = { 10, 20, 30, 40, 50 }
dim as int32 ptr first = @a( 0 )
print "first[2] ="; first[2]; ", *( first + 3 ) ="; *( first + 3 )

dim as uint8 ptr bytes = cast( uint8 ptr, first )
print "int32 ptr + 1 moves"; cast( integer, first + 1 ) - cast( integer, first )
print "uint8 ptr + 1 moves"; cast( integer, bytes + 1 ) - cast( integer, bytes )

'' Walking a run of elements, and the distance between two pointers.
dim as int32 ptr lastOne = @a( 4 ), q = first
print "walked:";
do while q <= lastOne
    print *q;
    q += 1
loop
print
print "lastOne - first ="; lastOne - first

Output

*p = 10
n is now 20
pt is 30 4
first[2] = 30, *( first + 3 ) = 40
int32 ptr + 1 moves 4
uint8 ptr + 1 moves 1
walked: 10 20 30 40 50
lastOne - first = 4
WrittenMeans
int32 ptrthe type: an address of an int32
@nthe address of n
*pthe value at p, for reading or writing
p->xfield x of the record at p; the same as ( *p ).x
p[i]the element i places after p; the same as *( p + i )

Writing through *p changed n, because p holds n's address: there is one value and two ways to reach it.

Pointer arithmetic counts elements, not bytes. first + 1 is the next int32, four bytes on; the same step on a uint8 ptr is one byte. That is why a pointer to an array's first element can be indexed like the array, and why subtracting two pointers into one array gives the number of elements between them, 4 here.

Pitfall

@v + 3, with v an int32, is not three bytes past v: it is three int32s, twelve bytes, outside v altogether. Reading there returns whatever is next in memory and nothing reports it. To step in bytes, step through a byte pointer: cast( uint8 ptr, @v ) + 3.

cast( uint8 ptr, first ) reinterprets an address as a pointer to another type. any ptr is an address with no type at all: every pointer converts to it and back without a cast, and it cannot be dereferenced until you convert it to a typed pointer.

Activities

  1. Print the bytes of the int32 value 258 through a uint8 ptr. Which byte comes first?
  2. Walk a backwards with a pointer, from lastOne down to first.

new and delete

new T allocates one T on the heap and returns a typed pointer to it; new T[ n ] allocates n of them. There is no sizeof to get wrong and no cast, and memory from new starts zeroed.

'' new knows the type: no sizeof, no cast, and the memory is zeroed.
dim as int32 ptr one = new int32( 42 )
print "new int32( 42 ):"; *one
delete one

dim as float64 ptr several = new float64[ 3 ]
several[0] = 1.5 : several[2] = 2.5
print "new float64[ 3 ]:"; several[0]; several[1]; several[2]
delete[] several                        '' [] matches the [] in new

'' For a type with no constructor, the arguments fill the fields.
type Point
    as int32 x, y
end type
dim as Point ptr pt = new Point( 3, 4 )
print "new Point( 3, 4 ):"; pt->x; pt->y
delete pt

'' For a type with a constructor, new calls it and delete runs the
'' destructor, once per element for the [] forms.
type Tracked
    as int32 id
    declare constructor( )
    declare destructor( )
end type

constructor Tracked( )
    static as int32 nextId = 0          '' counts every Tracked made
    nextId += 1
    id = nextId
    print "  construct"; id
end constructor

destructor Tracked( )
    print "  destroy"; id
end destructor

print "new Tracked[ 2 ]:"
dim as Tracked ptr pair = new Tracked[ 2 ]
print "delete[]:"
delete[] pair

Output

new int32( 42 ): 42
new float64[ 3 ]: 1.5 0 2.5
new Point( 3, 4 ): 3 4
new Tracked[ 2 ]:
  construct 1
  construct 2
delete[]:
  destroy 2
  destroy 1
  • new int32( 42 ) allocates and initialises a single value.
  • new float64[ 3 ] allocates three, all 0 until set: several[1] was never written and prints 0.
  • new Point( 3, 4 ) fills a constructor-less type's fields in order, as dim pt as Point = ( 3, 4 ) would.
  • For Tracked, which has a constructor, new Tracked[ 2 ] runs it once per element, and delete[] runs the destructor once per element, last element first.

Pair them exactly. Memory from new goes back with delete, memory from new[] with delete[], and memory from allocate with deallocate. Mixing the families is undefined behaviour; once a type has a destructor, only the matching form runs it the right number of times.

A type whose only constructor takes arguments cannot be made with new T[ n ]: there are no arguments to give each element, and the compiler says TKO0657: the type of '...' has no constructor that takes no argument.

Every new needs a delete on every path through the program, and the pages that follow show the tools that make that automatic: destructors, defer and shared pointers.

Activities

  1. Give Tracked a second field and initialise it in the constructor.
  2. Allocate a Point with new Point and no arguments, and print its fields.

nullptr

nullptr is the null pointer: an address of nothing. It converts to every pointer type, and it is how a pointer says "there is no value here", such as the end of a linked list.

'' nullptr is the null pointer: an address of nothing.
type Node
    as int32 value
    as Node ptr nextNode                '' nullptr marks the end
end type

dim as Node nodeC = ( 3, nullptr )
dim as Node nodeB = ( 2, @nodeC )
dim as Node nodeA = ( 1, @nodeB )

print "the list:";
dim as Node ptr at = @nodeA
do while at <> nullptr                  '' test before you follow
    print at->value;
    at = at->nextNode
loop
print

'' 0 is also null, but it is an integer to everything that has to
'' choose a type. nullptr is a pointer.
sub Show overload ( byval n as integer )
    print "  Show( integer )"
end sub

sub Show overload ( byval p as any ptr )
    print "  Show( any ptr )"
end sub

print "Show( 0 ) and Show( nullptr ):"
Show( 0 )
Show( nullptr )

var a = 0                               '' an integer
var b = nullptr                         '' a pointer
print "var from 0, then from nullptr:"
Show( a )
Show( b )

Output

the list: 1 2 3
Show( 0 ) and Show( nullptr ):
  Show( integer )
  Show( any ptr )
var from 0, then from nullptr:
  Show( integer )
  Show( any ptr )

The list's last node holds nullptr in nextNode, and the loop tests for it before following the link. That test is the whole discipline of null pointers: test a pointer that may be null before you follow it.

0 works as a null pointer too, but 0 is an integer, and wherever the compiler has to choose a type, it chooses an integer's. Given an integer overload and an any ptr one, Show( 0 ) picks the integer and Show( nullptr ) the pointer, and var copies the same choice. Write nullptr whenever you mean a pointer.

Following a null pointer is a bug, and under -exx the program stops at the access instead of crashing somewhere further on:

#cmdline "-exx"

type Point
    as int32 x, y
end type

'' -exx checks every access through a pointer for null.
dim as Point ptr p = nullptr
print p->x                              '' stops here
print "not reached"

Aborting due to runtime error 7 (null pointer access) at line 9 of nullcheck.tiko::()

The check covers *p, p->x and p[i]. The program ends with runtime error 7, and 7 is its exit status. Without -exx the same program crashes with an access violation. Runtime errors in general are covered in Runtime errors.

Activities

  1. Add a fourth node to the front of the list.
  2. In nullcheck.tiko, replace p->x with *cptr( int32 ptr, p ). Does the check still catch it?

References

A reference is a second name for a variable that already exists. Underneath it is an address, like a pointer, but it is used like the variable it names: no *, no ->, and no way for it to be null.

'' A byref parameter is a reference the caller sets up.
sub Double_( byref n as int32 )
    n *= 2
end sub

dim as int32 x = 21
Double_( x )
print "after Double_:"; x

'' dim byref makes a second name for an existing variable. It is
'' bound once, when declared, and stays bound to that variable.
dim as int32 b = 5
dim byref as int32 alias_ = b
alias_ = 9
print "b ="; b; ", same address: "; cbool( @alias_ = @b )

'' A function can return a reference: here, to an array element.
function Largest( a() as int32 ) byref as int32
    var best = lbound( a )
    for i as int32 = lbound( a ) + 1 to ubound( a )
        if a( i ) > a( best ) then best = i
    next
    return a( best )
end function

dim nums( 0 to 3 ) as int32 = { 4, 17, 8, 2 }
( Largest( nums() ) ) = 0               '' assigns to nums( 1 )
dim byref as int32 top = Largest( nums() )
top += 100                              '' now nums( 2 )
for each n in nums
    print n;
next
print

Output

after Double_: 42
b = 9, same address: true
 4 0 108 2

There are three places a reference appears:

  • A byref parameter. Double_( x ) hands the procedure x itself, so n *= 2 changes x.
  • A dim byref variable. alias_ is bound to b when it is declared and stays bound to it; alias_ and b have the same address. It must be bound where it is declared, so there is no null reference and no rebinding.
  • A byref result. Largest returns the largest element, not a copy of its value, so the caller can assign through the result or bind a dim byref to it. top += 100 changes nums( 2 ).

Prefer a reference whenever "nothing" is not a valid answer, and a pointer when it is.

Pitfall

At the start of a statement, F( x ) = value is not an assignment. It reads as a call to F whose argument is the comparison x = value:

type Board
    as int32 cells( 0 to 8 )
    declare function Cell( byval i as int32 ) byref as int32
end type

function Board.Cell( byval i as int32 ) byref as int32
    return cells( i )
end function

dim b as Board
b.Cell( 4 ) = 5                         '' meant as an assignment...
print "cell 4 holds"; b.cells( 4 )
( b.Cell( 4 ) ) = 5                     '' ...and written as one
print "cell 4 holds"; b.cells( 4 )
cell 4 holds 0
cell 4 holds 5

The first line changes nothing. The compiler warns, TKO0931: '=' is read as a comparison inside the argument of 'cell', not as an assignment to its byref result; to assign, write '( cell( ... ) ) = value', but the program still builds and runs. Wrap the call in parentheses, as the second line does, to assign through a byref result.

Status in this version

With an array argument, as in Largest( nums() ) = 0, the same statement stops the build with an internal compiler error, TKO0994, after the TKO0931 warning. The parenthesised form works.

Activities

  1. Write function Smallest( a() as int32 ) byref as int32 and use it to set the smallest element to 0.
  2. Bind a dim byref to an element of an array, then redim preserve the array to 1000 elements. Why must you not use the reference afterwards?

Destructors and scope

A type with a destructor cleans up after itself: the destructor runs when the object's block ends, however the block is left. Tie a resource to an object, acquiring it in the constructor and releasing it in the destructor, and the release happens without any code at the places the block ends.

'' A destructor runs when its object's block ends, on every way out.
type Resource
    as ustring tag
    declare constructor( byref tag as ustring )
    declare destructor( )
end type

constructor Resource( byref tag as ustring )
    this.tag = tag
    print "  open "; tag
end constructor

destructor Resource( )
    print "  close "; tag
end destructor

sub Work( byval stopEarly as boolean )
    dim a as Resource = Resource( "a" )
    dim b as Resource = Resource( "b" )
    scope
        dim inner as Resource = Resource( "inner" )
        print "  in the scope"
    end scope                           '' inner closes here
    if stopEarly then
        print "  returning early"
        exit sub                        '' b, then a, close here...
    end if
    dim c as Resource = Resource( "c" )
    print "  at the end"
end sub                                 '' ...or c, b, a close here

print "Work( false ):"
Work( false )
print "Work( true ):"
Work( true )

Output

Work( false ):
  open a
  open b
  open inner
  in the scope
  close inner
  open c
  at the end
  close c
  close b
  close a
Work( true ):
  open a
  open b
  open inner
  in the scope
  close inner
  returning early
  close b
  close a

Three rules are visible in that output:

  • Objects are destroyed in reverse order of construction. c was made last and closes first, then b, then a. Something made later may depend on something made earlier, never the other way round, so this order is always safe.
  • Every block counts. inner lives in a scope ... end scope block and closes at end scope, while Work is still running.
  • Every way out counts. exit sub in the second call leaves before c exists, and b and a still close, in the same reverse order. The same holds for return and exit for.

The order is the order you wrote things in, reversed. Nothing depends on a collector, a timer or where objects happen to be in memory.

The one exception is a runtime error that jumps out of a procedure to a handler further out, which skips that procedure's destructors; see Error numbers and handlers. Destructors themselves are introduced in Objects.

Activities

  1. Add dim d as Resource = Resource( "d" ) inside the if block, before exit sub. When does d close?
  2. Put the three dim lines inside a for loop that runs twice. What is opened and closed on each pass?

defer for cleanup

A procedure with three returns after its allocate needs three deallocates, and the fourth return someone adds later leaks. defer writes the release once, on the line after the allocation, and runs it on every way out of the block. The statement itself is introduced in defer; this page shows it paired with memory.

'' deallocate, and say so, so the program shows when it happens.
sub Release( byval p as any ptr )
    deallocate( p )
    print "  freed"
end sub

'' The squares 1, 4, 9 ... of the first n numbers, summed through a
'' scratch buffer. Every way out after the allocate frees it.
function SumOfSquares( byval n as int32 ) as int64
    if n < 1 then return 0                  '' nothing allocated yet

    dim as int64 ptr squares = allocate( n * sizeof( int64 ) )
    defer Release( squares )                '' written once, beside it
    if squares = nullptr then return -1     '' freeing null is harmless

    for i as int32 = 0 to n - 1
        squares[i] = ( i + 1 ) * ( i + 1 )
    next
    if n > 1000 then return -2              '' another way out

    dim as int64 total = 0
    for i as int32 = 0 to n - 1
        total += squares[i]
    next
    return total
end function

dim sizes( 0 to 2 ) as int32 = { 0, 3, 2000 }
for each n in sizes
    print "n ="; n
    var sum = SumOfSquares( n )
    print "  result"; sum
next

Output

n = 0
  result 0
n = 3
  freed
  result 14
n = 2000
  freed
  result-2

Release frees the block and prints freed, so the output shows when the deferred call runs.

  • With n = 0 the function returns before the allocate, and before the defer line is reached, so nothing is freed: a defer only takes effect once the program has passed it.
  • With n = 3 the function reaches its last return, and the block is freed on the way out.
  • With n = 2000 it leaves early through return -2, and the block is freed all the same.

Three habits make this reliable:

  • Write the defer on the line after the allocate, before the check for null. deallocate( nullptr ) does nothing, so the failure path is covered too, and the pair cannot drift apart.
  • The deferred statement reads its variables when it runs, at the end of the block, not when the defer line is reached. If a reallocate stores a new address in squares, the right block is still freed.
  • Allocate and defer in the procedure's own body, not inside an if. A defer runs at the end of the block it is written in, so inside an if it frees the memory at end if.

defer suits a resource used in one procedure. When the same kind of resource is acquired in many places, a type with a destructor puts the release where it cannot be forgotten.

Activities

  1. Add a second scratch buffer to SumOfSquares with its own defer. In which order are the two freed?
  2. Move the allocate and its defer inside an if n > 0 then ... end if block. Where does freed appear now?

Non-copyable types

A type that owns a resource has to decide what a copy means. If a Buffer that owns a block of memory were copied field by field, two objects would hold the same address, and two destructors would free it twice. For a resource with one owner, the right answer is usually to refuse copies.

'' A Buffer owns a block of memory for exactly as long as it lives.
'' A copy would share the block, and two destructors would free it
'' twice, so copying is declared private and never defined.
type Buffer
    declare constructor( byval length as int32 )
    declare destructor( )
    declare function Size( ) as int32
    declare function At( byval i as int32 ) byref as uint8
private:
    declare constructor( byref other as Buffer )    '' no copies
    declare operator let( byref other as Buffer )   '' no assignment
    as uint8 ptr bytes
    as int32 count
end type

constructor Buffer( byval length as int32 )
    bytes = callocate( length, 1 )
    count = length
    print "  allocated"; length; " bytes"
end constructor

destructor Buffer( )
    deallocate( bytes )
    print "  freed"; count; " bytes"
end destructor

function Buffer.Size( ) as int32
    return count
end function

function Buffer.At( byval i as int32 ) byref as uint8
    return bytes[i]
end function

'' Borrowing by reference is fine: no second owner is made.
function Total( byref b as Buffer ) as int32
    dim as int32 sum = 0
    for i as int32 = 0 to b.Size( ) - 1
        sum += b.At( i )
    next
    return sum
end function

scope
    dim block as Buffer = Buffer( 4 )
    for i as int32 = 0 to 3
        ( block.At( i ) ) = i * 10
    next
    print "  total"; Total( block )
end scope
print "after the scope"

Output

  allocated 4 bytes
  total 60
  freed 4 bytes
after the scope

The constructor allocates and the destructor frees, so the code that uses a Buffer contains no cleanup at all: block is freed at end scope.

The two private declarations are what make it safe. The copy constructor and operator let are declared and never defined, and because they are private, no code outside the type can reach them. Passing the buffer byref, as Total does, makes no copy, so it is allowed. Anything that would make a second owner is refused when the program is compiled:

type Buffer
    declare constructor( byval size as int32 )
private:
    declare constructor( byref other as Buffer )    '' no copies
    as int32 count
end type

constructor Buffer( byval size as int32 )
    count = size
end constructor

dim a as Buffer = Buffer( 4 )
dim b as Buffer = a                     '' a second owner? refused
nocopy.tiko(13,5): error TKO0633: 'b' needs a constructor or destructor that is not accessible here
     |
  13 | dim b as Buffer = a                     '' a second owner? refused
     |     ^

Assigning one buffer to another, b = a, reaches the private operator let instead and is refused with TKO0746: the operator let of 'buffer' is private or protected and cannot be reached from here. Passing a Buffer byval would copy it and is refused with TKO0633 too.

A type that owns something can copy it properly, be built from parts that copy themselves, or refuse to be copied. For a resource the system gives you once, such as a block of memory, a lock or a handle, refusing is usually right.

Activities

  1. Add a Fill( byval value as uint8 ) method and use it before Total.
  2. Write a Total that takes the buffer byval, and read the error.

Shared pointers

Sometimes one object has several owners, and none of them knows when the others have finished with it. SharedPtr( of T ), from the std/memory module, keeps a count of owners: each copy adds one, each copy that goes away subtracts one, and the object is destroyed when the count reaches zero.

#import "std/memory"

'' One object, several owners; the last owner to go destroys it.
type Session
    as ustring user
    declare destructor( )
end type

destructor Session( )
    print "  session for "; user; " closed"
end destructor

'' byref: the procedure uses the object and does not become an owner.
sub Show( byref s as memory.SharedPtr( of Session ) )
    print "  "; s->user; ", owners:"; s.UseCount( )
end sub

var s = memory.MakeShared( of Session )( )     '' a new Session
s->user = "ada"                                '' -> reaches its fields
Show( s )

scope
    var t = s                                  '' a second owner
    Show( t )
end scope                                      '' t lets go; s remains
Show( s )

'' A SharedPtr can also adopt an object made with new.
var other = memory.SharedPtr( of Session )( new Session )
other->user = "grace"
Show( other )

print "reset both:"
s.Reset( )                                     '' the last owner of ada
other.Reset( )
print "s is null: "; s.IsNull( )

Output

  ada, owners: 1
  ada, owners: 2
  ada, owners: 1
  grace, owners: 1
reset both:
  session for ada closed
  session for grace closed
s is null: true
Procedure or memberDoes
memory.MakeShared( of T )( )a new T on the heap, owned by the returned pointer
memory.SharedPtr( of T )( p )adopt an object made with new; never delete it yourself
p->field, *pthe object, as through a raw pointer, with no check for null
p.Ref( )the object, checked: runtime error 1 on an empty pointer
p.UseCount( )how many SharedPtrs own the object
p.Reset( )stop owning; the object goes if this was the last owner
p.IsNull( )whether this pointer owns nothing

var t = s makes a second owner, and the count is 2 inside the scope. When t goes, the count drops back to 1 and the session stays. s.Reset( ) lets go of the last owner, and the destructor runs at that moment.

Show takes its pointer byref, so calling it makes no new owner. Pass a SharedPtr byval only when the procedure keeps a copy that outlives the call.

Cycles, and WeakPtr

Counting owners has one failure it cannot detect: two objects that own each other. A WeakPtr( of T ) observes an object without owning it, and is the tool for every link that points back.

#import "std/memory"

'' A parent owns its child; the child's link back must not own.
type Node
    as ustring tag
    as memory.SharedPtr( of Node ) child        '' owns, downward
    as memory.WeakPtr( of Node ) parent         '' observes, upward
    declare destructor( )
end type

destructor Node( )
    print "  freed "; tag
end destructor

'' The same shape with an owning link back: a cycle.
type LeakyNode
    as ustring tag
    as memory.SharedPtr( of LeakyNode ) child
    as memory.SharedPtr( of LeakyNode ) parent
    declare destructor( )
end type

destructor LeakyNode( )
    print "  freed "; tag
end destructor

print "owning both ways:"
scope
    var root = memory.MakeShared( of LeakyNode )( )
    root->tag = "leaky root"
    root->child = memory.MakeShared( of LeakyNode )( )
    root->child->tag = "leaky child"
    root->child->parent = root
end scope
print "  (nothing was freed)"

print "weak link back:"
scope
    var root = memory.MakeShared( of Node )( )
    root->tag = "root"
    root->child = memory.MakeShared( of Node )( )
    root->child->tag = "child"
    root->child->parent = root

    '' Lock( ) gives a real owner for as long as you hold it, or null.
    var up = root->child->parent.Lock( )
    if up.IsNull( ) = false then print "  the child's parent is "; up->tag
    print "  root owners:"; root.UseCount( )
end scope

Output

owning both ways:
  (nothing was freed)
weak link back:
  the child's parent is root
  root owners: 2
  freed root
  freed child

In the first tree the root owns the child and the child owns the root. When the scope ends, the two local pointers let go, but each node still has one owner, the other node, so neither count reaches zero and neither destructor runs. The memory is lost for the rest of the program, and nothing reports it.

In the second tree the link back is a WeakPtr. It does not count, so when root goes the root's count reaches zero, the root is freed, its child pointer goes with it, and the child follows.

A WeakPtr is read through Lock( ), which returns a real SharedPtr, an owner for as long as you hold it, or an empty one if the object has gone. That is why root owners is 2 while up exists. Ownership flows one way: make every link from owner to owned a SharedPtr, and every link back a WeakPtr.

Note

std/memory has no single-owner smart pointer. For an object used inside one procedure, a destructor or defer does that job.

Activities

  1. Print root.UseCount( ) after up.Reset( ).
  2. In weak.tiko, make the root's child the weak link and parent the owning one. What is freed, and when?

Modules

A program of any size uses code it did not write: the standard library, libraries of your own, packages from other people. tiko organises that code in modules. A module is compiled once, shows the world only the names it marks public, and keeps those names behind its own name, so two modules can both have a Parse without colliding.

The standard library is a set of modules. You bring one into a file with #import, and reach its names through the module's name:

'' Each #import names a module of the standard library. The imports
'' come first in the file, before any other code.
#import "std/strings"
#import "std/containers"

'' A module's names are reached through the module's name.
print strings.Replace( "a-b-c", "-", " + " )
print strings.StartsWith( "tiko-lang", "tiko" )
print strings.Format( "{} has {} letters", "module", len( "module" ) )

'' Types are reached the same way.
dim words as containers.Array( of ustring )
words.Push( "pear" )
words.Push( "apple" )
words.Push( "fig" )
print "words:"; words.Count( )

Output

a + b + c
true
module has 6 letters
words: 3

#import "std/strings" binds the name strings, the last part of the path. After it, strings.Replace is the Replace procedure of that module, and containers.Array( of ustring ) is its Array type. A value of a module's type behaves like one of your own: words.Push and words.Count need no prefix, because a method belongs to its type.

Two rules apply to every import:

  • #import comes before any other code in the file, including #define lines. An #import further down is TKO0362, "'#import' must come before any code".
  • The path is quoted and uses / on every platform.

There is nothing to list on the command line: tiko importing.tiko reads the imports, builds each module it needs (or takes it from the build cache), and links the result.

The rest of this section shows how to use names without the prefix, write a module, decide what it exposes, pass imports on, group modules into packages, and keep names apart inside one file with namespaces.

Activities

  1. Print strings.Reverse( "tiko" ).
  2. Move one #import line below the first print and read the diagnostic.

using and import names

Writing containers. in front of every name is clear, but it gets long in a file that uses one module heavily. Two tools shorten it: #import … as gives the module another name in this file, and using makes a module's public names usable with no prefix at all.

'' "as" gives a module a different name in this file.
#import "std/strings" as txt
#import "std/containers"

'' using makes a module's public names usable without the prefix.
using containers

dim primes as Array( of int32 )
for n as int32 = 2 to 20
    var isPrime = true
    for d as int32 = 2 to n - 1
        if n mod d = 0 then isPrime = false : exit for
    next
    if isPrime then primes.Push( n )
next

print txt.Format( "{} primes below 20", primes.Count( ) )
print txt.Format( "the largest is {}", primes[primes.Count( ) - 1] )

'' The qualified name still works under using.
dim names as containers.Array( of ustring )
names.Push( "tiko" )
print txt.PadLeft( names[0], 8, "." )

Output

8 primes below 20
the largest is 19
....tiko
  • #import "std/strings" as txt renames the module for this file only. The module is still strings; only the spelling here changes. Use it when two imports end in the same name, or when a name is long. The new name must not be a keyword or a built-in: as str is refused, because str is a built-in function.
  • using containers makes Array a bare name from that line to the end of the file. Written inside a procedure, it lasts to the end of the procedure. The qualified spelling, containers.Array, keeps working.

using makes names available; it never chooses between two of them. When two modules you are using both supply a name, a bare use of it is refused:

'' Two modules under using both supply Contains.
#import "std/strings"
#import "std/containers"
using strings
using containers

print Contains( "tiko", "k" )      '' refused: which Contains?
ambiguous.tiko(7,7): error TKO0919: 'contains' is ambiguous: more than one USING in effect declares it; qualify it
     |
   7 | print Contains( "tiko", "k" )      '' refused: which Contains?
     |       ^

Write strings.Contains and the question has an answer in the source.

Activities

  1. Remove using containers and fix the program by qualifying Array.
  2. Move using containers inside a sub and find the first line where Array is no longer a bare name.

Writing a module

A module of your own is a directory of source files. The directory's name is the module's name, and every file in it says so on its first line:

writing.tiko
greet/
    greet.tiko
'' A module is a directory. Every file in it begins with #module and
'' the directory's name.
#module "greet"

public function Hello( byval who as ustring ) as ustring
    return "Hello, " & Capitalise_( who ) & "!"
end function

public const DEFAULT_NAME = "world"

function Capitalise_( byval s as ustring ) as ustring
    return ucase( left( s, 1 ) ) & mid( s, 2 )
end function

The program imports the directory by a path relative to itself:

'' "./greet" is the directory greet/ beside this file.
#import "./greet"

print greet.Hello( "ada" )
print greet.Hello( greet.DEFAULT_NAME )

Output

Hello, Ada!
Hello, World!
  • #module "greet" must be the first directive of every file of the module (comments may come before it), and the name must match the directory.
  • #import "./greet" starts with ./, so it is looked for beside the importing file. The import binds greet, and the program calls greet.Hello.
  • public marks what importers may use: Hello and DEFAULT_NAME. Capitalise_ has no keyword, so it belongs to the module alone. The next page shows the three levels.

tiko writing.tiko finds the greet directory, compiles it, caches the result, and links it in. A module may hold as many files as it needs; together they form one module. Inside a module, declaration order does not matter: a file can call a procedure declared further down, or in another file of the module, without a declare line. Hello calls Capitalise_ before it is declared.

Activities

  1. Add a public Goodbye function to greet and call it from the program.
  2. Change the name in #module to "greeter" and read the diagnostic.

Visibility

A module decides which of its names are its interface. The default is the cautious one: a name you forget to mark stays inside the module, so you can change it later without breaking anyone.

This module has two files. scores.tiko holds the interface:

#module "scores"

'' public: part of the module's interface, for every importer.
public function Grade( byval points as int32 ) as ustring
    return Letter_( Clamp_( points ) )
end function

and rules.tiko the helpers it calls:

'' A second file of the same module.
#module "scores"

'' No keyword: every file of this module may call it; importers may not.
function Letter_( byval points as int32 ) as ustring
    select case points
    case is >= 90 : return "A"
    case is >= 75 : return "B"
    case is >= 50 : return "C"
    case else     : return "F"
    end select
end function

'' private: the module's own business, invisible from outside.
private function Clamp_( byval points as int32 ) as int32
    if points < 0 then return 0
    if points > 100 then return 100
    return points
end function

The program can use only Grade:

#import "./scores"

dim results( 0 to 3 ) as int32 = { 97, 80, 51, 130 }
for i as int32 = 0 to 3
    print results( i ); " -> "; scores.Grade( results( i ) )
next

Output

 97 -> A
 80 -> B
 51 -> C
 130 -> A
You writeVisible to
publicevery file that imports the module
nothingevery file of this module, and no importer
privatethis module only; outside it the name does not exist

Grade in one file calls Letter_ and Clamp_ from the other: the files of a module see each other's names, whatever their visibility. From outside, the two helpers are refused in different ways. A module-private name is known but closed:

#import "./scores"

print scores.Letter_( 95 )      '' refused: Letter_ is not public
notpublic.tiko(3,14): error TKO0860: 'letter_' is not public in module 'main.scores'
     |
   3 | print scores.Letter_( 95 )      '' refused: Letter_ is not public
     |              ^

main.scores is the module's full name: a program with no tiko.toml is the package main. A private name is not there at all:

#import "./scores"

print scores.Clamp_( 130 )      '' refused: private names are not seen
notvisible.tiko(3,14): error TKO0745: the namespace 'scores' has no member named 'clamp_'
     |
   3 | print scores.Clamp_( 130 )      '' refused: private names are not seen
     |              ^

The trailing _ on the helpers' names is a convention, not a rule: a reminder in the code that a name is not part of the interface.

Activities

  1. Mark Letter_ public and call it from the program.
  2. Add a public function whose result type is a type declared without public, and read what the compiler says about it.

Re-exporting

An import belongs to the file that writes it. A module that uses scores internally does not hand scores to its own importers, so it is free to replace it later. When a module does want to pass one of its imports on, it adds public to the #import:

#module "kit"
#import "../greet" public   '' re-exported: importers reach kit.greet
#import "../scores"         '' kit's own business

public function Report( byval who as ustring, _
                        byval points as int32 ) as ustring
    return greet.Hello( who ) & " Your grade: " & scores.Grade( points )
end function
'' One import brings in kit, and with it the module kit passes on.
#import "./kit"

print kit.Report( "grace", 88 )
print kit.greet.Hello( "again" )

Output

Hello, Grace! Your grade: B
Hello, Again!

The program imports only kit. Because kit re-exports greet, the program reaches it as kit.greet.Hello. kit imports scores without public, so that one stays out of reach:

#import "./kit"

print kit.scores.Grade( 88 )    '' refused: kit did not re-export scores
noleak.tiko(3,11): error TKO0745: the namespace 'kit' has no member named 'scores'
     |
   3 | print kit.scores.Grade( 88 )    '' refused: kit did not re-export scores
     |           ^

A program that needs scores imports it itself. Re-export a module when your own interface is incomplete without it: when a type from that module appears in your public procedures, for example.

Activities

  1. Add public to kit's import of scores and make noleak.tiko compile.
  2. Import greet directly in reexport.tiko as well, and call it both ways.

Packages

Modules that sit beside one program are enough for that program. Modules meant for several programs become a package: a directory tree of modules with a tiko.toml at its root naming the package. A program that uses the package says where to find it, in a tiko.toml of its own.

units/
    tiko.toml
    length/length.tiko
    temperature/temperature.tiko
app/
    tiko.toml
    main.tiko
# A package: a directory tree of modules with a tiko.toml at its root.
[package]
name = "units"
#module "length"

public const FEET_PER_METRE = 3.28084

public function ToFeet( byval metres as float64 ) as float64
    return metres * FEET_PER_METRE
end function
#module "temperature"

public function ToFahrenheit( byval celsius as float64 ) as float64
    return celsius * 9 / 5 + 32
end function

The program's tiko.toml names the dependency by path, and declares the program it builds:

# The program's project: its name, what it depends on, what it builds.
[package]
name = "app"

[dependencies]
units = { path = "../units" }

[target.convert]
main = "main.tiko"
'' A path that starts with a dependency's name is found through
'' tiko.toml, wherever that package lives.
#import "units/length"
#import "units/temperature"
#import "std/strings"

print strings.Format( "100 m is {:.1f} ft", length.ToFeet( 100 ) )
print strings.Format( "25 C is {} F", temperature.ToFahrenheit( 25 ) )

Built with tiko build in the app directory, and run as out/debug/convert:

Output

100 m is 328.1 ft
25 C is 77 F
  • [package] name is the package's name. The module length inside units has the full name units.length, so it never collides with a length module from another package. The program still calls it length, the last part of the import path.
  • [dependencies] maps a package name to a directory, relative to the tiko.toml. An import whose first part is units is looked for there. The dependency is written in the repository, so a fresh checkout builds with no setup.
  • [target.convert] declares an executable built from main.tiko. The Building section covers targets, profiles and tiko build.

tiko --print-graph main.tiko lists the modules a program imports, in the order they are built, with the directory each came from. For this program it prints the standard library's modules and then:

units.length       ../units/length
units.temperature  ../units/temperature

Activities

  1. Add a module units/weight with a public ToPounds and use it from the program.
  2. Rename the dependency in app/tiko.toml to measures without changing the imports, and read what the build says.

Namespaces

A module keeps names apart between directories. Inside one file, a namespace does the same job: it gives a group of names a family name, so two parts of a program can each have an Area.

'' A namespace gives a group of names a family name, inside one file.
namespace Metric
    const CM_PER_M = 100

    function Area( byval w as float64, byval h as float64 ) as float64
        return w * h
    end function

    '' Namespaces nest.
    namespace Convert
        function ToInches( byval cm as float64 ) as float64
            return cm / 2.54
        end function
    end namespace
end namespace

namespace Imperial
    '' The same name in another namespace is a different procedure.
    function Area( byval w as float64, byval h as float64 ) as float64
        return w * h * 144      '' square feet to square inches
    end function
end namespace

'' A namespace can be reopened to add more names.
namespace Metric
    function Perimeter( byval w as float64, _
                        byval h as float64 ) as float64
        return 2 * ( w + h )
    end function
end namespace

print "metric area:  "; Metric.Area( 2, 3 )
print "imperial area:"; Imperial.Area( 2, 3 )
print "perimeter:    "; Metric.Perimeter( 2, 3 )
print "inches:       "; Metric.Convert.ToInches( 254 )

'' using makes the names bare until the end of the block.
sub Report( )
    using Metric
    print "in Report:    "; Area( 4, 5 ); CM_PER_M
end sub

Report( )

Output

metric area:   6
imperial area: 864
perimeter:     10
inches:        100
in Report:     20 100
  • Outside the namespace, a member is written qualified: Metric.Area, Imperial.Area. Inside it, the bare name is enough.
  • Namespaces nest, and the full path is written out: Metric.Convert.ToInches.
  • A namespace can be reopened. The second namespace Metric block adds Perimeter to the same namespace.
  • using Metric inside Report makes the members usable bare until the end of Report. Written at the top level, it lasts to the end of the file.

A name declared closer to where it is used always beats one that using brings in, so adding a using never changes the meaning of a name you already had.

Use a module for code that other files or programs share; a module is already a namespace, with visibility rules on top. Use namespace to group names within a file, or in a header that must stay a header.

Activities

  1. Add Imperial.Perimeter and call both Perimeters.
  2. Put using Metric and using Imperial in one sub and call Area bare. What does the compiler say?

The Preprocessor

Before the compiler reads a word of a program, the preprocessor rewrites its text. It replaces names with the text they stand for, keeps or drops whole blocks of lines, and reports problems of its own. Every line that starts with # is an instruction to it.

The simplest instruction is #define, which gives a name to a piece of text:

'' #define gives a name to a piece of text. The preprocessor puts the
'' text in place of every use of the name, before the program is
'' compiled.
#define APP_NAME "tally"
#define MAX_ITEMS 5
#define BANNER "-- " & APP_NAME & " --"

print BANNER
dim counts( 1 to MAX_ITEMS ) as int32
print "slots:"; ubound( counts )

'' #ifdef asks whether a name is defined; #undef removes it.
#undef MAX_ITEMS
#ifdef MAX_ITEMS
    print "MAX_ITEMS is defined"
#else
    print "MAX_ITEMS is not defined any more"
#endif

'' For a plain value, a const is usually the better choice: it has a
'' type and a scope, and the compiler checks it.
const LIMIT as int32 = 10
print "limit:"; LIMIT

Output

-- tally --
slots: 5
MAX_ITEMS is not defined any more
limit: 10
  • #define MAX_ITEMS 5 makes every later MAX_ITEMS read 5. The replacement is text, so BANNER becomes a whole expression, with APP_NAME replaced inside it.
  • #undef removes a definition, and #ifdef asks whether a name is defined at that point in the file. The conditional compilation page covers the full family.
  • const is the better tool for a plain value. A constant has a type and obeys scope, and a mistake in its value is reported where it is written, not at every use.

Use #define for what only the preprocessor can do: switches that choose which code is compiled, names for the build to set from outside, and macros that generate code.

Activities

  1. Change MAX_ITEMS to 8 and check the array follows.
  2. Use MAX_ITEMS after the #undef and read what the compiler says.

Macros

A #define with parameters is a macro. Each use is replaced by the body, with the text of each argument put in place of the parameter. That makes a macro able to do things a procedure cannot: turn code into a string, build a name, or expand to whole statements.

'' A macro with parameters substitutes the text of each argument.
'' Parenthesise every parameter, and the whole body.
#define LARGER( a, b ) iif( ( a ) > ( b ), ( a ), ( b ) )
#define SQUARE( x ) ( ( x ) * ( x ) )

'' #x turns an argument into a string literal; ## pastes two tokens.
#define SHOW( expr ) print #expr; " ="; expr
#define COLUMN( n ) column##n

dim as int32 column1 = 4, column2 = 9
SHOW( LARGER( column1, column2 ) )
SHOW( SQUARE( column1 + 1 ) )
SHOW( COLUMN( 2 ) )

'' #macro ... #endmacro spans several lines, so it can expand to
'' whole statements.
#macro SWAP_VALUES( a, b )
    scope
        var tmp_ = a
        a = b
        b = tmp_
    end scope
#endmacro

SWAP_VALUES( column1, column2 )
print "after the swap:"; column1; column2

Output

iif( ( column1 ) > ( column2 ), ( column1 ), ( column2 ) ) = 9
( ( column1 + 1 ) * ( column1 + 1 ) ) = 25
column2 = 9
after the swap: 9 4
  • LARGER( a, b ) and SQUARE( x ) take arguments. The output shows what each use turned into: the arguments are pasted in as text, so SQUARE( column1 + 1 ) evaluates column1 + 1 twice.
  • #expr turns an argument into a string literal. The argument is expanded first, which is why the first line shows LARGER's body, not its name.
  • column##n pastes two pieces into one token: COLUMN( 2 ) becomes the identifier column2.
  • #macro … #endmacro defines a macro over several lines, so it can expand to statements. SWAP_VALUES wraps its temporary in a scope block, so that two uses in one procedure do not both declare tmp_.

A macro cannot take the name of a keyword: #define FIELD( n ) … is refused with TKO0371, "'field' cannot be defined as a macro: it is a reserved word".

Pitfall

A macro substitutes text, not values. The parentheses in SQUARE are what make it work:

'' Without parentheses, the argument's text mixes with the body's.
#define BAD_SQUARE( x ) x * x
#define SQUARE( x ) ( ( x ) * ( x ) )

print "BAD_SQUARE( 1 + 2 ) ="; BAD_SQUARE( 1 + 2 )
print "SQUARE( 1 + 2 )     ="; SQUARE( 1 + 2 )
BAD_SQUARE( 1 + 2 ) = 5
SQUARE( 1 + 2 )     = 9

BAD_SQUARE( 1 + 2 ) becomes 1 + 2 * 1 + 2, which is 5. Wrap every parameter in parentheses, and the whole body too.

A procedure has none of these problems: its argument is evaluated once and has a type. Write a procedure first, and a macro only when you need the source text itself.

Activities

  1. Call SQUARE with a function that prints each time it runs, and count the calls.
  2. Write #macro REPEAT( n, body ) that runs body n times in a for loop, and use it to print three lines.

Conditional compilation

#if, #elseif, #else and #endif choose which lines are compiled at all. A line in a branch that is not taken is never compiled, so it costs nothing in the program and may even name things that do not exist.

'' Conditional compilation chooses which lines are compiled at all.
#define LOG_LEVEL 2
#define WITH_COLOUR

#if LOG_LEVEL >= 3
    print "logging: everything"
#elseif LOG_LEVEL = 2
    print "logging: warnings and errors"
#else
    print "logging: errors only"
#endif

'' #ifdef and #ifndef ask only whether a name is defined.
#ifdef WITH_COLOUR
    print "colour: on"
#endif
#ifndef WITH_SOUND
    print "sound: not built in"
#endif

'' defined( ) does the same inside an #if, and combines with andalso,
'' orelse and not.
#if defined( WITH_COLOUR ) andalso not defined( WITH_SOUND )
    print "a quiet, colourful build"
#endif

'' A line in a branch not taken is never compiled, so it may name
'' things that do not exist.
#if 0
    NoSuchProcedure( )
#endif
print "done"

Output

logging: warnings and errors
colour: on
sound: not built in
a quiet, colourful build
done
DirectiveKeeps the lines that follow when
#if exprthe constant expression is not zero
#elseif exprno earlier branch was taken and expr is not zero
#elseno earlier branch was taken
#ifdef NAMENAME is defined
#ifndef NAMENAME is not defined
defined( NAME )inside #if: true when NAME is defined

The expressions are worked out by the preprocessor, so they may use only constants and defined names. #if 0 … #endif is a quick way to switch off a block of code without commenting out every line.

Pitfall

#ifdef asks whether a name is defined, not what it is.

'' TRACE is 0 to turn tracing off -- or so the author thought.
#define TRACE 0

#ifdef TRACE
    print "#ifdef TRACE: tracing is on"
#endif

#if TRACE
    print "#if TRACE: tracing is on"
#else
    print "#if TRACE: tracing is off"
#endif
#ifdef TRACE: tracing is on
#if TRACE: tracing is off

TRACE is defined, as 0, so every #ifdef TRACE is true. Choose one convention per switch: either it is defined or not, and you test it with #ifdef, or it always has a value, and you test it with #if.

Activities

  1. Set LOG_LEVEL to 3, then to 1, and predict each output first.
  2. Define WITH_SOUND and see which two lines change.

Defines from the build

A switch is most useful when the build sets it, so one source makes several programs. The -d option defines a name before the first line of the file: tiko app.tiko -d EDITION=2 behaves as if the file began with #define EDITION 2. A #cmdline line puts such options in the source itself:

'' -d NAME=value defines a symbol before the first line of the file.
'' #cmdline puts compiler options in the source itself.
#cmdline "-d EDITION=2 -d SEATS=25"

'' A default, for a build that does not choose.
#ifndef EDITION
    #define EDITION 1
#endif

#if EDITION = 1
    print "standard edition"
#else
    print "professional edition"
#endif

#ifdef SEATS
    print "licensed for"; SEATS; " seats"
#endif

Output

professional edition
licensed for 25 seats
  • #cmdline "-d EDITION=2 -d SEATS=25" adds those options to the compile, as if they had been typed after tiko cmdline.tiko. When the compiler meets the line, it starts the compile again with them.
  • #ifndef EDITION gives the setting a default, so the program also builds when nothing chose a value.
  • The source wins. With the #cmdline line in place, tiko cmdline.tiko -d EDITION=1 still builds the professional edition. Take the line out and the same command builds the standard one; with no -d at all, the default applies and SEATS is not defined.

#cmdline counts only in the first source file named on the command line; in any other file it is warning TKO0334, "#cmdline is ignored outside the first source file". It is not limited to -d: any compiler option can go there, which is how the examples on this site carry flags such as -exx (see Building).

A module never sees the defines of the program that imports it, from -d or from #define: it is compiled once for every program that uses it. A setting for a module goes in its package's tiko.toml (see Packages).

Activities

  1. Remove the #cmdline line and build with -d EDITION=3.
  2. Add -d TRIAL=1 to the #cmdline line and print a notice under #ifdef TRIAL.

Platform detection

A program that runs on Windows, Linux and macOS sometimes has to do things differently on each: a path separator, the name of an environment variable, a system call that exists on only one of them. The compiler predefines a symbol for the platform it is building for, and conditional compilation picks the code for it.

'' One source, three platforms: the compiler predefines a symbol for the
'' platform it builds for, and the branches for the others are never
'' compiled.
#if defined( __FB_WIN32__ )
    const PLATFORM = "Windows"
    const PATH_SEP = "\"
    const TEMP_VAR = "TEMP"
#elseif defined( __FB_LINUX__ )
    const PLATFORM = "Linux"
    const PATH_SEP = "/"
    const TEMP_VAR = "TMPDIR"
#elseif defined( __FB_DARWIN__ )
    const PLATFORM = "macOS"
    const PATH_SEP = "/"
    const TEMP_VAR = "TMPDIR"
#else
    #error "this program supports Windows, Linux and macOS only"
#endif

print "platform:      "; PLATFORM
print "a path:        logs"; PATH_SEP; "today.txt"
print "temp variable: "; TEMP_VAR

#ifdef __FB_UNIX__
    print "a Unix-like system"
#endif
#ifdef __FB_64BIT__
    print "pointers:     "; sizeof( any ptr ) * 8; " bits"
#endif
#ifdef __TIKO_COMPILER__
    print "compiled by tiko"
#endif

This is the one kind of program whose output should differ between the two targets.

Output on windows-x86_64

platform:      Windows
a path:        logs\today.txt
temp variable: TEMP
pointers:      64 bits
compiled by tiko

Output on linux-x86_64

platform:      Linux
a path:        logs/today.txt
temp variable: TMPDIR
a Unix-like system
pointers:      64 bits
compiled by tiko

Output on macos-arm64

platform:      macOS
a path:        logs/today.txt
temp variable: TMPDIR
a Unix-like system
pointers:      64 bits
compiled by tiko
SymbolDefined when building for
__FB_WIN32__Windows (the name is historical: it is defined for 64-bit Windows)
__FB_LINUX__Linux
__FB_DARWIN__macOS
__FB_UNIX__Linux, macOS, and other Unix-like systems
__FB_64BIT__every 64-bit target, which today is every target
__TIKO_COMPILER__always, by tiko

The #else branch ends in #error, so a build for any other platform stops with a clear message instead of failing later on a constant that was never declared. A branch for the platform you are not building on is never compiled there, so a typo in it goes unnoticed until someone builds for that platform: build every target you claim to support.

Test platform symbols with #ifdef or defined( ), never by value: on the other platform the name does not exist at all.

Activities

  1. Add a const for the end-of-line sequence each platform uses in text files, and print its length.
  2. Build with -target linux-x86_64 on Windows using -c, which compiles without linking, and check that the Linux branch compiles.

Source information

The compiler knows the file, line and procedure of every statement, and a few predefined names hand that knowledge to the program. They are what makes a diagnostic message say where.

'' The compiler knows where every line came from.
sub Report( )
    print "in "; __FUNCTION__; ", line"; __LINE__
end sub

print "file: "; __FILE__
print "line:"; __LINE__
Report( )

'' In a macro they describe the place the macro is USED, which is what
'' makes a CHECK macro useful.
#define CHECK( cond ) _
    if not ( cond ) then _
        print __FILE__ & "(" & __LINE__ & "): failed: " & #cond

function Average( byval total as int32, byval n as int32 ) as int32
    CHECK( n > 0 )
    return iif( n > 0, total \ n, 0 )
end function

var a = Average( 10, 4 )
var b = Average( 10, 0 )
print "averages:"; a; b

'' The build date changes every day, so print only its shape.
print "an ISO date has"; len( __DATE_ISO__ ); " characters"

'' #line renumbers the lines that follow, and can rename the file.
#line 1000 "generated.tiko"
print "now: "; __FILE__; " line"; __LINE__

Output

file: sourceinfo.tiko
line: 7
in REPORT, line 3
sourceinfo.tiko(17): failed: n > 0
averages: 2 0
an ISO date has 10 characters
now: generated.tiko line 1001
NameGives
__FILE__the source file's name, as a string, spelled as it was given to the compiler
__LINE__the line it is written on, as a number
__FUNCTION__the enclosing procedure's name, as a string, in capitals
__DATE_ISO__the build date as "yyyy-mm-dd"
  • Inside a macro they expand where the macro is used. CHECK is defined on line 12, but the message names line 17, the line inside Average that used it. That is how assertion and logging macros report the caller's position.
  • #cond turns the macro's argument into text, so the message shows the condition as it was written.
  • #line 1000 "generated.tiko" renumbers the source: the directive's own line becomes 1000, so the line after it is 1001, and __FILE__ changes too. A program that writes tiko source for another uses it, so that errors point at the file a person edits. Compiler diagnostics follow #line as well.

__FILE__ is the path exactly as it was passed to the compiler: built as tiko src/sourceinfo.tiko, the program prints src/sourceinfo.tiko. Print the date or time of a build only where a changing value is wanted; a program's output that changes every day is hard to test.

Activities

  1. Add CHECK( total >= 0 ) to Average and call it with a negative total.
  2. Move the #line directive to the top of the file and predict every line number the program prints.

Compile-time checks

Some mistakes can be caught before the program ever runs: a record whose layout changed, a build made without a setting it needs. The preprocessor can test these while it reads the source, and stop the build when the answer is wrong.

'' Questions answered while compiling, not while running.
type Header
    magic as uint32
    size as uint32
end type

'' #assert stops the build when its condition is false: if a field is
'' ever added to Header, this program no longer compiles.
#assert sizeof( Header ) = 8

dim as int32 count = 0
dim as int64 total = 0

'' #if typeof( ) compares two types exactly.
#if typeof( count ) = typeof( int32 )
    print "count is an int32"
#endif
#if typeof( total ) <> typeof( count )
    print "total is a different type"
#endif

'' #print reports a note while the program compiles.
#print the Header layout is checked

print "Header is"; sizeof( Header ); " bytes"

Output

count is an int32
total is a different type
Header is 8 bytes

While it compiles, the program also reports its #print line:

checks.tiko(23,1): note TKO0315: #print the Header layout is checked
     |
  23 | #print the Header layout is checked
     | ^
  • #assert cond stops the build when cond is false. Change the 8 to 12 and the build fails with TKO0604, "#assert condition failed". A record that is written to files is a good place for one: the build stops before a file in the old layout is misread.
  • #if typeof( a ) = typeof( b ) compares two types exactly: int32 is not int64.
  • #print text reports its text as a note, TKO0315, and the build goes on.

#error stops the build with a message of your own:

'' This program needs a setting chosen when it is built.
#ifndef CONFIG
    #error "build with -d CONFIG=1 (development) or -d CONFIG=2 (release)"
#else
    print "configuration"; CONFIG
#endif
needconfig.tiko(3,5): error TKO0309: #error: "build with -d CONFIG=1 (development) or -d CONFIG=2 (release)"
     |
   3 |     #error "build with -d CONFIG=1 (development) or -d CONFIG=2 (release)"
     |     ^

Built with -d CONFIG=2, the same file prints configuration 2. Keep the code that needs the setting in the #else branch: the compiler reads on past an #error to report everything it can, so code after the #endif that used CONFIG would add a second error.

For checks on values while the program runs, see Error Handling.

Activities

  1. Add a flags as uint16 field to Header and read the error.
  2. Write an #if that stops the build with #error unless __FB_64BIT__ is defined.

Containers

A fixed array holds as many elements as its declaration says. Most collections are not like that: a list grows as items arrive, a word count gains a key for every new word, a set of visited cells answers "have I been here?" again and again. The std/containers module has a collection for each of those jobs.

#import "std/containers"

dim words( 0 to 7 ) as ustring = _
    { "red", "blue", "red", "green", "blue", "red", "teal", "green" }

'' Three containers, three questions about the same words.
dim order as containers.Array( of ustring )         '' all of them, in order
dim seen as containers.Set( of ustring )            '' each one once
dim counts as containers.Map( of ustring, int32 )   '' how many of each

for each w in words
    order.Push( w )
    if seen.Add( w ) then print "new word: "; w
    counts[ w ] += 1
next

print order.Count( ); " words,"; seen.Count( ); " distinct"
print "the fifth word is "; order[ 4 ]

'' A Map has no order of its own, so sort the keys before printing.
var keys = counts.Keys( )
containers.Sort( keys )
for each k in keys
    print k; ":"; counts[ k ]
next

Output

new word: red
new word: blue
new word: green
new word: teal
 8 words, 4 distinct
the fifth word is blue
blue: 2
green: 2
red: 3
teal: 1

#import "std/containers" brings in the module, and its types are named through it: containers.Array( of ustring ). Each container is a generic type, so you choose the element type when you declare one, and each works with for each.

ContainerHoldsGood at
Array( of T )elements in order, by index from 0appending, walking, indexing
Map( of K, V )a value for each keyfinding a value by its key
Set( of T )each value at most once"is it in?", set operations
OrderedMap, OrderedSetkeys kept sortedorder, nearest key
LinkedList( of T )elements in a chainadding and removing at both ends
Deque( of T )elements in a ringboth ends, and by index
PriorityQueue( of T )elements by prioritytaking the smallest next
BitSetsmall numbers, one bit eachflags, sets of indices

The rest of the section covers sorting, searching, rearranging, keys of your own and brace-list initialisers.

Copying a container copies every element in it, so pass containers to procedures byref, as the examples in this section do.

Activities

  1. Print the words in the order they first appeared, using seen.Add to skip repeats.
  2. Print the most frequent word and its count.

Array

containers.Array( of T ) is a list that grows. Push adds an element at the end; when the storage is full it doubles, so a long run of pushes reallocates only a few times. Use it instead of growing an ordinary array one element at a time with redim preserve.

#import "std/containers"

sub Show( byref label as ustring, byref xs as containers.Array( of int32 ) )
    print label; ":";
    for each x in xs
        print x;
    next
    print "   (count"; xs.Count( ); ")"
end sub

'' A brace list gives the starting elements.
dim xs as containers.Array( of int32 ) = { 5, 3, 9 }
Show( "start", xs )

xs.Push( 7 )                         '' append at the end
Show( "push 7", xs )

'' Indices start at 0, and xs[ i ] can be assigned.
print "first"; xs[ 0 ]; ", last"; xs[ xs.Count( ) - 1 ]
xs[ 1 ] = 30
Show( "xs[1] = 30", xs )

print "pop gives"; xs.Pop( )         '' remove and return the last
Show( "after pop", xs )

xs.Insert( 0, 1 )                    '' shifts the rest along
Show( "insert 1 at 0", xs )
xs.Remove( 2 )                       '' closes the gap, keeps the order
Show( "remove at 2", xs )

'' for each byref changes the elements in place.
for each byref x in xs
    x *= 2
next
Show( "doubled", xs )

xs.Clear( )
print "empty now: "; xs.IsEmpty( ); ", pop of empty gives"; xs.Pop( )

Output

start: 5 3 9   (count 3)
push 7: 5 3 9 7   (count 4)
first 5, last 7
xs[1] = 30: 5 30 9 7   (count 4)
pop gives 7
after pop: 5 30 9   (count 3)
insert 1 at 0: 1 5 30 9   (count 4)
remove at 2: 1 5 9   (count 3)
doubled: 2 10 18   (count 3)
empty now: true, pop of empty gives 0

Indices start at 0: xs[ 0 ] is the first element and xs[ xs.Count( ) - 1 ] the last. xs[ i ] can be read and assigned.

MemberDoes
Push( v ), Pop( )add at the end; remove and return the last
Insert( i, v ), Remove( i )add or remove at i, keeping the order
RemoveSwap( i )remove at i by moving the last element into the gap
Count( ), IsEmpty( )how many elements there are
Capacity( ), Reserve( n ), Shrink( )the storage behind them
Clear( )remove every element, keeping the storage

Insert and Remove move every element after i, so they cost more the longer the array is. RemoveSwap is the fast way to remove when the order does not matter.

for each x in xs gives you a copy of each element; for each byref x in xs gives you the element itself, so assigning to x changes the array. Pop on an empty array returns the element type's default value, 0 here, rather than failing: check IsEmpty( ) first when that matters.

Status in this version

Under -exx, xs[ i ] is checked against the array's storage, not against Count( ). An index past the last element but inside the capacity reads a stale value instead of stopping the program:

dim xs as containers.Array( of int32 ) = { 1, 2, 3, 4 }
xs.Pop( )
print xs[ 3 ]        '' prints 4; Count( ) is 3

Keep indices below Count( ) yourself.

Activities

  1. Use RemoveSwap( 0 ) instead of Remove( 2 ) and compare the order.
  2. Push the numbers 1 to 100 and print Capacity( ) after every tenth push.

Map

containers.Map( of K, V ) stores a value under each key and finds the value for a key in constant time on average, however many entries there are. It is a hash table: the key is turned into a number that says where in the table its entry lives.

#import "std/containers"

dim ages as containers.Map( of ustring, int32 )

'' Writing: [ ] inserts or replaces; Add only inserts.
ages[ "ada" ] = 36
ages[ "alan" ] = 41
print "add grace: "; ages.Add( "grace", 85 )
print "add ada:   "; ages.Add( "ada", 99 )      '' refused, ada keeps 36
ages[ "ada" ] = 37                               '' replaced
print "count:"; ages.Count( )

'' Reading without inserting: Contains and TryGet.
dim as int32 age
if ages.TryGet( "alan", age ) then print "alan is"; age
print "has edsger? "; ages.Contains( "edsger" )
if ages.TryGet( "edsger", age ) = false then print "edsger: not found"

print "remove alan: "; ages.Remove( "alan" )
print "remove alan: "; ages.Remove( "alan" )   '' nothing left to remove

'' Iteration order is the table's, not yours: sort the keys to print.
var names = ages.Keys( )
containers.Sort( names )
for each who in names
    print who; ages[ who ]
next

Output

add grace: true
add ada:   false
count: 3
alan is 41
has edsger? false
edsger: not found
remove alan: true
remove alan: false
ada 37
grace 85
MemberDoes
m[ k ]the value for k, which can be assigned; inserts a default value if k is absent
Add( k, v )inserts only if k is absent; false if it was there
Put( k, v )inserts or replaces; true if k was new
TryGet( k, v )true and the value in v if k is present
Contains( k )whether k is present
Remove( k )true if something was removed
Keys( ), Values( )the keys or the values, copied into an Array
Pairs( )(key, value) tuples, for for each (k, v) in m.Pairs( )

A Map keeps no order. for each kv in m (with kv.key and kv.value) visits the entries in the order the table holds them, which is neither the order you inserted them nor sorted, and which changes as the map grows. To print a map the same way every time, sort its Keys( ) first, as the example does, or use an OrderedMap.

Missing keys

m[ k ] += 1 works on a key the map has never seen, because the indexer inserts the value type's default, 0, before the += 1. That is what makes counting easy, and it has a cost when you only meant to look:

Pitfall

#import "std/containers"

dim stock as containers.Map( of ustring, int32 )
stock[ "apples" ] = 12
stock[ "pears" ] = 4

'' Reading through [ ] looks harmless...
if stock[ "plums" ] = 0 then print "no plums"
if stock[ "kiwis" ] = 0 then print "no kiwis"

'' ...but every miss inserted a zero.
print "entries:"; stock.Count( )

'' The same question asked with Contains leaves the map alone.
if stock.Contains( "figs" ) = false then print "no figs"
print "entries:"; stock.Count( )
no plums
no kiwis
entries: 4
no figs
entries: 4

Each if stock[ ... ] = 0 printed the right answer and added an entry. The map now holds four items where two were stocked, so a later Count( ) or for each is wrong. Use [ ] to write, and TryGet or Contains to read.

Activities

  1. Replace ages[ "ada" ] = 37 with ages.Put( "ada", 37 ) and print what Put returns.
  2. Walk the map with for each (who, age) in ages.Pairs( ) and see the order the table gives you.

Set

containers.Set( of T ) holds each value at most once and answers "is it in?" in constant time on average. It is a Map with keys and no values.

#import "std/containers"

'' A Set has no order: copy its items into an Array and sort them.
sub Show( byref label as ustring, byref s as containers.Set( of int32 ) )
    var items = s.Items( )
    containers.Sort( items )
    print label; ":";
    for each x in items
        print x;
    next
    print
end sub

dim evens as containers.Set( of int32 ) = { 2, 4, 6, 8 }
dim small as containers.Set( of int32 ) = { 1, 2, 3, 4 }

'' Add says whether the value was new.
print "add 10: "; evens.Add( 10 )
print "add 4:  "; evens.Add( 4 )
print "contains 6: "; evens.Contains( 6 ); ", contains 7: "; evens.Contains( 7 )
evens.Remove( 10 )

'' The set operations change the set they are called on, so copy first.
dim both as containers.Set( of int32 ) = evens
both.IntersectWith( small )
Show( "evens and small", both )

dim either as containers.Set( of int32 ) = evens
either.UnionWith( small )
Show( "evens or small", either )

dim onlyEven as containers.Set( of int32 ) = evens
onlyEven.ExceptWith( small )
Show( "evens, not small", onlyEven )

Show( "evens itself", evens )

Output

add 10: true
add 4:  false
contains 6: true, contains 7: false
evens and small: 2 4
evens or small: 1 2 3 4 6 8
evens, not small: 6 8
evens itself: 2 4 6 8

Add returns true when the value is new and false when it was already there, so if seen.Add( x ) then is the natural way to do something once per distinct value. Contains asks without changing anything, and Remove returns whether there was something to remove.

OperationLeaves in the set
a.IntersectWith( b )the values in both a and b
a.UnionWith( b )the values in either
a.ExceptWith( b )the values in a that are not in b

The three operations change the set they are called on. Declaring dim both as containers.Set( of int32 ) = evens copies evens, so the example can apply each operation to a fresh copy and evens itself is unchanged at the end.

Like a Map, a Set has no order. Items( ) copies the values into an Array, which Show sorts before printing. When you want the values kept in order all the time, use an OrderedSet.

Activities

  1. Use a Set( of ustring ) to print each distinct word of a sentence once.
  2. Compute the values in exactly one of evens and small (the symmetric difference) from the operations above.

OrderedMap and OrderedSet

containers.OrderedMap( of K, V ) and containers.OrderedSet( of T ) keep their contents sorted by key, always. They are trees rather than hash tables, so each operation costs O(log n) instead of O(1), and in exchange they answer questions about order that a Map or Set cannot.

#import "std/containers"

'' An OrderedMap keeps its keys sorted: here, departure times.
dim departures as containers.OrderedMap( of int32, ustring )
departures[ 1430 ] = "Leeds"
departures[ 905 ] = "York"
departures[ 1215 ] = "Hull"
departures[ 1750 ] = "Derby"

for each (hhmm, town) in departures.Pairs( )      '' always in key order
    print hhmm; " "; town
next

dim as int32 t
dim as ustring town
if departures.TryFirst( t, town ) then print "first:"; t; " "; town
if departures.TryLast( t, town ) then print "last:"; t; " "; town

'' The neighbours of a value that is not a key.
if departures.TryCeiling( 1300, t ) then print "next after 1300:"; t
if departures.TryFloor( 1300, t ) then print "last before 1300:"; t
print "anything after 1800? "; departures.TryCeiling( 1800, t )

'' An OrderedSet is the same tree with no values.
dim tags as containers.OrderedSet( of ustring ) = { "sale", "new", "eco" }
tags.Add( "gift" )
for each tag in tags
    print " "; tag;
next
print

Output

 905 York
 1215 Hull
 1430 Leeds
 1750 Derby
first: 905 York
last: 1750 Derby
next after 1300: 1430
last before 1300: 1215
anything after 1800? false
 eco gift new sale

The departures went in as 1430, 905, 1215, 1750 and come out of for each in key order. An OrderedMap has the same members as a Map ([ ], Add, Put, TryGet, Contains, Remove, Keys( ), Values( ), Pairs( )) and the indexer inserts on a miss here too. It adds four that only make sense when the keys are in order:

MemberFinds
TryFirst( k, v ), TryLast( k, v )the smallest and the largest entry
TryFloor( x, k )the largest key at or below x
TryCeiling( x, k )the smallest key at or above x

Each returns false when there is no such entry. x need not be a key itself: "the next train after 13:00" is TryCeiling( 1300, t ).

An OrderedSet is the same tree with no values: Add, Contains, Remove, the set operations, and TryFirst, TryLast, TryFloor and TryCeiling with one value instead of two.

The ordered containers need only < on the key, not a hash. Reach for them when you need the order; for plain lookup, the hashed ones are faster.

Activities

  1. Print the departures between 1200 and 1500 by starting at TryCeiling( 1200, t ) and stepping with TryCeiling( t + 1, t ).
  2. Remove "York" with Remove( 905 ) and print TryFirst again.

LinkedList

containers.LinkedList( of T ) is a chain of elements that adds and removes at both ends in constant time. That makes it a queue, a stack, or both at once (a double-ended queue).

#import "std/containers"

'' As a queue: in at the back, out at the front.
dim jobs as containers.LinkedList( of ustring )
jobs.PushBack( "build" )
jobs.PushBack( "test" )
jobs.PushFront( "fetch" )                '' jumps the queue
print "front: "; jobs.Front( ); ", back: "; jobs.Back( )
do until jobs.IsEmpty( )
    print "doing "; jobs.PopFront( )
loop

'' As a stack: in and out at the same end.
dim undo as containers.LinkedList( of ustring )
undo.PushBack( "type" )
undo.PushBack( "bold" )
undo.PushBack( "delete" )
print "undo "; undo.PopBack( ); ", then "; undo.PopBack( )
print undo.Count( ); " left: "; undo.Back( )

'' An empty list pops a default value instead of failing.
dim empty as containers.LinkedList( of int32 )
print "pop of empty:"; empty.PopFront( )

Output

front: fetch, back: test
doing fetch
doing build
doing test
undo delete, then bold
 1 left: type
pop of empty: 0
MemberDoes
PushBack( v ), PushFront( v )add at the back or the front
PopBack( ), PopFront( )remove and return from the back or the front
Back( ), Front( )look at either end without removing
Count( ), IsEmpty( ), Clear( )as for an Array

A queue takes from the opposite end to the one it adds to, a stack from the same end. do until jobs.IsEmpty( ) is the loop that drains one.

Popping an empty list returns the element type's default value, 0 or an empty string, rather than stopping the program. Test IsEmpty( ) first when the difference matters.

A LinkedList has no index: reaching the middle means walking to it with for each. For most lists an Array is smaller and faster; use a LinkedList when you need the front end too.

Activities

  1. Turn the undo stack into a queue by changing one call, and predict the output first.
  2. Fill a list with five numbers, sort it with containers.SortList, and print it.

Deque

containers.Deque( of T ) — a double-ended queue, said "deck" — adds and removes at both ends in constant time, like a LinkedList, and also reaches any element by index with d[ i ].

#import "std/containers"

'' Keep only the five most recent pages: add at the back, drop the front.
dim history as containers.Deque( of ustring )
dim visits( 0 to 6 ) as ustring = _
    { "home", "news", "sport", "weather", "news", "mail", "shop" }

for each page in visits
    history.PushBack( page )
    if history.Count( ) > 5 then history.PopFront( )
next

print "kept"; history.Count( ); " pages"
for i as int32 = 0 to history.Count( ) - 1
    print i; ": "; history[ i ]
next

'' Both ends work the same way.
history.PushFront( "start" )
print "oldest: "; history.Front( ); ", newest: "; history.Back( )
print "went back to "; history.PopBack( )

Output

kept 5 pages
 0: sport
 1: weather
 2: news
 3: mail
 4: shop
oldest: start, newest: shop
went back to shop
MemberDoes
PushBack( v ), PushFront( v )add at the back or the front
PopBack( ), PopFront( )remove and return from the back or the front
TryPopBack( v ), TryPopFront( v )the same, returning false when empty
Front( ), Back( ), d[ i ]look without removing; index 0 is the front
Count( ), IsEmpty( ), Clear( )as for an Array

The example keeps a bounded history: each page goes in at the back, and once there are more than five the oldest comes off the front. for each walks from front to back.

A Deque stores its elements in one block of memory used as a ring, which doubles when it fills, so there is no allocation per element. Reach for it before a LinkedList when you want a queue. d[ i ] needs an index from 0 to Count( ) - 1; the deque does not grow to make a bad one valid.

Activities

  1. Make the history keep three pages instead of five, and predict the output first.
  2. Declare a deque with a brace list, = { 1, 2, 3 }, and print it with for each.

PriorityQueue

containers.PriorityQueue( of T ) hands back its smallest element first, whatever order the elements went in. Use it when the next item to handle is the most urgent one, not the oldest.

#import "std/containers"

'' A PriorityQueue hands back its smallest element first.
dim deadlines as containers.PriorityQueue( of int32 )
dim days( 0 to 4 ) as int32 = { 14, 3, 30, 7, 3 }
for i as int32 = 0 to 4
    deadlines.Push( days( i ) )
next
print "soonest:"; deadlines.Peek( )
do until deadlines.IsEmpty( )
    print deadlines.Pop( );
loop
print

'' Any other order needs a comparer: does a leave before b?
type Bigger as function( byref a as int32, byref b as int32 ) as boolean
dim biggest as Bigger = function( byref a as int32, _
                                  byref b as int32 ) as boolean
    return a > b
end function

dim bids as containers.PriorityQueueBy( of int32, Bigger ) = _
    containers.PriorityQueueBy( of int32, Bigger )( biggest )
bids.Push( 120 ) : bids.Push( 450 ) : bids.Push( 300 )
print "top bid:"; bids.Pop( ); ", then"; bids.Pop( )

Output

soonest: 3
 3 3 7 14 30
top bid: 450, then 300

Push and Pop cost O(log n); Peek looks at the next element without removing it. Equal elements, like the two 3s, may leave in either order.

For any order other than smallest first, use containers.PriorityQueueBy( of T, TLess ) and give it a comparer when you construct it. less( a, b ) returns true when a must leave before b — the question a SortBy comparer answers. The second type argument is the comparer's type: the example names a procedure-pointer type, Bigger, for a plain lambda. A capturing lambda's type is written typeof( name ).

for each over a priority queue walks its internal order, which is not priority order. Take elements with Pop, or call ToSortedArray( ) for a sorted copy.

Activities

  1. Loop over deadlines with for each before popping, and compare the order with the popped one.
  2. Push records of a type with a priority field, ordered by a comparer.

BitSet

containers.BitSet is a set of small whole numbers stored as one bit each: a row of seats, the cells a search has visited, a sieve of primes. A million flags take 125 KB.

#import "std/containers"

'' One bit per seat: which seats in a row of 40 are taken?
dim taken as containers.BitSet = containers.BitSet( 40 )
taken.Set( 0 ) : taken.Set( 1 ) : taken.Set( 2 ) : taken.Set( 7 )
taken.Set( 39 )

print "taken:"; taken.CountOnes( ); " of"; taken.Length( )
print "seat 7 taken? "; taken.Test( 7 )
print "first free seat:"; taken.NextClear( 0 )
print "first free after 7:"; taken.NextClear( 7 )

print "taken seats:";
for each s in taken
    print s;
next
print

'' Seats a group asked for, minus the ones already gone.
dim wanted as containers.BitSet
wanted.Set( 2 ) : wanted.Set( 3 ) : wanted.Set( 4 )
dim clash as containers.BitSet = wanted
clash.IntersectWith( taken )
wanted.DifferenceWith( taken )
print "clash:"; clash.FirstSet( ); ", still free:"; wanted.CountOnes( )

Output

taken: 5 of 40
seat 7 taken? true
first free seat: 3
first free after 7: 8
taken seats: 0 1 2 7 39
clash: 2, still free: 2
MemberDoes
Set( i ), Reset( i ), Flip( i )change one bit; Set grows the set as needed
Test( i )whether bit i is set; false beyond the length
CountOnes( ), Length( )bits set; bits addressable
FirstSet( from ), NextClear( from )the next set or clear bit at or after from
UnionWith, IntersectWith, DifferenceWithset algebra, changing the left-hand set
Equals( other )same bits set, whatever the lengths

BitSet( 40 ) starts with 40 clear bits; a plain dim starts empty and grows on Set. for each visits the set bits from lowest to highest.

The set operations change the set they are called on, which is why the example copies wanted into clash before intersecting. A BitSet takes no brace list at its declaration: call Set for each bit.

Activities

  1. Take seats 3 and 4 too, then print the first free seat again.
  2. Use SymmetricDifferenceWith to find the seats that are in exactly one of taken and the original wanted.

Sorting

Sorting is done by free procedures in std/containers, not by members of Array: containers.Sort( xs ) sorts an Array in place.

#import "std/containers"

type Player
    as ustring name, team
    as int32 points
end type

sub Show( byref ps as containers.Array( of Player ) )
    for each p in ps
        print "  "; p.team; " "; p.name; p.points
    next
end sub

'' Sort uses the element's own '<'.
dim nums as containers.Array( of int32 ) = { 9, 4, 7, 1 }
print "sorted? "; containers.IsSorted( nums )
containers.Sort( nums )
print "sorted? "; containers.IsSorted( nums ); nums[ 0 ]; nums[ 3 ]

'' SortBy takes a comparer: true when a must come before b.
containers.SortBy( nums, function( byref a as int32, _
                                   byref b as int32 ) as boolean
    return a > b                         '' descending
end function )
print "descending:"; nums[ 0 ]; nums[ 1 ]; nums[ 2 ]; nums[ 3 ]

dim ps as containers.Array( of Player )
dim p as Player
p.name = "ada" : p.team = "red"  : p.points = 31 : ps.Push( p )
p.name = "bob" : p.team = "blue" : p.points = 27 : ps.Push( p )
p.name = "cy"  : p.team = "red"  : p.points = 27 : ps.Push( p )
p.name = "dee" : p.team = "blue" : p.points = 35 : ps.Push( p )

'' Best first...
containers.SortBy( ps, function( byref a as Player, _
                                 byref b as Player ) as boolean
    return a.points > b.points
end function )
print "by points:"
Show( ps )

'' ...then by team, STABLY: within a team, best-first survives.
containers.StableSortBy( ps, function( byref a as Player, _
                                       byref b as Player ) as boolean
    return a.team < b.team
end function )
print "by team, then points:"
Show( ps )

Output

sorted? false
sorted? true 1 9
descending: 9 7 4 1
by points:
  blue dee 35
  red ada 31
  blue bob 27
  red cy 27
by team, then points:
  blue dee 35
  blue bob 27
  red ada 31
  red cy 27

Sort orders by the element type's own <. To order any other way, pass a comparer to SortBy: a procedure, usually a lambda, that takes two elements and returns true when the first must come before the second. Return false for elements that are equal; a comparer such as a >= b breaks that rule. A record type has no < of its own, so it always needs a comparer.

ProcedureSortsStable
Sort( xs ), SortBy( xs, less )an Array, in placeno
StableSort( xs ), StableSortBy( xs, less )an Array, in placeyes
SortList( l ), SortListBy( l, less )a LinkedListyes
IsSorted( xs ), IsSortedBy( xs, less )nothing: they answer whether it is

A stable sort keeps elements that compare equal in the order they had before. That is what the second sort relies on: the players are first put best-first, then sorted by team with StableSortBy, and within each team the best-first order survives. With the unstable SortBy for that second step, the order within a team is whatever the algorithm leaves.

Sort is the faster of the two and is the one to use when stability does not matter, which includes every sort of plain numbers or strings. Comparers and lambdas in general are covered in Procedures.

Activities

  1. Sort the players by name, then print them.
  2. Sort an Array( of ustring ) by length, shortest first, with SortBy and len( a ) < len( b ).
  3. Count the comparisons a sort makes with a capturing lambda, function[ byref n ]( ... ), that adds one to n on every call.

Searching

containers.IndexOf( xs, v ) returns the index of the first element equal to v, or -1 when there is none; containers.Contains( xs, v ) says whether there is one. Both look at the elements one at a time, from the front. On a sorted array, BinarySearch, LowerBound and UpperBound halve the range at each step instead: about 20 looks for a million elements.

#import "std/containers"

dim scores as containers.Array( of int32 ) = { 70, 85, 90, 85 }

'' IndexOf and Contains walk the array from the front.
dim as int32 wanted = 85, missing = 60
print "first 85 at " & containers.IndexOf( scores, wanted )
print "60 at " & containers.IndexOf( scores, missing )
print "has 60? "; containers.Contains( scores, missing )

'' On a sorted array, a binary search halves the range at each step.
containers.Sort( scores )
print "sorted:"; scores[ 0 ]; scores[ 1 ]; scores[ 2 ]; scores[ 3 ]
dim as int32 top = 90
print "90 at " & containers.BinarySearch( scores, top )
dim r as int32 = containers.BinarySearch( scores, missing )
print "60 gives " & r & ", so it would go at " & -( r + 1 )

'' The bounds bracket every copy of a value.
print "85s from " & containers.LowerBound( scores, wanted ) & _
    " up to " & containers.UpperBound( scores, wanted )

Output

first 85 at 1
60 at -1
has 60? false
sorted: 70 85 85 90
90 at 3
60 gives -1, so it would go at 0
85s from 1 up to 3
ProcedureReturnsNeeds
IndexOf( xs, v )first index equal to v, or -1=
Contains( xs, v )whether there is one=
BinarySearch( xs, v )first index equal to v; on a miss, -( insertion point ) - 1sorted, <
LowerBound( xs, v )first index not less than vsorted, <
UpperBound( xs, v )first index greater than vsorted, <

A miss in BinarySearch is always negative, and -( r + 1 ) turns it back into the index where the value would go. UpperBound minus LowerBound counts the copies of a value. The sorted searches trust the array to be sorted: on an unsorted one they return some index without complaint. Each has a ...By form taking a comparer, for an array sorted with SortBy.

These are free procedures rather than members of Array because they need = or < on the element type. A member would demand that of every element type an Array is ever declared with; a free procedure asks only where it is called. When you search the same values often, an OrderedSet keeps them sorted for you, and a Set answers Contains without searching.

The value must have the element's type

The searches are generic, and their type is inferred from both arguments, exactly. That is why the example puts 85, 60 and 90 in int32 variables instead of writing them as literals:

#import "std/containers"

dim scores as containers.Array( of int32 ) = { 70, 85, 90 }
print containers.Contains( scores, 90 )   '' 90 is an integer, not an int32
searchliteral.tiko(4,26): error TKO0809: cannot infer the type arguments of 'contains'; write them, as 'contains( of ... )( ... )'

The literal 90 is an integer, and the array holds int32s, so no single element type fits both. Pass a variable of the element type, or name the type as the message suggests: containers.Contains( of int32 )( scores, 90 ).

Activities

  1. Search the sorted scores for 85 with BinarySearch, then with UpperBound. Which index does each give, and why?
  2. Insert 75 at the position a missed BinarySearch reports, and print the array to check it is still sorted.

Rearranging

Free procedures in std/containers that rearrange an Array in place — and two that answer "the k smallest" or "the median" without paying for a full sort.

#import "std/containers"

sub Show( byref label as ustring, byref xs as containers.Array( of int32 ) )
    print label;
    for each x in xs
        print x;
    next
    print
end sub

dim xs as containers.Array( of int32 ) = { 5, 5, 1, 3, 3, 3, 8 }

containers.ReverseArray( xs )
Show( "reversed:", xs )
containers.RotateArray( xs, 2 )
Show( "rotated 2:", xs )

'' Dedup removes RUNS of equal values; sort first to remove them all.
containers.Sort( xs )
print "dedup removed"; containers.Dedup( xs )
Show( "distinct:", xs )

'' Every ordering of three values, in sorted order.
dim p as containers.Array( of int32 ) = { 1, 2, 3 }
do
    Show( "  ", p )
loop while containers.NextPermutation( p )

'' The two smallest, and the middle value, without a full sort.
dim big as containers.Array( of int32 ) = { 42, 7, 19, 88, 3, 61, 25 }
containers.PartialSort( big, 2 )
print "two smallest:"; big[ 0 ]; big[ 1 ]
containers.NthElement( big, 3 )
print "median:"; big[ 3 ]

Output

reversed: 8 3 3 3 1 5 5
rotated 2: 3 3 1 5 5 8 3
dedup removed 3
distinct: 1 3 5 8
   1 2 3
   1 3 2
   2 1 3
   2 3 1
   3 1 2
   3 2 1
two smallest: 3 7
median: 25
ProcedureLeaves the array
ReverseArray( xs )in reverse order
RotateArray( xs, k )rotated left by k; negative k rotates right
Dedup( xs )without consecutive repeats; returns how many it removed
NextPermutation( xs )in the next ordering; false after the last
PartialSort( xs, k )with the k smallest at the front, sorted
NthElement( xs, k )with at xs[ k ] what a full sort would put there

Dedup only removes runs of equal neighbours, which is why the example sorts first. NextPermutation steps through the orderings in sorted order, so start from a sorted array to visit every one; do ... loop while runs the body once before the first test.

ReverseArray and RotateArray carry the type in their names because std/strings has a Reverse and std/numeric a Rotate of their own. The procedures that compare have a ...By form taking a comparer.

Activities

  1. Rotate by -1 instead of 2 and predict the result.
  2. Start the permutation loop from { 2, 1, 3 }. Which orderings are missed?

Keys of your own

Every built-in type can be a Map key or a Set element. A record type needs two things before it can be one: =, to say when two keys are the same, and a hash, a number computed from the key that decides where in the table it is stored. You supply the hash by extending containers.Hashable and overriding its HashOf( ):

#import "std/containers"

'' A key type extends Hashable and overrides HashOf( ).
type Cell extends containers.Hashable
    as int32 row, col
    declare constructor( byval row as int32 = 0, byval col as int32 = 0 )
    declare function HashOf( ) as uint64 override
end type

constructor Cell( byval row as int32, byval col as int32 )
    this.row = row
    this.col = col
end constructor

'' Hash exactly the fields that '=' compares.
function Cell.HashOf( ) as uint64
    return containers.HashInt( containers.HashOf( row ) ) xor containers.HashOf( col )
end function

operator = ( byref a as Cell, byref b as Cell ) as boolean
    return ( a.row = b.row ) andalso ( a.col = b.col )
end operator

dim terrain as containers.Map( of Cell, ustring )
terrain[ Cell( 2, 3 ) ] = "forest"
terrain[ Cell( 0, 0 ) ] = "river"

'' A different Cell with the same fields finds the same entry.
dim probe as Cell = Cell( 2, 3 )
dim as ustring found
if terrain.TryGet( probe, found ) then print "(2, 3) is "; found
print "has (3, 2)? "; terrain.Contains( Cell( 3, 2 ) )

'' The same key type works in a Set.
dim visited as containers.Set( of Cell )
print "first add: "; visited.Add( Cell( 1, 1 ) )
print "second add: "; visited.Add( Cell( 1, 1 ) )
print "count:"; visited.Count( )

Output

(2, 3) is forest
has (3, 2)? false
first add: true
second add: false
count: 1

probe is a different variable from the Cell( 2, 3 ) the entry was stored under, with the same fields, and the map finds it: the two are equal, so they hash the same and land in the same place. containers.HashOf has an overload for every built-in type, so a key's hash is usually built from the hashes of its fields, combined so that ( 2, 3 ) and ( 3, 2 ) do not come out equal: containers.HashInt scrambles the row's hash before the xor, so the row and the column are not interchangeable.

One rule makes it work: equal keys must produce equal hashes. Compute the hash from exactly the fields that = compares. A hash that used a field = ignores would put two equal keys in different places, and the map would lose entries with no error.

Without a hash, the key type is refused when the Map is declared:

#import "std/containers"

'' Equality, but no hash: not enough for a Map key.
type Cell
    as int32 row, col
end type

operator = ( byref a as Cell, byref b as Cell ) as boolean
    return ( a.row = b.row ) andalso ( a.col = b.col )
end operator

dim terrain as containers.Map( of Cell, ustring )
map.tiko(150,35): error TKO0632: no overload of 'hashof' matches these arguments

The message points into the module's own map.tiko, where the hash is used, not at your declaration. When you see hashof named in an error from inside std/containers, a key type is missing its HashOf( ).

The ordered containers need no hash, only < on the key. extends and override are covered in Inheritance.

Activities

  1. Give Cell a label field that = ignores, and check that HashOf must ignore it too.
  2. Make the hash containers.HashOf( row ) xor containers.HashOf( col ) and explain why Cell( 2, 3 ) and Cell( 3, 2 ) now collide, and why the map still works.

Collection initialisers

A brace list after a container's declaration gives it its starting elements, the same way an ordinary array takes one:

#import "std/containers"

'' A brace list fills a container as it is declared.
dim primes as containers.Array( of int32 ) = { 2, 3, 5, 7 }
dim tags as containers.Set( of ustring ) = { "new", "sale", "new" }
dim route as containers.LinkedList( of ustring ) = { "home", "shop", "home" }

'' The elements are ordinary expressions, evaluated in order.
dim as int32 w = 3, h = 4
dim sizes as containers.Array( of int32 ) = { w, h, w * h }

'' A map takes one { key, value } pair per entry.
dim ages as containers.OrderedMap( of ustring, int32 ) = _
    { { "ada", 36 }, { "alan", 41 }, { "grace", 85 } }

print primes.Count( ); tags.Count( ); route.Count( ); ages.Count( )
print sizes[ 0 ]; sizes[ 1 ]; sizes[ 2 ]
for each (who, age) in ages.Pairs( )
    print who; age
next

'' Any type with a constructor and an Add (or Push, or PushBack)
'' member takes a brace list too: one call per element.
type Tally
    as int32 total
    declare constructor( )
    declare sub Add( byval v as int32 )
end type

constructor Tally( )
    total = 0
end constructor

sub Tally.Add( byval v as int32 )
    total += v
end sub

dim score as Tally = { 10, 20, 12 }
print "tally:"; score.total

Output

 4 2 3 3
 3 4 12
ada 36
alan 41
grace 85
tally: 42

The list becomes one call per element, made right after the container is constructed: Push for an Array, Add for a Set, PushBack for a LinkedList. So the Set counts two tags, because Add ignores the repeated "new", and the elements can be any expressions, such as w * h.

For a Map or an OrderedMap, each element is itself a brace pair, { key, value }, and the pair becomes the two arguments of one Add.

Nothing about this is special to the standard containers. Tally has a constructor and an Add member, so it takes a brace list as well, and each element is passed to Add in turn.

A brace list belongs to the declaration. Assigning one later is refused:

#import "std/containers"

dim primes as containers.Array( of int32 ) = { 2, 3, 5 }
primes = { 7, 11 }          '' a brace list is for the declaration only
initassign.tiko(4,10): error TKO0846: a '{ ... }' list can only initialize an array, and nothing here is one

To refill a container, Clear( ) it and push the new elements.

Activities

  1. Declare a Map( of ustring, int32 ) of three colours with a brace list and print its sorted keys.
  2. Give Tally a Push member instead of Add and check it still takes a brace list.

Concurrency

A program that does one thing at a time leaves most of the machine idle. With concurrency, several pieces of work run at once: on different cores, or while another piece waits for a disk, a network or a timer.

#import "std/tasks"
#cmdline "-mt"

'' Add up one slice of the range and send the answer back.
sub SumRange( byval lo as int64, byval hi as int64, _
              byval results as tasks.SendChan( of int64 ) )
    dim as int64 total = 0
    for i as int64 = lo to hi
        total += i
    next
    results.Send( total )
end sub

'' Four goroutines, a quarter of the work each.
var results = tasks.Channel( of int64 )( 4 )
for part as int32 = 0 to 3
    spawn SumRange( part * 250000 + 1, ( part + 1 ) * 250000, results )
next

'' The parts arrive in any order; their sum does not depend on it.
dim as int64 total = 0, partial
for part as int32 = 1 to 4
    results.Receive( partial )
    total += partial
next
print "sum of 1 to 1000000:"; total

Output

sum of 1 to 1000000: 500000500000

spawn SumRange( ... ) starts a goroutine: the call runs alongside the rest of the program, and spawn returns at once. A goroutine has no return value, so each one sends its partial sum down a channel, and the main program receives four of them. They can arrive in any order, which is why the program adds them up rather than printing each one.

A program that uses goroutines needs two lines at its top:

#import "std/tasks"
#cmdline "-mt"

std/tasks holds goroutines, channels and the tools for coordinating them. -mt builds the program with the runtime that is safe to call from several threads at once. You have to ask for it yourself: std/tasks refuses to compile without it, with TKO0309: #error: "std/tasks needs the multithreaded runtime: build with -mt".

A program that starts threads of its own, without std/tasks, gets the thread-safe runtime without asking. When the compiler sees a call to threadcreate, it links that runtime by itself. Mutexes and condition variables alone do not count: a program with no second thread does not need the thread-safe runtime. Writing #cmdline "-mt" in such a program does no harm, and the examples in this section all carry it.

The section has two levels:

LevelPages
Goroutines and channels, from std/tasksspawn, channels, select, worker pool, cancellation
Operating-system threads and shared memorythreads, mutexes, condition variables, atomics

Reach for goroutines and channels first: values move between them by copy, and the order of events is visible in the code. Threads and locks are the foundation underneath, for the cases that need them.

Activities

  1. Split the work into eight goroutines instead of four.
  2. Print each partial sum as it arrives, run the program a few times, and watch the order change while the total does not.

Threads

Under goroutines are operating-system threads, and you can start one directly. threadcreate( @Proc, arg ) starts a thread running Proc( arg ), and threadwait( handle ) waits for it to finish.

#cmdline "-mt"

'' Everything a thread needs travels in one record, passed by address.
type Slice
    as int64 lo, hi
    as int64 result
end type

sub SumSquares( byval p as any ptr )
    var s = cptr( Slice ptr, p )
    dim as int64 total = 0
    for i as int64 = s->lo to s->hi
        total += i * i
    next
    s->result = total             '' only this thread writes this record
end sub

dim slices( 0 to 2 ) as Slice
dim handles( 0 to 2 ) as any ptr

for i as int32 = 0 to 2
    slices( i ).lo = i * 100 + 1
    slices( i ).hi = ( i + 1 ) * 100
    handles( i ) = threadcreate( @SumSquares, @slices( i ) )
next

'' threadwait returns when that thread has finished.
for i as int32 = 0 to 2
    threadwait( handles( i ) )
next

dim as int64 total = 0
for i as int32 = 0 to 2
    print "squares"; slices( i ).lo; " to"; slices( i ).hi; ":"; _
          slices( i ).result
    total += slices( i ).result
next
print "all:"; total

Output

squares 1 to 100: 338350
squares 101 to 200: 2348350
squares 201 to 300: 6358350
all: 9045050

The thread procedure takes exactly one parameter, byval p as any ptr, so everything a thread needs travels in one record whose address is passed. Here each thread gets a Slice of its own: it reads its range and writes its result into that record, and no two threads touch the same memory. That is why the program needs no lock.

threadwait is the join: once it returns, the thread has finished and everything it wrote is visible, so the main program can read slices( i ).result safely. Every thread you create should be waited for before the data it uses goes away.

The #cmdline "-mt" line is optional here. Without std/tasks, the call to threadcreate is enough: the compiler sees it and links the runtime that is safe to call from several threads at once by itself. The line just makes the program's needs explicit.

Use a thread rather than a goroutine for something that must own a thread for its whole life, such as a loop that blocks for a long time. For ordinary fan-out work, spawn and a channel are shorter. When threads do share a variable, it needs a mutex or an atomic.

Activities

  1. Split the range 1 to 1000 between five threads.
  2. Add a field to Slice that each thread fills with its count of even squares, and print the counts.

Mutexes

When two threads change the same variable, updates get lost. acct->balance += 1 is three steps (read, add, write back), and two threads can both read the same value, both add one, and both write it: two deposits, one recorded. A mutex makes a section of code run in one thread at a time.

#cmdline "-mt"

'' The shared total and the lock that guards it travel together.
type Account
    as any ptr lock
    as int64 balance
end type

sub Deposit( byval p as any ptr )
    var acct = cptr( Account ptr, p )
    for i as int32 = 1 to 100000
        mutexlock( acct->lock )       '' one thread at a time from here...
        acct->balance += 1
        mutexunlock( acct->lock )     '' ...to here
    next
end sub

dim acct as Account
acct.lock = mutexcreate( )

dim tellers( 0 to 3 ) as any ptr
for i as int32 = 0 to 3
    tellers( i ) = threadcreate( @Deposit, @acct )
next
for i as int32 = 0 to 3
    threadwait( tellers( i ) )
next

mutexdestroy( acct.lock )             '' no thread can use it any more
print "balance:"; acct.balance

Output

balance: 400000

Four tellers each deposit 1 a hundred thousand times, and the balance is exactly 400000. Between mutexlock and mutexunlock only one thread runs, so each += 1 happens whole. Without those two lines the balance comes out short, by a different amount on each run, and nothing reports an error.

CallDoes
mutexcreate( )makes a mutex and returns its handle, an any ptr
mutexlock( m )waits until no other thread holds m, then holds it
mutexunlock( m )lets the next waiting thread in
mutexdestroy( m )frees it, once no thread can use it any more

Keep the lock and the data it protects together, as Account does, so every procedure that can reach the data can reach its lock. Hold a mutex for as little code as you can: while one thread holds it, the others wait, and four threads taking turns on one lock run no faster than one.

Every mutexlock needs its mutexunlock on every path out, including an early exit or return. A thread that leaves while holding the lock makes every other thread wait for ever.

Activities

  1. Remove the mutexlock and mutexunlock lines and run the program a few times.
  2. Move the lock outside the loop, so each teller holds it for all its deposits. Is the result still right? What did the threads gain?

Condition variables

A mutex stops two threads running the same code at once. A condition variable lets a thread sleep until another thread tells it something has changed, instead of checking over and over.

#cmdline "-mt"

'' A one-slot mailbox: the producer waits while it is full, the
'' consumer waits while it is empty.
type Mailbox
    as any ptr lock, changed
    as boolean full
    as int32 item
end type

sub Producer( byval p as any ptr )
    var box = cptr( Mailbox ptr, p )
    for n as int32 = 1 to 5
        mutexlock( box->lock )
        do while box->full                 '' re-test after every wake-up
            condwait( box->changed, box->lock )
        loop
        box->item = n * 10
        box->full = true
        condsignal( box->changed )
        mutexunlock( box->lock )
    next
end sub

dim box as Mailbox
box.lock = mutexcreate( )
box.changed = condcreate( )
var worker = threadcreate( @Producer, @box )

for k as int32 = 1 to 5
    mutexlock( box.lock )
    do until box.full                      '' sleeps, releasing the lock
        condwait( box.changed, box.lock )
    loop
    print "took"; box.item
    box.full = false
    condsignal( box.changed )
    mutexunlock( box.lock )
next

threadwait( worker )
conddestroy( box.changed )
mutexdestroy( box.lock )

Output

took 10
took 20
took 30
took 40
took 50

The mailbox holds one item. The producer thread waits while it is full; the main program waits while it is empty. Each side changes full with the mutex held and then calls condsignal, which wakes a thread sleeping on changed. The five items arrive in order because neither side can get ahead of the other.

condwait( cond, mutex ) does three things: it releases the mutex, sleeps until it is signalled, and takes the mutex back before it returns. Releasing the mutex is what lets the other thread change full in the meantime.

Always test the condition in a loop, as both do while box->full and do until box.full do. A wake-up says only that something may have changed: the thread can be woken with nothing to do, or find that another thread has already taken what it was woken for.

CallDoes
condcreate( ), conddestroy( c )make and free a condition variable
condwait( c, m )release m, sleep until signalled, retake m
condsignal( c )wake one thread waiting on c
condbroadcast( c )wake every thread waiting on c

A channel with a capacity of 1 does the same job as this mailbox in a few lines. Reach for a condition variable when the condition is more than "a value is waiting".

Activities

  1. Make the mailbox hold up to three items, with a count instead of full.
  2. Start two consumer threads and use condbroadcast when the producer finishes.

Atomics

For a single shared counter or flag, a mutex is more than you need. An atomic operation reads, changes and writes a value in one step that no other thread can interrupt.

#cmdline "-mt"
#include once "fbatomic.bi"

'' Shared by four threads, with no mutex anywhere.
type Race
    as int32 hits                   '' counted by every thread
    as int32 winner                 '' 0 until one thread claims it
    as int32 claims                 '' how many claims succeeded
end type

sub Runner( byval p as any ptr )
    var r = cptr( Race ptr, p )
    for i as int32 = 1 to 100000
        FB.AtomicFetchAddLong( @r->hits, 1 )      '' one indivisible add
    next
    '' Store 1 only if the value is still 0; the old value says who won.
    if FB.AtomicCasLong( @r->winner, 0, 1 ) = 0 then
        FB.AtomicFetchAddLong( @r->claims, 1 )
    end if
end sub

dim r as Race
dim runners( 0 to 3 ) as any ptr
for i as int32 = 0 to 3
    runners( i ) = threadcreate( @Runner, @r )
next
for i as int32 = 0 to 3
    threadwait( runners( i ) )
next

print "hits:"; FB.AtomicLoadLong( @r.hits )
print "successful claims:"; FB.AtomicLoadLong( @r.claims )

Output

hits: 400000
successful claims: 1

fbatomic.bi declares three operations on a 32-bit integer, reached through a pointer to it:

CallDoesReturns
FB.AtomicFetchAddLong( p, v )adds v to *pthe value before the add
FB.AtomicCasLong( p, expected, desired )stores desired only if *p is expectedthe value before; success is result = expected
FB.AtomicLoadLong( p )reads *pthe value

Four runners add 1 a hundred thousand times each, with no lock, and hits is exactly 400000. Then each runner tries to change winner from 0 to 1. The compare-and-swap succeeds for exactly one of them, because only one can find the 0 still there; the other three see the 1 it stored, and do nothing. Which runner wins changes from run to run, so the program prints only how many did.

AtomicLoadLong exists for its ordering, not its atomicity: it makes sure the read sees what the other threads wrote, where an ordinary read of a variable another thread is changing may not. Every one of the three is a full memory barrier.

An atomic protects one variable. When an update touches two values that must stay consistent with each other, use a mutex.

Activities

  1. Use the value AtomicFetchAddLong returns to give each of the four threads a distinct number from 0 to 3, and print the sum of the numbers.
  2. Replace the atomic add with r->hits += 1 and run the program a few times.

spawn

spawn Proc( args ) runs a procedure call as a goroutine, alongside the code that spawned it. A goroutine is a work item on a pool of threads that std/tasks manages, not a thread of its own, so starting one is cheap and thousands of them are fine.

#import "std/tasks"
#import "std/containers"
#cmdline "-mt"

'' A goroutine has no result: it reports through a channel.
sub Label( byval id as int32, byval prefix as ustring, _
           byval out_ as tasks.SendChan( of ustring ) )
    out_.Send( prefix & "-" & id )
end sub

var labels = tasks.Channel( of ustring )( 8 )
dim as ustring prefix = "task"
for i as int32 = 1 to 4
    spawn Label( i, prefix, labels )   '' i and prefix are copied now
next
prefix = "changed"                     '' too late for all of them

'' They finish in any order; an OrderedSet keeps what they sent sorted.
dim got as containers.OrderedSet( of ustring )
dim as ustring s
for i as int32 = 1 to 4
    labels.Receive( s )
    got.Add( s )
next
for each g in got
    print g
next

Output

task-1
task-2
task-3
task-4

The arguments are evaluated and copied at the spawn statement. Each goroutine gets the i and the prefix that existed when it was spawned, so changing prefix afterwards changes nothing any of them see, however late they run. The four labels arrive in whatever order the goroutines finish, which is why the program collects them in an OrderedSet, which keeps them sorted.

Copying is what makes this safe, and it rules out a byref parameter: the goroutine would hold a reference into a frame that may be gone by the time it runs.

#import "std/tasks"
#cmdline "-mt"

sub Fill( byref total as int32 )
    total = 42
end sub

dim as int32 answer
spawn Fill( answer )        '' the goroutine may outlive 'answer'
spawnbyref.tiko(9,1): error TKO0812: 'spawn fill' cannot pass its byref parameter 'total'; a string or UDT parameter is byref unless it is written 'byval'

The second half of the message matters: a ustring or record parameter written without either keyword is byref, so write byval on every parameter of a procedure you mean to spawn. When the goroutine must change something of yours, pass a pointer, or better, send the result back through a channel.

Two more rules:

  • spawn takes a sub or function declared at module level; a function's result is discarded. The file must #import "std/tasks", or spawn is refused with TKO0423: 'spawn' needs '#import "std/tasks"' in this file (and -mt).
  • Nothing waits for a goroutine when the program ends. One still running is abandoned. A channel, as here, or a WaitGroup is how you wait.

Activities

  1. Spawn ten goroutines and check that all ten labels arrive.
  2. Remove byval from prefix and read the error.

Channels

A tasks.Channel( of T ) is a typed queue that goroutines use to hand values to one another. It is created with a capacity, the number of values it can hold before a sender has to wait.

#import "std/tasks"
#cmdline "-mt"

'' A channel made with a capacity of 3 buffers up to three values.
var box = tasks.Channel( of int32 )( 3 )
box.Send( 10 )
box.Send( 20 )
box.Send( 30 )
print "queued:"; box.Count( ); " of"; box.Capacity( )

'' TrySend never waits: with the buffer full it fails instead.
print "try to send 40: "; box.TrySend( 40 )

'' Values come out in the order they went in.
dim as int32 v
box.Receive( v )
print "first out:"; v

'' Close: no more sends, but what is queued can still be received.
box.Close( )
for each x in box                 '' drains, then ends
    print "drained:"; x
next

'' Receive on a closed, empty channel returns false at once.
v = -1
print "receive after close: "; box.Receive( v ); ", v is still " & v

'' ReceiveFor gives up after a number of milliseconds.
var quiet = tasks.Channel( of int32 )( 1 )
print "wait 50 ms for a value: "; quiet.ReceiveFor( v, 50 )

Output

queued: 3 of 3
try to send 40: false
first out: 10
drained: 20
drained: 30
receive after close: false, v is still -1
wait 50 ms for a value: false
OperationDoes
Send( v )waits until there is room, then queues v
Receive( v )waits for a value; false once the channel is closed and empty
TrySend( v ), TryReceive( v )never wait; false if they would have to
SendFor( v, ms ), ReceiveFor( v, ms )wait at most ms milliseconds
Close( )no more sends; values already queued can still be received
for each v in chreceives until the channel is closed and drained
Count( ), Capacity( ), IsClosed( )how things stand at this moment

Receive returning false is not an error: it is how a receiver learns that the sender has finished, and for each over a channel is built on it. The variable keeps its old value when that happens. Sending on a closed channel stops the program with runtime error 1: it means two parts of the program disagree about who sends.

A channel is a handle. Copying one, or passing it to a procedure, gives another handle to the same queue, which is how one channel is shared between goroutines.

Pipelines and channel directions

Channels join goroutines into a pipeline, each stage reading from one channel and writing to the next:

#import "std/tasks"
#cmdline "-mt"

'' Each stage says which end of a channel it holds.
sub Generate( byval upTo as int32, byval out_ as tasks.SendChan( of int32 ) )
    for i as int32 = 1 to upTo
        out_.Send( i )
    next
    out_.Close( )                 '' tells the next stage there is no more
end sub

sub KeepOdd( byval in_ as tasks.RecvChan( of int32 ), _
             byval out_ as tasks.SendChan( of int32 ) )
    dim as int32 v
    do while in_.Receive( v )     '' false once in_ is closed and empty
        if v mod 2 = 1 then out_.Send( v )
    loop
    out_.Close( )
end sub

'' Capacity 0: every send waits for a receiver, a handover.
var numbers = tasks.Channel( of int32 )( 0 )
var odds = tasks.Channel( of int32 )( 0 )

spawn Generate( 9, numbers )
spawn KeepOdd( numbers, odds )

for each v in odds                '' ends when KeepOdd closes odds
    print v;
next
print

Output

 1 3 5 7 9

A capacity of 0 makes every send wait until a receiver takes the value: a handover, with no buffer between the stages. Generate closes numbers when it has sent everything; KeepOdd sees Receive return false and closes odds in turn; and the main program's for each ends. The close travels down the pipeline, and no stage needs to know how many values there are.

The parameter types say which end each stage holds. A tasks.SendChan( of T ) can send and close; a tasks.RecvChan( of T ) can only receive. A Channel converts to either when it is passed. Closing is the sender's decision, so a receiving end that tries is refused:

#import "std/tasks"
#cmdline "-mt"

sub Consume( byval inbox as tasks.RecvChan( of int32 ) )
    inbox.Close( )            '' closing is the sender's decision
end sub
recvclose.tiko(5,11): error TKO0739: 'recvchan' has no member named 'close'

Activities

  1. Add a third stage between KeepOdd and the main program that squares each value.
  2. Give numbers a capacity of 4 instead of 0. The output does not change; what does?

select

select channel waits until one of several channel operations can go ahead, and runs that case. It is how a goroutine listens to more than one channel at once, and how it waits for a channel without waiting for ever.

#import "std/tasks"
#cmdline "-mt"

'' Sends msg on out_ after a delay.
sub After( byval ms as int32, byval msg as ustring, _
           byval out_ as tasks.SendChan( of ustring ) )
    sleep ms, 1
    out_.Send( msg )
end sub

var quick = tasks.Channel( of ustring )( 1 )
var slow = tasks.Channel( of ustring )( 1 )
dim as ustring msg

'' case else: if nothing is ready right now, do not wait at all.
select channel
    case receive msg from quick : print "quick: "; msg
    case receive msg from slow  : print "slow: "; msg
    case else                   : print "nothing ready yet"
end select

spawn After( 20, "quick reply", quick )
spawn After( 3000, "slow reply", slow )

'' Wait for whichever channel is ready first.
select channel
    case receive msg from quick : print "first: "; msg
    case receive msg from slow  : print "first: "; msg
    case timeout 1000           : print "no reply within 1000 ms"
end select

'' case timeout: wait, but only so long.
select channel
    case receive msg from quick : print "then: "; msg
    case receive msg from slow  : print "then: "; msg
    case timeout 100            : print "nothing else within 100 ms"
end select

Output

nothing ready yet
first: quick reply
nothing else within 100 ms

The first select has a case else, so it does not wait: nothing has been sent yet, and the else runs at once. The second waits for whichever channel has a value first; the quick reply comes after 20 ms, well inside the timeout. The third waits only 100 ms, and the slow reply is not due for three seconds, so the timeout case runs. The program then ends without receiving it.

CaseRuns when
case receive v from cha value can be received into v, declared beforehand
case receive from cha value can be received; it is thrown away
case send v to chv can be sent
case timeout msnothing else became ready within ms milliseconds
case elsenothing is ready right now

A select may have a timeout or an else, not both: the else would run every time, and the timeout could never be reached.

When several cases are ready at once, one of them is chosen, in no fixed order, so a channel that always has a value cannot starve the others. A closed channel always counts as ready to receive, which is what makes cancellation one more case. Like spawn, select channel needs #import "std/tasks" in the file.

Activities

  1. Change the second timeout to 10 and predict the output.
  2. Put the second and third select in a loop that runs until both replies have arrived, with a timeout of 5000.

Worker pool

A worker pool shares a stream of jobs between a fixed number of goroutines: each job goes to whichever worker is free. Spreading the work out is fan-out; collecting the results back into one place is fan-in.

#import "std/tasks"
#import "std/containers"
#cmdline "-mt"

'' Each worker takes jobs until the jobs channel is closed and empty.
sub Worker( byval jobs as tasks.RecvChan( of int32 ), _
            byval results as tasks.SendChan( of int32 ), _
            byval wg as tasks.WaitGroup ptr )
    dim as int32 n
    do while jobs.Receive( n )
        results.Send( n * n )
    loop
    wg->Done( )                       '' this worker has finished
end sub

'' Closes results once every worker has called Done( ).
sub CloseWhenDone( byval wg as tasks.WaitGroup ptr, _
                   byval results as tasks.SendChan( of int32 ) )
    wg->Wait( )
    results.Close( )
end sub

'' Sends the jobs 1 to upTo, then closes the channel.
sub Feed( byval upTo as int32, byval jobs as tasks.SendChan( of int32 ) )
    for n as int32 = 1 to upTo
        jobs.Send( n )
    next
    jobs.Close( )
end sub

var jobs = tasks.Channel( of int32 )( 4 )
var results = tasks.Channel( of int32 )( 4 )
dim wg as tasks.WaitGroup

'' Fan out: three workers share one jobs channel.
for w as int32 = 1 to 3
    wg.Add( )                         '' count it before it starts
    spawn Worker( jobs, results, @wg )
next
spawn CloseWhenDone( @wg, results )

'' Feed the jobs from another goroutine, so the main program is free
'' to collect results while they are still being sent.
spawn Feed( 10, jobs )

'' Fan in: one loop collects from all three.
dim squares as containers.Array( of int32 )
for each sq in results
    squares.Push( sq )
next
containers.Sort( squares )            '' arrival order varies
print squares.Count( ); " results:";
for each sq in squares
    print sq;
next
print

Output

 10 results: 1 4 9 16 25 36 49 64 81 100

Three Workers receive from the one jobs channel and send to the one results channel. Which worker squares which number, and the order the squares arrive in, change from run to run; the sorted list does not.

The main program's for each over results ends when results is closed, and it must be closed exactly once, after the last worker has finished. No worker knows that it is the last, so a tasks.WaitGroup counts them:

MemberDoes
Add( )one more goroutine to wait for (or Add( n ) for n)
Done( )the calling goroutine has finished
Wait( )returns when every Add has been matched by a Done
WaitFor( ms )the same, but gives up after ms milliseconds

CloseWhenDone waits on the group and then closes results. Two rules keep it correct. Call Add before spawn, never inside the goroutine: otherwise Wait can run before the goroutine has counted itself and return early. And pass the WaitGroup by pointer: it cannot be copied, because a copy would be a second counter, so passing one byval is refused with TKO0746: the operator let of 'waitgroup' is private or protected and cannot be reached from here.

Feed runs as a goroutine too, so the main program can collect results while jobs are still going out. Had the main program sent every job itself before collecting anything, it would work only while the jobs fit in the channels' buffers and the workers' hands. With 20 jobs instead of 10 it stops for good: both buffers are full, every worker waits to send a result, and the main program waits to send a job.

Activities

  1. Run the pool with one worker, then with ten. The output is the same; time the two with a job that sleeps for 10 ms.
  2. Make each result say which worker produced it, and count the jobs each one took.

Cancellation

A goroutine cannot be stopped from outside. Instead, you ask it to stop, through a tasks.Context it was given, and it notices and returns.

#import "std/tasks"
#cmdline "-mt"

'' Would count for ever; stops as soon as its context is done.
sub Counter( byval ctx as tasks.Context, _
            byval out_ as tasks.Channel( of int32 ), _
            byval report as tasks.SendChan( of ustring ) )
    dim as int32 sent = 0
    do
        select channel
            case send sent + 1 to out_     : sent += 1
            case receive from ctx.Done( )  : exit do
        end select
    loop
    report.Send( "sent " & sent & ", then " & ctx.Err( ) )
end sub

'' Polls in small steps until its context is done.
sub Poll( byval ctx as tasks.Context, _
          byval report as tasks.SendChan( of ustring ) )
    do until ctx.IsCancelled( )
        sleep 5, 1                         '' one small piece of work
    loop
    report.Send( "poll stopped: " & ctx.Err( ) )
end sub

var reports = tasks.Channel( of ustring )( 1 )
dim as ustring why

'' Cancelled by hand, after taking three values.
var ctx = tasks.WithCancel( tasks.Context( ) )
var values = tasks.Channel( of int32 )( 0 )
spawn Counter( ctx, values, reports )
dim as int32 v
for k as int32 = 1 to 3
    values.Receive( v )
    print "got"; v
next
ctx.Cancel( )
reports.Receive( why )
print why

'' Cancelled by its own deadline, 50 ms from now.
var deadline = tasks.WithTimeout( tasks.Context( ), 50 )
spawn Poll( deadline, reports )
reports.Receive( why )
print why

Output

got 1
got 2
got 3
sent 3, then cancelled
poll stopped: deadline exceeded

tasks.WithCancel( tasks.Context( ) ) makes a context that its Cancel( ) cancels; tasks.WithTimeout( tasks.Context( ), ms ) makes one that cancels itself after ms milliseconds. tasks.Context( ) on its own is the background context, which is never cancelled, and each new context is derived from a parent: cancelling a context cancels every context derived from it.

A goroutine has two ways to notice:

  • ctx.Done( ) is a channel that is closed when the context is cancelled, and a closed channel is always ready to receive. So cancellation is one more case in a select, as in Counter, which offers each value until the context is done.
  • ctx.IsCancelled( ) can be tested between pieces of work, as in Poll.

ctx.Err( ) says why the context ended: cancelled or deadline exceeded. Cancel may be called more than once, so the code that cancels need not check whether something else already did.

Counter reports exactly three values sent: values has a capacity of 0, so a send completes only when the main program receives it, and after the third receive nobody does.

Cancellation is a request. A goroutine that never looks at its context runs to the end, and one blocked in an operation that does not involve the context cannot see the request until that operation returns. That is deliberate: stopping a thread by force would skip its destructors and its defer blocks, the cleanup the language promises.

Activities

  1. Cancel the counter after five values instead of three.
  2. Give Poll a WithTimeout of 200 ms whose parent is ctx, and see which ends first.

Dates and Times

A date looks simple and is not: months have four lengths, the same moment reads differently on every city's clock, and 03/04/2025 means March in one country and April in the next. The std/chrono module gives each idea its own type, so a program says which one it means.

#import "std/chrono"

'' Five value types, each for one idea.
var day   = chrono.LocalDate( 2025, 3, 4 )
var clock = chrono.LocalTime( 14, 30, 5, 250 )
var when  = chrono.DateTime( 2025, 3, 4, 14, 30, 5, 0 )
var at    = when.WithOffset( 60 ).ToInstant( )
var span  = chrono.TimeSpan( 1, 2, 3, 4, 500 )

print "LocalDate  "; day.ToIsoString( )
print "LocalTime  "; clock.ToIsoString( )
print "DateTime   "; when.ToIsoString( )
print "Instant    "; at.ToIsoString( )
print "TimeSpan   "; span.ToIsoString( )

'' Values never change in place: AddDays returns a new date.
var next_ = day.AddDays( 1 )
print "day is still "; day.ToIsoString( ); _
      ", the next is "; next_.ToIsoString( )

Output

LocalDate  2025-03-04
LocalTime  14:30:05.2500000
DateTime   2025-03-04T14:30:05
Instant    2025-03-04T13:30:05Z
TimeSpan   P1DT2H3M4.5S
day is still 2025-03-04, the next is 2025-03-05

#import "std/chrono" brings in five value types:

TypeHolds
LocalDatea calendar date, with no time
LocalTimea time of day, with no date
DateTimea date and a time, with or without an offset from UTC
Instantone point on the UTC timeline, the same everywhere
TimeSpana fixed length of time

All of them count in ticks of 100 nanoseconds and cover the years 1 to 9999. The values are immutable: no method changes a value in place. AddDays( 1 ) returns a new date and leaves day as it was, so a date can be stored and passed around without anything changing it underneath you.

The constructors take their fields from largest to smallest: LocalDate( y, mo, d ), LocalTime( h, mi, s, ms ), DateTime( y, mo, d, h, mi, s, ms ) and TimeSpan( d, h, mi, s, ms ).

Every example in this section uses fixed dates, so its output is the same on every run. A real program reads the clock with chrono.WallClock.Today( ), Now( ) or UtcNow( ).

Activities

  1. Print the LocalDate of your own birthday.
  2. Make when one second before midnight and print when.AddSeconds( 1 ).

Dates

A LocalDate is a day on the calendar. Its parts are read with properties, and the calendar's facts, such as leap years and month lengths, are static functions on DateTime.

#import "std/chrono"

var d = chrono.LocalDate( 2024, 2, 29 )

'' The parts are read with properties.
print "year"; d.Year; ", month"; d.Month; ", day"; d.Day
print "day of year"; d.DayOfYear; ", quarter"; d.Quarter

'' DayOfWeek counts the ISO way: 1 is Monday, 7 is Sunday.
print "day of week"; d.DayOfWeek; " = "; _
      chrono.DateTime.WeekdayName( d.DayOfWeek )
print "last day of its month: "; d.IsLastDayOfMonth

'' Calendar facts, without making a date.
print "2024 is a leap year: "; chrono.DateTime.IsLeapYear( 2024 )
print "days in February 2025:"; chrono.DateTime.DaysInMonth( 2025, 2 )

'' There is no 29 February 2025: the value is Invalid, not March 1.
var bad = chrono.LocalDate( 2025, 2, 29 )
print "2025-02-29 valid: "; bad.IsValid; _
      ", as text: ["; bad.ToIsoString( ); "]"

'' A date declared with no value is Invalid too.
dim unset as chrono.LocalDate
print "unset date valid: "; unset.IsValid

Output

year 2024, month 2, day 29
day of year 60, quarter 1
day of week 4 = Thursday
last day of its month: true
2024 is a leap year: true
days in February 2025: 28
2025-02-29 valid: false, as text: []
unset date valid: false

DayOfWeek numbers the days the ISO 8601 way, 1 for Monday through 7 for Sunday, and DateTime.WeekdayName( n ) turns the number into its name in the user's language, as a ustring (English on the machine that made this output). DayOfYear, Quarter, IsoWeek and IsoWeekYear complete the set.

A date with an out-of-range part is not rolled into the next month. The constructor returns the Invalid value of its type, and IsValid says so:

  • Invalid absorbs arithmetic: adding days to it gives Invalid again, so a chain of calculations can be checked once, at the end.
  • It equals nothing, not even itself. Test with IsValid.
  • It prints as an empty string.

A LocalDate, DateTime or Instant declared with no value is Invalid, because no date is a sensible default. A TimeSpan starts at zero and a LocalTime at midnight.

Activities

  1. Print the day of the week you were born on.
  2. Find out which day of the year 31 December 2024 is, and why it is not 365.

Adding days and months

Days have a fixed length; months do not. std/chrono keeps the two kinds of arithmetic apart, and the difference shows at the end of a month.

#import "std/chrono"

var start = chrono.LocalDate( 2025, 1, 31 )

'' Days are exact.
print "+ 30 days:   "; start.AddDays( 30 ).ToIsoString( )
print "- 31 days:   "; start.AddDays( -31 ).ToIsoString( )

'' Months clamp to the end of a shorter month.
print "+ 1 month:   "; start.AddMonths( 1 ).ToIsoString( )
print "+ 2 months:  "; start.AddMonths( 2 ).ToIsoString( )
print "1 month, twice: "; _
      start.AddMonths( 1 ).AddMonths( 1 ).ToIsoString( )
print "+ 1 year from a leap day: "; _
      chrono.LocalDate( 2024, 2, 29 ).AddYears( 1 ).ToIsoString( )

'' Subtracting two dates gives the days between them.
var xmas = chrono.LocalDate( 2025, 12, 25 )
print "days from Jan 31 to Christmas:"; xmas - start

'' Dates compare like numbers.
print "start < xmas: "; ( start < xmas )

'' The month's first and last days.
print "month: "; start.StartOfMonth( ).ToIsoString( ); " to "; _
      start.AddMonths( 1 ).EndOfMonth( ).ToIsoString( )

Output

+ 30 days:   2025-03-02
- 31 days:   2024-12-31
+ 1 month:   2025-02-28
+ 2 months:  2025-03-31
1 month, twice: 2025-03-28
+ 1 year from a leap day: 2025-02-28
days from Jan 31 to Christmas: 328
start < xmas: true
month: 2025-01-01 to 2025-02-28

AddDays is exact. AddMonths and AddYears work on the calendar's fields and then clamp the day to the end of the month, so 31 January plus one month is 28 February. That loses information: two steps of one month end on 28 March, where one step of two months ends on 31 March.

Pitfall

Month arithmetic does not compose. Adding a month at a time in a loop drifts to the 28th and stays there. Add the whole number of months in one call, counting from the original date.

Subtracting one LocalDate from another gives the number of days between them, and dates compare with <, = and the other operators. StartOfMonth and EndOfMonth give the first and last day of a date's month. A DateTime also has StartOfDay, StartOfWeek, StartOfYear, NextWeekday( n ) and PrevWeekday( n ).

Activities

  1. Print the last day of every month of 2024 using EndOfMonth.
  2. Count the days from 4 March 2025 to the next 1 January.

Durations

A TimeSpan is a length of time that does not depend on the calendar: a lap, a timeout, the time between two moments. It has two families of readers, and mixing them up gives plausible numbers that are wrong.

#import "std/chrono"

'' A TimeSpan is a fixed length of time: days, hours, minutes, ...
var span = chrono.TimeSpan( 1, 2, 30, 0, 0 )
print "span:        "; span.ToIsoString( )

'' Total* is the whole span in one unit; the rest are its pieces.
print "TotalHours:"; span.TotalHours
print "Days"; span.Days; ", Hours"; span.Hours; _
      ", Minutes"; span.Minutes

'' Built from one unit, and combined with operators.
var lap = chrono.TimeSpan.FromSeconds( 95.5 )
print "lap:         "; lap.ToIsoString( )
print "three laps:  "; ( lap * 3 ).ToIsoString( )
print "span - lap:  "; ( span - lap ).ToIsoString( )
print "lap < span:  "; ( lap < span )

'' A negative span has negative pieces.
var back = chrono.TimeSpan( 1, 30, 0 ).Negate( )
print "negative:    "; back.ToIsoString( ); ", Hours "; back.Hours; _
      ", Minutes "; back.Minutes

'' Subtracting two moments gives a span.
var leave = chrono.Instant.ParseIso( "2025-03-04T09:00:00-05:00" )
var land  = chrono.Instant.ParseIso( "2025-03-04T21:40:00Z" )
var flight = land - leave
print "flight:      "; flight.ToIsoString( ); " ="; _
      flight.TotalMinutes; " minutes"

Output

span:        P1DT2H30M
TotalHours: 26.5
Days 1, Hours 2, Minutes 30
lap:         PT1M35.5S
three laps:  PT4M46.5S
span - lap:  P1DT2H28M24.5S
lap < span:  true
negative:    -PT1H30M, Hours -1, Minutes -30
flight:      PT7H40M = 460 minutes
ReaderGivesFor span
TotalHours, TotalMinutes, ...the whole span in one unit, with a fraction26.5
Hours, Minutes, ...one piece of it2

TimeSpan.FromSeconds, FromMinutes, FromHours, FromDays and FromMilliseconds build a span from one unit. Spans add, subtract, multiply and divide by a number, and compare. The pieces of a negative span are all negative, so they still add up to the whole.

Subtracting two Instant values gives a TimeSpan. The flight in the example leaves at 09:00 five hours behind UTC and lands at 21:40 UTC; because both are instants, the subtraction needs no offset arithmetic.

There is no FromMonths: a month is not a fixed length. Use AddMonths on a date for that.

Activities

  1. Add up three lap times and print the total in minutes.
  2. Print TotalSeconds and Seconds for TimeSpan( 0, 1, 1, 1, 0 ).

ISO 8601 text

ISO 8601 is the date format that sorts as text and means the same thing everywhere: 2025-03-04T14:30:05+01:00. Every chrono type writes it with ToIsoString( ), which returns a ustring, and reads it with TryParseIso.

#import "std/chrono"

'' ToIsoString writes one canonical form for each value.
var when = chrono.DateTime( 2025, 3, 4, 14, 30, 5, 0 )
print when.ToIsoString( )
print when.WithOffset( 330 ).ToIsoString( )
print when.AssumeUtc( ).ToIsoString( )

'' TryParseIso reads several ISO 8601 forms, and refuses guesses.
dim inputs( ... ) as ustring = { _
    "2025-03-04T14:30:05Z", "2025-03-04 14:30:05+05:30", _
    "20250304T143005Z", "2025-W10-2", "2025-063", _
    "03/04/2025", "2025-02-30" }

for i as int32 = 0 to ubound( inputs )
    dim d as chrono.DateTime
    '' ToIsoString is a ustring, so the fallback text is one too.
    dim shown as ustring = "refused"
    if chrono.DateTime.TryParseIso( inputs( i ), d ) then
        shown = d.ToIsoString( )
    end if
    print left( inputs( i ) & space( 27 ), 27 ); shown
next

'' Durations have an ISO form too. A month is not a fixed length.
dim spans( ... ) as ustring = { "PT90M", "P1W", "P1DT0.5S", "P1M" }
for i as int32 = 0 to ubound( spans )
    dim s as chrono.TimeSpan
    dim shown as ustring = "refused"
    if chrono.TimeSpan.TryParseIso( spans( i ), s ) then
        shown = s.ToIsoString( )
    end if
    print left( spans( i ) & space( 10 ), 10 ); shown
next

Output

2025-03-04T14:30:05
2025-03-04T14:30:05+05:30
2025-03-04T14:30:05Z
2025-03-04T14:30:05Z       2025-03-04T14:30:05Z
2025-03-04 14:30:05+05:30  2025-03-04T14:30:05+05:30
20250304T143005Z           2025-03-04T14:30:05Z
2025-W10-2                 2025-03-04T00:00:00
2025-063                   2025-03-04T00:00:00
03/04/2025                 refused
2025-02-30                 refused
PT90M     PT1H30M
P1W       P7D
P1DT0.5S  P1DT0.5S
P1M       refused

One value has exactly one string. ToIsoString( ) always writes a capital T, a four-digit year, Z for UTC, +HH:MM for an offset and nothing when the offset is unspecified, and a fraction of a second gets all seven digits or none.

Reading accepts the other ISO forms: the basic form with no separators, week dates (2025-W10-2 is the Tuesday of week 10), ordinal dates (2025-063 is the 63rd day), and a space in place of the T. It refuses anything that would need a guess, such as 03/04/2025, and dates that do not exist.

TryParseIso( text, result ) returns true and fills result, or returns false and sets result to Invalid; it never raises a runtime error. ParseIso( text ) returns the value or Invalid, and suits text you know is good, such as a literal in the program.

A TimeSpan reads and writes ISO durations: PT1H30M is an hour and a half. P1W is accepted as seven days; P1M is refused, because a month is not a fixed length.

Activities

  1. Parse "2025-12-31T23:59:59Z" as an Instant, add one second, and print the result.
  2. Find out what TryParseIso makes of "2025-03-04T24:00:00".

Custom formats

When ISO 8601 is not the format a person or a protocol expects, ToString( pattern ) builds the text from pattern letters, and TryParseExact reads it back with the same pattern.

#import "std/chrono"

var d = chrono.DateTime( 2025, 3, 4, 14, 30, 5, 0 ).WithOffset( 60 )

'' Pattern letters, always in English, whatever the machine.
print d.ToString( "dddd, d MMMM yyyy" )
print d.ToString( "ddd dd/MM/yy h:mm tt" )
print d.ToString( "yyyy-MM-dd'T'HH:mm:sszzz" )

'' One letter is a named format: "R" is the RFC 1123 HTTP form.
print d.ToString( "R" )

'' A letter that is not a pattern letter is an error: quote it.
dim text as ustring
print "bare T accepted: "; d.TryFormat( "yyyy-MM-ddTHH:mm", text )

'' A pattern says which number is the day when reading.
dim p as chrono.DateTime
if chrono.DateTime.TryParseExact( "04.03.2025 17:45", _
                                  "dd.MM.yyyy HH:mm", p ) then
    print "parsed: "; p.ToIsoString( )
end if

'' Repeated fields are checked: 4 March 2025 is not a Monday.
print "Monday 04.03.2025 accepted: "; _
      chrono.DateTime.TryParseExact( "Monday 04.03.2025", _
                                     "dddd dd.MM.yyyy", p )

Output

Tuesday, 4 March 2025
Tue 04/03/25 2:30 PM
2025-03-04T14:30:05+01:00
Tue, 04 Mar 2025 13:30:05 GMT
bare T accepted: false
parsed: 2025-03-04T17:45:00
Monday 04.03.2025 accepted: false
LettersFieldFor d
yyyy, yyyear2025, 25
M, MM, MMM, MMMMmonth3, 03, Mar, March
d, dd, ddd, ddddday, weekday name4, 04, Tue, Tuesday
H, HH, h, hhhour, 24- and 12-hour14, 14, 2, 02
mm, ssminute, second30, 05
ttAM or PMPM
zzz, Koffset; K writes Z for UTC+01:00

The names are always English, whatever the machine's language, so the text depends only on the value and the pattern. A one-letter pattern is a named format: "o" round-trip ISO, "s" sortable, "R" RFC 1123 (converted to GMT first, as the output shows), "d" the date and "T" the time.

Text in single quotes is copied as it is, and so is any character that is not a letter. A letter that is not a pattern letter is an error, not a literal: TryFormat returns false and ToString an empty string, so a typo shows at once.

TryParseExact( text, pattern, result ) says which number is the day, and cross-checks fields that appear twice: "Monday" does not parse beside a date that fell on a Tuesday.

Activities

  1. Print the date as 4-Mar-2025.
  2. Parse "12/31/2025" as month/day/year with TryParseExact.

Offsets and time zones

A wall-clock reading such as 09:00 names a moment only once you know whose clock it was read on. A DateTime carries that as an offset from UTC in minutes, and an Instant is the moment itself.

#import "std/chrono"

'' A DateTime with no offset is a reading on an unnamed clock.
var naive = chrono.DateTime( 2025, 3, 4, 9, 0, 0, 0 )
print "naive:      "; naive.ToIsoString( ); _
      "  has offset: "; naive.HasOffset
print "as instant valid: "; naive.ToInstant( ).IsValid

'' WithOffset says which clock it was read on (minutes from UTC).
var toronto = naive.WithOffset( -300 )
print "Toronto:    "; toronto.ToIsoString( )

'' ToOffset shows the same moment on another clock.
print "UTC:        "; toronto.ToOffset( 0 ).ToIsoString( )
print "Kolkata:    "; toronto.ToOffset( 330 ).ToIsoString( )

'' The same moment on two clocks is one Instant.
var a = toronto.ToInstant( )
var b = toronto.ToOffset( 330 ).ToInstant( )
print "same instant: "; ( a = b )

'' The machine's own zone: ask for its offset AT a given instant.
var jul = chrono.Instant.ParseIso( "2025-07-15T12:00:00Z" )
dim as int32 local_ = chrono.TimeZoneInfo.LocalOffsetAt( jul )
print "local offset within 18 hours: "; _
      cbool( abs( local_ ) <= 18 * 60 )
var there = chrono.DateTime.FromInstant( jul, local_ )
print "local reading converts back: "; ( there.ToInstant( ) = jul )

Output

naive:      2025-03-04T09:00:00  has offset: false
as instant valid: false
Toronto:    2025-03-04T09:00:00-05:00
UTC:        2025-03-04T14:00:00Z
Kolkata:    2025-03-04T19:30:00+05:30
same instant: true
local offset within 18 hours: true
local reading converts back: true

A DateTime is in one of three states: UTC (printed with Z), an offset (-05:00 is five hours behind UTC), or unspecified, where HasOffset is false. An unspecified reading cannot become an Instant without a guess, so ToInstant( ) gives Invalid; AssumeUtc( ) makes the guess and makes it visible in the code.

Two methods put an offset on a DateTime, and they do different things:

CallKeepsChanges
WithOffset( m )the wall-clock readingthe moment it names
ToOffset( m )the momentthe wall-clock reading

Pitfall

WithOffset where ToOffset was meant compiles, runs, and moves every timestamp by the difference between the two offsets. To show a moment on another clock, the call is ToOffset.

The machine's own zone comes from the operating system. TimeZoneInfo.LocalOffsetAt( instant ) is its offset at that instant, since January and July can differ; DateTime.FromInstant( instant, offset ) gives the local reading. The example prints only facts that hold in every zone, because the offset itself depends on where the program runs. There is no database of named zones: carry an Instant, store it in UTC, and convert for display.

Activities

  1. Print the moment 2025-06-01T12:00:00Z as it reads in Tokyo (+09:00).
  2. Print TimeZoneInfo.LocalOffsetAt for a January and a July instant on your machine, and compare them.

Measuring elapsed time

To time how long something takes, do not subtract two readings of the wall clock: the operating system corrects it and users change it, so it can jump backwards. A Stopwatch reads a clock that only moves forward.

#import "std/chrono"

function SumTo( byval n as int64 ) as int64
    dim as int64 total = 0
    for i as int64 = 1 to n
        total += i
    next
    return total
end function

'' A Stopwatch reads a clock that only moves forward.
var sw = chrono.Stopwatch.StartNew( )
var total = SumTo( 1000000 )
var taken = sw.Elapsed

'' The time differs on every run; these facts do not.
print "total:"; total
print "running: "; sw.IsRunning
print "elapsed >= 0: "; cbool( taken.Ticks >= 0 )
print "a later reading is not smaller: "; ( sw.Elapsed >= taken )

'' Stop_ pauses it: time spent stopped is not counted.
sw.Restart( )
sw.Stop_( )
var paused = sw.ElapsedTicks
sleep 200, 1
print "stopped, so unchanged: "; cbool( sw.ElapsedTicks = paused )
print "running: "; sw.IsRunning

Output

total: 500000500000
running: true
elapsed >= 0: true
a later reading is not smaller: true
stopped, so unchanged: true
running: false

Stopwatch.StartNew( ) makes a running stopwatch. Elapsed is a TimeSpan; ElapsedMilliseconds and ElapsedTicks are numbers. Restart( ) zeroes it and keeps it running, Reset( ) zeroes and stops it, and Start( ) resumes. Stop_( ) has a trailing underscore because stop is a statement. A stopwatch adds up only the time it was running, so the sleep while it was stopped is not counted.

The time taken differs on every run and every machine, so the example prints facts about it rather than the time. For a real measurement, run the work several times, keep the fastest, and build with optimisation on.

chrono.CpuClock.ProcessTime( ) is the other clock worth knowing: the processor time the program has used, which a sleep does not add to.

Activities

  1. Time SumTo( 10000000 ) and print the result with TotalMilliseconds.
  2. Stop the stopwatch, sleep, start it again, and check that IsRunning is true and the paused time is still not counted.

Numbers

The everyday numeric functions are built in and need no import: absolute value, square root, trigonometry, logarithms, rounding toward a whole number. Two of them round in a direction their names do not announce, and that is what to watch for.

'' The built-in numeric functions need no import.
print "abs( -3.5 ) ="; abs( -3.5 ); ", sgn( -2 ) ="; sgn( -2 )
print "sqr( 2 )    ="; sqr( 2 )
print "exp( 1 )    ="; exp( 1 )
print "log( 100 )  ="; log( 100 ); "  (natural log)"

'' Trigonometry works in radians.
dim as float64 pi = atn( 1 ) * 4
print "pi          ="; pi
print "sin( pi / 6 ) ="; sin( pi / 6 )
print "atan2( 1, 1 ) ="; atan2( 1, 1 )

'' int rounds down, fix rounds toward zero, frac is what fix drops.
print "int( -2.5 ) ="; int( -2.5 ); ", fix( -2.5 ) ="; fix( -2.5 ); _
      ", frac( -2.5 ) ="; frac( -2.5 )

'' / divides as floating point; \ divides whole numbers.
print "7 / 2 ="; 7 / 2; ", 7 \ 2 ="; 7 \ 2; ", 7 mod 2 ="; 7 mod 2

'' mod and \ truncate: the remainder takes the sign of the left side.
print "-7 mod 3 ="; -7 mod 3; ", -7 \ 3 ="; -7 \ 3

'' ^ raises to a power, in floating point.
print "2 ^ 10 ="; 2 ^ 10; ", 2 ^ 0.5 ="; 2 ^ 0.5

Output

abs( -3.5 ) = 3.5, sgn( -2 ) =-1
sqr( 2 )    = 1.414213562373095
exp( 1 )    = 2.718281828459045
log( 100 )  = 4.605170185988092  (natural log)
pi          = 3.141592653589793
sin( pi / 6 ) = 0.4999999999999999
atan2( 1, 1 ) = 0.7853981633974483
int( -2.5 ) =-3, fix( -2.5 ) =-2, frac( -2.5 ) =-0.5
7 / 2 = 3.5, 7 \ 2 = 3, 7 mod 2 = 1
-7 mod 3 =-1, -7 \ 3 =-2
2 ^ 10 = 1024, 2 ^ 0.5 = 1.414213562373095
FunctionGives
abs( x ), sgn( x )the magnitude; -1, 0 or 1
sqr( x )the square root
exp( x ), log( x )e to the power x, and the natural logarithm
sin, cos, tan, asin, acos, atn, atan2trigonometry, in radians
int( x )the largest whole number not above x: it rounds down
fix( x ), frac( x )the whole part toward zero, and what is left

int( -2.5 ) is -3, because int is a floor; code that uses it believing it truncates is wrong for every negative number. fix is the one that truncates.

/ always divides as floating point. \ divides whole numbers and mod gives the remainder; both truncate toward zero, so the remainder takes the sign of the left-hand side and -7 mod 3 is -1. ^ raises to a power, working in float64.

sin( pi / 6 ) prints 0.4999999999999999 rather than 0.5: pi holds the nearest float64 to pi, not pi itself. Results from the square root, logarithms and trigonometry come from each platform's maths library.

The std/numeric module fills the gaps: rounding that names its direction, exact integer arithmetic, overflow control, and more. The pages that follow use it.

Activities

  1. Print int( 2.7 ), int( -2.7 ), fix( 2.7 ) and fix( -2.7 ), and predict each before you run it.
  2. Convert 90 degrees to radians and print its sine.

The numeric library

#import "std/numeric" adds the functions the built-ins lack, each named for exactly what it does. Nothing existing changes: int, mod and ^ behave as before, and the library adds correct neighbours under other names.

#import "std/numeric"

'' Rounding that says which way it goes.
print "Round( 2.5 ) ="; numeric.Round( 2.5 ); _
      ", Round( -2.5 ) ="; numeric.Round( -2.5 )
print "RoundHalfEven( 2.5 ) ="; numeric.RoundHalfEven( 2.5 ); _
      ", RoundHalfEven( 3.5 ) ="; numeric.RoundHalfEven( 3.5 )
print "Floor( -1.5 ) ="; numeric.Floor( -1.5 ); _
      ", Ceil( -1.5 ) ="; numeric.Ceil( -1.5 ); _
      ", Truncate( -1.5 ) ="; numeric.Truncate( -1.5 )
print "Round( 3.14159, 2 ) ="; numeric.Round( 3.14159, 2 )

'' Selection.
print "Min ="; numeric.Min( 3, 8 ); ", Max ="; numeric.Max( 3, 8 ); _
      ", Clamp( 15, 0, 10 ) ="; numeric.Clamp( 15, 0, 10 )
print "Lerp( 0, 200, 0.25 ) ="; numeric.Lerp( 0.0, 200.0, 0.25 )

'' Constants and geometry.
print "PI ="; numeric.PI; ", Hypot( 3, 4 ) ="; numeric.Hypot( 3.0, 4.0 )
print "Log2( 1024 ) ="; numeric.Log2( 1024.0 ); _
      ", Log10( 1000 ) ="; numeric.Log10( 1000.0 )

'' Whole-number work, done exactly.
print "IPow( 3, 4 ) ="; numeric.IPow( 3, 4 ); _
      ", ISqrt( 99 ) ="; numeric.ISqrt( 99 )
print "GCD( 12, 18 ) ="; numeric.GCD( 12, 18 ); _
      ", LCM( 4, 6 ) ="; numeric.LCM( 4, 6 )

'' A remainder that never goes negative, for wrapping around.
print "-7 mod 3 ="; -7 mod 3; ", FloorMod( -7, 3 ) ="; _
      numeric.FloorMod( -7, 3 )

'' Quotient and remainder together, as a tuple.
var ( pages, rest ) = numeric.DivMod( 47, 10 )
print "47 lines, 10 a page:"; pages; " full pages and"; rest; " over"

Output

Round( 2.5 ) = 3, Round( -2.5 ) =-3
RoundHalfEven( 2.5 ) = 2, RoundHalfEven( 3.5 ) = 4
Floor( -1.5 ) =-2, Ceil( -1.5 ) =-1, Truncate( -1.5 ) =-1
Round( 3.14159, 2 ) = 3.14
Min = 3, Max = 8, Clamp( 15, 0, 10 ) = 10
Lerp( 0, 200, 0.25 ) = 50
PI = 3.141592653589793, Hypot( 3, 4 ) = 5
Log2( 1024 ) = 10, Log10( 1000 ) = 3
IPow( 3, 4 ) = 81, ISqrt( 99 ) = 9
GCD( 12, 18 ) = 6, LCM( 4, 6 ) = 12
-7 mod 3 =-1, FloorMod( -7, 3 ) = 2
47 lines, 10 a page: 4 full pages and 7 over

Rounding. Round takes a half away from zero; RoundHalfEven takes it to the even neighbour, which is banker's rounding. Both take an optional number of decimal places. Floor, Ceil and Truncate say which way they go.

Selection. Min, Max and Clamp( v, lo, hi ) work on every numeric type; Lerp( a, b, t ) moves from a toward b by the fraction t.

Constants and functions. PI, TAU, E, SQRT2, LN2 and LN10; Hypot, which does not overflow where sqr( x * x + y * y ) would; Log2, Log10, Cbrt, Pow and the hyperbolic functions.

Whole numbers. IPow multiplies integers, so it is exact where ^ rounds (see Overflow). ISqrt, GCD and LCM are exact too. FloorMod takes the sign of the divisor, so it is never negative for a positive divisor: the remainder you want for wrapping an index around. DivMod returns the quotient and remainder together as a tuple, which var ( pages, rest ) = ... takes apart.

The module's names are reached through numeric., or bare after using numeric.

Activities

  1. Round 2.5, 3.5 and 4.5 with both Round and RoundHalfEven.
  2. Use FloorMod to step backwards through the days of the week 0 to 6, starting at 2 and wrapping past 0.

Overflow and wrapping

Integer arithmetic wraps when a result does not fit its type: one past the largest int32 is the smallest, and nothing reports it. std/numeric lets a program say what should happen instead.

#import "std/numeric"

dim as int32 top = 2147483647

'' Stored back in an int32, a sum past the top wraps, silently.
dim as int32 wrapped = top + 1
print "top + 1 as int32 ="; wrapped

'' The same happens to unsigned types, at both ends.
dim as uint8 small = 255
small += 1
print "255 + 1 as uint8 = "; small
small -= 1
print "0 - 1 as uint8   = "; small

'' Checked: an empty Optional instead of a wrong number.
var sum = numeric.AddChecked( top, 1 )
print "AddChecked( top, 1 ) has a value: "; sum.HasValue( )
print "AddChecked( top, -1 ) ="; numeric.AddChecked( top, -1 ).Value( )

'' Saturating: stop at the end of the range.
print "AddSaturating( top, 1 ) ="; numeric.AddSaturating( top, 1 )

'' Wrapping: what + does, spelled so a reader knows it was meant.
print "AddWrapping( top, 1 )   ="; numeric.AddWrapping( top, 1 )

'' ^ works in float64, so above 2^53 an integer power is rounded.
dim as int64 b = 3, e = 38
dim as int64 viaPow = b ^ e
print "3 ^ 38       ="; viaPow
print "IPow( 3, 38 ) ="; numeric.IPow( b, e )

Output

top + 1 as int32 =-2147483648
255 + 1 as uint8 = 0
0 - 1 as uint8   = 255
AddChecked( top, 1 ) has a value: false
AddChecked( top, -1 ) = 2147483646
AddSaturating( top, 1 ) = 2147483647
AddWrapping( top, 1 )   =-2147483648
3 ^ 38       = 1350851717672992000
IPow( 3, 38 ) = 1350851717672992089

Unsigned types wrap at both ends: a uint8 holding 255 plus one is 0, and 0 minus one is 255.

FamilyOn overflow
AddChecked, SubChecked, MulChecked, DivChecked, NegChecked, AbsChecked, IPowCheckedreturn an empty Optional
AddSaturating and its siblingsstop at the end of the range
AddWrapping and its siblingswrap, exactly as the operators do

The wrapping family exists so that deliberate wrapping, in a hash or a checksum, says it is deliberate. A checked result is an Optional: HasValue( ) says whether there is one and Value( ) reads it; see Error Handling.

Pitfall

^ on integers is not exact above 2^53. It converts both sides to float64, and a float64 holds 53 bits of precision, so 3 ^ 38 comes back rounded, by 89. Use numeric.IPow for integer powers.

These functions protect the calls you thought to protect; overflow elsewhere is still silent.

Activities

  1. Use MulChecked to find the first factorial that does not fit in an int32.
  2. Give two uint8 variables the values 0 and 1, and compare numeric.SubSaturating of them with their difference stored in a uint8.

Infinity, NaN and negative zero

A float64 can hold more than numbers: positive and negative infinity, NaN ("not a number", the result of an operation with no answer), and a zero with a sign. Floating point does not wrap on overflow; it goes to infinity.

#import "std/numeric"

'' Infinity and NaN are made by functions, not constants.
var inf = numeric.Infinity( )
var nan_ = numeric.NaN( )

'' Overflow in floating point gives infinity, not a wrapped value.
dim as float64 big = 1e308
print "1e308 * 10 is infinite: "; numeric.IsInfinite( big * 10 )
print "inf > 1e308: "; cbool( inf > big )
print "inf - inf is NaN: "; numeric.IsNaN( inf - inf )

'' A NaN equals nothing, not even itself: ask with IsNaN.
print "nan = nan: "; cbool( nan_ = nan_ )
print "IsNaN( nan ): "; numeric.IsNaN( nan_ )

'' Classify names the kind of value.
print "Classify( 0.0 ) is FP_ZERO: "; _
      cbool( numeric.Classify( 0.0 ) = numeric.FP_ZERO )
print "Classify( 1.0 ) is FP_NORMAL: "; _
      cbool( numeric.Classify( 1.0 ) = numeric.FP_NORMAL )

'' Two zeros: equal as numbers, different in the sign bit.
dim as float64 negZero = -0.0
print "-0 = 0: "; cbool( negZero = 0.0 ); _
      ", IsNegative( -0 ): "; numeric.IsNegative( negZero )
print "1 / -0 is negative infinity: "; _
      cbool( 1 / negZero = numeric.NegInfinity( ) )

'' The library passes a NaN on rather than dropping it.
print "Max( nan, 1 ) is NaN: "; numeric.IsNaN( numeric.Max( nan_, 1.0 ) )

Output

1e308 * 10 is infinite: true
inf > 1e308: true
inf - inf is NaN: true
nan = nan: false
IsNaN( nan ): true
Classify( 0.0 ) is FP_ZERO: true
Classify( 1.0 ) is FP_NORMAL: true
-0 = 0: true, IsNegative( -0 ): true
1 / -0 is negative infinity: true
Max( nan, 1 ) is NaN: true

numeric.Infinity( ), NegInfinity( ) and NaN( ) make the special values. They are functions because a constant cannot be initialised by dividing by zero.

A NaN is not equal to anything, itself included, so x = x is false for a NaN. Ask IsNaN( x ). IsInfinite, IsFinite, IsNormal and IsSubnormal complete the questions, and Classify( x ) names the kind: FP_NAN, FP_INFINITE, FP_ZERO, FP_SUBNORMAL or FP_NORMAL.

-0.0 and 0.0 are equal as numbers and differ in the sign bit, which IsNegative reads. The sign shows when you divide by it: one over negative zero is negative infinity.

The library passes a NaN on rather than dropping it: Max( NaN, 1 ) is NaN. Losing the NaN there would turn an error you could find into a wrong answer you could not.

The example prints tests of the values rather than the values. How print shows an infinity or a NaN is the runtime's choice; a test means the same on every platform.

Activities

  1. Print IsNaN( sqr( -1 ) ).
  2. Find the smallest power of ten that is infinite in a float64 with a loop and IsInfinite.

Random numbers

A test or a simulation wants random numbers it can repeat; a game wants different ones each time. Both come from a seed: the same seed gives the same sequence.

#import "std/numeric"

'' randomize with a seed makes rnd repeat the same sequence.
randomize 42
print "rnd:";
for i as int32 = 1 to 3
    print rnd( );
next
print

'' numeric.Rng is a generator you hold: each one is its own stream.
dim dice as numeric.Rng = numeric.RngSeeded( 2025 )
print "ten dice:";
for i as int32 = 1 to 10
    print dice.NextInt( 1, 6 );    '' both ends included
next
print

'' The same seed gives the same stream, on every platform.
dim again as numeric.Rng = numeric.RngSeeded( 2025 )
print "same seed:";
for i as int32 = 1 to 10
    print again.NextInt( 1, 6 );
next
print

'' Shuffle an array; every order is equally likely.
dim cards( 1 to 6 ) as int32 = { 1, 2, 3, 4, 5, 6 }
numeric.Shuffle( dice, cards( ) )
print "shuffled:";
for i as int32 = 1 to 6
    print cards( i );
next
print

'' A double from 0 up to, but not including, 1.
var x = dice.NextDouble( )
print "0 <= x < 1: "; cbool( x >= 0 andalso x < 1 )

Output

rnd: 0.8665110266301781 0.1258447782602161 0.3785038157366216
ten dice: 5 3 3 4 6 2 4 3 6 4
same seed: 5 3 3 4 6 2 4 3 6 4
shuffled: 6 2 5 4 3 1
0 <= x < 1: true

The built-in pair is randomize, which seeds the program's one generator, and rnd( ), which returns a value from 0 up to but not including 1. With a fixed seed, as here, every run prints the same numbers.

numeric.Rng is a generator you hold. An Rng is a value: each one is its own stream, so one part of a program can have a repeatable stream without disturbing another's, and a copy carries on from the same point. RngSeeded( seed ) gives the same numbers on every platform, which is what tests need; RngFromEntropy( ) seeds from the operating system for a different stream on each run.

MemberGives
NextInt( lo, hi )a whole number from lo to hi, both included, with no bias
NextDouble( )a float64 from 0 up to but not including 1
NextBoolean( ), NextBooleanP( p )true half the time, or with probability p
NextNormal( ), NextNormalP( mean, sd )values on a bell curve

numeric.Shuffle( rng, a( ) ) puts an array in random order, every order equally likely, and numeric.Choice( rng, a( ) ) picks one element. For keys and tokens, where nobody may predict the value, use numeric.CryptoBytes or numeric.CryptoULongInt; they have no seed.

int( rnd( ) * 6 ) + 1 is the classic die roll. It is very slightly biased; NextInt( 1, 6 ) is not.

Activities

  1. Roll two dice 1000 times with a seeded Rng and count how often they sum to 7.
  2. Seed with RngFromEntropy( ) and run the program twice.

UUIDs

A UUID is a 128-bit identifier that can be made anywhere, with no central counter, and still be unique. std/uuids makes, parses and compares them.

#import "std/uuids"

'' NewV4 is random; NewV7 starts with the time, so later ones sort later.
dim ids( 0 to 2 ) as uuids.Uuid
for i as int32 = 0 to 2
    ids( i ) = uuids.NewV7( )
next
print "version:"; ids( 0 ).Version( )
print "sorted by creation? "; ids( 0 ) < ids( 1 ) andalso ids( 1 ) < ids( 2 )
print "a random one is version"; uuids.NewV4( ).Version( )

'' Parsing takes the usual spellings and always prints one form.
dim spellings( 0 to 3 ) as ustring = { _
    "F81D4FAE-7DEC-11D0-A765-00A0C91E6BF6", _
    "{f81d4fae-7dec-11d0-a765-00a0c91e6bf6}", _
    "f81d4fae7dec11d0a76500a0c91e6bf6", _
    "f81d4fae-7dec-11d0-a765" }
for each s in spellings
    var r = uuids.Parse( s )
    if r.IsOk( ) then
        print r.Value( ).ToString( )
    else
        print s; ": "; r.Failure( ).Message( )
    end if
next

Output

version: 7
sorted by creation? true
a random one is version 4
f81d4fae-7dec-11d0-a765-00a0c91e6bf6
f81d4fae-7dec-11d0-a765-00a0c91e6bf6
f81d4fae-7dec-11d0-a765-00a0c91e6bf6
f81d4fae-7dec-11d0-a765: uuid: invalid length

uuids.NewV4( ) is 122 random bits from the operating system's secure generator. uuids.NewV7( ) starts with the time in milliseconds, so later ones sort later — good for database keys — and within a program each one is greater than the one before. The time inside a v7 is readable with UnixMillis( ), so use v4 where creation time should stay private.

uuids.Parse accepts upper or lower case, with or without hyphens, in braces, or after urn:uuid:; ToString( ) always gives the lower-case hyphenated form. A Uuid has =, < and a hash, so it can be a Map key or be sorted.

Activities

  1. Print uuids.NilUuid( ).ToString( ) and uuids.MaxUuid( ).ToString( ).
  2. Keep a containers.Map( of uuids.Uuid, ustring ) of names, and look one up by a Uuid you parsed from text.

Money and decimals

A float64 stores binary fractions, and most decimal fractions are not one: 0.1 is stored as a nearby value, so 0.1 + 0.2 is not quite 0.3. For measurements that error is too small to matter. For money it is a cent that does not reconcile. #import "std/money" provides two exact decimal types.

#import "std/money"

'' Binary floating point cannot hold 0.1 exactly.
dim as float64 a = 0.1, b = 0.2
print "float64: 0.1 + 0.2 = 0.3 is "; cbool( a + b = 0.3 )

'' Decimal holds decimal fractions exactly.
var d1 = money.DecimalParse( "0.1" ).Value( )
var d2 = money.DecimalParse( "0.2" ).Value( )
var d3 = money.DecimalParse( "0.3" ).Value( )
print "Decimal: 0.1 + 0.2 = "; ( d1 + d2 ).ToString( ); " is "; _
      ( d1 + d2 = d3 )

'' A Decimal keeps its scale; Round( 2 ) makes it cents.
var price = money.DecimalParse( "59.97" ).Value( )
var rate = money.DecimalParse( "0.0825" ).Value( )
var tax = price * rate
print "tax at 8.25%: "; tax.ToString( ); ", to cents: "; _
      tax.Round( 2 ).ToString( )

'' Currency: always four places, in eight bytes.
var each_ = money.CurrencyParse( "19.99" ).Value( )
print "3 at 19.99 = "; ( each_ * 3 ).ToString( )
print "2 / 3      = "; ( money.CurrencyFromInt( 2 ) / 3 ).ToString( )

'' Parsing is strict: a fifth place is refused, not cut off.
print "1.23456 as Currency: "; _
      money.CurrencyParse( "1.23456" ).HasValue( )

'' Split 100.00 three ways so the parts add back to the total.
dim weights( 0 to 2 ) as int64 = { 1, 1, 1 }
dim parts( ) as money.Decimal
money.DecimalApportion( money.DecimalParse( "100.00" ).Value( ), 2, _
                        weights( ), parts( ) )
print "100.00 in three:";
for i as int32 = 0 to ubound( parts )
    print " "; parts( i ).ToString( );
next
print

Output

float64: 0.1 + 0.2 = 0.3 is false
Decimal: 0.1 + 0.2 = 0.3 is true
tax at 8.25%: 4.947525, to cents: 4.95
3 at 19.99 = 59.9700
2 / 3      = 0.6667
1.23456 as Currency: false
100.00 in three: 33.34 33.33 33.33
CurrencyDecimal
holdsan int64 counting ten-thousandthsa 96-bit whole number and a scale
decimal placesalways 40 to 28, per value
size8 bytes16 bytes

Both are made by named functions: DecimalParse( text ) and CurrencyParse( text ) return an Optional, empty when the text is not a number of the right form, and DecimalFromInt, CurrencyFromInt, DecimalFromDouble and CurrencyFromDouble say what they convert from.

  • Addition, subtraction and multiplication by a whole number are exact.
  • Division rounds half to even: 2 / 3 is 0.6667 in Currency.
  • A Decimal keeps its scale: 59.97 times 0.0825 is 4.947525, and Round( 2 ) makes it cents.
  • Parsing is strict: "1.23456" has too many places for Currency and is refused, not cut off.

Splitting an amount is where hand-written code goes wrong: 100.00 in three is 33.33 three times, which is 99.99. DecimalApportion( total, scale, weights( ), parts( ) ) hands out the leftover cent so the parts add up to the total.

Pitfall

Never keep money in a float64. Each amount is very slightly wrong, and the errors add up across a ledger until a total is off by a cent.

Activities

  1. Add 0.10 to a Decimal ten times and compare the total with 1.00.
  2. Split 10.00 in the ratio 1 : 2 : 3 with DecimalApportion.

Statistics

std/numeric has the common summary statistics. They take their data as a containers.Array of numbers, not a fixed-size array, so import std/containers beside it.

#import "std/numeric"
#import "std/containers"

'' The statistics functions take a containers.Array of numbers.
dim scores as containers.Array( of float64 )
dim raw( ... ) as float64 = { 72, 85, 91, 64, 85, 78, 99, 70 }
for i as int32 = 0 to ubound( raw )
    scores.Push( raw( i ) )
next

print "count: "; numeric.Count( scores )
print "mean:"; numeric.Mean( scores )
print "median:"; numeric.Median( scores )
print "min:"; numeric.MinOf( scores ); ", max:"; numeric.MaxOf( scores )
print "stddev:"; numeric.Round( numeric.StdDev( scores ), 3 )
print "75th percentile:"; numeric.Percentile( scores, 75 )

'' Median sorts a copy: the array keeps its order.
print "first score is still"; scores[ 0 ]

'' RunningStats sees each value once and keeps only a few numbers.
dim acc as numeric.RunningStats
for i as int32 = 0 to ubound( raw )
    acc.Add( raw( i ) )
next
print "running mean:"; acc.Mean( ); ", max:"; acc.MaxValue( )

'' The mean of nothing is NaN, not zero.
dim empty as containers.Array( of float64 )
print "mean of nothing is NaN: "; numeric.IsNaN( numeric.Mean( empty ) )

Output

count: 8
mean: 80.5
median: 81.5
min: 64, max: 99
stddev: 11.674
75th percentile: 86.5
first score is still 72
running mean: 80.5, max: 99
mean of nothing is NaN: true
FunctionGives
Count, Sum, Meanhow many, the total, the average
Median, Percentile( a, p )the middle value, and the value p percent of the way up
MinOf, MaxOf, Rangethe extremes, and the distance between them
Variance, StdDevspread, for a sample (dividing by n - 1)
VariancePopulation, StdDevPopulationspread, for a whole population (dividing by n)
Covariance, Correlationhow two arrays move together

Median and Percentile sort a copy, so asking does not reorder the array. The mean of an empty array is NaN, not zero, because zero is a real mean.

RunningStats sees each value once and keeps only a few numbers, so it can summarise a stream too large to hold: Add, then Count, Mean, Variance, StdDev, MinValue and MaxValue. It cannot give a median, which needs every value.

Sum is more careful than a loop: it keeps a running correction, so adding many small values to a large one does not lose them.

containers.Array is covered in Containers.

Activities

  1. Add the scores to a RunningStats and print its StdDev; compare it with numeric.StdDev.
  2. Print the 25th, 50th and 75th percentiles and check the 50th against Median.

Program and Environment

A program does not run alone. Whoever starts it passes arguments, the process inherits environment variables from its parent, and when it ends it hands back an exit code that says whether it succeeded. These are the channels a command-line tool, a build script or a service uses to talk to the world around it.

'' A program learns about its surroundings from its arguments and its
'' environment, and reports back through its exit code.
print "arguments:";
dim as int32 i = 1
do while len( command( i ) ) > 0
    print " ["; command( i ); "]";
    i += 1
loop
print

setenviron "GREETING=hello"
print "GREETING is "; environ( "GREETING" )

'' Reaching the end of the program exits with code 0: success.

Run as overview red "green blue":

Output

arguments: [red] [green blue]
GREETING is hello

The arguments arrive already split, with the shell's quotes removed: "green blue" is one argument. setenviron and environ set and read a variable, and reaching the end of the program is a successful exit, code 0.

The pages in this section take each channel in turn:

Reading and writing files is covered in its own section.

Activities

  1. Run the program with no arguments, then with five.
  2. Pass an argument that contains a quote character, and see how your shell wants it written.

Command-line arguments

command( n ) returns the n-th argument the program was started with, counting from 1. There is no count to read first: past the last argument, command( n ) is an empty string, so a loop stops at the first empty one.

'' command( n ) is the n-th argument, with its quotes removed.
dim as int32 n = 1
do while len( command( n ) ) > 0
    print n; ": "; command( n )
    n += 1
loop

'' Past the last argument, command( n ) is an empty string.
print "command( 9 ) = ["; command( 9 ); "]"

'' command( ) is every argument, joined into one string.
print "all: "; command( )

'' command( 0 ) is the program itself; its path depends on how it
'' was started, so print only whether there is one.
print "has a program path: "; cbool( len( command( 0 ) ) > 0 )

'' At the top level, the count and the C-style array of arguments.
print "__FB_ARGC__ ="; __FB_ARGC__
print "argv[ 2 ] = "; *__FB_ARGV__[ 2 ]

Run as args --verbose "two words" 3:

Output

 1: --verbose
 2: two words
 3: 3
command( 9 ) = []
all: --verbose two words 3
has a program path: true
__FB_ARGC__ = 4
argv[ 2 ] = two words
ExpressionGives
command( n )argument n, or "" past the last
command( )all the arguments in one string, separated by spaces
command( 0 )the program's own path, as it was started
__FB_ARGC__the number of arguments, counting the program itself
__FB_ARGV__the arguments as a C array, a zstring ptr ptr

command( ) loses the boundaries between arguments: two words cannot be told apart from two arguments two and words. Loop over command( n ) when the boundaries matter.

command( 0 ) is whatever path the program was started by, absolute or relative, so the example prints only that it is there.

__FB_ARGC__ and __FB_ARGV__ are the form a C library that parses its own command line expects. They belong to the top level of the main file; inside a procedure they are not declared. *__FB_ARGV__[ 2 ] reads the third entry, counting the program as entry 0.

Activities

  1. Make the program print usage: args NAME and stop when it is given no arguments.
  2. Accept --verbose anywhere in the arguments and print the others.

Environment variables

Every process has a set of named text values, its environment, copied from the process that started it. Settings such as a search path, a language or a log level often travel this way.

'' environ( name ) reads a variable; an unset one reads as "".
print "unset: ["; environ( "BYEXAMPLE_NOT_SET" ); "]"

'' setenviron sets one for this process and the programs it starts.
setenviron "BYEXAMPLE_MODE=draft"
print "BYEXAMPLE_MODE = "; environ( "BYEXAMPLE_MODE" )

'' Setting it again replaces the value.
setenviron "BYEXAMPLE_MODE=final"
print "BYEXAMPLE_MODE = "; environ( "BYEXAMPLE_MODE" )

'' A default for a variable that may be missing.
function EnvOr( byref name_ as ustring, byref fallback as ustring ) _
         as ustring
    dim as ustring value = environ( name_ )
    return iif( len( value ) > 0, value, fallback )
end function

print "BYEXAMPLE_LEVEL = "; EnvOr( "BYEXAMPLE_LEVEL", "3" )

Output

unset: []
BYEXAMPLE_MODE = draft
BYEXAMPLE_MODE = final
BYEXAMPLE_LEVEL = 3

environ( name ) reads a variable. A variable that is not set reads as an empty string, the same as one set to nothing, so a program that needs a default writes a helper like EnvOr.

setenviron "NAME=value" sets a variable for this process and for the programs it starts afterwards, which inherit a copy (see Running other programs). It never changes the environment of the shell or program that started yours.

environ returns a string. EnvOr stores it in a ustring before the iif, because iif needs both of its results to be the same kind of string: mixing them is TKO0946: iif cannot choose between a narrow string and a ustring.

The example sets its own variables rather than reading PATH or HOME, because those differ from machine to machine.

Activities

  1. Set BYEXAMPLE_LEVEL in your shell before running the program, and watch EnvOr return it.
  2. Print environ( "PATH" ) and count its entries.

Exit codes

When a program ends it returns a number to whatever started it. Zero means success to every shell and build tool; anything else is a failure whose number the caller can test. A program that can fail should say so through its exit code, not only through what it prints.

'' The exit code is what a script or a build tool checks.
function CountErrors( ) as int32
    return 4
end function

var errors = CountErrors( )
print errors; " errors found"

'' end n stops the program here with exit status n.
if errors > 0 then end errors

print "not printed: the program has already ended"

Output, and exit code 4:

 4 errors found

end n stops the program at once with exit status n. Reaching the end of the program, or a bare end, exits with 0. The caller reads the code as $? in a Unix shell, %ERRORLEVEL% in cmd.exe, or as the result of exec or shell in another tiko program.

On Linux

An exit code is one byte: only 0 to 255 reach the caller, so end 300 arrives as 44. Keep exit codes in that range on every platform.

end does not return to the caller of the current procedure; it ends the whole program, so a procedure deep in the program can stop it. Use it for the program's final answer, and let errors inside the program travel by the means in Error Handling.

Activities

  1. Change CountErrors to return 0 and check the exit code in your shell.
  2. Exit with 1 when the program is given no arguments.

The os module

command and environ are built in and short, but each loses something. An empty argument looks like the end of the list, and a variable that is not set reads the same as one set to nothing. std/os keeps both apart.

#import "std/os"
#import "std/containers"
#import "std/outcome"

'' Every argument, including an empty one.
dim args as containers.Array( of ustring ) = os.Args( )
print "arguments:"; args.Count( ) - 1
for i as int32 = 1 to args.Count( ) - 1
    print "  ["; args[ i ]; "]"
next

'' Not set, and set to nothing, are different answers.
os.SetEnv( "BYEXAMPLE_EMPTY", "" )
print "BYEXAMPLE_EMPTY set? "; os.GetEnv( "BYEXAMPLE_EMPTY" ).HasValue( )
print "BYEXAMPLE_NONE set?  "; os.GetEnv( "BYEXAMPLE_NONE" ).HasValue( )

'' A default for a variable that is not set.
dim level as ustring = os.GetEnv( "BYEXAMPLE_LEVEL" ).ValueOr( "info" )
print "level: "; level

'' Variables inside a string.
os.SetEnv( "BYEXAMPLE_NAME", "report" )
print os.ExpandEnv( "${BYEXAMPLE_NAME}-final.txt" )
os.UnsetEnv( "BYEXAMPLE_NAME" )
print "after unset: ["; os.ExpandEnv( "$BYEXAMPLE_NAME" ); "]"

'' The system it runs on.
dim as boolean known = cbool( os.OS_NAME = "windows" orelse _
                              os.OS_NAME = "linux" orelse os.OS_NAME = "macos" )
print "os known? "; known; ", arch: "; os.ARCH_NAME

Run as os first "" "third one":

Output

arguments: 3
  [first]
  []
  [third one]
BYEXAMPLE_EMPTY set? true
BYEXAMPLE_NONE set?  false
level: info
report-final.txt
after unset: []
os known? true, arch: x86_64
CallGives
os.Args( )every argument in an Array( of ustring ), the program first
os.GetEnv( name )an Optional( of ustring ), empty when not set
os.SetEnv( name, value )sets it; a Result that fails on a name with =
os.UnsetEnv( name )removes it
os.Environment( )every variable, sorted, in an OrderedMap
os.ExpandEnv( text )$NAME and ${NAME} replaced; %NAME% too on Windows

GetEnv( ... ).ValueOr( "info" ) is the usual way to give a setting a default. A variable that is not set expands to nothing, as in a shell.

The module also knows about the machine: os.OS_NAME and os.ARCH_NAME, OsVersion( ), Hostname( ), UserName( ), ProcessId( ), PageSize( ), TotalMemory( ), IsTerminal( fd ), and CpuCount( ) — the processors this program may use, which is fewer than the machine has inside a container. The example prints only facts that are the same everywhere.

Threads

Set the environment before starting threads. On Linux, changing a variable while another thread reads the environment is not safe.

Activities

  1. Set BYEXAMPLE_LEVEL in your shell and run the program again.
  2. Print os.CpuCount( ) and compare it with what your system monitor says.

Ctrl-C

Pressing Ctrl-C normally ends a console program wherever it happens to be. A program that is halfway through writing a file would rather finish the record it is on. os.CatchInterrupt( ) turns Ctrl-C into a flag the program checks.

#import "std/os"

'' Ctrl-C now sets a flag; the program decides when to stop.
os.CatchInterrupt( )

dim as int32 saved = 0
do while saved < 10
    if os.Interrupted( ) then exit do
    sleep 10                     '' save one record
    saved += 1
loop

print "saved"; saved; " records"
if os.Interrupted( ) then
    print "interrupted by kind"; os.LastInterrupt( )
    os.ExitWith( 130 )
end if
print "no interruption"

Output, when nobody presses a key:

saved 10 records
no interruption

Press Ctrl-C while it runs and the loop stops at the next check, prints how many records it saved and interrupted by kind 1, and exits with 130 — the code a shell shows for a program stopped by Ctrl-C.

CallDoes
os.CatchInterrupt( )start catching Ctrl-C, Ctrl-Break and termination requests
os.Interrupted( )has one arrived? never waits
os.WaitInterrupt( ms )wait for one, at most ms milliseconds; -1 for ever
os.LastInterrupt( )INT_CTRL_C (1), INT_BREAK (2), INT_TERMINATE (3), INT_HANGUP (4)
os.ResetInterrupt( )clear the flag
os.ReleaseInterrupt( )let Ctrl-C end the program again

A second Ctrl-C ends the program at once, with code 130, so a program that is stuck in a long step can still be stopped. os.CatchInterrupt( false ) keeps counting instead.

INT_TERMINATE is SIGTERM on Linux — what a service manager sends to stop a program — and on Windows the console window closing, the user logging off or the machine shutting down. Windows then allows about five seconds before it ends the program regardless.

With goroutines, tasks.InterruptContext( ) is a context that Ctrl-C cancels, so every goroutine watching it stops too.

os.ExitWith( code ) is end code as a procedure call. os.Abort( ) ends the program at once, without running destructors or flushing files.

Activities

  1. Run the program and press Ctrl-C before it finishes.
  2. Pass false to CatchInterrupt and press Ctrl-C three times. What does os.InterruptCount( ) say?

Running other programs

A program can start another and wait for it. The example uses itself as the other program: started with the argument child, it does the child's work and exits with code 7. That way it needs no program that might be missing, and it runs the same on every platform.

'' This program is its own child: given "child", it does the child's
'' part and stops, so the example needs no other program.
if command( 1 ) = "child" then
    print "  child: hello from the child"
    end 7
end if

'' exec runs a program directly and returns its exit code.
print "parent: running the child with exec"
var code = exec( command( 0 ), "child" )
print "parent: exec returned"; code

'' shell runs a command line through the system's shell.
print "parent: shell( ""exit 3"" ) returned"; shell( "exit 3" )

'' open pipe reads what a command prints, line by line.
dim as ustring cmdline = """" & command( 0 ) & """ child"
dim as int32 f = freefile( )
open pipe cmdline for input as #f
dim as ustring got
line input #f, got
close #f
print "parent: read ["; got; "]"

Output

parent: running the child with exec
  child: hello from the child
parent: exec returned 7
parent: shell( "exit 3" ) returned 3
parent: read [  child: hello from the child]
StatementRunsReturns
exec( program, args )a program, directlyits exit code
shell( "command" )a command line, through the system shellits exit code
open pipe "command" for input as #fa command line, whose output the program readsa file number to read from

exec starts the program with no shell in between, so nothing in the arguments is interpreted: the safe choice when an argument came from a user. The child writes to the same output as the parent, which is why its line appears between the parent's.

shell hands the command line to cmd.exe on Windows and /bin/sh on Linux, so the line is in that shell's language. exit 3 happens to mean the same in both.

open pipe ... for input also goes through the shell, and reads the command's output a line at a time, as if it were a file. The quotes around command( 0 ) keep a path with spaces in one piece. open pipe ... for output sends lines to a command's input instead.

The child inherits a copy of the parent's environment, including anything set with setenviron before it was started.

Activities

  1. Make the child read an environment variable that the parent sets just before exec, and print it.
  2. Have the child print three lines and read them all through the pipe with a do until eof( f ) loop.

Files

Files are the std/fs module. It opens and closes files, reads and writes them whole or a line at a time, takes paths apart, walks directories and says where a program should keep its own files. Every operation that can fail returns a Result: a missing file is a value you look at, not a crash.

#import "std/fs"

print "Files"
'' A scratch directory of our own, under a fresh name.
dim as ustring dir_ = fs.CreateTempDir( "tbe" ).Value( )
dim as ustring p = fs.PathJoin( dir_, "greeting.txt" )

fs.WriteAllText( p, "hello, file" )
print fs.ReadAllText( p ).Value( )

var info = fs.Stat( p )
print "size:"; info.Value( ).Size( ); ", file: "; info.Value( ).IsFile( )

'' A failure is a value to look at, not a crash.
var missing = fs.ReadAllText( fs.PathJoin( dir_, "nope.txt" ) )
print "read a missing file: "; missing.IsOk( )
print "not found: "; cbool( missing.Failure( ).Code( ) = fs.FS_NOT_FOUND )

print "removed:"; fs.RemoveDirAll( dir_ ).Value( )

Output

Files
hello, file
size:11, file: true
read a missing file: false
not found: true
removed: 2

CreateTempDir makes a directory under a fresh, unpredictable name, which the examples in this section use so that they never touch anything else. PathJoin builds a path with the platform's separator. WriteAllText and ReadAllText write and read a whole file as text, and Stat gathers the facts about a file in one call.

ReadAllText of a file that is not there returns a failure instead of a value. IsOk( ) says which you have, Value( ) is the value, and Failure( ) is an FsError whose Code( ) says what went wrong. Value( ) on a failure is a runtime error, so test first when failure is possible. RemoveDirAll deletes a whole tree and returns how many objects it removed.

The pages in this section:

Activities

  1. Write a second file into the scratch directory and check that the count RemoveDirAll prints goes up by one.
  2. Print missing.Failure( ).Message( ) to see the error as text.

Paths

A path is a ustring, and the Path functions take it apart and build it without touching the disk. They accept either separator, / or \, on Windows; on Linux and macOS only / separates.

#import "std/fs"

'' Taking a path apart touches no disk.
dim as ustring p = "projects/tiko/src/main.tiko"
print fs.PathFileName( p )
print fs.PathStem( p )
print fs.PathExtension( p )
print fs.PathFileName( fs.PathParent( p ) )
print fs.PathChangeExtension( "notes.txt", ".md" )
print fs.PathFilePrefix( "backup.tar.gz" )
print fs.PathAddExtension( "backup.tar", "gz" )
print "["; fs.PathExtension( ".gitignore" ); "]"

'' The pieces, one at a time.
for each c in fs.PathComponents( "projects/./tiko/../docs" )
    print "<"; c.Text( ); ">";
next
print

'' Joining uses the platform's separator, so compare, don't print.
dim as ustring j = fs.PathJoin( "projects", "tiko", "main.tiko" )
print cbool( fs.PathEquals( j, "projects/tiko/main.tiko" ) )
print cbool( fs.PathNormalize( "projects/./tiko/../docs" ) = _
             fs.PathJoin( "projects", "docs" ) )

Output

main.tiko
main
.tiko
src
notes.md
backup
backup.tar.gz
[]
<projects><tiko><..><docs>
true
true
FunctionGives
PathFileName( p )the last part
PathParent( p )everything before the last part
PathStem( p )the file name without its last extension
PathExtension( p )the last extension, with its dot
PathFilePrefix( p )the file name before its first dot
PathChangeExtension( p, ext )p with a different extension
PathAddExtension( p, ext )p with one more extension
PathComponents( p )the parts, one at a time, for for each
PathJoin( a, b, ... )the parts joined with the platform's separator
PathNormalize( p ). and .. resolved, separators made uniform
PathEquals( a, b )the same path, by the platform's rules

A name that starts with a dot, .gitignore, has no extension: the dot is part of the name. PathComponents drops a . but keeps a .., because what .. means depends on links on the disk; PathNormalize removes both, without looking.

PathJoin writes \ on Windows and / on Linux and macOS, which is why the example compares joined paths rather than printing them. PathEquals compares the way the platform does: on Windows it ignores case and treats both separators alike.

Pitfall

PathJoin( "uploads", name ) with an absolute name throws uploads away, and a name containing .. climbs out of it. Before joining a name that came from a user, check it with PathIsLocal( name ), which is false for both, and for device names such as NUL on Windows.

Activities

  1. Print fs.PathParent( "main.tiko" ). What is the parent of a bare name?
  2. Use PathComponents to count how many directories deep a path is.

Reading and writing files

For a file small enough to hold in memory, one call reads or writes all of it.

#import "std/fs"
#import "std/containers"

print "Whole files"
dim as ustring dir_ = fs.CreateTempDir( "tbe" ).Value( )
dim as ustring p = fs.PathJoin( dir_, "todo.txt" )

fs.WriteAllText( p, "buy milk" & chr( 10 ) )
fs.AppendAllText( p, "call home" & chr( 10 ) )

dim lines as containers.Array( of ustring )
print fs.ReadAllLines( p, lines ).Value( ); " lines"
for each ln in lines
    print "- "; ln
next

'' Bytes, not text: exactly what is on disk.
dim bytes as containers.Array( of ubyte )
fs.ReadAllBytes( p, bytes )
print bytes.Count( ); " bytes"

'' Replace a file so that a reader sees the old one or the new one.
fs.WriteAtomic( p, "all done" & chr( 10 ) )
print fs.ReadAllText( p ).Value( );

fs.RemoveDirAll( dir_ )

Output

Whole files
 2 lines
- buy milk
- call home
 19 bytes
all done
FunctionDoes
WriteAllText( p, s )creates or replaces p, with s as UTF-8
AppendAllText( p, s )adds s to the end, creating p if needed
ReadAllText( p )the whole file as one ustring
ReadAllLines( p, lines )the lines, into an Array( of ustring ); returns the count
ReadAllBytes( p, bytes )the bytes, into an Array( of ubyte )
WriteAllBytes( p, bytes )creates or replaces p with exactly those bytes
WriteAtomic( p, s )replaces p so that no reader sees half of it

ReadAllLines removes each line's ending, whether it is a line feed or a carriage return and line feed, and a final line feed does not make an empty last line. Text is read as UTF-8 unless the file starts with a byte-order mark that says otherwise.

WriteAtomic writes a temporary file beside p and then puts it in p's place. A program reading at the same moment sees the old contents or the new, and a crash part-way through leaves the old file as it was. Use it to save a configuration or a document.

Activities

  1. Read a file that is not there with ReadAllLines and print the failure's Message( ).
  2. Write the text with a carriage return and line feed after each line and check that ReadAllLines gives the same two lines.

Line by line

A File is an open file. It is opened in place, by a method, and it closes itself when it goes out of scope. A TextWriter writes lines to it, and ReadLines reads a file back one line at a time.

#import "std/fs"

print "Line by line"
dim as ustring dir_ = fs.CreateTempDir( "tbe" ).Value( )
dim as ustring p = fs.PathJoin( dir_, "log.txt" )

scope
    dim f as fs.File
    f.Create( p )                     '' creates, or empties, the file
    dim w as fs.TextWriter = fs.TextWriter( f )
    w.WriteLine( "started" )
    w.WriteLine( "working" )
end scope                             '' the File closes itself here

scope
    dim f as fs.File
    f.OpenAppend( p )                 '' keeps what is there
    dim w as fs.TextWriter = fs.TextWriter( f )
    w.WriteLine( "finished" )
end scope

dim as int32 n = 0
for each ln in fs.ReadLines( p )
    n += 1
    print n; " "; ln.Value( )
next

fs.RemoveDirAll( dir_ )

Output

Line by line
 1 started
 2 working
 3 finished

Create makes the file, or empties one that is there. OpenAppend keeps what is there and writes at the end. Each scope is where its File ends: the TextWriter, declared after the File, is destroyed first, then the File closes its handle.

WriteLine ends a line the way the platform does: carriage return and line feed on Windows, line feed on Linux. Set the writer's NewLine to chr( 10 ) to write the same bytes everywhere.

for each ln in fs.ReadLines( p ) opens the file, yields one Result( of ustring, FsError ) per line and closes the file at the end, or as soon as the loop is left with exit for. Only one line is in memory at a time, so it reads a file of any size. If the file cannot be opened, the failure is the first and only element.

A File cannot be copied. Pass it to a procedure byref.

Activities

  1. Stop the loop with exit for after the second line.
  2. Read a file that does not exist and print what the one element says.

Text encodings

Text in a file is bytes, and an encoding says which bytes stand for which characters. std/fs reads and writes UTF-8 unless told otherwise. Give any text call an encoding as its last argument to read or write UTF-16, a Windows code page, or the raw bytes.

#import "std/fs"
#import "std/containers"

print "Text encodings"

sub ShowBytes( byref label as string, byref p as const ustring )
    dim as containers.Array( of ubyte ) b
    fs.ReadAllBytes( p, b )
    dim as string h
    for i as long = 0 to b.Count( ) - 1
        h &= " " & hex( b[i], 2 )
    next
    print label; h
end sub

dim as ustring dir_ = fs.CreateTempDir( "tbe" ).Value( )
dim as ustring p = fs.PathJoin( dir_, "note.txt" )
'' "cafe", the last letter an e with an acute accent
dim as ustring text = "caf" & wchr( &hE9 )

'' The same four characters, written three ways.
fs.WriteAllText( p, text )                      '' UTF-8, the default
ShowBytes( "UTF-8:       ", p )
fs.WriteAllText( p, text, fs.Utf16LE( ) )       '' BOM, two bytes a character
ShowBytes( "UTF-16LE:    ", p )
fs.WriteAllText( p, text, fs.CodePage( 1252 ) ) '' Windows Western, one byte
ShowBytes( "Windows-1252:", p )

'' Read it back in the encoding it was written in...
dim as ustring t = fs.ReadAllText( p, fs.CodePage( 1252 ) ).Value( )
print "as 1252, last character: U+"; hex( t[3], 4 )
'' ...because the default, UTF-8, cannot make sense of the byte E9.
t = fs.ReadAllText( p ).Value( )
print "as UTF-8, last character: U+"; hex( t[3], 4 )

'' Not sure? A BOM, else valid UTF-8, else the fallback you name.
dim as fs.TextEncoding found
fs.ReadAllText( p, found, fs.TextDetect( fs.CodePage( 1252 ) ) )
print "detected: "; found.Name( )

'' A character the encoding has no byte for is refused; nothing is written...
var r = fs.WriteAllText( p, "snow" & wchr( &h2603 ), fs.CodePage( 1252 ) )
print "a snowman in 1252: "; iif( r.IsOk( ), "written", "refused" )
'' ...unless you allow a '?' in its place; then the result says it was lossy.
r = fs.WriteAllText( p, "snow" & wchr( &h2603 ), fs.CodePage( 1252 ), _
                     fs.TEXT_REPLACE )
print "with TEXT_REPLACE: exact = "; r.Value( )
ShowBytes( "bytes:       ", p )

fs.RemoveDirAll( dir_ )

Output

Text encodings
UTF-8:        63 61 66 C3 A9
UTF-16LE:     FF FE 63 00 61 00 66 00 E9 00
Windows-1252: 63 61 66 E9
as 1252, last character: U+00E9
as UTF-8, last character: U+FFFD
detected: windows-1252
a snowman in 1252: refused
with TEXT_REPLACE: exact = false
bytes:        73 6E 6F 77 3F

An encoding is a TextEncoding, made by one of these:

CallWrites
Utf8( )UTF-8, no BOM: the default. Utf8( true ) adds the BOM EF BB BF
Utf16LE( ), Utf16BE( )UTF-16 with a BOM (FF FE or FE FF); pass false for none
CodePage( n )a Windows code page: 1252 is Western Europe, 1251 Cyrillic, 932 Japanese
Ansi( )the code page Windows is set to on this machine; Windows only
BinaryMode( )byte n is character n: any file reads, and writes back unchanged

The same overloads exist on ReadAllText, ReadAllLines, WriteAllText, AppendAllText, WriteAtomic, ReadLines, TextReader and TextWriter.

Reading believes the encoding you give: a BOM that matches it is skipped, and anything else is text. Bytes that make no sense in it become U+FFFD, the replacement character, as the UTF-8 read of E9 shows above; pass TEXT_STRICT to fail instead. When you do not know the encoding, the TextDetect overload takes a BOM if there is one, else UTF-8 if the bytes are valid UTF-8, else the fallback you name, and says in found which it used.

Writing is strict by default. A character the encoding has no byte for fails with FS_INVALID_DATA, and the file is not touched. With TEXT_REPLACE it becomes ?, the write succeeds, and the result's value is false: written, but not exactly.

A file that starts with a UTF-32 byte-order mark is refused by every read that chooses the encoding itself, with FS_UNSUPPORTED, rather than misread as UTF-16.

Activities

  1. Write text with Utf16BE( ) and with Utf8( true ). Which bytes are the BOM in each?
  2. Read the Windows-1252 file with Utf16LE( ). How many characters come back, and why?
  3. Change the TextDetect fallback to BinaryMode( ). Which name is detected now?

Opening a File

Each way of opening a File returns a Result( of boolean, FsError ), so an open that fails says why.

#import "std/fs"

print "Opening a File"
dim as ustring dir_ = fs.CreateTempDir( "tbe" ).Value( )
dim as ustring p = fs.PathJoin( dir_, "once.txt" )

dim f as fs.File
var r = f.OpenRead( p )
print "open a missing file: "; r.IsOk( )
print "not found: "; cbool( r.Failure( ).Code( ) = fs.FS_NOT_FOUND )

'' CreateNew refuses a file that is already there.
print "first CreateNew: "; f.CreateNew( p ).IsOk( )
f.Close( )
var again = f.CreateNew( p )
print "second CreateNew: "; again.IsOk( )
print "already exists: "; _
      cbool( again.Failure( ).Code( ) = fs.FS_ALREADY_EXISTS )

'' Options say exactly what the program needs.
dim o as fs.OpenOptions
o.Read = true
o.Write = true
print "read and write: "; f.Open( p, o ).IsOk( )
f.Close( )

'' A File refuses what it was not opened for.
f.OpenRead( p )
print "write to a read-only File: "; f.WriteUtf8( "x" ).IsOk( )
f.Close( )
print "still open: "; f.IsOpen( )

fs.RemoveDirAll( dir_ )

Output

Opening a File
open a missing file: false
not found: true
first CreateNew: true
second CreateNew: false
already exists: true
read and write: true
write to a read-only File: false
still open: false
MethodOpens forAn existing file isA missing file
OpenRead( p )readingreadFS_NOT_FOUND
Create( p )writingemptiedcreated
CreateNew( p )writingrefused, FS_ALREADY_EXISTScreated
OpenAppend( p )writing at the endkeptcreated
Open( p, options )what the options sayas the options sayas the options say

An OpenOptions names what the program needs: Read, Write, Append, Truncate, Create and CreateNew. A combination that makes no sense, such as Append with Truncate, is refused before anything is opened.

An operation the File was not opened for fails: writing to a File opened with OpenRead returns a failure. Close( ) closes the file and returns a Result of its own; after it IsOpen( ) is false, and the same File can be opened again.

Other programs may read, write, rename and delete a file you have open. A program you start does not get your open files unless Inheritable is set.

Activities

  1. Open the file twice with CreateNew in a loop and print each result.
  2. Set Append and Truncate together and print the failure's Message( ).

Records at a position

A file of fixed-size records can be read and changed by record number, without touching the records around the one you want. A RecordFile( of T ) is such a file, holding records of the type T.

#import "std/fs"

print "Records at a position"
type Score
    player as int32
    points as int32
end type

dim as ustring dir_ = fs.CreateTempDir( "tbe" ).Value( )
dim as fs.RecordFile( of Score ) scores
'' Read and write, and created if it is missing.
scores.Open( fs.PathJoin( dir_, "scores.dat" ) )

dim s as Score
for n as int32 = 1 to 4
    s.player = n
    s.points = n * 100
    scores.Append( s )                  '' returns the new record's number
next
print "records:"; scores.Count( ).Value( ); _
      ", each"; scores.RecordLength( ); " bytes"

'' Straight to record 3, change it, write it back in place.
scores.GetRecord( 3, s )
s.points += 5
scores.PutRecord( 3, s )

for each r in scores                    '' each a Result, in record order
    print "player"; r.Value( ).player; ":"; r.Value( ).points
next

'' There is no record 9: the value is false, and s is left as it was.
print "record 9 there? "; scores.GetRecord( 9, s ).Value( )
scores.Close( )
fs.RemoveDirAll( dir_ )

Output

Records at a position
records: 4, each 8 bytes
player 1: 100
player 2: 200
player 3: 305
player 4: 400
record 9 there? false

Open( path ) opens the file for reading and writing, and creates it if it is not there. Records are numbered from 1. Append adds a record after the last one and returns its number; PutRecord( n, s ) writes record n and GetRecord( n, s ) reads it. Count( ) is the number of whole records, and for each walks them in order, one Result per record.

Reading a record that is not all there, past the end or a short last record, is not an error: the value is false and the record is left as it was. Writing past the end grows the file, and the records in the gap read as zeros.

The record must hold only fixed-size fields: numbers, zstring * n, and types made of those. A ustring is a pointer to its text, and a pointer written to a file means nothing when it is read back. The bytes are the record's bytes in memory, so add field = 1 to the type for a layout with no padding.

Underneath, these are ReadAt and WriteAt, which read and write bytes at a byte position in any File without moving its position. Use them directly for a file that mixes records with other data, such as a header, and ReadRecord( of T ), WriteRecord( of T ) and RecordCount( of T ) for records in a File you opened yourself.

Activities

  1. Add a bonus as int32 field to Score. Which numbers in the output change?
  2. Write record 6 with PutRecord, then print Count( ) and record 5.

Binary formats

A binary file format puts each number at a known place, in a known width and byte order. std/codec reads and writes them in a containers.Array( of ubyte ), which fs.WriteAllBytes and fs.ReadAllBytes move to and from a file.

#import "std/codec"
#import "std/containers"
#import "std/fs"

'' A record in a fixed binary layout: id, temperature, then a name.
dim rec as containers.Array( of ubyte )
codec.AppendU32LE( rec, 1042 )               '' 4 bytes, little-endian
codec.AppendF32LE( rec, 21.5 )               '' 4 bytes, IEEE 754
dim tag as ustring = "lab"
codec.AppendUVarint( rec, len( tag ) )       '' the length, then the bytes
for i as int32 = 1 to len( tag )
    rec.Push( asc( tag, i ) )
next
print "record: "; codec.HexEncode( rec, true )

dim scratch as ustring = fs.CreateTempDir( ).Value( )
dim path as ustring = fs.PathJoin( scratch, "record.dat" )
fs.WriteAllBytes( path, rec )

'' Read the file back and pick the fields out again.
dim got as containers.Array( of ubyte )
fs.ReadAllBytes( path, got )
dim at as int32 = 8
dim n as uint64 = codec.ReadUVarint( got, at ).Value( )
dim label as ustring
for i as int32 = 0 to n - 1
    label &= chr( got[ at + i ] )
next
print "id: "; codec.GetU32LE( got, 0 ); ", temp:"; codec.GetF32LE( got, 4 ); _
    ", label: "; label
fs.RemoveDirAll( scratch )

Output

record: 120400000000AC41036C6162
id: 1042, temp: 21.5, label: lab

Every name says what it does: Get, Put or Append, then the type — U16, U32, U64, I16, I32, I64, F32, F64 — then LE for little-endian (least significant byte first) or BE for big-endian. Offsets count bytes from 0. Append writes at the end; Put writes at an offset and grows the array if it has to; Get past the end stops the program, as indexing an array does.

AppendUVarint writes a whole number in as few bytes as it needs, seven bits per byte — here the label's length, 3, takes one byte. ReadUVarint( buf, at ) reads one and moves at past it, so the next field starts where at now points. AppendVarint and ReadVarint do the same for numbers that can be negative.

Activities

  1. Write the id big-endian instead, and see which bytes of the record change.
  2. Make the label 200 characters long. How many bytes does its length take now?

Streams

A Stream is something bytes can be read from, written to, or both. A File is one and a MemoryStream is another, and whatever works with a stream works with either.

#import "std/fs"

print "Streams"
'' Text built in memory: a MemoryStream is a Stream, like a File.
dim m as fs.MemoryStream
scope
    dim w as fs.TextWriter = fs.TextWriter( m )
    w.NewLine = chr( 10 )
    w.WriteLine( "alpha" )
    w.WriteLine( "beta" )
end scope
print m.Length( ); " bytes in memory"

'' Copy it to a file in one call. Copy starts at the position.
dim as ustring dir_ = fs.CreateTempDir( "tbe" ).Value( )
dim as ustring p = fs.PathJoin( dir_, "out.txt" )
m.Seek( 0 )
dim f as fs.File
f.Create( p )
print fs.Copy( m, f ).Value( ); " bytes copied"
f.Close( )

'' A TextReader reads lines from any stream.
f.OpenRead( p )
dim r as fs.TextReader = fs.TextReader( f )
dim as ustring ln
do while r.ReadLine( ln ).ValueOr( false )
    print "["; ln; "]"
loop
f.Close( )
fs.RemoveDirAll( dir_ )

Output

Streams
 11 bytes in memory
 11 bytes copied
[alpha]
[beta]

The TextWriter writes into memory here, and fs.Copy( source, destination ) moves the bytes into the file. Copy starts at the source's position, which is the end after writing, so the example seeks back to 0 first. A TextReader reads lines from any stream; ReadLine returns false when there are none left.

TypeIs
Filea file on disk; each Read and Write is one call to the system
MemoryStreama growable buffer of bytes in memory
BufferedReader, BufferedWritera buffer over another stream
TextReader, TextWriterlines of text over another stream

A reader or writer keeps a pointer to the stream it was given, so declare the stream first: it must outlive the wrapper.

Your own stream

A type that extends fs.Stream and overrides some of its operations is a stream too:

#import "std/fs"
#import "std/outcome"
using outcome

'' A stream that keeps nothing and counts the lines written to it.
type LineCounter extends fs.Stream
    as int64 lines = 0
    declare const function CanWrite( ) as boolean override
    declare function Write( byval buf as const ubyte ptr, _
                            byval n as longint ) as fs.FsResultI64 override
end type

const function LineCounter.CanWrite( ) as boolean
    return true
end function

function LineCounter.Write( byval buf as const ubyte ptr, _
                            byval n as longint ) as fs.FsResultI64
    for i as longint = 0 to n - 1
        if buf[i] = 10 then this.lines += 1
    next
    return Ok( of longint, fs.FsError )( n )
end function

print "Your own stream"
dim c as LineCounter
dim w as fs.TextWriter = fs.TextWriter( c )
w.NewLine = chr( 10 )
for i as int32 = 1 to 5
    w.WriteLine( "line " & i )
next
print c.lines; " lines"
print "can read: "; c.CanRead( )

Output

Your own stream
 5 lines
can read: false

Read, Write, Seek and Flush are virtual, and each returns a Result. LineCounter overrides Write, and CanWrite to say so. Whatever it does not override fails with FS_UNSUPPORTED, and CanRead( ) is false. The TextWriter neither knows nor cares what it is writing to.

A network connection is a stream as well: std/net's TcpStream takes the same WriteUtf8, ReadExact and TextWriter.

Activities

  1. Wrap the File in a BufferedWriter and the TextWriter around that.
  2. Make LineCounter also count the bytes, and print both.

File information

Stat( p ) gathers everything about a file in one call: its kind, size, times and flags.

#import "std/fs"

print "File information"
dim as ustring dir_ = fs.CreateTempDir( "tbe" ).Value( )
dim as ustring p = fs.PathJoin( dir_, "data.bin" )
fs.WriteAllText( p, "12345" )

var r = fs.Stat( p )
if r.IsOk( ) then
    var info = r.Value( )
    print "size:"; info.Size( )
    print "file: "; info.IsFile( ); ", directory: "; info.IsDirectory( )
    print "read-only: "; info.IsReadOnly( )
end if

'' Every failure has a code, so "absent" and "not allowed" differ.
var gone = fs.Stat( fs.PathJoin( dir_, "gone" ) )
print "missing: "; cbool( gone.Failure( ).Code( ) = fs.FS_NOT_FOUND )

'' Yes-or-no questions fold every failure into false.
print fs.Exists( p ); " "; fs.IsDir( dir_ ); " "; fs.IsFile( dir_ )

fs.RemoveDirAll( dir_ )

Output

File information
size:5
file: true, directory: false
read-only: false
missing: true
true true false

Stat returns a Result holding a FileInfo. Size( ) is in bytes; IsFile( ), IsDirectory( ) and IsSymlink( ) give the kind; IsReadOnly( ) and IsHidden( ) the flags; and Modified( ), Created( ) and Accessed( ) the times. A path that is not there is a failure with code FS_NOT_FOUND, which is a different answer from FS_ACCESS_DENIED.

Exists, IsFile and IsDir answer yes or no and fold every failure into false. They read well in an if, but they cannot tell "absent" from "not allowed to look". Use Stat when the difference matters, and use it once rather than asking Exists and then the size: the file can change between two calls.

Stat follows a symbolic link to what it points at; LStat describes the link itself.

Activities

  1. Make the file read-only with fs.SetReadOnly( p, true ) and Stat it again.
  2. Stat the scratch directory and print its IsDirectory( ).

Directories

#import "std/fs"

print "Directories"
dim as ustring dir_ = fs.CreateTempDir( "tbe" ).Value( )
fs.CreateDirAll( fs.PathJoin( dir_, "src", "net" ) )   '' every level
fs.WriteAllText( fs.PathJoin( dir_, "README" ), "" )
fs.WriteAllText( fs.PathJoin( dir_, "src", "main.tiko" ), "" )
fs.WriteAllText( fs.PathJoin( dir_, "src", "net", "http.tiko" ), "" )

'' The whole tree, in name order, indented by depth.
dim o as fs.WalkOptions
o.SortByName = true
for each e in fs.WalkDir( dir_, o )
    if e.IsOk( ) then
        print space( e.Value( ).Depth( ) * 2 ); e.Value( ).Name( )
    end if
next

'' rm -rf: the count includes the directory itself.
print "removed:"; fs.RemoveDirAll( dir_ ).Value( )
print "still there: "; fs.Exists( dir_ )

Output

Directories
README
src
  main.tiko
  net
    http.tiko
removed: 6
still there: false

CreateDirAll creates every missing level of a path, and is not an error when the directory is already there; CreateDir creates one level and fails with FS_ALREADY_EXISTS if it exists.

WalkDir( p, options ) walks a whole tree. Each element is a Result, because a directory part-way down may not be readable; the walk reports it and carries on. Depth( ) counts from 0 for the root's own entries, and Name( ) is the last part of the path. The order is whatever the system gives unless SortByName is set. ReadDir( p ) lists one directory the same way.

RemoveDirAll( p ) deletes a tree and returns how many objects it removed, the directory itself included. A symbolic link inside the tree is removed as a link; what it points at is not touched. A p that is itself a link is refused with FS_NOT_A_DIRECTORY, and nothing is deleted.

CallDoes
CreateDir( p ), CreateDirAll( p )create one level, or every level
ReadDir( p )the entries of one directory, for for each
WalkDir( p [, options ] )every entry in the tree, for for each
Glob( pattern, results )the paths that match *, ? and **
CopyDir( src, dst )a copy of the tree; returns the count
RemoveDir( p )delete one empty directory
RemoveDirAll( p )delete the tree; returns the count

Activities

  1. Set o.ContentsFirst = true and see each directory listed after what is in it.
  2. Set o.MaxDepth = 1 to list only the entries directly inside dir_.

Where files live

A program should keep its settings, data and caches where the platform expects them, not beside its executable or in the current directory.

#import "std/fs"

print "Where files live"
'' Each call says where a directory should be. Nothing is created.
var cfg = fs.ConfigDir( )
if cfg.IsOk( ) then
    dim as ustring mine = fs.PathJoin( cfg.Value( ), "myapp" )
    print "config is absolute: "; fs.PathIsAbsolute( mine )
end if
print "data: "; fs.DataDir( ).IsOk( ); _
      ", cache: "; fs.CacheDir( ).IsOk( ); _
      ", state: "; fs.StateDir( ).IsOk( )

'' The program's own file, however it was started.
print "this program: "; fs.PathStem( fs.ExecutablePath( ).ValueOr( "" ) )

'' A leading ~ is the home directory; anything else is left alone.
dim as ustring home = fs.HomeDir( ).ValueOr( "" )
print cbool( fs.ExpandHome( "~/notes" ).ValueOr( "" ) = _
             fs.PathJoin( home, "notes" ) )
print fs.ExpandHome( "a/~" ).ValueOr( "" )

Output

Where files live
config is absolute: true
data: true, cache: true, state: true
this program: userdirs
true
a/~
FunctionWindowsLinuxmacOS
HomeDir( )the user's profile folder$HOME, else the user's account entry$HOME, else the user's account entry
ConfigDir( )AppData\Roaming$XDG_CONFIG_HOME, else ~/.config$XDG_CONFIG_HOME, else ~/Library/Application Support
DataDir( )AppData\Local$XDG_DATA_HOME, else ~/.local/share$XDG_DATA_HOME, else ~/Library/Application Support
CacheDir( )AppData\Local$XDG_CACHE_HOME, else ~/.cache$XDG_CACHE_HOME, else ~/Library/Caches
StateDir( )AppData\Local$XDG_STATE_HOME, else ~/.local/state$XDG_STATE_HOME, else ~/Library/Application Support

Nothing is created and no program name is added. Join your own program's name and create the directory with CreateDirAll before writing there. None of the answers ends with a separator. On Linux and macOS an XDG_ variable counts only when it holds an absolute path.

ExecutablePath( ) is the full path of the running program, however it was started, which command( 0 ) is not. PathParent of it is the directory the program is installed in. ExpandHome replaces a leading ~ with the home directory and leaves every other path as it was.

The example prints properties of the paths, not the paths, because they differ from one machine and user to the next.

Activities

  1. Build the path of settings.ini in ConfigDir( ) under a name of your own, create its directory and write the file.
  2. Print fs.PathParent( fs.ExecutablePath( ).Value( ) ).

Checksums and hashes

A hash turns any amount of data into a short value of fixed size: change one byte of the data and the value changes. std/hash computes them, from a string, from bytes, or from a file.

#import "std/hash"
#import "std/fs"
#import "std/codec"
#import "std/containers"

'' A checksum catches a file that changed by accident.
print "crc32:  "; hash.DigestHex( hash.HASH_CRC32, "hello" )

'' SHA-256 also stands up to someone changing it on purpose.
print "sha256: "; hash.DigestHex( hash.HASH_SHA256, "hello" )

'' Feeding the data in pieces gives the same answer.
dim h as hash.Sha256
h.UpdateText( "hel" )
h.UpdateText( "lo" )
print "pieces match? "; cbool( h.FinishHex( ) = hash.DigestHex( hash.HASH_SHA256, "hello" ) )

'' A file is hashed a block at a time, however large it is.
dim scratch as ustring = fs.CreateTempDir( ).Value( )
dim path as ustring = fs.PathJoin( scratch, "hello.txt" )
fs.WriteAllText( path, "hello" )
print "file:   "; codec.HexEncode( hash.DigestFile( hash.HASH_SHA256, path ).Value( ) )
fs.RemoveDirAll( scratch )

'' A keyed hash proves the sender knew the key.
function Sign( byref key as const ustring, byref msg as const ustring ) as containers.Array( of ubyte )
    dim mac as hash.Hmac( of hash.Sha256 ) = hash.Hmac( of hash.Sha256 )( key )
    mac.UpdateText( msg )
    return mac.Finish( )
end function

dim sent as containers.Array( of ubyte ) = Sign( "secret", "pay 10 to ada" )
print "hmac:   "; codec.HexEncode( sent )
dim fromAda as containers.Array( of ubyte ) = Sign( "secret", "pay 10 to ada" )
dim forged as containers.Array( of ubyte ) = Sign( "guess", "pay 10 to ada" )
print "genuine? "; hash.ConstantTimeEquals( sent, fromAda )
print "forged?  "; hash.ConstantTimeEquals( sent, forged )

Output

crc32:  3610a686
sha256: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
pieces match? true
file:   2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hmac:   7182391ffd767fa5f8c66b8baae2abb0b4e376b7ea193a7404d18dd751c9aa5a
genuine? true
forged?  false

DigestHex( algorithm, data ) is the whole job in one call. Each algorithm is also a type — hash.Sha256, hash.Crc32 and the rest — that takes its data in pieces with UpdateText, UpdateBytes or Update( pointer, count ) and gives the same answer as one call. DigestFile reads a file a block at a time, so its size does not matter, and returns the digest's bytes in a Result.

ChooseWhen
HASH_CRC32, HASH_CRC32C, HASH_ADLER32checking for accidental damage
HASH_FNV1A64, HASH_XXHASH64fast hashing of keys and large data
HASH_SHA256, HASH_SHA512anything someone might change on purpose
HASH_MD5, HASH_SHA1only to match a format that already uses them

A CRC is quick, but anyone can change a file without changing its CRC. SHA-256 is the default when that matters.

An HMAC mixes a secret key into the hash. Hmac( of hash.Sha256 ) takes the key, then the message, and only someone who knows the key can produce the same bytes — so a receiver who shares the key can tell a genuine message from a forged one. Compare MACs with hash.ConstantTimeEquals, never with =: it takes the same time wherever the first difference is, so the timing gives nothing away. hash.HmacHex( algorithm, key, message ) is the one-call form when hex text is all you need.

Activities

  1. Hash "hello" with every algorithm in the table and compare the lengths.
  2. Change one letter of the message Sign receives from Ada and check that it no longer matches.

TOML settings

TOML is a settings format with types: strings, integers, floats, booleans, dates, arrays and nested tables. std/config reads it into a tree of TomlValues, and every question you ask the tree answers a value or nothing, so a missing setting falls back to a default instead of stopping the program.

#import "std/config"
#import "std/fs"

'' A settings file, written here so the example has one to read.
dim scratch as ustring = fs.CreateTempDir( "tbe" ).Value( )
dim path as ustring = fs.PathJoin( scratch, "app.toml" )
fs.WriteAllText( path, """
    name = "tbe"
    retries = 3

    [window]
    size = [ 800, 600 ]
    """ )

var r = config.LoadToml( path )
if r.IsOk( ) = false then
    print r.Failure( ).Message( )
    end 1
end if
dim cfg as config.TomlValue = r.Value( )

'' The value at a path, or an empty one whose As... answers are empty.
function Setting( byref t as config.TomlValue, byref p as ustring ) _
        as config.TomlValue
    return t.Path( p ).ValueOr( config.TomlValue( ) )
end function

print "name:    "; Setting( cfg, "name" ).AsString( ).ValueOr( "" )
print "retries:"; Setting( cfg, "retries" ).AsInt( ).ValueOr( 1 )
print "width:  "; Setting( cfg, "window.size.0" ).AsInt( ).ValueOr( 0 )
print "missing:"; Setting( cfg, "window.dpi" ).AsInt( ).ValueOr( 96 )
print "keys:    "; cfg.KeyAt( 0 ); ", "; cfg.KeyAt( 1 ); ", "; cfg.KeyAt( 2 )

'' A broken file is a value too.
fs.WriteAllText( path, !"retries = 3\nretries = 4\n" )
print "broken:  "; config.LoadToml( path ).Failure( ).Message( )
fs.RemoveDirAll( scratch )

Output

name:    tbe
retries: 3
width:   800
missing: 96
keys:    name, retries, window
broken:  line 2, column 1: duplicate key: 'retries'

LoadToml( path ) reads a file and ParseToml( text ) reads text; both return a Result whose value is the root table. Path( "window.size.0" ) follows keys separated by dots, a number indexing an array, and returns an Optional. AsInt( ), AsString( ), AsBool( ), AsFloat( ) and AsDateTime( ) each return an Optional too, empty when the value is another kind — so Setting plus ValueOr turns "absent" and "wrong type" into the default.

Tables keep their keys in the order the file wrote them: KeyAt( i ) and Keys( ) walk them that way. A file that breaks the rules — the same key twice, a table defined twice, a number too big for 64 bits — fails with a ConfigError naming the line and column.

To change a document, SetKey, Push and RemoveKey edit the tree and config.ToTomlText( root ) writes it back. Comments are not part of the tree, so they do not survive the round trip.

Activities

  1. Change retries = 3 to retries = "3" and see which default is printed.
  2. Add cfg.SetKey( "retries", config.TomlValue.FromInt( 5 ) ) and print config.ToTomlText( cfg ).Value( ).

INI files

An INI file is sections of key = value lines, every value text. It is the format of many older programs' settings, and the easiest one for a person to edit by hand.

#import "std/config"

var r = config.ParseIni( """
    ; comments start with ; or #
    [Paths]
    Data = /var/lib/tbe
    logs: /var/log/tbe ; part of the value
    """ )
dim ini as config.IniFile = r.Value( )

print "data:  "; ini.Get( "paths", "data" ).ValueOr( "?" )
print "logs:  "; ini.Get( "Paths", "logs" ).ValueOr( "?" )
print "cache: "; ini.Get( "Paths", "cache" ).ValueOr( "(none)" )

ini.SetValue( "Paths", "cache", "/var/cache/tbe" )
ini.Remove( "Paths", "logs" )
print "--- written back:"
print ini.ToText( );

dim strict as config.IniOptions
strict.Duplicates = config.INI_DUP_ERROR
var bad = config.ParseIni( !"[a]\nk = 1\nk = 2\n", strict )
print "strict: "; bad.Failure( ).Message( )

Output

data:  /var/lib/tbe
logs:  /var/log/tbe ; part of the value
cache: (none)
--- written back:
[Paths]
Data = /var/lib/tbe
cache = /var/cache/tbe
strict: line 3, column 1: duplicate key: 'k'

ParseIni( text ) and LoadIni( path ) return an IniFile. Get( section, key ) returns an Optional( of ustring ), and section and key names ignore case, so "paths" finds [Paths]. Both = and : separate a key from its value. A line starting with ; or # is a comment; a ; later in the line is part of the value, as logs shows. Put a value in double quotes to keep spaces at its ends.

SetValue replaces a value or adds the key at the end of its section, Remove( section, key ) deletes a key, and Remove( section ) a whole section. ToText( ) gives the file back as text and SaveIni( ini, path ) writes it; comments are not kept.

A repeated key normally replaces the earlier one. With IniOptions you can make that an error, or make names case-sensitive with CaseSensitiveKeys.

Activities

  1. Add a line Data = /tmp under [Paths] and check which value Get returns.
  2. Set strict.CaseSensitiveKeys = true and look up "paths" with it.

Networking

The std/net module talks to other machines. It has three parts: IP addresses and host lookup, TCP and UDP sockets, and URLs. Like std/fs, every operation that can fail returns a Result, so a refused connection or a malformed address is a value you look at, not a crash.

#import "std/net"

'' An address from text, printed back in its standard form.
var a = net.ParseIp( "2001:DB8:0:0::1" )
print a.Value( ).ToString( )

'' Every failure is a value with a reason.
var bad = net.ParseIp( "300.1.2.3" )
print bad.IsOk( ), bad.Failure( ).Message( )

'' A URL taken apart.
var u = net.ParseUrl( "https://example.com/a%20b?x=1" )
print u.Value( ).Host, u.Value( ).Path, u.Value( ).RawQuery

Output

2001:db8::1
false         ParseIp '300.1.2.3': invalid address
example.com   /a b          x=1

ParseIp reads an address and ToString( ) writes it back in its standard form, which for IPv6 is lower case with the longest run of zeros written ::. 300.1.2.3 is not an address, and the failure's Message( ) says which call refused what. ParseUrl takes a URL apart into fields; the path comes back decoded, %20 as a space.

Every socket call waits until it is done or until its timeout runs out. The examples in this section connect only to this machine, so they run anywhere, with or without a network.

The pages in this section:

  • Addresses: IPv4 and IPv6, ports, and looking up a name.
  • TCP: a connection, which reads and writes like a file.
  • UDP: single messages, sent and received whole.
  • URLs: taking URLs apart, resolving links, query strings.
  • HTTP: fetching a web page, with http or https.

Activities

  1. Parse "::1" and print IsLoopback( ).
  2. Print u.Value( ).Query( ).Get( "x" ).ValueOr( "" ) for the URL above.

Addresses

An IpAddress is an IPv4 or IPv6 address held as a value: copy it, compare it with Equals, print it with ToString( ). A SocketAddress is an address and a port — what a connection is made to.

#import "std/net"

sub Show( byref text as ustring )
    var r = net.ParseIp( text )
    if r.IsOk( ) = false then
        print text; ": refused"
        return
    end if
    var a = r.Value( )
    print text; " -> "; a.ToString( ); _
        iif( a.IsLoopback( ), " loopback", "" ); iif( a.IsPrivate( ), " private", "" ); _
        iif( a.IsMulticast( ), " multicast", "" ); iif( a.IsGlobalUnicast( ), " global", "" )
end sub

Show( "127.0.0.1" )
Show( "10.20.30.40" )
Show( "8.8.8.8" )
Show( "FF02:0:0:0:0:0:0:1" )
Show( "::ffff:192.168.0.1" )
Show( "192.168.000.001" )

'' A port makes a SocketAddress; IPv6 is written in brackets.
var sa = net.ParseSocketAddress( "[2001:db8::7]:443" ).Value( )
print sa.Ip.ToString( ), sa.Port

'' Building one, and comparing.
dim as net.SocketAddress local_ = net.SocketAddress( net.Loopback( ), 8080 )
print local_.ToString( ), local_.Equals( net.ParseSocketAddress( "127.0.0.1:8080" ).Value( ) )

'' A name is looked up; an address written as text comes back as itself.
var r = net.Resolve( "127.0.0.1", 80 )
print r.Value( ).Count( ), r.Value( )[ 0 ].ToString( )

Output

127.0.0.1 -> 127.0.0.1 loopback
10.20.30.40 -> 10.20.30.40 private global
8.8.8.8 -> 8.8.8.8 global
FF02:0:0:0:0:0:0:1 -> ff02::1 multicast
::ffff:192.168.0.1 -> ::ffff:192.168.0.1 private global
192.168.000.001: refused
2001:db8::7   443
127.0.0.1:8080              true
 1            127.0.0.1:80

Parsing is strict. 192.168.000.001 is refused: some programs read a leading zero as octal, others as decimal, and an address that two programs read differently is how a filter gets bypassed. IPv6 is accepted in any of its spellings and printed in one.

The Is… questions sort addresses into kinds: IsLoopback (this machine), IsPrivate (10/8, 172.16/12, 192.168/16, fc00::/7), IsLinkLocal, IsMulticast, IsUnspecified and IsGlobalUnicast. An IPv4 address written as IPv6 (::ffff:192.168.0.1) is answered as the IPv4 address it holds; Unmap( ) turns it back.

ParseSocketAddress needs the port, and brackets around an IPv6 address, because its colons would otherwise run into the port's. Loopback( ) and AnyAddress( ) are the two addresses a program names most: this machine, and every interface of it.

Resolve( host, port ) asks the system for a name's addresses and returns all of them, each with the port. An address written as text is returned as it is, without asking anyone.

Activities

  1. Add Show( "fe80::1%eth0" ) and print Zone( ) for it.
  2. Resolve "localhost" and print every address it has on your machine.

TCP

TCP is a connection: a stream of bytes in each direction between two programs. A TcpListener waits for connections and a TcpStream is one end of one. The example plays both sides on this machine.

#import "std/net"
#import "std/fs"

'' A server on a free port of this machine.
dim server as net.TcpListener
server.Bind( net.SocketAddress( net.Loopback( ), 0 ) )
var where = server.LocalAddress( ).Value( )

'' Connect, then accept: the system completes the connection on its own,
'' so one thread can be both client and server.
dim client as net.TcpStream
var ok = client.Connect( where, 2000 )
print "connected: "; ok.IsOk( )
dim conn as net.TcpStream
server.Accept( conn, 2000 )

'' A TcpStream is an fs.Stream, so the stream helpers work on it.
client.WriteUtf8( "ping" )
dim as string buf = space( 4 )
conn.ReadExact( cptr( ubyte ptr, strptr( buf ) ), 4 )
print "server read: "; buf
conn.WriteUtf8( "pong" )
client.ReadExact( cptr( ubyte ptr, strptr( buf ) ), 4 )
print "client read: "; buf

'' The peer's address is the client's own.
print "same ends: "; conn.PeerAddress( ).Value( ).Equals( client.LocalAddress( ).Value( ) )

'' With nothing to read, a timeout ends the wait.
client.SetReadTimeout( 100 )
dim as ubyte one( 0 to 0 )
var r = client.Read( @one( 0 ), 1 )
print "read: "; r.Failure( ).Message( ); ", code FS_TIMED_OUT: "; cbool( r.Failure( ).Code( ) = fs.FS_TIMED_OUT )

'' Closing the connection: the other side reads 0 bytes, the end of the data.
client.Close( )
print "after close: "; conn.Read( @one( 0 ), 1 ).Value( ); " bytes"

Output

connected: true
server read: ping
client read: pong
same ends: true
read: timed out, code FS_TIMED_OUT: true
after close:  0 bytes

Bind to port 0 lets the system choose a free port, and LocalAddress( ) says which. Connect takes a SocketAddress and a timeout in milliseconds; ConnectHost( "example.com", 80, 5000 ) looks a name up first and tries its addresses in turn. Accept waits for the next connection and opens the TcpStream it is given.

A TcpStream is a stream, the same kind as an open file, so WriteUtf8, WriteAll, ReadExact and ReadToEnd work on it. A plain Read returns whatever has arrived, which may be less than asked for: TCP delivers bytes, not the pieces they were written in. A read of 0 bytes means the other end has finished.

Reads and writes report an fs.FsError, as a file's do: FS_TIMED_OUT when a SetReadTimeout runs out, FS_CONNECTION_RESET when the other end drops the connection. Connecting, binding and accepting report a NetError, such as NET_CONNECTION_REFUSED when nothing listens on the port.

Activities

  1. Close server before connecting and print the Connect failure's Message( ).
  2. Send 100,000 bytes with WriteAll and read them on the other side with ReadExact. Then call client.SetWriteTimeout( 2000 ) and try 10,000,000. Why does the write give up when nobody reads yet?

UDP

UDP sends datagrams: single messages that arrive whole or not at all, with no connection to set up first. A UdpSocket is bound to an address and port, and then sends to and receives from anyone.

#import "std/net"

dim a as net.UdpSocket, b as net.UdpSocket
a.Bind( net.SocketAddress( net.Loopback( ), 0 ) )
b.Bind( net.SocketAddress( net.Loopback( ), 0 ) )
b.SetReadTimeout( 1000 )

'' One datagram, sent to b's address.
dim as string msg = "hello, datagram"
a.SendTo( cptr( ubyte ptr, strptr( msg ) ), len( msg ), b.LocalAddress( ).Value( ) )

dim as string buf = space( 64 )
dim from as net.SocketAddress
var r = b.ReceiveFrom( cptr( ubyte ptr, strptr( buf ) ), 64, from )
print left( buf, r.Value( ) ); " ("; r.Value( ); " bytes, from a: "; from.Equals( a.LocalAddress( ).Value( ) ); ")"

'' A buffer too small: the datagram is cut, and the receive says so.
a.SendTo( cptr( ubyte ptr, strptr( msg ) ), len( msg ), b.LocalAddress( ).Value( ) )
dim as string small = space( 5 )
r = b.ReceiveFrom( cptr( ubyte ptr, strptr( small ) ), 5, from )
print "["; small; "] too long: "; cbool( r.Failure( ).Code( ) = net.NET_MESSAGE_TOO_LONG )

'' Nothing more is coming: the timeout ends the wait.
b.SetReadTimeout( 100 )
r = b.ReceiveFrom( cptr( ubyte ptr, strptr( buf ) ), 64, from )
print cbool( r.Failure( ).Code( ) = net.NET_TIMED_OUT )

Output

hello, datagram ( 15 bytes, from a: true)
[hello] too long: true
true

SendTo( p, n, address ) sends n bytes as one datagram. ReceiveFrom( p, n, from ) waits for the next one and fills from with its sender, so a server knows where to reply. A datagram longer than the buffer fills the buffer, and the rest is lost: the receive fails with NET_MESSAGE_TOO_LONG so the program knows it saw only part of it.

Connect( address ) fixes the other end; after it, Send and Receive need no address, and datagrams from anyone else are not delivered. Nothing promises that a datagram arrives, or that two arrive in the order they were sent: on this machine they do, across a network they may not.

Activities

  1. Reply from b to from, and receive the reply on a.
  2. Use a.Connect and a.Send instead of SendTo.

URLs

ParseUrl takes a URL apart into a Url, whose fields can be read, changed and printed again. It follows the URL standard, RFC 3986.

#import "std/net"

var u = net.ParseUrl( "http://user@shop.example:8080/cat%C3%A9gorie/../items?id=7&sort=price+asc#reviews" ).Value( )
print "scheme "; u.Scheme; ", user "; u.User; ", host "; u.Host; ", port"; u.Port
print u.Path
print u.RawQuery
print u.Fragment

'' Links on the page, made absolute.
print u.Resolve( "item?id=8" ).Value( ).ToString( )
print u.Resolve( "/" ).Value( ).ToString( )
print u.Resolve( "//cdn.example/app.js" ).Value( ).ToString( )

'' The query, as name/value pairs.
var q = u.Query( )
print q.Get( "sort" ).ValueOr( "" ), q.Get( "missing" ).ValueOr( "(none)" )
q.SetValue( "id", "9" )
q.Add( "tag", "new & used" )
print q.Encode( )

'' Changing a field: ToString escapes it.
u.Path = "/items/summer sale"
u.RawQuery = q.Encode( )
u.Fragment = ""
print u.ToString( )

Output

scheme http, user user, host shop.example, port 8080
/catégorie/../items
id=7&sort=price+asc
reviews
http://user@shop.example:8080/item?id=8
http://user@shop.example:8080/
http://cdn.example/app.js
price asc     (none)
id=9&sort=price+asc&tag=new+%26+used
http://user@shop.example:8080/items/summer%20sale?id=9&sort=price+asc&tag=new+%26+used

The fields hold the parts decoded: Path has é where the URL had %C3%A9. RawQuery is the exception, kept as written, because only a query parser knows where its pairs end; Query( ) is that parser. Port is -1 when the URL names none.

Resolve( link ) turns a link found on a page into an absolute URL, as a browser does: item?id=8 replaces the last segment of the path, / goes to the top of the site, and //cdn.example/app.js keeps only the scheme. Dot segments are removed as the link is resolved — which is why /catégorie/../item became /item.

QueryValues is a list of name and value pairs in order; a name may appear more than once, and GetAll returns every value. Encode( ) writes it back, escaping each name and value, so & inside a value cannot split it.

Setting a field and calling ToString( ) escapes what needs escaping: the space in summer sale becomes %20. A URL with a broken escape, such as a lone %, does not parse at all.

Activities

  1. Print u.Resolve( "../../../x" ).Value( ).ToString( ). Where does it stop?
  2. Parse "http://[::1]:9000/" and print Host and HostPort( ).

HTTP

std/http fetches web pages and talks to web APIs. http.Fetch( url ) sends a GET and returns the whole reply as a Response: the status, the headers and the body. https URLs work the same way, encrypted by std/tls.

#cmdline "-mt"
#import "std/http"
#import "std/net"
#import "std/fs"

'' A web server that answers one request, on this machine, so the example
'' needs no network. It reads the request up to the blank line, then replies.
sub Serve( byval p as any ptr )
    dim as net.TcpListener ptr lis = p
    dim c as net.TcpStream
    if lis->Accept( c, 5000 ).IsOk( ) = false then exit sub
    dim as string seen
    dim as ubyte b( 0 to 1023 )
    while instr( seen, !"\r\n\r\n" ) = 0
        dim got as fs.FsResultI64 = c.Read( @b( 0 ), 1024 )
        if got.IsOk( ) = false orelse got.Value( ) = 0 then exit while
        for i as long = 0 to got.Value( ) - 1
            seen &= chr( b( i ) )
        next
    wend
    c.WriteUtf8( !"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n" & _
                 !"Content-Length: 13\r\n\r\nhello, client" )
    c.Close( )
end sub

sub Main( )
    dim lis as net.TcpListener
    lis.Bind( net.SocketAddress( net.Loopback( ), 0 ) )
    dim as long port = lis.LocalAddress( ).Value( ).Port
    dim th as any ptr = threadcreate( @Serve, @lis, 1024 * 1024 )

    dim r as http.HttpResultResponse = http.Fetch( "http://127.0.0.1:" & port & "/greeting" )
    if r.IsOk( ) then
        print r.Value( ).StatusCode; " "; r.Value( ).Reason
        print r.Value( ).Header.Get( "content-type" ).ValueOr( "?" )
        print r.Value( ).Text( )
    end if
    threadwait( th )

    dim bad as http.HttpResultResponse = http.Fetch( "ftp://example.com/file" )
    print bad.Failure( ).Message( )
end sub

Main( )

Output

 200 OK
text/plain
hello, client
Send 'ftp://example.com/file': unsupported URL scheme

The example runs its own tiny server on this machine so that it needs no network: Serve accepts one connection on a second thread, reads the request and writes a reply by hand. That is the whole of HTTP/1.1 on the wire — a status line, headers, a blank line, then the body.

On the client side Header.Get finds a header whatever its case, and returns an Optional, because a header may be missing. Text( ) decodes the body as UTF-8. A status such as 404 is not an error: the request worked and the server answered, so check StatusCode. An error is a request that could not be made at all, like the ftp: URL, which std/http does not speak.

For more than one request make an http.Client: it keeps connections open and reuses them, follows redirects, and holds settings such as TimeoutMs. Client.Open hands the body over as a stream instead of reading it all, for downloads too large to keep in memory.

Activities

  1. Change the server's status line to HTTP/1.1 404 Not Found and print r.Value( ).IsSuccess( ).
  2. Fetch "http://127.0.0.1:1/", where nothing listens, and print the failure's Message( ).

Libraries

Three libraries cover jobs most programs end up doing. std/logs records what a program did, with levels, typed fields and several destinations. std/tui draws full-screen terminal interfaces. vendor/sqlite3 puts the SQLite database an #import away. This first example is the shortest useful log.

#import "std/logs"

'' Logs normally go to stderr with a timestamp. This sends them to
'' stdout with no timestamp, so the output is the same on every run.
dim tr as logs.TextRenderer
tr.stamp = logs.TS_NONE
dim con as logs.ConsoleSink = logs.ConsoleSink( logs.CT_STDOUT, @tr )
logs.LogInit( @con, logs.LOG_INFO )

logs.LogInfo( "starting" )
logs.LogDebug( "not shown: debug is below the INFO level" )
logs.LogWarn( "disk 91% full" )

logs.LogShutdown( )

Output

INFO   starting
WARN   disk 91% full

logs.LogInit connects the program's default logger to a sink and sets its level; after that, logs.LogInfo and its siblings can be called from anywhere. The debug message is dropped because LOG_DEBUG is below the LOG_INFO level the logger was given. logs.LogShutdown( ) flushes and closes the sinks at the end.

A ConsoleSink normally writes to stderr, because logs are diagnostics and must not mix into output that is piped to another program, and each line normally starts with the time. The examples in this section send their logs to stdout without a timestamp, so that the output is the same on every run.

The pages that follow:

Activities

  1. Change the level passed to LogInit to logs.LOG_DEBUG.
  2. Remove tr.stamp = logs.TS_NONE and run the program twice.

Logging

A log is three kinds of object. A Logger is what the program calls. A sink is where records go: ConsoleSink to the terminal, FileSink to a file, and a logger may have several. A renderer turns a record into text: TextRenderer for people, JsonRenderer for programs.

#import "std/logs"

dim tr as logs.TextRenderer
tr.stamp = logs.TS_NONE
dim con as logs.ConsoleSink = logs.ConsoleSink( logs.CT_STDOUT, @tr )

'' A Logger of your own, with a level and one sink.
dim lg as logs.Logger
lg.MinLevel = logs.LOG_DEBUG
lg.AddSink( @con )

'' One call per record. Below MinLevel, nothing is built.
lg.Info( "server started" )
lg.Debug( "cache warm" )
lg.Trace( "not shown: trace is below debug" )

'' Fields are typed and chained; Msg( ) finishes and sends the record.
lg.Warn( ).AddText( "user", "quinn" ).AddInt( "attempts", 3 ) _
          .Msg( "login failed" )

'' A child logger adds its own field to every record it writes.
var db = lg.WithField( "component", logs.LogValue( "db" ) )
db.Info( ).AddInt( "rows", 42 ).AddFloat( "ms", 1.5 ) _
          .Msg( "query complete" )

'' Levels as text, both ways, for a setting read from a file.
dim lv as logs.LogLevel
if logs.LevelFromName( "warn", lv ) then
    print "warn is level"; lv; ", "; logs.LevelName( logs.LOG_ERROR ); _
          " is level"; logs.LOG_ERROR
end if

Output

INFO   server started
DEBUG  cache warm
WARN   login failed  user=quinn attempts=3
INFO   query complete  component=db rows=42 ms=1.5
warn is level 40, ERROR is level 50

lg.AddSink( @con ) connects a sink. The logger keeps a pointer and does not own the sink, so the sink must live at least as long as the logger.

Levels, from least to most severe: LOG_TRACE, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR and LOG_FATAL, numbered 10 to 60. MinLevel discards everything below it, and a discarded record is never built. LOG_FATAL is only a severity: it does not end the program.

Fields. lg.Info( "text" ) logs a line. lg.Warn( ) with no message starts a record instead; AddText, AddInt, AddFloat, AddBool and the other field methods add a name and a typed value, and Msg( "text" ) finishes the record and sends it. A record that is never finished is never sent.

Child loggers. WithField( name, value ) returns a new logger that adds that field to every record it writes, with the parent's level and sinks, so a whole layer of a program can be marked at once.

LevelName and LevelFromName convert between levels and text. LevelFromName returns false and leaves its result alone for a name that is not a level, so a typo in a setting does not switch logging off.

Activities

  1. Raise MinLevel to LOG_WARN and predict which lines remain.
  2. Make a second child logger with "component" set to "http" and log a request through it.

The default logger

Passing a logger to every procedure that might log is tedious. std/logs holds one default logger for the whole program, and a set of macros that write to it from anywhere.

#import "std/logs"

'' Any procedure can log through the default logger: nothing is passed.
sub SaveSettings( byref path as ustring, byval bytes as int32 )
    logs.LogEventInfo( AddText( "path", path ).AddInt( "bytes", bytes ), _
                       "settings saved" )
end sub

dim tr as logs.TextRenderer
tr.stamp = logs.TS_NONE
dim con as logs.ConsoleSink = logs.ConsoleSink( logs.CT_STDOUT, @tr )
logs.LogInit( @con, logs.LOG_INFO )

logs.LogInfo( "starting" )
SaveSettings( "settings.ini", 512 )
logs.LogEventError( AddInt( "code", 5 ), "upload failed" )

'' The macros and the logger itself reach the same place.
logs.DefaultLogger( ).Warn( ).AddBool( "retry", true ).Msg( "slow" )

'' Flush and close the sinks: LogShutdown is not called for you.
logs.LogShutdown( )

Output

INFO   starting
INFO   settings saved  path=settings.ini bytes=512
ERROR  upload failed  code=5
WARN   slow  retry=true
MacroWrites
logs.LogTrace( m ) ... logs.LogFatal( m )a message at that level
logs.LogEventInfo( fields, m ) and its siblingsa message with fields

The fields argument of a LogEvent macro is a chain of field calls with no object in front, AddText( "path", path ).AddInt( "bytes", bytes ), which the macro attaches to the record it starts. logs.DefaultLogger( ) is the logger itself, for anything the macros do not cover.

Because they are macros, they capture the file, line and procedure they were written in, which a renderer prints when its showsite is set. They are also removed from the program entirely below the LOG_MIN_LEVEL setting, which a project sets in the [defines] of its tiko.toml.

LogInit( @sink, level ) attaches the sink and sets the level. LogShutdown( ) is not called for you when the program ends: call it last, or the final lines may be lost.

Activities

  1. Call logs.LogEventWarn from inside SaveSettings when bytes is zero.
  2. Set tr.showsite = true and look at what each line gains.

Terminal UI

std/tui draws full-screen terminal programs: dashboards, menus, editors. An application is a type that extends tui.TuiApp and has three methods:

  • Init sets up the state.
  • Update receives an event, such as a key, the mouse, a resize or a timer tick, and changes the state. It never draws.
  • View draws the whole screen from the state. It never changes anything.
#import "std/tui"

'' An application is three methods: Init, Update and View.
type CounterApp extends tui.TuiApp
    as int32 count
    declare function Init( ) as tui.TuiCmd override
    declare function Update( byref e as tui.TuiEvent ) _
                             as tui.TuiCmd override
    declare sub View( byref f as tui.TuiFrame ) override
end type

function CounterApp.Init( ) as tui.TuiCmd
    count = 0
    return tui.TuiNone( )
end function

'' Update changes the state, and says what should happen next.
function CounterApp.Update( byref e as tui.TuiEvent ) as tui.TuiCmd
    if e.kind <> tui.TUIEVENT_KEY then return tui.TuiNone( )
    select case e.key.code
    case tui.TUIKEY_ESCAPE
        return tui.TuiQuit( 0 )
    case tui.TUIKEY_UP
        count += 1
    case tui.TUIKEY_DOWN
        count -= 1
    end select
    return tui.TuiRedraw( )
end function

'' View draws the whole screen from the state, every frame.
sub CounterApp.View( byref f as tui.TuiFrame )
    dim as tui.TuiBlock b
    b.borderKind = tui.TUIBORDER_ASCII
    b.sides = tui.TUISIDE_ALL
    b.AddTitle( " counter ", tui.TuiStyleDefault( ), _
                tui.TUISIDE_TOP, tui.TUIALIGN_CENTER )
    b.Render( f, f.Area( ) )
    dim as tui.TuiParagraph p
    p.align = tui.TUIALIGN_CENTER
    p.text = "count: " & str( count )
    p.Render( f, b.Inner( f.Area( ) ) )
end sub

'' Print the cells a screen holds, one row per line.
sub Dump( byref scr as tui.TuiScreen )
    dim as tui.TuiBuffer ptr bf = scr.Front( )
    for y as int32 = 0 to bf->rows - 1
        dim as ustring row
        for x as int32 = 0 to bf->cols - 1
            if bf->At( x, y )->width > 0 then row &= bf->At( x, y )->text
        next
        print rtrim( row )
    next
end sub

'' No terminal needed: render one frame into a 24 x 3 screen.
dim as CounterApp app
dim as tui.TuiScreen scr
scr.Resize( 24, 3 )
app.Init( )
var first = tui.TuiRenderOnce( app, scr )
Dump( scr )

'' Feed Update two Up keys, as the event loop would, and draw again.
dim as tui.TuiEvent up
up.kind = tui.TUIEVENT_KEY
up.key.code = tui.TUIKEY_UP
app.Update( up )
app.Update( up )
var second = tui.TuiRenderOnce( app, scr )
Dump( scr )

'' Only the changed cells are sent; an unchanged frame sends nothing.
var third = tui.TuiRenderOnce( app, scr )
print "bytes sent:"; len( first ); len( second ); len( third )

'' A real program hands the app to the loop instead:
''     tui.TuiRun( app, tui.TuiTermOptionsDefault( ) )

Output

+------ counter -------+
|       count: 0       |
+----------------------+
+------ counter -------+
|       count: 2       |
+----------------------+
bytes sent: 104 14 0

A real program ends with tui.TuiRun( app, tui.TuiTermOptionsDefault( ) ). It takes over the terminal, runs the loop (wait for events, apply them, draw once) and restores the terminal on the way out. It returns a Result, and when its output is not a terminal it fails with not a terminal instead of drawing into a file.

The example does what a test does instead. TuiRenderOnce( app, screen ) draws one frame into a screen with no terminal at all and returns the bytes it would have sent; screen.Front( ) holds the cells, which Dump prints. Since Update is a plain method, feeding it an event is a call.

The byte counts show how drawing works. View describes the whole screen every time, and the library compares it with the previous frame and sends only the cells that changed: everything the first time, the changed digit the second, nothing the third.

Update returns a command: TuiNone( ), TuiRedraw( ), TuiQuit( code ), or TuiTick( ms ) to receive a timer event.

Activities

  1. Make the + and - characters change the count as well: a character key arrives with e.key.code = tui.TUIKEY_CHAR and the character in e.key.ch.
  2. Show the count in a second line that says whether it is odd or even.

Layout and widgets

A screen is built by dividing a rectangle with constraints and drawing a widget into each piece. Widgets are values made in View and thrown away; what must survive between frames, such as a list's selection, lives in a state record the application owns.

#import "std/tui"

'' What must outlive one frame, such as a selection, lives in the app.
type JobsApp extends tui.TuiApp
    as tui.TuiListState listState
    declare function Init( ) as tui.TuiCmd override
    declare function Update( byref e as tui.TuiEvent ) _
                             as tui.TuiCmd override
    declare sub View( byref f as tui.TuiFrame ) override
end type

function JobsApp.Init( ) as tui.TuiCmd
    listState.selected = 2
    return tui.TuiNone( )
end function

function JobsApp.Update( byref e as tui.TuiEvent ) as tui.TuiCmd
    return tui.TuiNone( )
end function

function Boxed( byref title as ustring ) as tui.TuiBlock
    dim as tui.TuiBlock b
    b.borderKind = tui.TUIBORDER_ASCII
    b.sides = tui.TUISIDE_ALL
    b.AddTitle( title, tui.TuiStyleDefault( ), _
                tui.TUISIDE_TOP, tui.TUIALIGN_LEFT )
    return b
end function

sub JobsApp.View( byref f as tui.TuiFrame )
    '' Rows: a body that fills the space, then a 1-row gauge.
    dim as tui.TuiConstraintArray rowsC
    rowsC.Push( tui.TuiFill( ) )
    rowsC.Push( tui.TuiLength( 1 ) )
    dim as tui.TuiRectArray band = tui.TuiSplitV( f.Area( ), rowsC )

    '' Columns: the body splits into a 16-column list and the rest.
    dim as tui.TuiConstraintArray colsC
    colsC.Push( tui.TuiLength( 16 ) )
    colsC.Push( tui.TuiFill( ) )
    dim as tui.TuiRectArray col = tui.TuiSplitH( band[ 0 ], colsC )

    var lb = Boxed( " jobs " )
    lb.Render( f, col[ 0 ] )
    dim as tui.TuiList lst
    lst.items.Push( "backup" )
    lst.items.Push( "index" )
    lst.items.Push( "report" )
    lst.state = @listState
    lst.selectPrefix = "> "
    lst.Render( f, lb.Inner( col[ 0 ] ) )

    var db = Boxed( " detail " )
    db.Render( f, col[ 1 ] )
    dim as tui.TuiParagraph p
    p.text = "report: 3 of 4 steps"
    p.Render( f, db.Inner( col[ 1 ] ) )

    dim as tui.TuiGauge g
    g.ratio = 0.75
    g.showLabel = true
    g.Render( f, band[ 1 ] )
end sub

dim as JobsApp app
dim as tui.TuiScreen scr
scr.Resize( 40, 6 )
app.Init( )
tui.TuiRenderOnce( app, scr )

dim as tui.TuiBuffer ptr bf = scr.Front( )
for y as int32 = 0 to bf->rows - 1
    dim as ustring row
    for x as int32 = 0 to bf->cols - 1
        if bf->At( x, y )->width > 0 then row &= bf->At( x, y )->text
    next
    print rtrim( row )
next

Output

+ jobs --------++ detail --------------+
|  backup      ||report: 3 of 4 steps  |
|  index       ||                      |
|> report      ||                      |
+--------------++----------------------+
##################75%#########

Layout. TuiSplitV stacks pieces top to bottom and TuiSplitH puts them side by side. Each piece has a constraint: TuiLength( n ) for exactly n cells, TuiPercent, TuiRatio, TuiAtLeast and TuiAtMost, and TuiFill( ) for a share of what is left. Splitting a piece again builds any screen, and the result is the same for the same sizes on every platform.

Widgets. The example uses four:

WidgetDraws
TuiBlocka border with titles; Inner is the space inside it
TuiListitems, with the selected one marked by selectPrefix
TuiParagraphtext, aligned
TuiGaugea bar filled to ratio, with a label

TuiList reads its selection from the TuiListState its state points at, which is a field of the app, not of the list. Among the others are TuiTable, TuiTabs, TuiTextInput, TuiCheckbox, TuiMenuBar, TuiTree and TuiSparkline.

The example draws TUIBORDER_ASCII borders so that its output prints anywhere; a real program usually wants TUIBORDER_ROUNDED or TUIBORDER_PLAIN, which use box-drawing characters, and colour from a TuiStyle.

Activities

  1. Change the list's width to TuiPercent( 50 ).
  2. Set the gauge's ratio to 0.3 and its label to "3 of 10".

SQLite

SQLite is a database in a library: no server, one file per database, or none at all for a database in memory. tiko ships it as the vendor package vendor/sqlite3, and what you call is SQLite's own C interface, written the way SQLite's documentation writes it.

'' SQLite's own C interface, from the toolchain's vendor package.
#import "vendor/sqlite3"

'' Column text is UTF-8 bytes that SQLite owns; copy it out at once.
function ColumnText( byval st as sqlite3_stmt ptr, byval i as int32 ) _
         as ustring
    return *cast( zstring ptr, sqlite3_column_text( st, i ) )
end function

'' A connection is a sqlite3 ptr; ":memory:" is a private database
'' that lives as long as the connection.
dim db as sqlite3 ptr
if sqlite3_open( ":memory:", @db ) <> SQLITE_OK then
    print "cannot open: "; *sqlite3_errmsg( db )
    end 1
end if

'' sqlite3_exec runs SQL that returns no rows.
dim as string sql = _
    "create table city( name text, country text, people int );" & _
    "insert into city values( 'Lagos', 'Nigeria', 15388000 );" & _
    "insert into city values( 'Osaka', 'Japan', 19060000 );" & _
    "insert into city values( 'Lima', 'Peru', 11283000 );"
if sqlite3_exec( db, sql, nullptr, nullptr, nullptr ) <> SQLITE_OK then
    print "failed: "; *sqlite3_errmsg( db )
    end 1
end if

'' A query is prepared, then stepped through one row at a time.
dim st as sqlite3_stmt ptr
sqlite3_prepare_v2( db, "select name, country, people from city " & _
                        "order by people desc", -1, @st, nullptr )
do while sqlite3_step( st ) = SQLITE_ROW
    print left( ColumnText( st, 0 ) & space( 8 ), 8 ); _
          left( ColumnText( st, 1 ) & space( 10 ), 10 ); _
          sqlite3_column_int64( st, 2 )
loop
sqlite3_finalize( st )

'' A query with one value in its one row.
sqlite3_prepare_v2( db, "select sum( people ) from city", _
                    -1, @st, nullptr )
if sqlite3_step( st ) = SQLITE_ROW then
    print "total people:"; sqlite3_column_int64( st, 0 )
end if
sqlite3_finalize( st )

sqlite3_close( db )

Output

Osaka   Japan      19060000
Lagos   Nigeria    15388000
Lima    Peru       11283000
total people: 45731000

A connection is a sqlite3 ptr, which sqlite3_open fills in through its address, @db. The name :memory: opens a private database that lasts as long as the connection; any other name is a file, created if it does not exist. sqlite3_close closes it.

Almost every function returns a result code: SQLITE_OK for success, and anything else is a failure that sqlite3_errmsg( db ) explains. Nothing raises a runtime error, so an ignored code is an ignored failure.

  • sqlite3_exec runs SQL that returns no rows; several statements may be given at once, separated by ;.
  • sqlite3_prepare_v2 compiles a query into a sqlite3_stmt ptr. sqlite3_step returns SQLITE_ROW for each row and SQLITE_DONE at the end; sqlite3_column_int64, sqlite3_column_text and their siblings read the row's columns, counting from 0. sqlite3_finalize frees the statement.

sqlite3_column_text returns a pointer to UTF-8 bytes that SQLite owns, valid until the next step. ColumnText copies them into a ustring straight away.

Building the program copies SQLite's shared library beside it, so the two run wherever they travel together. #import "vendor/sqlite3" link static links SQLite into the program itself instead. Other C libraries are reached the same way; see Calling C.

Activities

  1. Add a fourth city and print only the cities with more than 12 million people.
  2. Change :memory: to cities.db, run the program twice, and explain the second run's output.

Prepared statements

A value that comes from outside the program, such as a name typed by a user, must never be pasted into the text of an SQL statement. It goes in as a parameter: a ? in the SQL, and a value bound to it separately.

#import "vendor/sqlite3"

'' SQLite reports failure as a result code; this turns one into a stop.
sub Check( byval rc as int32, byval db as sqlite3 ptr )
    if rc <> SQLITE_OK andalso rc <> SQLITE_DONE then
        print "SQLite error"; rc; ": "; *sqlite3_errmsg( db )
        end 1
    end if
end sub

sub Execute( byval db as sqlite3 ptr, byref sql as ustring )
    Check( sqlite3_exec( db, sql, nullptr, nullptr, nullptr ), db )
end sub

dim db as sqlite3 ptr
Check( sqlite3_open( ":memory:", @db ), db )
Execute( db, "create table item( id integer primary key, " & _
         "name text not null, price int )" )

'' Each ? is a parameter: the values never become part of the SQL.
dim as ustring names( ... ) = { "tea", "O'Brien's jam", "bread" }
dim as int64 prices( ... ) = { 450, 799, 320 }
dim ins as sqlite3_stmt ptr
Check( sqlite3_prepare_v2( db, _
    "insert into item( name, price ) values( ?, ? )", _
    -1, @ins, nullptr ), db )
for i as int32 = 0 to ubound( names )
    sqlite3_bind_text( ins, 1, names( i ), -1, SQLITE_TRANSIENT )
    sqlite3_bind_int64( ins, 2, prices( i ) )
    Check( sqlite3_step( ins ), db )
    sqlite3_reset( ins )
next
sqlite3_finalize( ins )
print "last id:"; sqlite3_last_insert_rowid( db )

'' A parameter in a query, too.
dim sel as sqlite3_stmt ptr
Check( sqlite3_prepare_v2( db, "select name from item where price > ?", _
    -1, @sel, nullptr ), db )
sqlite3_bind_int64( sel, 1, 400 )
do while sqlite3_step( sel ) = SQLITE_ROW
    print "over 4.00: "; *cast( zstring ptr, sqlite3_column_text( sel, 0 ) )
loop
sqlite3_finalize( sel )

'' A transaction: everything in it happens, or nothing does.
Execute( db, "begin" )
Execute( db, "delete from item" )
Execute( db, "rollback" )
Check( sqlite3_prepare_v2( db, "select count( * ) from item", _
    -1, @sel, nullptr ), db )
sqlite3_step( sel )
print "after the rollback:"; sqlite3_column_int( sel, 0 ); " items"
sqlite3_finalize( sel )

'' A failure is a result code, and SQLite says why.
dim errMsg as zstring ptr
if sqlite3_exec( db, "insert into item( price ) values( 1 )", _
                 nullptr, nullptr, @errMsg ) <> SQLITE_OK then
    print "refused: "; *errMsg
    sqlite3_free( errMsg )
end if
sqlite3_close( db )

Output

last id: 3
over 4.00: tea
over 4.00: O'Brien's jam
after the rollback: 3 items
refused: NOT NULL constraint failed: item.name

A statement is used in four steps:

  1. sqlite3_prepare_v2 compiles the SQL once.
  2. sqlite3_bind_text, sqlite3_bind_int64 and their siblings give a value to each ?, counting from 1.
  3. sqlite3_step runs it. sqlite3_reset makes it ready to run again with new values.
  4. sqlite3_finalize frees it.

Pitfall

Never build SQL by pasting values into it. "... values( '" & name & "' )" breaks on O'Brien's jam, and lets whoever controls the value rewrite the query. A bound value travels apart from the SQL and cannot change what it means.

The -1 in sqlite3_bind_text means "the text ends at its zero byte", and SQLITE_TRANSIENT asks SQLite to keep its own copy, which is always safe. A ustring passed where SQLite wants text is converted to UTF-8 for the call.

A transaction groups statements: begin, then commit to keep everything or rollback to undo it. They are SQL like any other, run with sqlite3_exec.

When sqlite3_exec is given the address of a zstring ptr as its last argument, SQLite stores its error message there, and the program frees it with sqlite3_free. Execute is named so because run is already a statement: a sub Run is TKO0813: 'run' is already declared by the runtime library.

Activities

  1. Add an update item set price = ? where name = ? statement and bind both values.
  2. Replace the rollback with commit and check the count again.

Calling C

Most of the code a program could use is written in C: the operating system, the C runtime, compression, databases, graphics. tiko calls C directly, with no wrapper between: a C function is declared, and then called like any other.

'' The C runtime is always linked, so its functions need only a
'' declaration.

'' One at a time: cdecl is C's calling convention, and alias gives the
'' exact name the linker looks for, so the tiko name can differ.
declare function CLength cdecl alias "strlen" _
    ( byval s as const zstring ptr ) as uinteger

'' A block: everything inside is cdecl and keeps its exact spelling.
extern "C"
    declare function toupper( byval c as int32 ) as int32
    declare function CAbs alias "abs" ( byval n as int32 ) as int32
    declare function atoi( byval s as const zstring ptr ) as int32
end extern

dim as zstring * 16 word = "interface"
print "strlen: "; CLength( @word )

for i as int32 = 0 to len( word ) - 1
    word[ i ] = toupper( word[ i ] )
next
print "toupper: "; word

print "abs:"; CAbs( -42 )
print "atoi:"; atoi( "  123 apples" )

Output

strlen: 9
toupper: INTERFACE
abs: 42
atoi: 123

A C function is declared with declare, plus what C needs:

  • cdecl: C's calling convention.
  • alias "name": the exact name the linker looks for. tiko names are not case-sensitive and C's are, so alias fixes the spelling; it also lets the tiko name differ, as CLength is C's strlen.
  • lib "name", optionally: the library the function lives in, which is then linked. The C runtime is always linked, so the example names none.

extern "C" ... end extern declares a block at once. Everything inside is cdecl and keeps the exact spelling it was written with, so toupper needs no alias. Inside the block alias still renames: CAbs is C's abs, which needs another name because abs is already a tiko function.

Pitfall

A declaration is a promise the compiler cannot check. It believes every parameter type and the result type. Copy them from the C header with care, or let the compiler read the header itself with #cinclude.

The pages that follow cover C types, C strings, callbacks, structs, reading a C header and loading a library at run time.

A larger C library is easiest to use as a vendor package: its header already translated and its compiled code beside it, reached with one #import. The toolchain ships SQLite this way; see SQLite.

Activities

  1. Declare C's tolower in the extern "C" block and lower-case the word again.
  2. Declare atoi with declare ... cdecl alias instead of in the block.

C types

Every parameter in a C declaration needs the tiko type with the same size and meaning. Most C types have one tiko equivalent on every platform; two do not, and that is what this example shows. It prints different answers on each platform, and the difference is the point.

#include once "crt/long.bi"

'' Most C types have one tiko equivalent on every platform.
print "int       int32  "; sizeof( int32 )
print "long long int64  "; sizeof( int64 )
print "double    float64"; sizeof( float64 )
print "void *    any ptr"; sizeof( any ptr )

'' C's long and wchar_t do not: their size depends on the platform.
print "long      clong  "; sizeof( clong )
print "wchar_t   wstring"; sizeof( wstring * 1 )

#ifdef __FB_WIN32__
    print "built for Windows"
#endif
#ifdef __FB_LINUX__
    print "built for Linux"
#endif
#ifdef __FB_DARWIN__
    print "built for macOS"
#endif

Output on Windows:

int       int32   4
long long int64   8
double    float64 8
void *    any ptr 8
long      clong   4
wchar_t   wstring 2
built for Windows

Output on Linux:

int       int32   4
long long int64   8
double    float64 8
void *    any ptr 8
long      clong   8
wchar_t   wstring 4
built for Linux

Output on macOS:

int       int32   4
long long int64   8
double    float64 8
void *    any ptr 8
long      clong   8
wchar_t   wstring 4
built for macOS
Ctiko
char, unsigned charbyte, uint8
short, unsigned shortint16, uint16
int, unsigned intint32, uint32
long long, unsigned long longint64, uint64
float, doublefloat32, float64
size_t, intptr_tuinteger, integer
T *, void *T ptr, any ptr
const char *const zstring ptr
long, unsigned longclong, culong, from crt/long.bi
wchar_t *wstring ptr

C's long is 4 bytes on Windows and 8 on Linux and macOS. clong and culong follow the platform. wchar_t is 2 bytes on Windows and 4 on Linux and macOS, and tiko's wstring follows the same rule, so a wstring ptr is a wchar_t * everywhere.

Pitfall

long is not int32. A C function returning long declared as int32 works on Windows, where the two agree, and loses the top half of every value on Linux and macOS. Nothing complains.

__FB_WIN32__ and __FB_LINUX__ are defined on their platforms, for the rare declaration that must differ.

Activities

  1. Add a line for short and for size_t.
  2. Declare C's labs, which takes and returns a long, using clong.

C strings

A C string is a run of bytes ending in a zero byte, reached through a char *. In tiko that is a zstring ptr. Text crosses the boundary in both directions, and each direction has one rule to remember.

extern "C"
    declare function strlen( byval s as const zstring ptr ) as uinteger
    declare function strchr( byval s as const zstring ptr, _
                             byval c as int32 ) as zstring ptr
    declare function strstr( byval s as const zstring ptr, _
                             byval find as const zstring ptr ) _
                             as zstring ptr
    declare function malloc( byval n as uinteger ) as any ptr
    declare sub free( byval p as any ptr )
    declare function strcpy( byval dst as zstring ptr, _
                             byval src as const zstring ptr ) _
                             as zstring ptr
end extern

'' Into C: strptr is a pointer to a string's own bytes and its zero.
dim as string s = "key=value"
print "strlen: "; strlen( strptr( s ) )

'' A literal can be passed straight to a const zstring ptr.
print "strlen of a literal: "; strlen( "four" )

'' Out of C: a pointer into C's memory. *p copies the characters out.
dim as zstring ptr eq = strchr( strptr( s ), asc( "=" ) )
if eq <> nullptr then
    dim as ustring rest = *( eq + 1 )
    print "after '=': "; rest
end if

'' A C function that finds nothing returns a null pointer.
print "strstr found 'xyz': "; _
      cbool( strstr( strptr( s ), "xyz" ) <> nullptr )

'' A ustring's text goes to C as UTF-8: one character, two bytes.
dim as ustring u = "caf" & wchr( 233 )
print "len:"; len( u ); ", strlen: "; strlen( u )

'' Memory C allocates, C must free. Copy the text out first.
dim as zstring ptr copy_ = malloc( len( s ) + 1 )
strcpy( copy_, strptr( s ) )
dim as ustring kept = *copy_
free( copy_ )
print "copied: "; kept

Output

strlen: 9
strlen of a literal: 4
after '=': value
strstr found 'xyz': false
len: 4, strlen: 5
copied: key=value

Into C. strptr( s ) is a pointer to a string's own bytes, which end in a zero, so C can read them in place. It stays valid until the string next changes. A string literal passes straight to a const zstring ptr. A ustring passed where C wants a const zstring ptr is converted to UTF-8 for the call, which is why café has 4 characters for len and 5 bytes for strlen.

Out of C. A zstring ptr that C returns points at C's memory. *p reads the characters into a tiko string, which copies them; copy at once, because C may reuse or free that memory. Pointer arithmetic works as in C: eq + 1 is the character after the =. A C function that finds nothing usually returns a null pointer, so compare with nullptr before reading through it.

Memory C allocates, C must free. The example copies the text into memory from malloc, reads it into a ustring (a copy) and only then calls free. A C library's documentation says who frees what it returns; the C runtime's free is right only for the C runtime's malloc.

Activities

  1. Declare C's strrchr and print what follows the last = in "a=b=c".
  2. Declare toupper and use it to upper-case a zstring * 16 in place.

Callbacks

Some C functions take a pointer to a function and call back into the program. C's qsort sorts any array, and calls a comparison function you supply each time it needs to order two elements.

'' C's qsort calls back into the program to compare two elements.
type Compare as function cdecl( byval a as const any ptr, _
                                byval b as const any ptr ) as int32

extern "C"
    declare sub qsort( byval base_ as any ptr, byval n as uinteger, _
                       byval size as uinteger, byval cmp as Compare )
end extern

'' A named procedure: cdecl, and exactly the parameters C passes.
function Ascending cdecl( byval a as const any ptr, _
                          byval b as const any ptr ) as int32
    dim as int32 x = *cptr( const int32 ptr, a )
    dim as int32 y = *cptr( const int32 ptr, b )
    return iif( x < y, -1, iif( x > y, 1, 0 ) )
end function

sub Show( nums( ) as int32 )
    for i as int32 = 0 to ubound( nums )
        print nums( i );
    next
    print
end sub

dim nums( 0 to 5 ) as int32 = { 42, 7, 19, 3, 88, 21 }
qsort( @nums( 0 ), 6, sizeof( int32 ), @Ascending )
Show( nums( ) )

'' A lambda that captures nothing is a plain function too.
qsort( @nums( 0 ), 6, sizeof( int32 ), _
       function cdecl( byval a as const any ptr, _
                       byval b as const any ptr ) as int32
           return *cptr( const int32 ptr, b ) - _
                  *cptr( const int32 ptr, a )
       end function )
Show( nums( ) )

Output

 3 7 19 21 42 88
 88 42 21 19 7 3

A tiko procedure can be the callback if it is cdecl and takes exactly the parameters C passes, with the right types. @Ascending is its address. The type Compare as function cdecl( ... ) alias names the pointer type, so the qsort declaration reads the way C's does.

qsort hands the comparison two const any ptrs to elements of the array, and expects a negative number, zero or a positive number. Ascending compares instead of subtracting, which cannot overflow; the lambda's subtraction is fine for these small values.

A lambda that captures nothing is an ordinary function, so it converts to a function pointer and can be passed straight to C. A lambda that captures variables cannot: it carries the captured values with it, and C has nowhere to put them. C's own way to pass state into a callback is the extra void * parameter that most callback interfaces have, often called user or context: pass a pointer to your data, and cast it back inside the callback.

Lambdas are covered in Procedures.

Activities

  1. Sort an array of float64 values with qsort.
  2. Sort the integers by their last digit.

Structs and layout

A type lays its fields out the way C lays out a struct on the same platform. So a type with the same fields in the same order as a C struct has the same size and offsets, and C can fill one in, read one, or return one.

'' A type has C's struct layout, so C can fill one in or return one.
type DivResult
    as int32 quot
    as int32 rem_       '' a field cannot be named rem: rem is a comment
end type

extern "C"
    declare function div( byval num as int32, byval den as int32 ) _
                     as DivResult
end extern

var r = div( 47, 10 )
print "div( 47, 10 ): quot"; r.quot; ", rem"; r.rem_

'' Natural alignment: value starts on a 4-byte boundary, as in C.
type Natural
    as uint8 tag
    as int32 value
end type

'' field = 1 packs the fields, as C's #pragma pack( 1 ) does.
type Packed field = 1
    as uint8 tag
    as int32 value
end type

print "Natural: size"; sizeof( Natural ); _
      ", value at"; offsetof( Natural, value )
print "Packed:  size"; sizeof( Packed ); _
      ", value at"; offsetof( Packed, value )

'' A fixed-size zstring is an inline char array, like C's char[ 8 ].
type Record
    as zstring * 8 label
    as int32 id
end type
print "Record:  size"; sizeof( Record ); _
      ", id at"; offsetof( Record, id )

Output

div( 47, 10 ): quot 4, rem 7
Natural: size 8, value at 4
Packed:  size 5, value at 1
Record:  size 12, id at 8

C's div returns a div_t, a struct of two ints, by value. DivResult has the same two fields, so the declaration can say it returns a DivResult, and tiko passes it back by the platform's rules exactly as a C compiler would. The field names do not have to match C's, only the types and the order; rem is the one name that cannot be used, because rem starts a comment.

Alignment. Each field sits on a boundary of its own size, with padding before it where needed, and the whole type is rounded up to its largest alignment. That is why Natural is 8 bytes and not 5. offsetof( T, field ) gives a field's offset, to check against the C header.

Packing. field = 1 removes the padding, as C's #pragma pack( 1 ) does; field = 2 and field = 4 limit the alignment the same way. Use them only to match a C struct that is packed. An unpacked C struct read through a packed type is garbage.

A fixed-size zstring * n is an inline array of n characters, like C's char name[ n ]. A ustring field is not: it holds a pointer to its text, and never belongs in a struct that C will read.

Activities

  1. Add an int64 field after value in Natural and predict the new size.
  2. Declare C's lldiv, which returns two long longs, and call it.

Reading a C header

Writing a binding by hand means copying every type and declaration out of a header and getting each one right. #cinclude reads the header itself: it hands the header to clang, the C compiler, and turns what clang reports into tiko declarations, for the platform being built.

'' #cinclude reads a C header and makes tiko declarations from it.
#cinclude once "stdlib.h"
#cinclude once "string.h"

'' div_t and div come from the header. A field named like a tiko
'' keyword gets an underscore: C's rem is rem_ here.
dim as div_t d = div( 47, 10 )
print "div( 47, 10 ):"; d.quot; ","; d.rem_

'' So do strlen and qsort, with C's own parameter types.
print "strlen: "; strlen( "header" )

function ByValue cdecl( byval a as const any ptr, _
                        byval b as const any ptr ) as int32
    return *cptr( const int32 ptr, a ) - *cptr( const int32 ptr, b )
end function

dim nums( 0 to 4 ) as int32 = { 5, 3, 9, 1, 7 }
qsort( @nums( 0 ), 5, sizeof( int32 ), @ByValue )
for i as int32 = 0 to 4
    print nums( i );
next
print

'' A #define that is a constant comes across too.
print "RAND_MAX is positive: "; cbool( RAND_MAX > 0 )

Output

div( 47, 10 ): 4, 7
strlen: 6
 1 3 5 7 9
RAND_MAX is positive: true

The example has no type, no declare and no extern. div_t, div, strlen, qsort and the constant RAND_MAX all come from the C runtime's own headers, as they are written on the platform being built. C's int becomes a 32-bit integer and its long becomes clong, so the types are right on every platform without your having to think about them.

What comes across: structs and unions, enums, typedefs, functions, function pointer types, global variables, and #defines that are constants. Macros with parameters do not, since they are C code rather than declarations. Every name keeps its C spelling through alias, and a name that would collide with a tiko keyword or function gets an underscore: C's field rem is rem_, and C's abs is abs_, so abs stays tiko's own.

A quoted header is looked for under the directory the compiler runs in, then beside the file that says #cinclude, and then among the system's headers. -cinclude dir adds a directory to search, and tiko -cimport header.h -cimport-out - prints the declarations a header becomes, without compiling anything.

The layout of every imported struct is checked against clang's, field by field, so a struct comes across correctly or not at all.

Activities

  1. #cinclude "ctype.h" and call isdigit on each character of "a1b2".
  2. Run tiko -cimport string.h -cimport-out - and find strlen in the output.

Loading a library at run time

A library linked when the program is built must be present for the program to start at all. dylibload opens a shared library while the program runs, so the program can decide what to do when a plugin or an optional library is missing.

'' Load a library while the program runs, and find a function in it.
#if defined( __FB_WIN32__ )
    const LIBNAME = "ucrtbase.dll"
#elseif defined( __FB_DARWIN__ )
    const LIBNAME = "libm.dylib"
#else
    const LIBNAME = "libm.so.6"
#endif

type MathFn as function cdecl( byval x as float64 ) as float64

dim as any ptr lib_ = dylibload( LIBNAME )
if lib_ = nullptr then
    print "could not load the maths library"
    end 1
end if

'' dylibsymbol returns an address, or nullptr for a name it lacks.
dim as MathFn root = dylibsymbol( lib_, "sqrt" )
dim as MathFn missing = dylibsymbol( lib_, "no_such_function" )
print "found sqrt: "; cbool( root <> nullptr )
print "found no_such_function: "; cbool( missing <> nullptr )

if root <> nullptr then
    print "sqrt( 144 ) ="; root( 144.0 )
end if

'' A library that is not there is a null handle, not a crash.
print "missing library loads: "; _
      cbool( dylibload( "no_such_library_here" ) <> nullptr )

dylibfree( lib_ )

Output

found sqrt: true
found no_such_function: false
sqrt( 144 ) = 12
missing library loads: false
CallDoes
dylibload( name )opens the library and returns a handle, or nullptr
dylibsymbol( handle, "name" )finds a function or variable by name and returns its address, or nullptr
dylibfree( handle )closes the library

Library names differ by platform: name.dll on Windows, libname.so on Linux, often with a version number as in libm.so.6, and libname.dylib on macOS. The example loads the C runtime's maths library on each, chosen with #if defined( __FB_WIN32__ ) and its #elseifs, and calls its sqrt.

dylibsymbol returns an untyped address. Assigning it to a variable of a function pointer type, here MathFn, makes it callable, and that type is a promise exactly as a declare is: nothing checks it. A plugin system usually agrees on one entry point with a fixed signature, looks that up, and gets everything else through it.

After dylibfree, every address taken from the library is invalid. Look a symbol up once and keep the pointer while the library is open; the search costs time.

Activities

  1. Look up cbrt as well and print the cube root of 27.
  2. Make the program print a message and carry on when the library is missing, instead of ending.

Testing

A program that compiles is a program the compiler could read. Whether it does what you meant is a separate question, and a test answers it every time you build. tiko ships a unit-test library, fbcunit.bi: tests are blocks of code that check results with assertion macros, grouped into suites, and one call runs them all and prints a summary.

'' A procedure, and the tests that pin its behaviour down.
#include once "fbcunit.bi"

function Clamp( byval v as int32, byval lo as int32, _
                byval hi as int32 ) as int32
    if v < lo then return lo
    if v > hi then return hi
    return v
end function

SUITE( clamping )
    TEST( inside_the_range )
        CU_ASSERT_EQUAL( Clamp( 5, 0, 10 ), 5 )
    END_TEST

    TEST( on_the_edges )
        CU_ASSERT_EQUAL( Clamp( 0, 0, 10 ), 0 )
        CU_ASSERT_EQUAL( Clamp( 10, 0, 10 ), 10 )
    END_TEST

    TEST( outside_the_range )
        CU_ASSERT_EQUAL( Clamp( -3, 0, 10 ), 0 )
        CU_ASSERT_EQUAL( Clamp( 99, 0, 10 ), 10 )
    END_TEST
END_SUITE

'' Run every test, print the summary, and exit 0 only if all passed.
end iif( fbcu.run_tests( ), 0, 1 )

Output


SUMMARY

 Asserts    Passed    Failed  Suite                                      Tests
--------  --------  --------  --------------------------------------  --------
       5         5         0  clamping                                       3
--------  --------  --------  --------------------------------------  --------
       5         5         0  Total                                          3
  • #include once "fbcunit.bi" brings in the library. It is a header, not a module, so it is included rather than imported.
  • SUITE( clamping ) … END_SUITE groups related tests. Each TEST( name ) … END_TEST is one test, and its body is ordinary code.
  • CU_ASSERT_EQUAL( a, b ) records a pass when a = b and a failure otherwise. The summary counts both, per suite.
  • fbcu.run_tests( ) runs every test, prints the summary, and returns true only if every assertion passed. end iif( …, 0, 1 ) turns that into the program's exit code, which is what a build script or a CI job checks.

There is no separate test command: a test program is an ordinary program, built with tiko firsttests.tiko and run like any other.

Name a suite after the behaviour it checks, clamping, not after the procedure it tests, Clamp. A suite is a namespace, and inside a suite called clamp the name Clamp would mean the suite, not the function.

Activities

  1. Add a test that Clamp( 5, 5, 5 ) is 5.
  2. Break Clamp on purpose and watch the summary and the exit code change.

Assertions

Each assertion macro checks one thing and records the result. Choosing the macro that says what you mean makes a failure message easier to read.

#include once "fbcunit.bi"

function Initials( byval first as ustring, _
                   byval last as ustring ) as ustring
    return ucase( left( first, 1 ) & left( last, 1 ) )
end function

SUITE( assertions )
    TEST( truth )
        CU_ASSERT( len( Initials( "ada", "lovelace" ) ) = 2 )
        CU_ASSERT_TRUE( Initials( "ada", "lovelace" ) = "AL" )
        CU_ASSERT_FALSE( Initials( "ada", "lovelace" ) = "al" )
    END_TEST

    TEST( equality )
        CU_ASSERT_EQUAL( Initials( "alan", "turing" ), "AT" )
        CU_ASSERT_NOT_EQUAL( Initials( "alan", "turing" ), "TA" )
    END_TEST

    TEST( floating_point )
        '' 0.1 + 0.2 is not exactly 0.3; compare within a tolerance.
        dim as float64 sum = 0.1 + 0.2
        CU_ASSERT_DOUBLE_EQUAL( sum, 0.3, 1e-12 )
        CU_ASSERT_NOT_EQUAL( sum, 0.3 )
    END_TEST
END_SUITE

end iif( fbcu.run_tests( ), 0, 1 )

Output


SUMMARY

 Asserts    Passed    Failed  Suite                                      Tests
--------  --------  --------  --------------------------------------  --------
       7         7         0  assertions                                     3
--------  --------  --------  --------------------------------------  --------
       7         7         0  Total                                          3
MacroPasses when
CU_ASSERT( c )c is true
CU_ASSERT_TRUE( c ), CU_ASSERT_FALSE( c )c is true, or false
CU_ASSERT_EQUAL( a, b )a = b
CU_ASSERT_NOT_EQUAL( a, b )a <> b
CU_ASSERT_DOUBLE_EQUAL( a, e, g )a is within g of e
CU_FAIL( msg )never: marks code a test must not reach

The floating_point test shows why CU_ASSERT_DOUBLE_EQUAL exists. 0.1 + 0.2 is not exactly 0.3 in binary floating point, so CU_ASSERT_NOT_EQUAL( sum, 0.3 ) passes. A test of a computed float64 compares it within a tolerance that fits the calculation.

A failed assertion does not stop its test: the test carries on, and every failure is listed before the summary. That is what lets one test hold several related checks.

Activities

  1. Add CU_ASSERT_EQUAL( sum, 0.3 ) to floating_point and read the failure.
  2. Tighten the tolerance to 1e-20 and see whether the test still passes.

A failing test

A test earns its keep the day it fails. Here WordCount counts the spaces instead of the words, and the test catches it:

#include once "fbcunit.bi"

'' Meant to count the words in s -- but it counts the spaces.
function WordCount( byval s as ustring ) as int32
    dim as int32 n = 0
    for i as int32 = 1 to len( s )
        if mid( s, i, 1 ) = " " then n += 1
    next
    return n
end function

SUITE( words )
    TEST( counts_words )
        CU_ASSERT_EQUAL( WordCount( "" ), 0 )
        CU_ASSERT_EQUAL( WordCount( "one two three" ), 3 )
        CU_ASSERT_EQUAL( WordCount( "single" ), 1 )
    END_TEST
END_SUITE

end iif( fbcu.run_tests( ), 0, 1 )

Output

      failing.tiko(15) : error : TESTS.WORDS.COUNTS_WORDS CU_ASSERT_EQUAL(WordCount( "one two three" ),3)
      failing.tiko(16) : error : TESTS.WORDS.COUNTS_WORDS CU_ASSERT_EQUAL(WordCount( "single" ),1)

SUMMARY

 Asserts    Passed    Failed  Suite                                      Tests
--------  --------  --------  --------------------------------------  --------
       3         1         2  words                                          1
--------  --------  --------  --------------------------------------  --------
       3         1         2  Total                                          1

The program exits with code 1.

Each failed assertion is printed before the summary with three things you need to find it: the file and line, the test's full name (TESTS.WORDS.COUNTS_WORDS, the suite and test in capitals), and the assertion as it was written. The first check passed, because the empty string has no spaces either; the other two failed.

The summary's Failed column is the number of failed assertions, not of failed tests, and run_tests returns false when it is anything but zero. That is why the exit code is 1: a build script that runs the tests stops here.

Write the test that shows a bug before you fix the bug. When the test passes, you know the fix works, and the test stays to make sure the bug does not come back.

Activities

  1. Fix WordCount (count the starts of words, not the spaces) and run the tests again.
  2. Add CU_ASSERT_EQUAL( WordCount( "two spaces" ), 2 ) and check that your fix handles it.

Testing a module

In a real project the code under test lives in a module, and its tests are a separate program that imports it. The module is the one from the failing test, fixed:

#module "wordstats"

'' The number of words in s, where words are separated by spaces.
public function WordCount( byval s as ustring ) as int32
    dim as int32 n = 0
    dim as boolean inWord = false
    for i as int32 = 1 to len( s )
        if mid( s, i, 1 ) = " " then
            inWord = false
        elseif not inWord then
            inWord = true
            n += 1
        end if
    next
    return n
end function

'' The longest word in s; the first one when several tie.
public function LongestWord( byval s as ustring ) as ustring
    dim as ustring best, word
    for i as int32 = 1 to len( s ) + 1
        var c = mid( s, i, 1 )
        if c = " " orelse c = "" then
            if len( word ) > len( best ) then best = word
            word = ""
        else
            word &= c
        end if
    next
    return best
end function

and its tests:

'' The tests of the wordstats module: a program of their own, which
'' sees the module exactly as any other importer does.
#import "./wordstats"
#include once "fbcunit.bi"

SUITE( text_statistics )
    TEST( counts_words )
        CU_ASSERT_EQUAL( wordstats.WordCount( "" ), 0 )
        CU_ASSERT_EQUAL( wordstats.WordCount( "single" ), 1 )
        CU_ASSERT_EQUAL( wordstats.WordCount( "one two three" ), 3 )
    END_TEST

    TEST( ignores_extra_spaces )
        CU_ASSERT_EQUAL( wordstats.WordCount( "  two   words " ), 2 )
    END_TEST

    TEST( finds_the_longest_word )
        CU_ASSERT_EQUAL( wordstats.LongestWord( "a quick fox" ), "quick" )
        CU_ASSERT_EQUAL( wordstats.LongestWord( "ab cd" ), "ab" )
        CU_ASSERT_EQUAL( wordstats.LongestWord( "" ), "" )
    END_TEST
END_SUITE

end iif( fbcu.run_tests( ), 0, 1 )

Output


SUMMARY

 Asserts    Passed    Failed  Suite                                      Tests
--------  --------  --------  --------------------------------------  --------
       7         7         0  text_statistics                                3
--------  --------  --------  --------------------------------------  --------
       7         7         0  Total                                          3

The test program sees the module exactly as any other importer does: its public procedures, through the module's name, and nothing else. Tests written against the public interface keep passing while you rearrange the inside of the module, and fail when you change what its callers depend on, which is the change you want to hear about.

#import lines come before any other code, so the #include of the test library goes after them.

Pitfall

Tests do not run in the same order on every platform. Each TEST and each SUITE registers itself when the program starts, and the two platforms walk those registrations in opposite orders: a program with two suites lists them in declaration order on Linux and in reverse on Windows, and run verbosely, with fbcu.run_tests( true, true ), it lists the tests of one suite the same way. Nothing goes wrong while every test stands alone. A test that relies on something an earlier test did passes on one platform and fails on the other. Give every test its own setup.

Activities

  1. Add a public UpperWords to wordstats that capitalises each word, and a test for it.
  2. Try to test a procedure of the module that is not public, and read the diagnostic.

assert in tests

Not every check needs a test library. assert( condition ) checks a condition while the program runs, does nothing when it holds, and stops the program when it does not. It suits quick self-checks in a small test program, and checks of assumptions inside the code itself.

'' assert checks a condition while the program runs. It is compiled
'' in only when asked for: -eassert here, or any -g build.
#cmdline "-eassert"

function Half( byval n as int32 ) as int32
    return n \ 2
end function

sub TestHalf( )
    assert( Half( 10 ) = 5 )
    assert( Half( 0 ) = 0 )
    assert( Half( -6 ) = -3 )
end sub

TestHalf( )
print "all checks passed"

Output

all checks passed

Every condition held, so the only output is the program's own line. When one fails:

'' A failed assert reports its file, line, procedure and condition on
'' standard error, and ends the program with exit code 1.
#cmdline "-eassert"

function Half( byval n as int32 ) as int32
    return n \ 2
end function

sub TestHalf( )
    assert( Half( 10 ) = 5 )
    assert( Half( 7 ) = 4 )        '' wrong: 7 \ 2 is 3
    print "not reached"
end sub

TestHalf( )

Output

assertfail.tiko(11): assertion failed at TESTHALF: Half( 7 ) = 4

The program exits with code 1. The message goes to standard error and names the file, the line, the procedure (in capitals) and the condition as written. The first failure ends the program, so not reached is never printed: unlike a test library's assertions, assert cannot report two failures in one run.

assert is compiled in only when you ask for it. -eassert turns it on, and so does -g. Built without either, every assert vanishes from the program, and assertfail.tiko would print not reached and exit with 0. So never put work the program needs inside an assert: the call disappears with the check.

assertwarn( condition ) is the gentler form: it prints the same message and lets the program carry on.

More on errors at run time is in Error Handling.

Activities

  1. Remove the #cmdline line, build assertfail.tiko again, and run it.
  2. Change the second assert to assertwarn and see what is printed now.

Tracing

Sometimes the quickest way to see what a program did is to have it tell you. A trace macro built on the source information names reports where each message came from, and the preprocessor can remove it completely from a finished build.

'' TRACE( msg ) reports where it was used, on standard error. Remove
'' the #define and every TRACE compiles to nothing.
#define TRACING

#ifdef TRACING
    #define TRACE( msg ) TraceAt( __FILE__, __LINE__, __FUNCTION__, msg )
#else
    #define TRACE( msg )
#endif

sub TraceAt( byval file as ustring, byval lineNo as int32, _
             byval proc as ustring, byval msg as ustring )
    var f = freefile( )
    open err for output as #f
    print #f, file & "(" & lineNo & ") " & proc & ": " & msg
    close #f
end sub

function Mean( values( ) as float64 ) as float64
    var n = ubound( values ) - lbound( values ) + 1
    TRACE( "count " & n )
    dim total as float64
    for i as int32 = lbound( values ) to ubound( values )
        total += values( i )
    next
    TRACE( "total " & total )
    return total / n
end function

sub Report( )
    dim readings( 0 to 3 ) as float64 = { 3.5, 4.0, 5.5, 3.0 }
    var m = Mean( readings( ) )
    TRACE( "mean " & m )
end sub

Report( )

Output (on standard error)

tracing.tiko(21) MEAN: count 4
tracing.tiko(26) MEAN: total 16
tracing.tiko(33) REPORT: mean 4

Two decisions in this program are worth copying:

  • Trace output goes to standard error. open err opens the standard error stream as a file number. The program's real output stays on standard output, so program > result.txt still captures exactly the result, and the trace goes to the screen, or to a file of its own with 2> trace.txt.
  • Tracing is switched off by the preprocessor, not by an if. Remove the #define TRACING line and TRACE( … ) expands to nothing: the calls and the work of building their messages are gone from the program. TraceAt itself is still compiled; nothing calls it.

__FILE__, __LINE__ and __FUNCTION__ in the macro's body expand where the macro is used, so each line names the procedure and line of the TRACE call, not of the #define.

Note

Standard output and standard error are separate streams with separate buffers. When both go to the same terminal, how their lines interleave is not fixed. Send a trace and the output it describes to different places, or send everything through the trace.

Activities

  1. Add a TRACE inside the for loop and watch the output grow.
  2. Remove #define TRACING and run the program: what does it print now?

Debugging in tkEdit

When a result is wrong and you do not know where, stop the program and look. tkEdit, the editor that comes with tiko, has a debugger: set a breakpoint, run to it, and read every variable while the program waits.

'' A program to stop inside: in tkEdit, F9 on the "total +=" line, then F6.
type Score
    player as ustring
    points as int32
end type

function Average( scores( ) as Score ) as float64
    dim total as int32
    for i as int32 = lbound( scores ) to ubound( scores )
        total += scores( i ).points
    next
    return total / ( ubound( scores ) - lbound( scores ) + 1 )
end function

dim scores( 1 to 3 ) as Score
scores( 1 ).player = "Ana"  : scores( 1 ).points = 12
scores( 2 ).player = "Bo"   : scores( 2 ).points = 15
scores( 3 ).player = "Cyd"  : scores( 3 ).points = 21
print "average:"; Average( scores( ) )

Output

average: 16

Open the file in tkEdit, put the cursor on line 10 (total += …) and press F9: a red circle in the margin marks the breakpoint. Press F6. tkEdit builds the program with -g -exx and without -O, runs it in its terminal, and stops on line 10 inside Average, with a yellow arrow in the margin. The output pane opens on its Debug tab, and the Locals tab shows:

NameValueType
scores(1 to 3)Score()
total0long
i1long
  • scores opens to scores(1), scores(2) and scores(3): elements are named by the subscripts the program uses, so they match the source. Each element opens to player and points, and a ustring shows its text, u"Ana". An array with more than a hundred elements opens to groups of them first.
  • The Type column uses the debugger's names for the numeric types: an int32 is a long and a float64 a double.
  • F10 runs line 10 and stops on the next one; press it again and again to watch total grow and i count. A value that changed since the last stop is shown in red.
  • F6 continues to the next breakpoint — here, line 10 again, for the next element. Take the breakpoint away with F9 first, and Shift+F11 runs to the end of Average and stops back in the main program.

The keys:

Key
F9breakpoint on the cursor's line, on or off
F6build and start; in a session, continue
F10 / F11 / Shift+F11step over / step into / step out
Ctrl+F10run to the cursor's line
Ctrl+F6break into the running program
Shift+F6stop; the program ends
Ctrl+Shift+F9remove every breakpoint

On macOS each Ctrl is Cmd. All of them are on the Debug menu too.

Watch takes a name the way the program writes it: add a row with the + button, type scores(2).player, and press Enter. Case does not matter, and the subscript is the program's. A watch is a variable, a field or an element, not a calculation, and a name the stopped procedure cannot see shows <not in scope>.

Call Stack lists Average and, below it, main, the main program; a click on a frame shows that frame's locals and goes to its line. Breakpoints lists them all, and the buttons at the end of its tab row remove the selected one, or every one. Resting the mouse on a name in the source shows its value.

lldb-dap

tkEdit drives lldb-dap, from LLVM. On Windows it is beside clang, and it needs Python 3.10 (64-bit) on the PATH. On Linux it is in the lldb package. On macOS it comes with the Command Line Tools, and macOS lets it control a program only in Developer Mode (sudo DevToolsSecurity -enable) or when you allow it. When it is somewhere else, set its path as adapter in the [debug] table of settings.toml.

Activities

  1. Step through the loop with F10 and stop when total is 27. What is i?
  2. Add a watch on total / 3 and on total. Which one shows a value, and why?
  3. Set a second breakpoint on the print line, remove the first one on the Breakpoints tab, and press F6 again.

Building

Every program so far was built the same way: one command, one source file, one executable. The tiko command does much more than that. Its options choose checks, optimisation and debug information; it compiles files separately and builds libraries; and for a project with a tiko.toml it builds everything the project declares, remembering what it built before.

A program can see how it was built. With no options at all:

'' Built with no options: tiko defaults.tiko -o defaults
'' Each option defines a symbol the source can test.

print "this build:"
print "runtime error checks: "; cbool( __FB_ERR__ <> 0 )
print "thread-safe runtime:  "; cbool( __FB_MT__ )
print "debug build:          "; cbool( __FB_DEBUG__ )
print "optimisation level:   "; __FB_OPTIMIZE__

Output

this build:
runtime error checks: false
thread-safe runtime:  false
debug build:          false
optimisation level:    0

That is the default build: no runtime checks, no debug information, no optimisation, and the ordinary runtime. Each option below changes one of those, and defines a symbol the program can test.

OptionDoes
-o filenames the output file, exactly as written; without it, the program is named after the first source file (app.exe on Windows, app on Linux)
-ccompiles to an object file and does not link
-libbuilds a static library, libname.a
-eruntime error checks
-exx-e, plus array-bounds and null-pointer checks, and where each error happened
-gdebug information for a debugger; also turns assert on
-O0 … -O3how hard the optimiser works; -O0, none, is the default
-mtthe thread-safe runtime; std/tasks requires it, and a threadcreate call turns it on by itself
-target namewindows-x86_64 or linux-x86_64; the platform you are on is the default
-d NAME=valuedefines a preprocessor symbol
-vshows every tool tiko runs

tiko --help lists every option, by category.

The pages that follow put options in the source, show what a checked build catches, compile files separately and into a library, build projects with profiles, and look at the two targets.

Activities

  1. Build defaults.tiko with -exx -O2 on the command line and compare the output.
  2. Build it with -g and check which lines change.

Options in the source

A program that needs particular options to build correctly should not depend on someone remembering to type them. #cmdline writes them in the source, so the file alone says how it is built:

'' A program can see how it was built: each option it was given
'' defines a symbol the source can test.
#cmdline "-exx -mt -O2"

print "this build:"
print "runtime error checks: "; cbool( __FB_ERR__ <> 0 )
print "thread-safe runtime:  "; cbool( __FB_MT__ )
print "debug build:          "; cbool( __FB_DEBUG__ )
print "optimisation level:   "; __FB_OPTIMIZE__

Output

this build:
runtime error checks: true
thread-safe runtime:  true
debug build:          false
optimisation level:    2

This is the same program as on the previous page, with one line added. When the compiler meets #cmdline, it starts the compile again with those options added, so tiko buildinfo.tiko builds a checked, optimised, thread-safe program. Every example on this site that needs an option carries it this way.

  • The source wins. An option in #cmdline takes precedence over the same option on the command line.
  • Only the first file counts. #cmdline in any other source file named on the command line is warning TKO0334 and is ignored.

The program can name its own output with -x, which takes the file name exactly as written:

'' The source names its own program. -x names the output exactly as
'' written, and an option in #cmdline wins over the command line's.
#cmdline "-x renamed.exe"

print "built as renamed.exe"

Output (of renamed.exe)

built as renamed.exe

tiko named.tiko writes renamed.exe, and so does tiko named.tiko -o other.exe: -x wins over -o, and the source wins over the command line. Like -o, -x uses the name exactly as written, so give the extension yourself; a program for Linux usually has none.

Activities

  1. Add -g to buildinfo.tiko's #cmdline and predict the new output.
  2. Build buildinfo.tiko with -O0 on the command line. Which level does the program report?

Checked builds

The same source can be built to find bugs or to run fast. A checked build adds code that tests every array index and every pointer, and stops the program at the first bad one, with its location:

'' A checked build: -exx adds array-bounds and null-pointer checks,
'' and records where each error happens.
#cmdline "-exx"

function Total( scores( ) as int32, byval count as int32 ) as int32
    dim as int32 sum = 0
    for i as int32 = 1 to count
        sum += scores( i )
    next
    return sum
end function

dim scores( 1 to 3 ) as int32 = { 70, 85, 90 }
print Total( scores( ), 4 )        '' one too many

Output


Aborting due to runtime error 6 (out of bounds array access) at line 8 of checked.tiko::TOTAL(), 
'SCORES' accessed with invalid index = 4, must be between 1 and 3

The program exits with code 6, the number of the runtime error, and the message goes to standard error. It names the line, the procedure and the array, the index used and the bounds the array has, which is usually enough to see the bug without a debugger: the loop runs to count, and the caller passed one more than the array holds.

Without -exx, nothing checks the index. The same program reads whatever memory lies past the end of the array, adds it to the total, and prints a number as if nothing were wrong.

BuildOptionsUse it for
checked-exx, often with -gdevelopment and tests
release-O2 or -O3the program you ship

The checks cost time: every index is compared with both bounds, and every pointer with null. Test with the checks on and ship with them off, or, for a program where a wrong answer costs more than speed buys, ship checked. The parts of -exx are available one at a time as -earray, -enullptr and -elocation.

Arrays and Error Handling say more about the errors themselves.

Activities

  1. Remove the #cmdline line, build again, and run the program.
  2. Replace -exx with -earray and compare the message.

Separate compilation

A large program is built a part at a time. -c compiles a source file without linking it, into an object file; a later command links the objects into a program. A part whose source has not changed need not be compiled again.

'' Compiled on its own: tiko -c greet.tiko writes greet.o.
sub Greet( byval who as ustring )
    print "hello, "; who
end sub

'' The top level of a file that is not the main one runs before the
'' program starts.
print "greet.tiko is set up"
'' Linked with the object file: tiko main.tiko greet.o
declare sub Greet( byval who as ustring )

print "main starts"
Greet( "world" )

Built with two commands:

tiko -c greet.tiko
tiko main.tiko greet.o

Output

greet.tiko is set up
main starts
hello, world
  • tiko -c greet.tiko writes greet.o.
  • tiko main.tiko greet.o compiles main.tiko and links it with the object. main.tiko declares Greet with declare, because it is compiled on its own and knows only what it has been told.
  • The first file named is the main one. Its top-level code is the program. The top-level code of every other file runs before it, which is why greet.tiko is set up comes first.

For your own code, modules do this job with no declare lines and no build commands: #import compiles each module once and caches it. Separate compilation is what you use with code that is not a module, and underneath every library.

Activities

  1. Name the object file first: tiko greet.o main.tiko. Does the order change the output?
  2. Remove the declare line from main.tiko and read the diagnostic.

Static libraries

A static library packs compiled code into one file that other programs link. The linker copies in the parts a program uses, so the program runs with nothing beside it.

'' A small library: tiko -lib mathlib.tiko writes libmathlib.a.
function Gcd( byval a as int64, byval b as int64 ) as int64
    do while b <> 0
        dim as int64 t = a mod b
        a = b
        b = t
    loop
    return abs( a )
end function

function Lcm( byval a as int64, byval b as int64 ) as int64
    return ( a \ Gcd( a, b ) ) * b
end function
'' #inclib names the library to link: libmathlib.a, found here.
#inclib "mathlib"

declare function Gcd( byval a as int64, byval b as int64 ) as int64
declare function Lcm( byval a as int64, byval b as int64 ) as int64

print "Gcd( 84, 36 ) ="; Gcd( 84, 36 )
print "Lcm( 4, 6 )   ="; Lcm( 4, 6 )

'' The smallest number that 1 to 20 all divide.
dim as int64 n = 1
for i as int32 = 2 to 20
    n = Lcm( n, i )
next
print "Lcm( 1..20 )  ="; n

Built with:

tiko -lib mathlib.tiko
tiko uselib.tiko

Output

Gcd( 84, 36 ) = 12
Lcm( 4, 6 )   = 12
Lcm( 1..20 )  = 232792560
  • tiko -lib mathlib.tiko compiles the file and writes libmathlib.a, on both targets.
  • #inclib "mathlib" tells the linker to use it. The name has no lib prefix and no extension. -l mathlib on the command line does the same.
  • The linker searched the current directory. -p dir on the command line, or #libpath "dir" in the source, adds other places to look.

The program declares the two functions itself. A library meant for others ships a header with those declarations, which every user includes. The parentheses in Lcm matter: \ binds more loosely than *, so a \ Gcd( a, b ) * b would divide by the product.

Activities

  1. Move the two declare lines into mathlib.ti and #include it.
  2. Add a Factorial function to the library and use it from the program.

Projects

Command lines grow with a program: every file, every option and the output's name typed again for each build. A project writes them down once, in a tiko.toml, and tiko build reads it.

# One program, built from main.tiko and the module it imports.
[package]
name = "greeter"

[target.greeter]
main = "main.tiko"

# As though -d TIMES=2 had been typed.
[target.greeter.defines]
TIMES = "2"
'' The main file of a project. tiko.toml beside it says how to build
'' it, so 'tiko build' needs no file names and 'tiko run' no -o.
#import "./text"

dim as ustring who = "world"
if len( command( 1 ) ) > 0 then who = command( 1 )
for i as int32 = 1 to TIMES
    print text.Greeting( who )
next
#module "text"

public function Greeting( byval who as ustring ) as ustring
    return "Hello, " & who & "!"
end function

tiko run -- tiko, in the project's directory:

Output

Hello, tiko!
Hello, tiko!
  • [target.greeter] declares a target: something the project builds, here an executable called greeter, whose main file is main.tiko.
  • tiko build, run in the directory that holds tiko.toml, builds every target it declares. It takes no file names and no -o: the program is written to out/debug/greeter.exe on Windows and out/debug/greeter on Linux. tiko build greeter builds the one target by name.
  • [target.greeter.defines] passes TIMES as though -d TIMES=2 had been typed, which is why the greeting appears twice.
  • tiko run builds the project's executable and runs it. Everything after -- goes to the program, so tiko run -- tiko greets tiko; without it, the program greets world.

tiko build remembers what it compiled. A second build with nothing changed compiles nothing and does not even link; tiko build -v says so:

module greeter.text: hit
unit main.tiko: hit
link: out/debug/greeter.exe is up to date
CommandDoes
tiko build [target]builds the targets, into out/<profile>/
tiko run [target] [-- args]builds one executable target and runs it
tiko checkreports every error and warning a build would, and writes nothing
tiko cleanremoves what tiko build wrote

A target takes more keys: sources for further files, kind = "lib" for a static library, options for compiler options, libs and libpaths for libraries to link. Every path in tiko.toml is relative to the tiko.toml itself, so the project builds the same from any directory. A project that depends on packages of modules lists them under [dependencies], as the Packages page shows.

Activities

  1. Change TIMES to 3 in tiko.toml and run the project again.
  2. Run tiko check after introducing a typo in main.tiko, and confirm that no program was written.

Profiles

The debug in out/debug is the project's profile: a named set of compiler options. tiko build and tiko run use debug unless told otherwise, and --profile chooses another. This program reports what its profile gave it:

[package]
name = "report"

[target.report]
main = "main.tiko"

# A profile of your own: the release options, plus the
# thread-safe runtime.
[profile.fast]
inherits = "release"
options  = ["-mt"]
'' Prints the options its profile built it with.
print "this build:"
print "debug information:   "; cbool( __FB_DEBUG__ )
print "error checks:        "; cbool( __FB_ERR__ <> 0 )
print "optimisation level:  "; __FB_OPTIMIZE__
print "thread-safe runtime: "; cbool( __FB_MT__ )

Built with tiko build, the default profile:

Output

this build:
debug information:   true
error checks:        true
optimisation level:   0
thread-safe runtime: false

The other two profiles, built and run the same way, print:

> tiko build --profile release
this build:
debug information:   false
error checks:        false
optimisation level:   2
thread-safe runtime: false
> tiko build --profile fast
this build:
debug information:   false
error checks:        false
optimisation level:   2
thread-safe runtime: true
ProfileOptions
debug-g -exx -O0: a checked build, with debug information
release-O2

Both are built in. A [profile.name] table changes one or declares another:

KeySets
inheritsthe profile this one starts from
optthe optimisation level, 0 to 3
errorsthe checks: "e", "ex", "exx", or "" for none
debugwhether -g is given
optionsfurther compiler options

Each profile writes to its own directory, out/debug, out/release, out/fast, so a debug build and a release build of the same project sit side by side. Each profile's compiled files are cached separately, so switching back to a profile does not compile again what it compiled before.

Activities

  1. Add a profile checked that inherits release and sets errors = "exx", and build with it.
  2. Run tiko run --profile release and find the program it ran.

Targets

tiko builds for three targets, windows-x86_64, linux-x86_64 and macos-arm64. Each is the default on its own platform, and -target names one explicitly. All three are 64-bit, two on x86-64 and one on Apple Silicon, and tiko's types have the same sizes on each:

'' Both targets are 64-bit x86-64, and tiko's types have the same size
'' on each -- long included, which is 32 bits on Windows and Linux.
print "target sizes:"
print "pointer:"; sizeof( any ptr ); " bytes"
print "integer:"; sizeof( integer ); " bytes"
print "int32:  "; sizeof( int32 ); " bytes"
print "long:   "; sizeof( long ); " bytes"
print "float64:"; sizeof( float64 ); " bytes"
#ifdef __FB_64BIT__
    print "a 64-bit build"
#endif

Output

target sizes:
pointer: 8 bytes
integer: 8 bytes
int32:   4 bytes
long:    4 bytes
float64: 8 bytes
a 64-bit build

integer is the size of a pointer, 8 bytes on all three. long is 4 bytes on every target, even though the C type of that name is 8 bytes on Linux and macOS: a program that uses tiko's types gets the same layout everywhere. Where a program must differ between the platforms, platform detection chooses the code for each.

TargetBuildsLinks against
windows-x86_64a Windows executableMicrosoft's Universal C Runtime
linux-x86_64a Linux executablethe system's C library
macos-arm64a macOS executable, for Apple Siliconthe system's C library, linked by Apple's linker with the macOS SDK

Cross-compiling

On Windows, -target linux-x86_64 with -c compiles a file for Linux, and writes a Linux object file. That is a quick way to check that the Linux branches of a program compile without leaving Windows:

> tiko -c -target linux-x86_64 platform.tiko -o platform.o
> file platform.o
platform.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

Linking needs the target system's own C library and start-up files, which a Windows machine does not have. Without -c, the link stops with the linker's errors about the files it could not find, and TKO0116: link failed. Build the finished program on each platform you ship for, or on a Linux system reached from Windows.

Activities

  1. Add a type with an int32 field and a float64 field, predict its sizeof, and print it.
  2. Compile targets.tiko with -c -target linux-x86_64 on Windows (or -target windows-x86_64 on Linux) and look at the object file it writes.

Compatibility

tiko is compatible with FreeBASIC: it implements FreeBASIC's fb dialect, and a great deal of existing FreeBASIC code compiles with it unchanged. This section is for reading and bringing over that code. It shows the older forms tiko still accepts, the ones it refuses, and what modern tiko code writes instead. Everywhere else on this site the modern forms are used.

Older code usually lives in .bas files, with .bi headers. tiko compiles a .bas file exactly as it compiles a .tiko one:

'' Older code that tiko still compiles: a .bas file, the legacy
'' string type, long and integer, while ... wend, + to join strings.
dim as string names(1 to 3)
names(1) = "ada" : names(2) = "grace" : names(3) = "alan"

dim as long i = 1
dim as integer total = 0
while i <= 3
    print ucase(left(names(i), 1)) + mid(names(i), 2)
    total = total + len(names(i))
    i = i + 1
wend
print "letters:"; total

Output

Ada
Grace
Alan
letters: 12

The same program, written the way the rest of this site writes it:

'' The same program as modern tiko writes it.
dim names( 1 to 3 ) as ustring = { "ada", "grace", "alan" }

dim as int32 total = 0
for each n in names
    print ucase( left( n, 1 ) ) & mid( n, 2 )
    total += len( n )
next
print "letters:"; total

Output

Ada
Grace
Alan
letters: 12
Older formModern tiko
.bas sources, .bi headers.tiko sources, .ti headers
stringustring, which holds text rather than bytes (next pages)
long, integerint32, int64; integer only where pointer size is the point
while … wenddo while … loop
+ to join strings&, which also converts numbers
a counter and a loopfor each, and arrays with initialisers

FreeBASIC has several dialects, chosen with #lang; tiko implements fb only. Code that asks for another is refused:

'' Older code sometimes asks for an older dialect. tiko has one.
#lang "qb"
print "hello"
dialect.bas(2,1): error TKO0317: unsupported language mode 'qb'; tiko-lang implements "fb" only
     |
   2 | #lang "qb"
     | ^

Code written for the qb or fblite dialects has to be brought across to fb. The pages that follow cover the forms that matter most: inline data, jumps and subroutines, and type suffixes and the two string types.

Activities

  1. Rewrite classic.bas with do while … loop and &, keeping the .bas extension, and check it still compiles.
  2. Rename classic.bas to classic.tiko. Does anything change?

data, read and restore

Older BASIC code keeps tables inside the program with data statements, and tiko still accepts them:

'' A quiz whose questions live in DATA statements.
dim as string question, answer
dim as long count

'' The first DATA item says how many questions follow.
read count
for i as long = 1 to count
    read question, answer
    print "Q"; i; ": "; question; " -- "; answer
next

'' RESTORE with a label reads again from that label's DATA.
restore bonus
read question, answer
print "Bonus: "; question; " -- "; answer

data 3
data "Capital of France?", "Paris"
data "Largest planet?", "Jupiter"
data "Square root of 81?", "9"

bonus:
data "Year the first BASIC ran?", "1964"

Output

Q 1: Capital of France? -- Paris
Q 2: Largest planet? -- Jupiter
Q 3: Square root of 81? -- 9
Bonus: Year the first BASIC ran? -- 1964
  • data holds a list of constants, numbers and strings, anywhere in the file.
  • read takes the next values from that list, in the order the data statements appear, converting each to the type of the variable it fills.
  • restore moves the reading position: to the first data after a label, or with no label to the first data in the file.

Nothing checks that a read takes values of the types the data holds, or that each record has as many fields as the code expects. One missing field shifts every later read by one, and reading past the last item is not an error, even with -exx: the variable gets 0 or an empty string.

Modern tiko code keeps the same table in an array of records:

'' The same table as an array of records: the compiler checks every
'' value's type, and the count comes from the array itself.
type Question
    as ustring text, answer
    declare constructor( )
    declare constructor( byval t as ustring, byval a as ustring )
end type

constructor Question( )
end constructor

constructor Question( byval t as ustring, byval a as ustring )
    text = t
    answer = a
end constructor

dim quiz( 1 to 3 ) as Question = { _
    Question( "Capital of France?", "Paris" ), _
    Question( "Largest planet?", "Jupiter" ), _
    Question( "Square root of 81?", "9" ) }
var bonus = Question( "Year the first BASIC ran?", "1964" )

for i as int32 = lbound( quiz ) to ubound( quiz )
    print "Q"; i; ": "; quiz( i ).text; " -- "; quiz( i ).answer
next
print "Bonus: "; bonus.text; " -- "; bonus.answer

Output

Q 1: Capital of France? -- Paris
Q 2: Largest planet? -- Jupiter
Q 3: Square root of 81? -- 9
Bonus: Year the first BASIC ran? -- 1964

Every value goes through the constructor, so the compiler checks its type and the number of values per record. The count comes from the array, so it cannot disagree with the table. A table too large for the source belongs in a file the program reads.

Status in this version

The shorter form, with no constructors and each record written as a parenthesised list,

dim quiz( 1 to 3 ) as Question = { ( "Capital of France?", "Paris" ), … }

compiles, but the program crashes when it starts, because Question has ustring fields. Until that is fixed, give a record with string fields a constructor, as above.

Activities

  1. Remove one "Paris" from the data lines and predict what the program prints.
  2. Add a fourth question to quiz.tiko. What else had to change?

Jumps and subroutines

Older code moves around with labels: goto to a label, on n goto to the nth of a list, and gosub to a subroutine that returns. tiko keeps the first two and refuses the third.

gosub belongs to the older dialects:

'' GOSUB jumps to a label; RETURN comes back. tiko refuses it.
dim as long total = 0
gosub add_one
gosub add_one
print total
end

add_one:
    total = total + 1
    return
gosub.bas(3,7): error TKO0409: 'GOSUB' is not available in the fb dialect
     |
   3 | gosub add_one
     |       ^
gosub.bas(4,7): error TKO0409: 'GOSUB' is not available in the fb dialect
     |
   4 | gosub add_one
     |       ^

A gosub subroutine has no parameters and no local variables: everything it touches is shared with its caller. Rewritten as a sub, the variable it changes becomes a byref parameter:

'' The subroutine as a sub: what it changes is passed byref.
sub AddOne( byref total as int32 )
    total += 1
end sub

dim as int32 total = 0
AddOne( total )
AddOne( total )
print total

Output

 2

on … goto still compiles:

'' ON n GOTO jumps to the n-th label in its list. tiko accepts it.
for choice as long = 0 to 4
    print "choice"; choice; ": ";
    on choice goto add, list, quit
    '' Out of range -- 0, or past the end of the list -- falls through.
    print "no such option"
    goto next_choice
add:
    print "adding a record"
    goto next_choice
list:
    print "listing the records"
    goto next_choice
quit:
    print "quitting"
next_choice:
next

Output

choice 0: no such option
choice 1: adding a record
choice 2: listing the records
choice 3: quitting
choice 4: no such option

A value of 0, or one past the end of the list, jumps nowhere, and execution carries on with the next line. In modern code, select case says the same thing with no labels and no goto at the end of every branch:

'' The same menu with select case: no labels, no jumps.
for choice as int32 = 0 to 4
    print "choice"; choice; ": ";
    select case choice
    case 1
        print "adding a record"
    case 2
        print "listing the records"
    case 3
        print "quitting"
    case else
        print "no such option"
    end select
next

Output

choice 0: no such option
choice 1: adding a record
choice 2: listing the records
choice 3: quitting
choice 4: no such option

Other older statements are refused the same way, each with TKO0409 naming the keyword: resume, let in front of an assignment, call in front of a call, the def statements such as defint, and option statements. Numbered lines are refused too; a label is a name followed by a colon on a line of its own.

Activities

  1. Add a fourth option to both menus and compare how much each one changed.
  2. Make AddOne take the amount to add as a second parameter.

Suffixes and string types

In older code a variable often had no declaration at all: a suffix on its name gave it a type, % an integer, & a long, ! a single, # a double, $ a string. tiko needs every variable declared with its type:

'' A type suffix gave a variable its type in older code: % an integer,
'' $ a string. tiko needs every variable declared with its type.
dim count%, title$
count% = 3
title$ = "tiko"
print title$; count%
suffix.bas(3,5): warning TKO0206: suffix ignored in 'count%'
suffix.bas(3,13): warning TKO0206: suffix ignored in 'title$'
...
suffix.bas(3,5): error TKO0320: 'count' has no type; tiko-lang implements #lang "fb" only, where default types and suffixes are not valid
suffix.bas(3,13): error TKO0320: 'title' has no type; tiko-lang implements #lang "fb" only, where default types and suffixes are not valid

(The warnings repeat for each use, and each line comes with its source excerpt, left out here.) Each warning is a suffix being thrown away; the errors stop the build, because without its suffix the variable has no type. The fix is a declaration: dim as int32 count and dim as ustring title.

The $ suffix stood for string, which tiko still has. It is the legacy string type, and it holds bytes. Modern tiko code uses ustring, which holds text:

'' string is the legacy type: a run of bytes. ustring is text: a run
'' of characters. The same literal, with one accented letter:
dim as string bytes = "café"
dim as ustring text = "café"

print "string:  "; bytes; ", len"; len( bytes )
print "ustring: "; text; ", len"; len( text )

'' ucase knows the accented letter only as text.
print "ucase:   "; ucase( bytes ); " / "; ucase( text )

'' asc reads a byte from a string, a code point from a ustring.
print "asc 4:  "; asc( bytes, 4 ); " /"; asc( text, 4 )

Output

string:  café, len 5
ustring: café, len 4
ucase:   CAFé / CAFÉ
asc 4:  195 / 233

The source is UTF-8, so the accented letter é (U+00E9) is two bytes. A string stores those two bytes and counts them: len is 5, asc reads the first byte, 195, and ucase cannot see a letter in them. A ustring stores characters: len is 4, asc reads the code point, 233, and ucase turns é into É.

Old code that treats a string as a byte buffer (file records, binary protocols) keeps working. Code that handles text, typed by people or read from UTF-8 files, should move to ustring; the Strings section shows it in full.

Activities

  1. Declare the two variables in suffix.bas properly and make it compile.
  2. Print mid( bytes, 4, 1 ) and mid( text, 4, 1 ) and explain the difference.