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
- Change the text and print a second line under the first.
- Compile with
-vadded 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
- Remove the block comment in the
dimline and predict the new output. - Comment out the
printline 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 nextprintcontinues 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
- Print a three-column table of names and ages using
,between the columns. - Print the numbers 1 to 3 on one line using a trailing
;on eachprint.
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:
| Part | Example | Meaning |
|---|---|---|
| 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
- Print
piwith 2, 5 and 10 digits after the point. - Print the numbers 1, 10 and 100 right-aligned in a column 6 wide.
- 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
- Add a
zfield toPointand include it in the text. - Write a
Moneytype 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.
| Type | Bytes | Holds |
|---|---|---|
int8 int16 int32 int64 | 1 2 4 8 | signed whole numbers |
uint8 uint16 uint32 uint64 | 1 2 4 8 | whole numbers from 0 up |
float32 float64 | 4 8 | floating point, about 7 and 16 digits |
boolean | 1 | true or false |
ustring | text 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
- Add an
int16set to -32768 and auint16set to 65535 to the table. - Add a row for
integerand compare its size withint64.
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
| Prefix | Base | Example |
|---|---|---|
| none | 10 | 255 |
&h | 16 | &hFF |
&o | 8 | &o377 |
&b | 2 | &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:
| Suffix | Type |
|---|---|
L | int32 |
LL | int64 |
U | uinteger |
UL | uint32 |
ULL | uint64 |
F | float32 |
D | float64 |
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
- Print
&h7FFFFFFFand&b10000000and check the values by hand. - 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
- Print
-7 / 2,-7 \ 2andfix( -7 / 2 ). Which two agree? - Use
andwith a mask to print the low four bits of 1234. - Start with
dim as uint8 flags = 0and set bits 0 and 3 usingor=andshl.
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
- Write a function that returns the smallest and largest of three
int32values as a tuple with named elements. - 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
- Declare
dim as int32 squares( 1 to 10 ), fill it in a loop, and print the last element. - Change
daysto hold all seven days and print the number of elements asubound( 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
- Add a fourth item with a count of 0 and check that it is flagged.
- Add a
Consumablesmember toCategoryand use it fortape.
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
- Add a
Recttype holding twoPoints, and print its width and height. - Write a function that takes a
Pointand 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
- Use a
withblock to print every field ofson one line. - Inside a
with docblock, openwith .layoutand try to set.pathfrom 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.
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
- Add
OrangebetweenRedandGreenand predict every value printed. - Write a function that returns the
Levelone step above its argument, staying atHighat 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.
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
- Store -1.0 in
u.asFloatand work out which bit changed from 1.0. - Add a
KindBoolmember and abooleanfield toValue's union, and extendShowto 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
- Add
const Area = Rows * Colsand use it to declare a second array. - 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
- Declare three
float64variables on onedimline and print their sum. - Write
var emptywith 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:
| Declaration | Value known | Use it for |
|---|---|---|
const MaxScore = 100 | when compiling | fixed settings, array bounds |
dim as const int32 lives = StartingLives( ) | when the line runs | a 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
- Add
dim as const ustring player = "Ada"and try to append to it. - Write a
subthat takes abyref ... as constrecord 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
- Inside the first
scope, assign tomessagewithoutdimbefore the inner declaration, and see which variable changes. - Move
dim as int32 doubledabove theforloop. What does the loop print, and what isdoubledafter 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
- Declare a
dim as float64 readings( 1 to 5 )and print the sum of its elements before assigning any of them. - Move
dim as int32 nabove theforloop 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
- Change
itemsto afloat32and printsizeof( cost ). - Assign 6.5 to
roundedand 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:
| Function | Result type |
|---|---|
cint | integer |
clng | int32 |
clngint | int64 |
cubyte, cbyte | uint8, int8 |
cushort, cshort | uint16, int16 |
culng | uint32 |
csng | float32 |
cdbl | float64 |
cbool | boolean |
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
- Print
cast( int16, 40000 )andcast( uint16, 40000 ). Why does one change and not the other? - Print
cdbl( 7 ) / 2,7 \ 2andcint( 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.
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
- Add an
int16overload and call it withcshort( 1 ). Which overload does a plain1pick now? - Print
TypeOf_( 2147483647 )andTypeOf_( 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.
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
- Declare
var half = 1 / 2and printsizeof( half )andhalf. - Declare
dim as typeof( mean ) spread = 0and 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
- Add
type Fahrenheit as float64and assign aCelsiusto it. Does the compiler object? - Write a function returning a
Spanthat covers two otherSpans.
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
- Add an
int64field to the end ofTightand predict itssizeofbefore you run it. - Print
sizeof( Loose ) * 1000000andsizeof( 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
| From | To | With |
|---|---|---|
| text | number | val, valint, vallng, or numeric.TryParse... to reject bad input |
| number | number | assignment, cast( T, x ), cint, cdbl and the other c... functions |
| number | text | str( x ), the & operator, strings.Format |
| your type | another type | a 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
- Change
quantityTextto"12 boxes". What doesvalintmake of it? - Change
priceTextto"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
- Print
ratiowith 0, 2 and 4 digits after the point. - 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
- Pass
" 7", with a leading space, toTryParseLongand tovalint. - Read
"9999999999"withTryParseLongand then withTryParseLongInt.
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:
| Declared | Runs 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 float64 | a float64 is expected, or cast( float64, ... ) asks |
operator cast( ) as ustring | text 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
- Add a constructor that builds a
Fahrenheitfrom aCelsius, and convertaback. - Remove the
operator letand read the error forb = 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
| Function | Rule | 2.5 | -2.5 | 2.6 |
|---|---|---|---|---|
cint, cast, assignment | nearest, half to even | 2 | -2 | 3 |
int | down, towards minus infinity | 2 | -3 | 2 |
fix | towards zero, dropping the fraction | 2 | -2 | 2 |
numeric.Round | nearest, half away from zero | 3 | -3 | 3 |
numeric.RoundHalfEven | nearest, half to even | 2 | -2 | 3 |
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
- Add 3.5 and -3.5 to
samplesand check each column. - Cut 2.71828 to two places with
fix( x * 100 ) / 100, and compare the result withnumeric.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
- Print
y = 36directly, thencbool( y = 36 ). - Write a function
Clamp( x, lo, hi )and use it inside aprint.
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
- Print
"1 item"or"n items"correctly for n from 0 to 3. - 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.
| Operator | Evaluates the right side | Works on |
|---|---|---|
andalso | only if the left is true | conditions |
orelse | only if the left is false | conditions |
and, or, xor | always | conditions 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
- Change the guard in the loop to use
andand run the program. - Write
if s <> "" andalso left( s, 1 ) = "#" thenfor austringcalleds, 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:
| Level | Operators |
|---|---|
| 1 | ^ |
| 2 | unary - |
| 3 | * / |
| 4 | \ |
| 5 | mod |
| 6 | shl shr |
| 7 | + - |
| 8 | & |
| 9 | = <> < <= > >= |
| 10 | not |
| 11 | and |
| 12 | or |
| 13 | xor |
| 14 | andalso orelse |
Operators on one level group from left to right.
The places that catch people out, all shown in the example:
-2 ^ 2is -4.^binds tighter than the minus sign, so it means-(2 ^ 2). And2 ^ 3 ^ 2groups to the left, giving 64, not 512.\andmodsit below*.7 \ 2 * 2is7 \ 4, which is 1, not 6.9 mod 5 \ 2is9 mod 2, which is 1, not 2.- Shifts bind tighter than
+.1 shl 2 + 3shifts first, giving 7. - Comparisons bind tighter than
and.n and 1 <> 0compares first and gives 6, not the bit test it looks like. Write( n and 1 ) <> 0. notis looser than comparisons.not a = bmeansnot ( a = b ).andalsoandorelseshare a level. They group left to right, sotrue orelse false andalso falseis( true orelse false ) andalso false, which is false.
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
- Predict
2 + 3 * 4 ^ 2 \ 5and check it. - 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.
| Statement | Use it to | Page |
|---|---|---|
if / elseif / else | choose between a few conditions | if/else |
select case | compare one value against many cases | select case |
for ... next | count through a range | for |
do ... loop | repeat while (or until) a condition holds | do loop |
exit / continue | leave a loop, or skip to its next pass | exit and continue |
for each | visit every element of an array | for each |
defer | run a statement when a block is left | defer |
Activities
- Change the first loop so that it adds up the even numbers instead.
- Make the
doloop 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.
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
- Set
temperatureto 30, then to 31, and predict each output before running it. - Rewrite the
if/elseifchain so that it tests forcoldfirst.
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
| Case | Matches |
|---|---|
case 0 | one value |
case 1, 2, 3 | any value in the list |
case 4 to 9 | a range, both ends included |
case is < 0 | any value the comparison accepts (is >, is <=, is <> ...) |
case 10 to 99, 1000 | a list that mixes the forms |
case else | anything 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.
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
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
- Add a case for the numbers 100 to 999 and check which samples move into it.
- Move
case is < 0to 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
- Print the multiples of 7 from 7 to 70 with one
forand astep. - 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
| Form | Tests | Body runs |
|---|---|---|
do while c ... loop | before each pass; continues while c is true | zero or more times |
do until c ... loop | before each pass; continues until c is true | zero or more times |
do ... loop while c | after each pass | at least once |
do ... loop until c | after each pass | at least once |
do ... loop | never | until 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
- Rewrite the
do until n = 3loop as ado whileloop with the same output. - Change
loop until n * n > 50toloop 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
- Change the grid search to find the first value under 5 instead.
- In the row printer, replace
continue for, forwithexit 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
- Print the planets in a single line, separated by commas.
- Find the largest prime in
primeswithfor 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
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
- Add a third resource to
Copyand check the order the three are closed in. - Move the
deferinInAnIfabove theifand 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 valuesets the result and leaves the function at once.function = valuesets the result and carries on.Signuses 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
- Write a
function Cubeand print the cubes of 1 to 5. - Remove the
if n = 0line fromSignand predict whatSign( 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 type | Passed, when no keyword is written |
|---|---|
numbers, boolean, enums, pointers | byval: a copy |
ustring | byref: the caller's variable |
| your own types and tuples | byref: the caller's variable |
| arrays | always by reference (see Passing arrays) |
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
- Write
sub Swap2( byref a as int32, byref b as int32 )that exchanges two values, then change one parameter tobyvaland watch it stop working. - Change
NoKeywordTextto takebyval s as ustringand 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
- Give
Scaleda defaultvalueas well and call it with no arguments. - Call
Ruleso that it prints== headingfollowed 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
- Add a
Describeforbooleanand call it withtrue. - Change one argument in
ambiguous.tikoso 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
- Write a recursive
Fib( n )and print the first ten Fibonacci numbers. - Change
Countdownto printliftofffirst 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.
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.
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
- Write
function Largest( values( ) as const int32 ... ) as int32. Decide what it should return when called with no arguments. - Call
Joinwith 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
- Add a
Negatefunction and pass it toTabulatewithout changingTabulate. - Make
Pickreturn@Negatefor 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, likecubeandgreet; - straight into an argument, like the one
ApplyAllreceives.
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
- Pass
ApplyAlla lambda that replaces every value with its square. - Store a lambda that returns the larger of two
int32values in avar, 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
| Capture | The closure holds | Writes inside the lambda |
|---|---|---|
byref calls | the caller's variable | reach the caller |
byval step_ | its own copy, taken when the lambda expression runs | stay 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.
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
- Write a closure that appends each word it is given to a
ustringcapturedbyref, and call it three times. - Change
addStepto capturebyrefand 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.SortBytakes a comparer that answers one question, "doesacome beforeb?". Answeringa > bsorts from highest to lowest.containers.ForEachhands each element to an action. This one capturespassMarkby value, because it only reads it, andpassedby 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
- Write
function CountIf( of F )( values( ) as int32, byref test as F ) as int32and use it with a lambda that tests for even numbers. - Sort
scoresfrom 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
- Make
NextTicketstart at 1 and print five tickets. - Change
Rememberto 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
- Write
function Stats( values( ) as int32 ) as ( count as int32, total as int64 )and print both parts. - 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.
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
- Print every month with 31 days by its number.
- Declare
dim as int32 a( 5 )and print itslbound,uboundand 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.
| Statement | Effect |
|---|---|
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 a | frees the elements; the array is empty again |
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
- Read numbers into a dynamic array until one is negative, with the values coming from a fixed array instead of the keyboard.
- Change
redim preserve values( 1 to 2 )to plainredimand 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
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
- Build a 10 by 10 multiplication table and print it.
- 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 )
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
- Write
sub Reverse( values( ) as int32 )that reverses an array in place, and test it onsmalland onodd. - 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, sosqruns 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
- Write
function Evens( byval n as int32 ) as int32( )returning the firstneven numbers, indexed from 1. - 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
- Change
ito 0 in the first example and read the new message. - 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 priceshands you a copy of each element. Writingp = 0changes the copy, and the array is untouched.for each byref p in priceshands you the element itself, sop += p \ 10changes 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
- Use
for each byrefto round every price up to the next multiple of 25. - Find the
Itemwith the largestcountusingfor eachand 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:
- Built-in functions:
left,mid,instr,trimand the rest that come with the language. - The strings module and Split and Join: what
std/stringsadds. - Slices and Slices and lifetime: naming part of a string without copying it.
- Block strings and Escape sequences: writing text in the source.
- Unicode: what
lencounts. - Building strings: assembling text from many pieces.
- Base64, hex and URLs: bytes as text, and text inside URLs.
- Writing CSV: quoting fields so a CSV reader gets them back.
Laying out values with strings.Format is covered in
Formatted print, and making your own types printable in
Display.
Activities
- Print your name and its length on one line.
- Compare
"Zebra"and"apple"with<. Then compare them again after passing both throughlcase( ), 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.
instrreturns 0 when the text is absent, which is never a valid position, soif instr( s, x ) > 0is the test.
| Function | Gives |
|---|---|
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, rtrim | the 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
- Use
instrin adoloop to count how many times"o"appears ins, passing the position after each match as the start of the next search. - Print
swith its first letter in lower case and the rest in upper case, usingleft,mid,lcaseanducase.
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.
| Group | Procedures |
|---|---|
| Tests | StartsWith, EndsWith, Contains, IsNumeric, IsBlank |
| Counting | Tally (non-overlapping matches) |
| Pieces | Extract (text before a match), Remain (text after it), Between |
| Changes | Replace, Remove, Shrink, MCase, Reverse, Repeat |
| Layout | PadLeft, 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
- Use
ReplacewithignoreCaseto change everycatin"Cat, cat and CAT"todog. - Print a three-row table where the first column is padded to 10 characters
with
PadRightand the second right-aligned in 6 withPadLeft.
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.
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
- Split
"1969-07-20"on"-"and print the three parts as numbers withval. - Split a sentence into words with
SplitChars, thenJointhem 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:
| Type | Slices | Unit |
|---|---|---|
uslice | ustring | a UTF-16 code unit |
slice | string, zstring, a zstring ptr | a byte |
wslice | wstring, a wstring ptr | a 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.
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
- Print every word of
"the quick brown fox"usingSplitSlices, then print only those thatStartsWith"b". - Use
Findto print the part of"key=value"after the=, without callingmid.
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()
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
- Change
Beforeto takebyval s as ustringand read the error. - Move
print initialabove the&=inslicelock.tikoand 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.
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
- Move the closing
"""ofqueryto the left edge and see where the spaces go. - 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:
| Escape | Character |
|---|---|
\t | tab |
\n | line feed |
\r | carriage return |
\\ | a backslash |
\" | a quote |
\xHH, \&hHH | the character with that hexadecimal code |
\NNN | the character with that decimal code |
\uXXXX | the Unicode code point U+XXXX |
\UXXXXXXXX | any 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
- Print a three-column table using
\tbetween the columns. - Spell the word "naive" with an i-diaeresis, written
\u00EF, print it and itslen.
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.
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.
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
- Write
CharAt( s, n ), which returns the nth character of a string as austring, keeping a surrogate pair together. - Print the UTF-8 length of
café, then of a plain ASCII word of the same length. When dolenof thestringand of theustringagree?
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:
&=one piece at a time. Austringkeeps 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.- 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.Joinmeasures the result first and copies each piece once. - Make room once, then overwrite. When you know the final size,
space( n )allocates it in one go and themidstatement 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.
&= 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
- 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?
- 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
| Procedure | Gives |
|---|---|
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
- Encode the same token with
codec.B64_URLand no padding, as a JSON Web Token does, and decode it back withcodec.DECODE_ALLOW_MISSING_PAD. - 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
- Add a field containing a line break and check that it survives the round trip.
- Write the rows to a file with
fs.FileandCreate, 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:
| Page | Covers |
|---|---|
| Constructors | how objects are created |
| Destructors | what runs when they go away |
| Properties | methods that read and write like fields |
| Access control | public: and private: |
| Static members | fields and methods that belong to the type |
| Const methods | methods that promise not to change the object |
| Operator overloading | +, =, <, let, += on your own types |
| Indexing | operator [] |
| Copying | copy constructors and operator let |
Types that build on other types are in Inheritance.
Activities
- Add a
Resetmethod toCounterthat sets the count back to 0. - Add a method
Addthat takes anotherCounterand adds its count, then callclicks.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:
| Form | Calls |
|---|---|
dim as Temperature a | the default constructor |
var b = Temperature( 5.5 ) | the float64 constructor |
dim as Temperature c = Temperature( "31" ) | the ustring constructor |
dim as Temperature d = -4 | the 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.
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
- 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?
- Remove the default constructor and read the error for
dim as Temperature a. Then give thefloat64parameter 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 in | Destroyed |
|---|---|
a scope or other block | at its end |
| a procedure | when the procedure returns |
| a loop body | at the end of every pass |
| an expression, as a temporary | when the statement finishes |
| the program's top level | when 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
- Add a third
Tracerto the scope and predict the order of the lines. - Put
exit forin the loop on the first pass. Doeslapstill 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.
Fahrenheitis computed fromcelsius_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
- Add a read-write
Kelvinproperty that stores into the samecelsius_field. - Make the
Presetsetter 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, setreads and writes like a plain field.get, private setcan be read anywhere but written only by the type's own code, asBirthdaydoes. The setter may be less visible than the getter, never more.get, init(orgetalone) can be read anywhere and assigned only by a constructor.= 18gives 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 atend getis returned.returnworks 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
staticproperty 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
byrefparameter is written back through its setter when the call returns, soBumpchanges the cell. - Compound assignment (
+=,&=, ...) andswapcall the getter and then the setter.
Activities
- Give
PersonanEmailauto-property that onlyPersonmay change, and aSetEmailmethod that refuses a value with no@. - Turn
Settings.Levelinto a property block overlevel_.
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:
| Label | Members 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.
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
- Add a public
Entries( )function that returns how many entriesRecordhas made. - Move
Depositinto the private section and read the error foracct.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
- Add a static
Peak( )that reports the highest number ever issued, even after aReset. - 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
- Remove
constfromDescribeand read the error. Which call does the compiler point at? - Add a
const function IsSquare( ) as booleanand call it fromReport.
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:
| Operators | Declared | Why |
|---|---|---|
+ - * /, =, <>, <, unary -, ... | outside the type | neither operand is privileged |
let, += and the other compound assignments, cast | inside the type | they change, or convert from, this object |
[] | inside the type | see 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 asnot ( a = b )to keep the two in agreement. - Precedence does not change.
half + third * halfmultiplies first, because the expression is parsed before any operator procedure is chosen. operator let( byval whole as int64 )makestotal = 3work: it is assignment from another type. Assignment from anotherFractionis the ordinary copy, covered in Copying.operator cast( ) as ustringis what letsprintshow 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.
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
- Add
operator -for two fractions, andoperator >. - Add
operator cast( ) as float64and printcast( 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.
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
- Make the capacity a constant and change
Recentto keep the last eight readings. - Make
[]refuse an index outside0toCount( ) - 1by stopping the program with anassert.
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:
| Procedure | Runs for | Must |
|---|---|---|
copy constructor, constructor( byref other as Samples ) | dim as Samples y = x, and every byval argument | make a new block and copy the values |
operator let( byref other as Samples ) | z = x, on an object that already exists | free 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.
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
- Remove the
if @this = @otherline, runz = z, and printz.items[ 0 ]. What did you get, and why is it not reliable? - Change
Inspectto takebyref as const Samplesand 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:
| Page | Covers |
|---|---|
| Virtual methods | letting the object, not the variable, choose the method |
| Overriding | override, and when an override can be overridden again |
| Abstract methods | methods every derived type must supply |
| Calling the base | base( ... ) and base.Method( ) |
| Runtime type checks | is |
| Interfaces | interface, implements and class: a contract any type can take on |
| Several interfaces | one type, many contracts; renamed and private mappings; trycast |
| Virtual destructors | deleting through a base pointer |
| Slicing | what passing by value does to a derived object |
Activities
- Add a
Truckthat extendsVehiclewith aloadfield and a constructor passing 6 tobase( ... ), and pass one toReport. - 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.Objectis 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.virtualonAnimal.Soundsays 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.overrideonDog.SoundandCat.Soundsays 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
- Add a
Cowtovirtual.tikoand put it in thezooarray. What did you have to change outsideCowitself? - Remove
virtualfromAnimal.Soundand read the compiler's message for theoverridelines.
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 as | Replaces an open virtual method of the base | Open to the next level down |
|---|---|---|
virtual | yes, if there is one | yes |
override | yes, and it is an error if there is none | no |
virtual ... override | yes, and it is an error if there is none | yes |
| neither | yes, if there is one | no |
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
- Change
Cat.Soundtovirtual ... overrideand check whatSpeak( kit )prints now. - Misspell
SoundasSundinPuppy, keepingoverride, 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
- Add a
Trianglewith a base and a height, and put one in the array. Which existing lines did you have to change? - Leave
Nameout ofTriangleand 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.
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
- Add a
Checkingtype that extendsAccountwith anoverdraftfield, and give it aDescribethat builds onbase.Describe( ). - Fix
nodefault.tikothe 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).
| Asking | Refused with |
|---|---|
p is More, where p's type does not extend Object | TKO0799: 'is' needs run-time type information, and 'plain' has none: it does not extend 'object' |
t is TextMessage, where t is already a TextMessage | TKO0798: 'is textmessage' needs a type derived from 'textmessage', and 'textmessage' is not one |
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
- Add a
Voicemessage with asecondsfield and makeShowprint its length. - Replace the
istests with a virtualDetail( )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
- Add a
CsvEncoderthat writeswidth,640, and pass one toSave. - Give
IEncodera third member,function Footer( ) as ustring, and read what the compiler says about each encoder until both provide it. - Rename
IniEncoder.NametoLabel, keepingimplements 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:TitleimplementsIShape.Name. - One method can provide several members.
CloseimplementsIDocument.CloseandIResource.Closeat once. - A private method can provide a member.
WriteOutcannot be called on aDrawingfrom outside (TKO0625), yetdoc.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
- Add an
IPrintableinterface withsub Show( )toDrawing, mapped to a private method, and call it through anIPrintable. - Make
Closeprint which interface it was called through. Why can it not tell? - Write a
Labeltype that implements onlyIShape, and check withtrycastthat it is not anIDocument.
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.
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
- Change
Socket's destructor to a plaindeclare destructor( )and check the output against the pitfall above. - 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.
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
- Change
ByValueto take its parameterbyrefand compare the output. - Put
rexin adim as Animal pets( 0 to 0 )array and callSoundon the element. Then try an array ofAnimal 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:
- Generic types: a
Stackthat holds any element type. - Explicit type arguments: writing
( of T )yourself, and naming an instantiation withtype ... as. - What a type parameter asks for: the error you get when a type lacks an operator the generic uses.
- Library demands: Hashable keys: making your own type a
Mapkey. - Generics and lambdas: passing any callable to a generic.
Activities
- Write
Smaller( of T )and call it with three different types. - Call
Largerwith austringvariable and a string literal, and read the error. What does it tell you about howTis 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
- Add a
Peek( )method that returns the top item without removing it. - Try to assign
numberstowordsand read the error. - Write a
Queue( of T ), first in, first out, withEnqueueandDequeue.
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.
2is an integer and2.5is afloat64, so the arguments would bindTto two different types, and inference does not pick a winner. With( of float64 )written, both arguments convert tofloat64. Tis only in the result.Blank( of T )( )has no parameters, so there is nothing to infer from. A newint32is0and a newustringis 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
- Call
Larger( of int32 )( 2, 2.5 ). What does it print, and why? - 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
- Write a generic
Contains( of T )( items() as T, byval x as T )and use it withVersion. Which operator does it need? - 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.
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
- Add a
containers.Set( of Cell )and add the same cell twice. What is its count? - Make
HashOf( )return0for 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.
overLimitcaptureslimitwith[ byval limit ], so it is a closure: a small object holding its own copy oflimit. It can still be passed, becauseFbecomes the closure's type.Transformtakes a lambda that returns a new value, andfor each byrefwrites it back into the array.- With
words,Tisustring, and the lambda's parameter is austringto 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
- Write
Sum( of T, F )that adds upf( x )for every element, and use it to total the squares ofnums. - Capture
limitwithbyrefinstead, changelimitafter creatingoverLimit, 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:
- Destructuring and swapping:
(a, b) = (b, a). - Named elements: where element names come from, and why they are not part of the type.
- for each over containers: arrays, sets, maps and pairs.
- for each and strings: bytes, characters and words.
- Making a type iterable:
GetIterator,IsValid,Value,MoveNext. - An iterable generic collection: a ring buffer.
- Lazy sequences: elements computed as they are walked.
- Changing a collection while walking it.
Activities
- Make
MinMaxreturn a third element, the number of values, and print it. - Call
MinMaxon 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:
| Form | Does |
|---|---|
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
- Reverse a five-element array using only tuple assignments.
- Write
(a, a) = (1, 2)and printa. 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.
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
- Rewrite
wrongnames.tikowithtype Sizeholding fieldswandh. Which line now refuses to compile? - Print
s._2for thesinwrongnames.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
| Container | for 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 map | a (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
- Walk
ages.Keys( ), anArrayof the keys, and print each key. - Add 1 to every age.
for each byrefover the map is refused, so walkages.Keys( )and write throughages[ 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 acontainers.Arrayofustring, andfor eachwalks that like any array. - Characters. A
ustringis indexed withforandmid. Eachmid( word, i, 1 )is one character, socaféhas four. - Bytes. A
stringis a sequence of bytes, andfor eachover it yields each byte as a number. Converted to astring,é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
| ^
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
- Split
"one two three"on" ". How many elements are there, and why? - Count the vowels in a
ustringwith aforloop andmid.
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
| Member | On | Job |
|---|---|---|
GetIterator( ) as I | the collection | hand out a new iterator; called once per loop |
IsValid( ) as boolean | the iterator | is there a current element? |
Value( ) as E | the iterator | the current element; E is the loop variable's type |
MoveNext( ) | the iterator | advance 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 collectionIsValid( )is false at once, as it is forbackwards, and the body never runs. IsValid( )is a question. Asking twice without aMoveNext( )in between gives the same answer.Value( )decides whatbyrefcan do. Declaredbyref as E, it hands out the element itself, andfor each byrefcan 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
- Give
RangeaCount( )function and use it to print how many elements a range has before walking it. - Make
Value( )return the square ofat. 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
- Add a
Count( )method toRingand print it after eachAdd. - 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 forafter 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
- Write a
Squarestype whose iterator yields 1, 4, 9, ... for ever, and print the first square above 1000 withexit for. - 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.
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
- Use the same pattern to double the stock of every item with fewer than 5,
adding a new
"<item> (reorder)"entry for each. - Rewrite the first loop so that it does visit the fourth element: a
doloop that testsubound( 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:
| Tool | For | Page |
|---|---|---|
-exx runtime checks | bugs: a bad index, a null pointer | Runtime errors |
assert, assertwarn | conditions that must hold if the program is right | assert |
outcome.Optional( of T ) | a value that may be absent, with no reason needed | Optional |
outcome.Result( of T, E ) | a value, or the reason there is none | Result |
returning a Result up | stopping at the first failure in a chain of calls | Propagating errors |
error n, on error goto | numbered runtime errors and their handlers | Error numbers and handlers |
#assert | facts about constants, checked while compiling | Static 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
- Change
Averageto return anoutcome.Optional( of float64 )after reading the Optional page. - Think of a function in your own code that returns
-1,0or""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
- Remove
#cmdline "-exx"and run the program again. What does it print? - 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.
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
- Compile without
#cmdline "-g". What happens to the two messages, and to the program? - Add
assert( total >= 0 )and callAverage( -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 member | Does |
|---|---|
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
Optionalis empty.dim later as outcome.Optional( of ustring )holds no value; there is no third, uninitialised state. Sometakes its type from its argument.outcome.Some( i )with anint32iis anOptional( of int32 ). A bare literal is aninteger, sooutcome.Some( 42 )does not convert to anOptional( of int32 )(TKO0738); writeoutcome.Some( of int32 )( 42 ).Nonehas no argument to infer from, so its type is always written.Value( )on an emptyOptionalis a bug at the call site, and raises runtime error 1, "illegal function call". Error numbers and handlers shows it being caught. AskHasValue( )first, or useValueOr( ).
When the caller also needs to know why there is no value, use a Result instead.
Activities
- Write
FirstNegative( a() as int32 )returning anOptional( of int32 ), and use it withValueOr. - Rewrite
Averagefrom the section overview to return anOptional( 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 member | Does |
|---|---|
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
- Give
ParsePorta fourth failure: a port below 1024 is "reserved". - Replace the
ustringfailure with atype PortErrorholding anenumcode 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:
ParsePortchecks one number and fails with the reason.ParseEndpointcan fail on its own, with no:, and callsParsePort. When that fails it returns at once, building its own failure fromport.Failure( ). AResult( of int32, ... )and aResult( of Endpoint, ... )are different types, so the failure is re-wrapped rather than returned as it is.LoadSettingadds the setting's name to any failure from below. On successepalready 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
- Make
ParseEndpointrefuse an empty host, and check thatLoadSettingadds its context to the new failure without any change of its own. - 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:
| Function | Gives |
|---|---|
err | the error number |
erl | the 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:
| Number | Meaning |
|---|---|
| 1 | illegal function call: Value( ) of an empty Optional, for one |
| 6 | an array index out of bounds (-exx) |
| 7 | a null pointer followed (-exx) |
| 11 | a 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
| ^
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
- Move the
on error gotoand the handler intoTransfer. What doesescape.tikoprint now? - Print
*ermn( )inReport'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
- Add a
uint8field toHeader. Which assertion fails? - Assert that
SLOTSis a power of two, usingand.
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.
| Stack | Heap | |
|---|---|---|
| made by | dim, var, parameters | new, allocate |
| destroyed | at the end of its block, automatically | by delete or deallocate |
| reached through | its name | a pointer |
| a mistake looks like | rare | a leak, or a use after free |
The pages in this section, from raw to managed:
- Pointers:
ptr,@,*,->and pointer arithmetic. - new and delete: typed heap allocation.
- nullptr: the null pointer, and what
-exxdoes with it. - References:
byrefvariables and results. - Destructors and scope: cleanup that runs by itself.
- defer for cleanup: freeing on every way out.
- Non-copyable types: one owner for one resource.
- Shared pointers:
SharedPtrandWeakPtr.
Activities
- Remove the
delete keptline. Which line of output disappears? - Make
MakeOnHeapreturn@onStackinstead. 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
| Written | Means |
|---|---|
int32 ptr | the type: an address of an int32 |
@n | the address of n |
*p | the value at p, for reading or writing |
p->x | field 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.
@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
- Print the bytes of the
int32value 258 through auint8 ptr. Which byte comes first? - Walk
abackwards with a pointer, fromlastOnedown tofirst.
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, all0until set:several[1]was never written and prints0.new Point( 3, 4 )fills a constructor-less type's fields in order, asdim pt as Point = ( 3, 4 )would.- For
Tracked, which has a constructor,new Tracked[ 2 ]runs it once per element, anddelete[]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
- Give
Trackeda second field and initialise it in the constructor. - Allocate a
Pointwithnew Pointand 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
- Add a fourth node to the front of the list.
- In
nullcheck.tiko, replacep->xwith*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
byrefparameter.Double_( x )hands the procedurexitself, son *= 2changesx. - A
dim byrefvariable.alias_is bound tobwhen it is declared and stays bound to it;alias_andbhave the same address. It must be bound where it is declared, so there is no null reference and no rebinding. - A
byrefresult.Largestreturns the largest element, not a copy of its value, so the caller can assign through the result or bind adim byrefto it.top += 100changesnums( 2 ).
Prefer a reference whenever "nothing" is not a valid answer, and a pointer when it is.
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.
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
- Write
function Smallest( a() as int32 ) byref as int32and use it to set the smallest element to 0. - Bind a
dim byrefto an element of an array, thenredim preservethe 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.
cwas made last and closes first, thenb, thena. Something made later may depend on something made earlier, never the other way round, so this order is always safe. - Every block counts.
innerlives in ascope ... end scopeblock and closes atend scope, whileWorkis still running. - Every way out counts.
exit subin the second call leaves beforecexists, andbandastill close, in the same reverse order. The same holds forreturnandexit 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
- Add
dim d as Resource = Resource( "d" )inside theifblock, beforeexit sub. When doesdclose? - Put the three
dimlines inside aforloop 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 = 0the function returns before theallocate, and before thedeferline is reached, so nothing is freed: adeferonly takes effect once the program has passed it. - With
n = 3the function reaches its lastreturn, and the block is freed on the way out. - With
n = 2000it leaves early throughreturn -2, and the block is freed all the same.
Three habits make this reliable:
- Write the
deferon the line after theallocate, 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
deferline is reached. If areallocatestores a new address insquares, the right block is still freed. - Allocate and defer in the procedure's own body, not inside an
if. Adeferruns at the end of the block it is written in, so inside anifit frees the memory atend 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
- Add a second scratch buffer to
SumOfSquareswith its owndefer. In which order are the two freed? - Move the
allocateand itsdeferinside anif n > 0 then ... end ifblock. Where doesfreedappear 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
- Add a
Fill( byval value as uint8 )method and use it beforeTotal. - Write a
Totalthat takes the bufferbyval, 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 member | Does |
|---|---|
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, *p | the 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.
std/memory has no single-owner smart pointer. For an object used inside one
procedure, a destructor or defer does that job.
Activities
- Print
root.UseCount( )afterup.Reset( ). - In
weak.tiko, make the root'schildthe weak link andparentthe 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:
#importcomes before any other code in the file, including#definelines. An#importfurther down isTKO0362, "'#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
- Print
strings.Reverse( "tiko" ). - Move one
#importline below the firstprintand 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 txtrenames the module for this file only. The module is stillstrings; 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 stris refused, becausestris a built-in function.using containersmakesArraya 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
- Remove
using containersand fix the program by qualifyingArray. - Move
using containersinside asuband find the first line whereArrayis 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 bindsgreet, and the program callsgreet.Hello.publicmarks what importers may use:HelloandDEFAULT_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
- Add a public
Goodbyefunction togreetand call it from the program. - Change the name in
#moduleto"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 write | Visible to |
|---|---|
public | every file that imports the module |
| nothing | every file of this module, and no importer |
private | this 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
- Mark
Letter_publicand call it from the program. - Add a public function whose result type is a
typedeclared withoutpublic, 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
- Add
publictokit's import ofscoresand makenoleak.tikocompile. - Import
greetdirectly inreexport.tikoas 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] nameis the package's name. The modulelengthinsideunitshas the full nameunits.length, so it never collides with alengthmodule from another package. The program still calls itlength, the last part of the import path.[dependencies]maps a package name to a directory, relative to thetiko.toml. An import whose first part isunitsis looked for there. The dependency is written in the repository, so a fresh checkout builds with no setup.[target.convert]declares an executable built frommain.tiko. The Building section covers targets, profiles andtiko 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
- Add a module
units/weightwith a publicToPoundsand use it from the program. - Rename the dependency in
app/tiko.tomltomeasureswithout 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 Metricblock addsPerimeterto the same namespace. using MetricinsideReportmakes the members usable bare until the end ofReport. 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
- Add
Imperial.Perimeterand call bothPerimeters. - Put
using Metricandusing Imperialin onesuband callAreabare. 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 5makes every laterMAX_ITEMSread5. The replacement is text, soBANNERbecomes a whole expression, withAPP_NAMEreplaced inside it.#undefremoves a definition, and#ifdefasks whether a name is defined at that point in the file. The conditional compilation page covers the full family.constis 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
- Change
MAX_ITEMSto8and check the array follows. - Use
MAX_ITEMSafter the#undefand 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 )andSQUARE( x )take arguments. The output shows what each use turned into: the arguments are pasted in as text, soSQUARE( column1 + 1 )evaluatescolumn1 + 1twice.#exprturns an argument into a string literal. The argument is expanded first, which is why the first line showsLARGER's body, not its name.column##npastes two pieces into one token:COLUMN( 2 )becomes the identifiercolumn2.#macro…#endmacrodefines a macro over several lines, so it can expand to statements.SWAP_VALUESwraps its temporary in ascopeblock, so that two uses in one procedure do not both declaretmp_.
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".
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
- Call
SQUAREwith a function that prints each time it runs, and count the calls. - Write
#macro REPEAT( n, body )that runsbodyntimes in aforloop, 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
| Directive | Keeps the lines that follow when |
|---|---|
#if expr | the constant expression is not zero |
#elseif expr | no earlier branch was taken and expr is not zero |
#else | no earlier branch was taken |
#ifdef NAME | NAME is defined |
#ifndef NAME | NAME 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.
#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
- Set
LOG_LEVELto3, then to1, and predict each output first. - Define
WITH_SOUNDand 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 aftertiko cmdline.tiko. When the compiler meets the line, it starts the compile again with them.#ifndef EDITIONgives the setting a default, so the program also builds when nothing chose a value.- The source wins. With the
#cmdlineline in place,tiko cmdline.tiko -d EDITION=1still builds the professional edition. Take the line out and the same command builds the standard one; with no-dat all, the default applies andSEATSis 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
- Remove the
#cmdlineline and build with-d EDITION=3. - Add
-d TRIAL=1to the#cmdlineline 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
| Symbol | Defined 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
- Add a
constfor the end-of-line sequence each platform uses in text files, and print its length. - Build with
-target linux-x86_64on 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
| Name | Gives |
|---|---|
__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.
CHECKis defined on line 12, but the message names line 17, the line insideAveragethat used it. That is how assertion and logging macros report the caller's position. #condturns 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#lineas 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
- Add
CHECK( total >= 0 )toAverageand call it with a negative total. - Move the
#linedirective 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 condstops the build whencondis false. Change the8to12and the build fails withTKO0604, "#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:int32is notint64.#print textreports 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
- Add a
flags as uint16field toHeaderand read the error. - Write an
#ifthat stops the build with#errorunless__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.
| Container | Holds | Good at |
|---|---|---|
Array( of T ) | elements in order, by index from 0 | appending, walking, indexing |
Map( of K, V ) | a value for each key | finding a value by its key |
Set( of T ) | each value at most once | "is it in?", set operations |
OrderedMap, OrderedSet | keys kept sorted | order, nearest key |
LinkedList( of T ) | elements in a chain | adding and removing at both ends |
Deque( of T ) | elements in a ring | both ends, and by index |
PriorityQueue( of T ) | elements by priority | taking the smallest next |
BitSet | small numbers, one bit each | flags, 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
- Print the words in the order they first appeared, using
seen.Addto skip repeats. - 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.
| Member | Does |
|---|---|
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.
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
- Use
RemoveSwap( 0 )instead ofRemove( 2 )and compare the order. - 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
| Member | Does |
|---|---|
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:
#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
- Replace
ages[ "ada" ] = 37withages.Put( "ada", 37 )and print whatPutreturns. - 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.
| Operation | Leaves 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
- Use a
Set( of ustring )to print each distinct word of a sentence once. - Compute the values in exactly one of
evensandsmall(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:
| Member | Finds |
|---|---|
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
- Print the departures between 1200 and 1500 by starting at
TryCeiling( 1200, t )and stepping withTryCeiling( t + 1, t ). - Remove "York" with
Remove( 905 )and printTryFirstagain.
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
| Member | Does |
|---|---|
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
- Turn the undo stack into a queue by changing one call, and predict the output first.
- 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
| Member | Does |
|---|---|
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
- Make the history keep three pages instead of five, and predict the output first.
- Declare a deque with a brace list,
= { 1, 2, 3 }, and print it withfor 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
- Loop over
deadlineswithfor eachbefore popping, and compare the order with the popped one. - Push records of a type with a
priorityfield, 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
| Member | Does |
|---|---|
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, DifferenceWith | set 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
- Take seats 3 and 4 too, then print the first free seat again.
- Use
SymmetricDifferenceWithto find the seats that are in exactly one oftakenand the originalwanted.
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.
| Procedure | Sorts | Stable |
|---|---|---|
Sort( xs ), SortBy( xs, less ) | an Array, in place | no |
StableSort( xs ), StableSortBy( xs, less ) | an Array, in place | yes |
SortList( l ), SortListBy( l, less ) | a LinkedList | yes |
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
- Sort the players by name, then print them.
- Sort an
Array( of ustring )by length, shortest first, withSortByandlen( a ) < len( b ). - Count the comparisons a sort makes with a capturing lambda,
function[ byref n ]( ... ), that adds one tonon 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
| Procedure | Returns | Needs |
|---|---|---|
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 ) - 1 | sorted, < |
LowerBound( xs, v ) | first index not less than v | sorted, < |
UpperBound( xs, v ) | first index greater than v | sorted, < |
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
- Search the sorted scores for 85 with
BinarySearch, then withUpperBound. Which index does each give, and why? - Insert 75 at the position a missed
BinarySearchreports, 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
| Procedure | Leaves 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
- Rotate by -1 instead of 2 and predict the result.
- 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
- Give
Cellalabelfield that=ignores, and check thatHashOfmust ignore it too. - Make the hash
containers.HashOf( row ) xor containers.HashOf( col )and explain whyCell( 2, 3 )andCell( 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
- Declare a
Map( of ustring, int32 )of three colours with a brace list and print its sorted keys. - Give
TallyaPushmember instead ofAddand 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:
| Level | Pages |
|---|---|
Goroutines and channels, from std/tasks | spawn, channels, select, worker pool, cancellation |
| Operating-system threads and shared memory | threads, 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
- Split the work into eight goroutines instead of four.
- 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
- Split the range 1 to 1000 between five threads.
- Add a field to
Slicethat 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.
| Call | Does |
|---|---|
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
- Remove the
mutexlockandmutexunlocklines and run the program a few times. - 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.
| Call | Does |
|---|---|
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
- Make the mailbox hold up to three items, with a count instead of
full. - Start two consumer threads and use
condbroadcastwhen 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:
| Call | Does | Returns |
|---|---|---|
FB.AtomicFetchAddLong( p, v ) | adds v to *p | the value before the add |
FB.AtomicCasLong( p, expected, desired ) | stores desired only if *p is expected | the value before; success is result = expected |
FB.AtomicLoadLong( p ) | reads *p | the 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
- Use the value
AtomicFetchAddLongreturns to give each of the four threads a distinct number from 0 to 3, and print the sum of the numbers. - Replace the atomic add with
r->hits += 1and 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:
spawntakes asuborfunctiondeclared at module level; a function's result is discarded. The file must#import "std/tasks", orspawnis refused withTKO0423: '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
WaitGroupis how you wait.
Activities
- Spawn ten goroutines and check that all ten labels arrive.
- Remove
byvalfromprefixand 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
| Operation | Does |
|---|---|
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 ch | receives 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
- Add a third stage between
KeepOddand the main program that squares each value. - Give
numbersa 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.
| Case | Runs when |
|---|---|
case receive v from ch | a value can be received into v, declared beforehand |
case receive from ch | a value can be received; it is thrown away |
case send v to ch | v can be sent |
case timeout ms | nothing else became ready within ms milliseconds |
case else | nothing 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
- Change the second
timeoutto10and predict the output. - Put the second and third
selectin a loop that runs until both replies have arrived, with atimeoutof 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:
| Member | Does |
|---|---|
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
- 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.
- 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 aselect, as inCounter, which offers each value until the context is done.ctx.IsCancelled( )can be tested between pieces of work, as inPoll.
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
- Cancel the counter after five values instead of three.
- Give
PollaWithTimeoutof 200 ms whose parent isctx, 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:
| Type | Holds |
|---|---|
LocalDate | a calendar date, with no time |
LocalTime | a time of day, with no date |
DateTime | a date and a time, with or without an offset from UTC |
Instant | one point on the UTC timeline, the same everywhere |
TimeSpan | a 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
- Print the
LocalDateof your own birthday. - Make
whenone second before midnight and printwhen.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
- Print the day of the week you were born on.
- 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.
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
- Print the last day of every month of 2024 using
EndOfMonth. - 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
| Reader | Gives | For span |
|---|---|---|
TotalHours, TotalMinutes, ... | the whole span in one unit, with a fraction | 26.5 |
Hours, Minutes, ... | one piece of it | 2 |
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
- Add up three lap times and print the total in minutes.
- Print
TotalSecondsandSecondsforTimeSpan( 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
- Parse
"2025-12-31T23:59:59Z"as anInstant, add one second, and print the result. - Find out what
TryParseIsomakes 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
| Letters | Field | For d |
|---|---|---|
yyyy, yy | year | 2025, 25 |
M, MM, MMM, MMMM | month | 3, 03, Mar, March |
d, dd, ddd, dddd | day, weekday name | 4, 04, Tue, Tuesday |
H, HH, h, hh | hour, 24- and 12-hour | 14, 14, 2, 02 |
mm, ss | minute, second | 30, 05 |
tt | AM or PM | PM |
zzz, K | offset; 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
- Print the date as
4-Mar-2025. - Parse
"12/31/2025"as month/day/year withTryParseExact.
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:
| Call | Keeps | Changes |
|---|---|---|
WithOffset( m ) | the wall-clock reading | the moment it names |
ToOffset( m ) | the moment | the wall-clock reading |
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
- Print the moment
2025-06-01T12:00:00Zas it reads in Tokyo (+09:00). - Print
TimeZoneInfo.LocalOffsetAtfor 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
- Time
SumTo( 10000000 )and print the result withTotalMilliseconds. - Stop the stopwatch, sleep, start it again, and check that
IsRunningis 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
| Function | Gives |
|---|---|
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, atan2 | trigonometry, 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
- Print
int( 2.7 ),int( -2.7 ),fix( 2.7 )andfix( -2.7 ), and predict each before you run it. - 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
- Round 2.5, 3.5 and 4.5 with both
RoundandRoundHalfEven. - Use
FloorModto 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.
| Family | On overflow |
|---|---|
AddChecked, SubChecked, MulChecked, DivChecked, NegChecked, AbsChecked, IPowChecked | return an empty Optional |
AddSaturating and its siblings | stop at the end of the range |
AddWrapping and its siblings | wrap, 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.
^ 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
- Use
MulCheckedto find the first factorial that does not fit in anint32. - Give two
uint8variables the values 0 and 1, and comparenumeric.SubSaturatingof them with their difference stored in auint8.
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
- Print
IsNaN( sqr( -1 ) ). - Find the smallest power of ten that is infinite in a
float64with a loop andIsInfinite.
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.
| Member | Gives |
|---|---|
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
- Roll two dice 1000 times with a seeded
Rngand count how often they sum to 7. - 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
- Print
uuids.NilUuid( ).ToString( )anduuids.MaxUuid( ).ToString( ). - Keep a
containers.Map( of uuids.Uuid, ustring )of names, and look one up by aUuidyou 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
Currency | Decimal | |
|---|---|---|
| holds | an int64 counting ten-thousandths | a 96-bit whole number and a scale |
| decimal places | always 4 | 0 to 28, per value |
| size | 8 bytes | 16 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
Decimalkeeps its scale: 59.97 times 0.0825 is 4.947525, andRound( 2 )makes it cents. - Parsing is strict:
"1.23456"has too many places forCurrencyand 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.
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
- Add 0.10 to a
Decimalten times and compare the total with 1.00. - 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
| Function | Gives |
|---|---|
Count, Sum, Mean | how many, the total, the average |
Median, Percentile( a, p ) | the middle value, and the value p percent of the way up |
MinOf, MaxOf, Range | the extremes, and the distance between them |
Variance, StdDev | spread, for a sample (dividing by n - 1) |
VariancePopulation, StdDevPopulation | spread, for a whole population (dividing by n) |
Covariance, Correlation | how 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
- Add the scores to a
RunningStatsand print itsStdDev; compare it withnumeric.StdDev. - 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:
- Command-line arguments:
command( n )and its relatives. - Environment variables:
environandsetenviron. - Exit codes:
end n. - The os module: arguments, the environment and the machine, without losing anything.
- Ctrl-C: stopping cleanly when the user asks.
- Running other programs:
exec,shellandopen pipe.
Reading and writing files is covered in its own section.
Activities
- Run the program with no arguments, then with five.
- 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
| Expression | Gives |
|---|---|
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
- Make the program print
usage: args NAMEand stop when it is given no arguments. - Accept
--verboseanywhere 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
- Set
BYEXAMPLE_LEVELin your shell before running the program, and watchEnvOrreturn it. - 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.
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
- Change
CountErrorsto return 0 and check the exit code in your shell. - 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
| Call | Gives |
|---|---|
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.
Set the environment before starting threads. On Linux, changing a variable while another thread reads the environment is not safe.
Activities
- Set
BYEXAMPLE_LEVELin your shell and run the program again. - 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.
| Call | Does |
|---|---|
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
- Run the program and press Ctrl-C before it finishes.
- Pass
falsetoCatchInterruptand press Ctrl-C three times. What doesos.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]
| Statement | Runs | Returns |
|---|---|---|
exec( program, args ) | a program, directly | its exit code |
shell( "command" ) | a command line, through the system shell | its exit code |
open pipe "command" for input as #f | a command line, whose output the program reads | a 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
- Make the child read an environment variable that the parent sets just
before
exec, and print it. - 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:
- Paths: taking paths apart and putting them together.
- Reading and writing files: a whole file in one call.
- Line by line:
File,TextWriterandReadLines. - Text encodings: UTF-16, Windows code pages and raw bytes.
- Opening a File: the ways to open, and what each refuses.
- Records at a position:
RecordFile,ReadAtandWriteAt. - Binary formats: numbers in a byte order, and varints.
- Streams: one interface for files, memory and your own types.
- File information:
Statand the yes-or-no questions. - Directories: creating, walking and removing trees.
- Where files live: the configuration, data and cache directories, and the program's own path.
- Checksums and hashes: CRC-32, SHA-256 and HMAC.
- TOML settings: reading a configuration file with types.
- INI files: sections of
key = value, read and written.
Activities
- Write a second file into the scratch directory and check that the count
RemoveDirAllprints goes up by one. - 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
| Function | Gives |
|---|---|
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.
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
- Print
fs.PathParent( "main.tiko" ). What is the parent of a bare name? - Use
PathComponentsto 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
| Function | Does |
|---|---|
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
- Read a file that is not there with
ReadAllLinesand print the failure'sMessage( ). - Write the text with a carriage return and line feed after each line and
check that
ReadAllLinesgives 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
- Stop the loop with
exit forafter the second line. - 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:
| Call | Writes |
|---|---|
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
- Write
textwithUtf16BE( )and withUtf8( true ). Which bytes are the BOM in each? - Read the Windows-1252 file with
Utf16LE( ). How many characters come back, and why? - Change the
TextDetectfallback toBinaryMode( ). 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
| Method | Opens for | An existing file is | A missing file |
|---|---|---|---|
OpenRead( p ) | reading | read | FS_NOT_FOUND |
Create( p ) | writing | emptied | created |
CreateNew( p ) | writing | refused, FS_ALREADY_EXISTS | created |
OpenAppend( p ) | writing at the end | kept | created |
Open( p, options ) | what the options say | as the options say | as 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
- Open the file twice with
CreateNewin a loop and print each result. - Set
AppendandTruncatetogether and print the failure'sMessage( ).
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
- Add a
bonus as int32field toScore. Which numbers in the output change? - Write record 6 with
PutRecord, then printCount( )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
- Write the id big-endian instead, and see which bytes of the record change.
- 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.
| Type | Is |
|---|---|
File | a file on disk; each Read and Write is one call to the system |
MemoryStream | a growable buffer of bytes in memory |
BufferedReader, BufferedWriter | a buffer over another stream |
TextReader, TextWriter | lines 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
- Wrap the
Filein aBufferedWriterand theTextWriteraround that. - Make
LineCounteralso 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
- Make the file read-only with
fs.SetReadOnly( p, true )andStatit again. Statthe scratch directory and print itsIsDirectory( ).
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.
| Call | Does |
|---|---|
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
- Set
o.ContentsFirst = trueand see each directory listed after what is in it. - Set
o.MaxDepth = 1to list only the entries directly insidedir_.
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/~
| Function | Windows | Linux | macOS |
|---|---|---|---|
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
- Build the path of
settings.iniinConfigDir( )under a name of your own, create its directory and write the file. - 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.
| Choose | When |
|---|---|
HASH_CRC32, HASH_CRC32C, HASH_ADLER32 | checking for accidental damage |
HASH_FNV1A64, HASH_XXHASH64 | fast hashing of keys and large data |
HASH_SHA256, HASH_SHA512 | anything someone might change on purpose |
HASH_MD5, HASH_SHA1 | only 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
- Hash
"hello"with every algorithm in the table and compare the lengths. - Change one letter of the message
Signreceives 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
- Change
retries = 3toretries = "3"and see which default is printed. - Add
cfg.SetKey( "retries", config.TomlValue.FromInt( 5 ) )and printconfig.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
- Add a line
Data = /tmpunder[Paths]and check which valueGetreturns. - Set
strict.CaseSensitiveKeys = trueand 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
httporhttps.
Activities
- Parse
"::1"and printIsLoopback( ). - 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
- Add
Show( "fe80::1%eth0" )and printZone( )for it. - 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
- Close
serverbefore connecting and print theConnectfailure'sMessage( ). - Send 100,000 bytes with
WriteAlland read them on the other side withReadExact. Then callclient.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
- Reply from
btofrom, and receive the reply ona. - Use
a.Connectanda.Sendinstead ofSendTo.
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
- Print
u.Resolve( "../../../x" ).Value( ).ToString( ). Where does it stop? - Parse
"http://[::1]:9000/"and printHostandHostPort( ).
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
- Change the server's status line to
HTTP/1.1 404 Not Foundand printr.Value( ).IsSuccess( ). - Fetch
"http://127.0.0.1:1/", where nothing listens, and print the failure'sMessage( ).
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:
- Logging: your own
Logger, levels, fields and child loggers. - The default logger: the logging macros.
- Terminal UI and Layout and widgets.
- SQLite and Prepared statements.
Activities
- Change the level passed to
LogInittologs.LOG_DEBUG. - Remove
tr.stamp = logs.TS_NONEand 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
- Raise
MinLeveltoLOG_WARNand predict which lines remain. - 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
| Macro | Writes |
|---|---|
logs.LogTrace( m ) ... logs.LogFatal( m ) | a message at that level |
logs.LogEventInfo( fields, m ) and its siblings | a 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
- Call
logs.LogEventWarnfrom insideSaveSettingswhenbytesis zero. - Set
tr.showsite = trueand 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:
Initsets up the state.Updatereceives an event, such as a key, the mouse, a resize or a timer tick, and changes the state. It never draws.Viewdraws 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
- Make the
+and-characters change the count as well: a character key arrives withe.key.code = tui.TUIKEY_CHARand the character ine.key.ch. - 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:
| Widget | Draws |
|---|---|
TuiBlock | a border with titles; Inner is the space inside it |
TuiList | items, with the selected one marked by selectPrefix |
TuiParagraph | text, aligned |
TuiGauge | a 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
- Change the list's width to
TuiPercent( 50 ). - Set the gauge's
ratioto 0.3 and itslabelto"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_execruns SQL that returns no rows; several statements may be given at once, separated by;.sqlite3_prepare_v2compiles a query into asqlite3_stmt ptr.sqlite3_stepreturnsSQLITE_ROWfor each row andSQLITE_DONEat the end;sqlite3_column_int64,sqlite3_column_textand their siblings read the row's columns, counting from 0.sqlite3_finalizefrees 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
- Add a fourth city and print only the cities with more than 12 million people.
- Change
:memory:tocities.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:
sqlite3_prepare_v2compiles the SQL once.sqlite3_bind_text,sqlite3_bind_int64and their siblings give a value to each?, counting from 1.sqlite3_stepruns it.sqlite3_resetmakes it ready to run again with new values.sqlite3_finalizefrees it.
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
- Add an
update item set price = ? where name = ?statement and bind both values. - Replace the
rollbackwithcommitand 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, soaliasfixes the spelling; it also lets the tiko name differ, asCLengthis C'sstrlen.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.
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
- Declare C's
tolowerin theextern "C"block and lower-case the word again. - Declare
atoiwithdeclare ... cdecl aliasinstead 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
| C | tiko |
|---|---|
char, unsigned char | byte, uint8 |
short, unsigned short | int16, uint16 |
int, unsigned int | int32, uint32 |
long long, unsigned long long | int64, uint64 |
float, double | float32, float64 |
size_t, intptr_t | uinteger, integer |
T *, void * | T ptr, any ptr |
const char * | const zstring ptr |
long, unsigned long | clong, 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.
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
- Add a line for
shortand forsize_t. - Declare C's
labs, which takes and returns along, usingclong.
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
- Declare C's
strrchrand print what follows the last=in"a=b=c". - Declare
toupperand use it to upper-case azstring * 16in 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
- Sort an array of
float64values withqsort. - 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
- Add an
int64field aftervalueinNaturaland predict the new size. - Declare C's
lldiv, which returns twolong 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
#cinclude"ctype.h"and callisdigiton each character of"a1b2".- Run
tiko -cimport string.h -cimport-out -and findstrlenin 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
| Call | Does |
|---|---|
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
- Look up
cbrtas well and print the cube root of 27. - 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_SUITEgroups related tests. EachTEST( name )…END_TESTis one test, and its body is ordinary code.CU_ASSERT_EQUAL( a, b )records a pass whena = band a failure otherwise. The summary counts both, per suite.fbcu.run_tests( )runs every test, prints the summary, and returnstrueonly 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
- Add a test that
Clamp( 5, 5, 5 )is5. - Break
Clampon 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
| Macro | Passes 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
- Add
CU_ASSERT_EQUAL( sum, 0.3 )tofloating_pointand read the failure. - Tighten the tolerance to
1e-20and 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
- Fix
WordCount(count the starts of words, not the spaces) and run the tests again. - 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.
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
- Add a public
UpperWordstowordstatsthat capitalises each word, and a test for it. - 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
- Remove the
#cmdlineline, buildassertfail.tikoagain, and run it. - Change the second
asserttoassertwarnand 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 erropens the standard error stream as a file number. The program's real output stays on standard output, soprogram > result.txtstill captures exactly the result, and the trace goes to the screen, or to a file of its own with2> trace.txt. - Tracing is switched off by the preprocessor, not by an
if. Remove the#define TRACINGline andTRACE( … )expands to nothing: the calls and the work of building their messages are gone from the program.TraceAtitself 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.
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
- Add a
TRACEinside theforloop and watch the output grow. - Remove
#define TRACINGand 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:
| Name | Value | Type |
|---|---|---|
scores | (1 to 3) | Score() |
total | 0 | long |
i | 1 | long |
scoresopens toscores(1),scores(2)andscores(3): elements are named by the subscripts the program uses, so they match the source. Each element opens toplayerandpoints, and austringshows 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
int32is alongand afloat64adouble. F10runs line 10 and stops on the next one; press it again and again to watchtotalgrow andicount. A value that changed since the last stop is shown in red.F6continues to the next breakpoint — here, line 10 again, for the next element. Take the breakpoint away withF9first, andShift+F11runs to the end ofAverageand stops back in the main program.
The keys:
| Key | |
|---|---|
F9 | breakpoint on the cursor's line, on or off |
F6 | build and start; in a session, continue |
F10 / F11 / Shift+F11 | step over / step into / step out |
Ctrl+F10 | run to the cursor's line |
Ctrl+F6 | break into the running program |
Shift+F6 | stop; the program ends |
Ctrl+Shift+F9 | remove 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.
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
- Step through the loop with
F10and stop whentotalis27. What isi? - Add a watch on
total / 3and ontotal. Which one shows a value, and why? - Set a second breakpoint on the
printline, remove the first one on the Breakpoints tab, and pressF6again.
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.
| Option | Does |
|---|---|
-o file | names the output file, exactly as written; without it, the program is named after the first source file (app.exe on Windows, app on Linux) |
-c | compiles to an object file and does not link |
-lib | builds a static library, libname.a |
-e | runtime error checks |
-exx | -e, plus array-bounds and null-pointer checks, and where each error happened |
-g | debug information for a debugger; also turns assert on |
-O0 … -O3 | how hard the optimiser works; -O0, none, is the default |
-mt | the thread-safe runtime; std/tasks requires it, and a threadcreate call turns it on by itself |
-target name | windows-x86_64 or linux-x86_64; the platform you are on is the default |
-d NAME=value | defines a preprocessor symbol |
-v | shows 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
- Build
defaults.tikowith-exx -O2on the command line and compare the output. - Build it with
-gand 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
#cmdlinetakes precedence over the same option on the command line. - Only the first file counts.
#cmdlinein any other source file named on the command line is warningTKO0334and 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
- Add
-gtobuildinfo.tiko's#cmdlineand predict the new output. - Build
buildinfo.tikowith-O0on 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.
| Build | Options | Use it for |
|---|---|---|
| checked | -exx, often with -g | development and tests |
| release | -O2 or -O3 | the 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
- Remove the
#cmdlineline, build again, and run the program. - Replace
-exxwith-earrayand 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.tikowritesgreet.o.tiko main.tiko greet.ocompilesmain.tikoand links it with the object.main.tikodeclaresGreetwithdeclare, 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 upcomes 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
- Name the object file first:
tiko greet.o main.tiko. Does the order change the output? - Remove the
declareline frommain.tikoand 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.tikocompiles the file and writeslibmathlib.a, on both targets.#inclib "mathlib"tells the linker to use it. The name has nolibprefix and no extension.-l mathlibon the command line does the same.- The linker searched the current directory.
-p diron 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
- Move the two
declarelines intomathlib.tiand#includeit. - Add a
Factorialfunction 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 calledgreeter, whose main file ismain.tiko.tiko build, run in the directory that holdstiko.toml, builds every target it declares. It takes no file names and no-o: the program is written toout/debug/greeter.exeon Windows andout/debug/greeteron Linux.tiko build greeterbuilds the one target by name.[target.greeter.defines]passesTIMESas though-d TIMES=2had been typed, which is why the greeting appears twice.tiko runbuilds the project's executable and runs it. Everything after--goes to the program, sotiko run -- tikogreetstiko; without it, the program greetsworld.
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
| Command | Does |
|---|---|
tiko build [target] | builds the targets, into out/<profile>/ |
tiko run [target] [-- args] | builds one executable target and runs it |
tiko check | reports every error and warning a build would, and writes nothing |
tiko clean | removes 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
- Change
TIMESto3intiko.tomland run the project again. - Run
tiko checkafter introducing a typo inmain.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
| Profile | Options |
|---|---|
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:
| Key | Sets |
|---|---|
inherits | the profile this one starts from |
opt | the optimisation level, 0 to 3 |
errors | the checks: "e", "ex", "exx", or "" for none |
debug | whether -g is given |
options | further 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
- Add a profile
checkedthat inheritsreleaseand setserrors = "exx", and build with it. - Run
tiko run --profile releaseand 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.
| Target | Builds | Links against |
|---|---|---|
windows-x86_64 | a Windows executable | Microsoft's Universal C Runtime |
linux-x86_64 | a Linux executable | the system's C library |
macos-arm64 | a macOS executable, for Apple Silicon | the 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
- Add a
typewith anint32field and afloat64field, predict itssizeof, and print it. - Compile
targets.tikowith-c -target linux-x86_64on Windows (or-target windows-x86_64on 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 form | Modern tiko |
|---|---|
.bas sources, .bi headers | .tiko sources, .ti headers |
string | ustring, which holds text rather than bytes (next pages) |
long, integer | int32, int64; integer only where pointer size is the point |
while … wend | do while … loop |
+ to join strings | &, which also converts numbers |
| a counter and a loop | for 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
- Rewrite
classic.baswithdo while … loopand&, keeping the.basextension, and check it still compiles. - Rename
classic.bastoclassic.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
dataholds a list of constants, numbers and strings, anywhere in the file.readtakes the next values from that list, in the order thedatastatements appear, converting each to the type of the variable it fills.restoremoves the reading position: to the firstdataafter a label, or with no label to the firstdatain 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.
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
- Remove one
"Paris"from thedatalines and predict what the program prints. - 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
- Add a fourth option to both menus and compare how much each one changed.
- Make
AddOnetake 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
- Declare the two variables in
suffix.basproperly and make it compile. - Print
mid( bytes, 4, 1 )andmid( text, 4, 1 )and explain the difference.