§Tiro Reference Documentation

§Basics

Tiro accepts UTF-8 encoded source files as input. Source files must conform to the rules outlined in this document to form a valid program.

This section describes the lexical structure of tokens accepted by the language.

Note

Grammar rules in this section work on characters. No whitespace is not allowed unless it matches one of the required patterns.

§Whitespace

Whitespace tokens consists of a sequence of characters having the Pattern_White_Space Unicode property (see (see Unicode® Standard Annex #31). Whitespace is not significant: its only use is to separate characters of adjacent tokens that would otherwise be parsed as a single token.

§Comments

Comments are used to annotate source code, but they have no meaning of their own. They are simply treated as whitespace.

LineComment
// (~\n)*
BlockComment
/* (BlockComment | ~*/*)* */

Line comments are introduced by // and continue until the end of the line. Block comments are delimited by /* and */ and may be nested.

§Keywords

The following words have special meaning in the Tiro programming language:

assertbreakconstcontinuedefer
elseexportfalseforfunc
ifimportinnullreturn
truevarwhile

§Reserved keywords

The following keywords are reserved for future use:

ascatchclassinterfaceis
packageprotocolscopestructswitch
throwtryyield

§Identifiers

Identifiers are used to name items. They consist of one or more identifier characters as defined by the XID_Start and XID_Continue Unicode properties (see Unicode® Standard Annex #31). Keywords cannot be used as identifiers.

Identifier
KeywordOrIdentifierExcluding keywords
KeywordOrIdentifier
(XID_Start | _) XID_Continue*

§Number literals

DecInteger
DecDigit (DecDigit | _)*
BinInteger
0b (BinDigit | _)* BinDigit (BinDigit | _)*
OctInteger
0o (OctDigit | _)* OctDigit (OctDigit | _)*
HexInteger
0x (HexDigit | _)* HexDigit (HexDigit | _)*
DecDigit
0 - 9
BinDigit
0 | 1
OctDigit
0 - 7
HexDigit
0 - 9 | a - f | A - F

Todo

Document floating point numbers.

§String literals

Strings are sequences of characters enclosed by matching quote characters (" or '). Within a string, variables can be interpolated when prefixed with a $ sign and full expressions can be interpolated when enclosed within a block started with ${ and closed with a } sign.

StringDelimiter
" | '
PlainStringContent
~(\ | $ | StringDelimitermatches starting delimiter)*
InterpolatedValue
$ VarExpr | ${ Expr }

Todo

Open design questions:

  • Should strings be multi line by default (current state)? Or should a \ at the end of a line be necessary to continue to the following line?
  • Should all strings support interpolation by default? Or provide a variant (e.g. delimited by ') that does not support interpolation?
  • Should all strings be guaranteed (required) to be valid UTF-8?
Example:
import std;
export func main() {
const greeting = "Hello";
std.print("$greeting ${get_target()}!");
}
func get_target() {
return "World";
}

§Escape sequences

EscapedCharacter
\\ | \n | \r | \t | \" | \' | \$
AsciiEscape
\x HexDigit HexDigit
UnicodeEscape
\u{ HexDigit+ }

Escape sequences produce the following string content:

Escape SequenceProduction
\\Literal \
\nNewline
\rCarriage return
\tTabulator
\"Literal "
\'Literal '
\$Literal $
\x digits..A single byteThe numeric value of the two hex digits is produced as a byte (TODO: Must be valid utf-8?)
\u{ digits.. }A single unicode code pointThe sequence of hex digits is interpreted as the value of a unicode code point.

§Symbol literals

Symbol
# Identifier

Todo

Documentation

§Grammar

This section describes higher level syntax constructions based on tokens.

Note

Grammar rules in this section work on tokens. Any amount of white space may appear between matching tokens.

§Items

File
Item*
Item
export? (ImportDecl ; | VarDecl ; | FuncDecl)

A Tiro source file is a sequence of Items. An item containing the export modifier will be exported from the current module. Exported items are visible to other modules and may be imported.

§Imports

ImportDecl
import ImportPath (as Identifier)?
ImportPath
Identifier (. Identifier)*

An import item imports the module referenced by the ImportPath and introduces it into the current scope. The last Identifier in the ImportPath serves as the name of the imported reference by default. A custom name may be provided by using the as keyword.

Todo

The import path must currently point to a module. It should also allow for entities within a module.

For example, import std.print as myprint should be valid.

Grouping imports should be possible, for example import std { print, PI as MY_PI}.

Example:
import std;
export func main() {
std.print("Hello World");
}

§Variables

VarDecl
(const | var) Binding (, Binding)*
Binding
BindingPattern (= Expr)?
TupleBindingPattern
( Identifier (, Identifier)* )

Variable declarations introduce one or more variables into the current scope. A variable declaration contains Bindings, which may provide an initializer expression for the Identifier or Identifiers mentioned in the BindingPattern.

Bindings introduce a single new variable when the BindingPattern consists of a single identifier. The optional initializer expression may yield any value in this case.

A TupleBindingPattern introduces multiple variables at once. The optional initializer for a tuple pattern must yield a tuple value with a compatible size (at least the number of declared variable names).

Using the var keyword creates a mutable variable, while a variable declared using the const keyword cannot be reassigned after its initialization. Constants must have an initializer.

Declaration kindExampleMeaning
Simple declarationvar x;Declares the initialized variable x in the current scope
Multiple variablesvar x, y;Declares x and y in the current scope
With initializervar x = 3.14;Declares x and with the initial value 3.14
Constant declarationconst x = 3.14; Like the above, but x cannot be reassigned anymore
Tuple unpackingvar (x, y) = (1, 2);Unpacks the right hand side tuple into the new variables x and y
Example:
import std;
export func main() {
var a = "single variable";
const b = 1, c = 2;
var (d, e) = ("first", "second");
std.print(a, b, c, d, e);
}

§Functions

FuncDecl
func Identifier? ( ParamList? ) FuncBody
ParamList
Identifier (, Identifier)*
FuncBody
= Expr | BlockExpr

A function declaration creates a new function. Functions may have a name, a set of parameters and a body. When a function is invoked, values passed as arguments in the function call expression will be bound to the declared parameters of that function.

The body of a function may be specified as either a BlockExpr or as a single expression introduced by a = sign. The shorthand syntax is useful for very simple functions.

Example:
import std;
export func main() {
std.print(block_body(1));
std.print(shorthand_body(2));
}
func block_body(p) {
const result = p * 2;
return result;
}
func shorthand_body(p) = p * 2;

Todo

Open design question: Use -> or => instead of the plain = in the shorthand syntax?

§Statements

Statements are constructs used in block expressions such as function bodies. Most statements must be terminated by a semicolon. The semicolon is optional for block-like statements, where the end of the statement is obvious from the position of the closing }.

§Assertions

AssertStmt
assert ( Expr (, String)? )

An assertion verifies that the expression yields a truthful value. A failed assertion results in a panic.

An optional message argument may be specified; it will be included in the panic's diagnostic error message.

Note

Assertions should be used to verify program invariants during development. They may be disabled by compilation flags. Do not rely on their presence at runtime. Most importantly, avoid side effects inside assertions.

Example:
export func main() {
var n = 1;
assert(n % 2 == 0, "n must be even!"); // fails
}

§Defer statements

DeferStmt
defer Expr

The defer statement registers an expression to be evaluated when the program leaves the current scope. It's main use is to ensure that resources are cleaned up properly.

Defer statements that have been visited by the program will run even if the scope is being left abruptly, such as by an early return, break, continue or a panic.

Example:
import std;
export func main() {
std.print("acquire resource");
defer std.print("release resource");
std.print("use resource");
}

§While loops

WhileStmt
while ExprExcluding BlockExpr BlockExpr

A while loop consists of a condition expression and a loop body. The condition will be repeatedly evaluated, and while it is truthful, the loop body will also be executed. The loop's execution stops as soon as the condition is false.

Example:
import std;
export func main() {
var i = 0;
while i < 3 {
std.print(i);
i += 1;
}
std.print("done");
}

§For-each loops

ForEachStmt
for BindingPattern in ExprExcluding BlockExpr BlockExpr

The for-each loop allows iteration over a collection or sequence such as an array. It requires a BindingPattern that specifies the variable names used for iteration and an expression that yields an iterable sequence.

Example:
import std;
export func main() {
const numbers = [1, 2, 3];
for c in numbers {
std.print(c);
}
const constants = map{
"pi": 3.14,
"e": 2.72
};
for (name, value) in constants {
std.print("$name = $value");
}
}

§Classic for loops

ForStmt
for (VarDecl? ; Expr? ; Expr?)Excluding BlockExpr BlockExpr

The classic for loop allows iteration using one or more control variables, a condition expression and an update step.

When the loop starts its execution, the optional control variables are defined and possibly initialized.

Before every iteration, the optional condition expression is evaluated, and if it is truthful, the loop body is executed. If the condition is false, loop execution stops. If the condition is not defined, it is implicitly truthful.

The optional update expression is evaluated after every completed loop iteration.

Example:
import std;
export func main() {
for var i = 0; i < 3; i += 1 {
std.print(i);
}
}

§Expressions

Expressions are syntax constructs that yield a value. Tiro is an expression-oriented language; most syntax constructions are expressions.

Todo

Open design question: should loops be expressions?

§Evaluation order

All expressions are evaluated strictly from left to right.

§Variable expressions

VarExpr
Identifier

A variable expression yields the current value of the variable identified by the given identifier. Tiro is a lexically scoped language: variables may only be referenced from within the scope in which they have been defined. If there are multiple variable declarations for the same identifier in scope, the closest variable declaration is referenced.

Example:
import std;
export func main() {
const x = 0;
{
const x = 1;
{
const x = 2;
std.print(x);
}
std.print(x);
}
std.print(x);
}

§Field expressions

FieldExpr
Expr (. | ?.) Identifier
TupleFieldExpr
Expr (. | ?.) NonNegativeInt

A field expression evaluates the expression left to the . symbol and yields the specified field of that value.

The ?. token can be used to make the field lookup optional. If the left hand side is null, the field expression will also yield null.

The tuple field expression is a variant of the above that allows to lookup a tuple member instead. The left hand side expression must yield a tuple value and the right hand side token must be a non negative integer.

Example:
import std;
export func main() {
const rec = (x: 1);
const no_rec = null;
std.print(rec.x);
std.print(rec?.x);
std.print(no_rec?.x);
const tuple = (1, 2);
const no_tuple = null;
std.print(tuple.1);
std.print(tuple?.1);
std.print(no_tuple?.1);
}

§Index expressions

IndexExpr
Expr ([ | ?[) Expr ]

Index expressions are used to lookup a value within a container by its index.

The ?[ token can be used to make the value lookup optional. If the left hand side is null, the index expression will also yield null and the expression between brackets will not be evaluated.

Example:
import std;
export func main() {
const numbers = [1, 2, 3];
std.print(numbers[1]);
const constants = map{
"pi": 3.14,
"e": 2.72
};
std.print(constants["pi"]);
const no_container = null;
std.print(no_container?[1]);
}

§Call expressions

CallExpr
Expr (( | ?() CallArguments? )
CallArguments
Expr (, Expr)*

A Call expression invokes a function with a set of arguments.

The first expression must yield a function. Expressions passed as arguments (between parentheses) will be evaluated, then control will be passed to function with all arguments bound to the corresponding function parameter. The call expression yields the return value of the invoked function.

The ?( token can be used to make the function call optional. If the function value yields null, the arguments will not be evaluated and the entire expression will also yield null.

Example:
import std;
export func main() {
std.print(add(1, 2));
std.print(add(4, 2 * 3));
}
func add(x, y) {
return x + y;
}

§Unary expressions

UnaryExpr
UnaryOp Expr
UnaryOp
+ | - | ! | ~

Unary expressions evaluate the right hand side expression and then apply the specified operator to its result.

OperatorDescription
+Unary plus. Requires a number operand. Yields the number as-is.
-Unary minus. Requires a number operand. Yields the arithmetic negative of its operand.
!Unary NOT. Returns the logical negative of its operand: false for all truthful values, true otherwise.
~Bitwise NOT. Requires an integer operand. Yields an integer with all bits inverted.

§Binary expressions

BinaryExpr
Expr BinaryOp Expr
BinaryOp
+ | - | * | ** | / | % | & | | | << | >> | ^ | == | != | < | > | <= | >= | && | || | ??

Binary expressions apply an operation to the left hand and right hand side expressions.

§Arithmetic operators

These operators require two numeric operands and apply the specified arithmetic operation on them.

OperatorNameDescription
a + bAdditionYields the sum of a and b.
a - bSubtractionYields the difference of a and b.
a * bMultiplicationYields the product of a and b.
a ** bPowerYields a raised to the power of b.
a / bDivisionYields a divided by b.
a % bModuloYields the remainder after dividing a by b.
§Binary operators

Operators that operate on the bits of their operands.

OperatorNameDescription
a & bBinary ANDTODO
a | bBinary ORTODO
a ^ bBinary XORTODO
a << bBitwise left shift.TODO
a >> bBitwise right shift.TODO
§Relational operators

Relational operators compare their two operands and yield a boolean value depending on the comparison's result.

OperatorNameDescription
a == bEqual toYields true if a is equal to b, false otherwise.
a != bNot equal toYields false if a is equal to b, true otherwise.
a < bLess thanRequires both operands to be numbers. Yields true if a is less than b, false otherwise.
a > bGreater thanRequires both operands to be numbers. Yields true if a is greater than b, false otherwise.
a <= bLess than or equal toRequires both operands to be numbers. Yields true if a is less than or equal to b, false otherwise.
a >= bGreater than or equal toRequires both operands to be numbers. Yields true if a is greater than or equal to b, false otherwise.
§Logical operators
OperatorNameDescription
a && bLogical ANDYields a if is not truthful. Otherwise yields b. Note that b is only evaluated if a is truthful.
a || bLogical ORYields a if it is truthful. Otherwise yields b. Note that b is only evaluated if a is not truthful.

Todo

Should these operation always return a boolean instead of the result yielded by its operands?
§Other operators
OperatorNameDescription
a ?? bNull coalescingYields a if it is not null. Otherwise yields b. Note that b is only evaluated if a is null.

§Assignment expressions

AssignExpr
Expr AssignOp Expr
AssignOp
= | += | -= | *= | **= | /= | %=

Assignment expressions evaluate their operands and assign the result to the location specified at the left hand side.

The = assignment operator simply evaluates its right hand side and assigns it to the target location. The other compound assignment operators take the current value of the left hand side expression into account:

Compound operatorEquivalent Expression
a += ba = a + b
a -= ba = a - b
a *= ba = a * b
a **= ba = a ** b
a /= ba = a / b
a %= ba = a % b

Not all kinds of expression are valid targets for assignments. The following expressions are valid:

  • Var expressions. The result is written to the specified variable.
  • (Tuple) field expressions. The result is written to the specified (tuple) field.
  • Element expressions. The result is written to the specified element.
  • Tuple expressions containing expressions of the previously mentioned kind. The result should be a tuple that will be decomposed and assigned to the specified locations.

The left hand side of an assignment expression may not use the optional variants of the field-, element- and call expressions.

Example:
import std;
export func main() {
var x = 3;
x = 4;
std.print("(1)", x); // (1) 4
var y = 3;
(x, y) = (y, x);
std.print("(2)", x, y); // (2) 3 4
const z = [1, 2];
(z[0], z[1]) = ("a", "b");
std.print("(3)", z[0], z[1]); // (3) a b
}

§Break expressions

BreakExpr
break

Aborts execution of the current (most nested) for, for each or while loop. Execution resumes directly after the loop statement.

Note that pending defer statements will be executed before execution resumes after the loop.

Example:
import std;
export func main() {
var i = 0;
while (true) {
if i == 3 {
break;
}
std.print(i);
i += 1;
}
std.print("done");
}

§Continue expressions

ContinueExpr
continue

Jumps directly to the end of the current (most nested) for, for each or while loop's body.

Note that pending defer statements will be executed before execution of the loop resumes.

Example:
import std;
export func main() {
for var i = 0; i <= 6; i += 1 {
if i % 2 == 0 {
continue; // Skip even numbers
}
std.print(i);
}
}

§Return expressions

ReturnExpr
return Expr?

Returns from the current (most nested) function. Program execution will continue in that function's caller.

An optional expression may be specified. The value yielded by that expression will be come the function's return value. If the expression is omitted, null is returned.

Note that pending defer statements will be executed when returning from a function.

Example:
import std;
export func main() {
std.print(next_even(1));
std.print(next_even(2));
}
func next_even(n) {
if n % 2 == 0 {
return n;
}
return n + 1;
}

§Grouped expressions

GroupedExpr
( Expr )

Parentheses can be used to group expressions, for example to override the default precedence rules within expressions:

Example:
export func main() {
std.print(1 + 2 * 3);
std.print((1 + 2) * 3);
}

§If expressions

IfExpr
if ExprExcluding BlockExpr BlockExpr (else (IfExpr | BlockExpr))?

If expressions provide support for conditional execution. They consist of a mandatory condition, a then block expression, an optional series of else if expressions with additional conditions and finally an optional else block.

If the condition yields a truthful value, the then block is executed and all other blocks are skipped. Otherwise, conditions of any else if expressions are attempted in order, and the block expression after the first truthful condition will be executed. If no condition yielded a truthful value, the optional else block is executed instead.

Example:
import std;
export func main() {
if_example(3);
if_example(5);
if_example(10);
}
func if_example(x) {
if x == 3 {
std.print("x is 3");
} else if x % 2 == 1 {
std.print("x is odd");
} else {
std.print("x is something else");
}
}

If expressions may be used in positions where a value is expected. If that is the case, then the expression must have an else block and all blocks must yield a value as well.

Example:
import std;
export func main() {
const u = 3;
const v = if u > 0 {
u + 1;
} else {
u - 1;
};
std.print(v);
}

Todo

Open design question: should a shorthand syntax (no block statement) be allowed? For example `const foo = if condition 1 else 2;`

§Function expressions

FuncExpr
FuncDecl

Function expressions yield the declared function as a value.

Example:
import std;
export func main() {
const twice = func(x) = x * 2;
std.print(twice(1));
}

§Block expressions

BlockExpr
{ Stmt* }

Block expressions contain a sequence of statements. When a block expression is evaluated, all its statements are evaluated in order.

Example:
import std;
export func main() {
// this function body is a block expression in its own right
std.print("a");
std.print("b");
// blocks can be nested using if expressions, loops etc. or plainly like this:
{
std.print("c");
std.print("d");
}
}

Block expressions may be used in positions where a value is expected. If that is the case, there must be at least one statement and the last statement must yield a value. That value will then be yielded by the block expression.

Example:
import std;
export func main() {
// This pattern is helpful for constants with complex initialization logic
const value = {
var v = 1;
if true {
v += 1;
}
if !false {
v *= 2;
}
v;
};
std.print(value);
}

§Literals

Literals are expressions are source code representations of concrete values. Literals themselves do not require and additional context to evaluate (although nested expressions might).

§Tuple literals

TupleLiteral
( TupleElements? )
TupleElements
(Expr ,)+ Expr?

Creates a tuple containing the values yielded by the nested expressions in their written order.

In order to avoid ambiguity with grouped expressions, a one-element tuple must be contain a trailing comma.

Example:
import std;
export func main() {
const t0 = (); // empty tuple
std.print(std.debug_repr(t0));
const t1 = (1, ); // one element
std.print(std.debug_repr(t1));
const t2 = (1, 2); // two elements
std.print(std.debug_repr(t2));
}

§Record literals

RecordLiteral
( (RecordElements | :) )
RecordElements
RecordEntry (, RecordEntry)* ,?
RecordEntry
Identifier : Expr

Creates a record containing the values yielded by the nested expressions associated with the declared field names.

In order to avoid ambiguity with empty tuples, an empty record must be spelled (:).

Example:
import std;
export func main() {
const r0 = (:);
std.print(std.debug_repr(r0));
const r1 = (field: "value");
std.print(std.debug_repr(r1));
const r2 = (
x: 3.14,
y: std.PI
);
std.print(std.debug_repr(r2));
}

§Array literals

ArrayLiteral
[ ArrayElements? ]
ArrayElements
Expr (, Expr)* ,?

Creates an array containing the values yielded by the nested expressions in their written order.

Example:
import std;
export func main() {
const a0 = [];
std.print(std.debug_repr(a0));
const a1 = [1, 2, 3];
std.print(std.debug_repr(a1));
a1.append(4);
std.print(std.debug_repr(a1));
}

§Map literals

MapLiteral
map { MapElements? }
MapElements
MapElement (, MapElement)* ,?
MapElement
Expr : Expr

Creates a map with the specified elements. Every element is specified using two expressions. The expression to the right of the : specifies the value, and the left expression specifies the associated key.

Example:
import std;
export func main() {
const pi = 3.14;
const m = map{
1: 2,
"a": null,
"pi": pi,
#foo: "bar",
};
std.print(std.debug_repr(m));
}

§Set literals

SetLiteral
set { SetElements? }
SetElements
Expr (, Expr)* ,?

Creates a set with the specified elements.

Example:
import std;
export func main() {
const pi = 3.14;
const s = set{
2,
null,
pi,
"bar",
};
std.print(std.debug_repr(s));
}

§Modules

Todo

Documentation