Toit syntax fundamentals

This document describes the basics of the Toit language. To learn about Toit's documentation convention, see the language documentation convention section.

Whitespace and indentation

Toit uses whitespace and indentation to construct the code structure: comment. For example:

// Defines globals first, and then uses main function to
// print out something.
i := 1
j := 10

main:
  while i < j:
    print i
    i = i + 1

Declaration and assignment

In the above, we declared new variables i and j using colon-equals. We assigned a new value to the existing variable, i, using a single equals sign. To declare a variable without giving it a value, you can use a question mark: := ?.

  x := 0
  if (random 2) == 0:
    x = 42

  y := ?
  if (random 2) == 0:
    y = 1
  else:
    y = 42

The compiler will check that the new variable is assigned a value before it is used.

Comments

In Toit, a single line comment begins with a double slash // followed by the comment. For example:

// This is a single line comment in Toit.

For multi-line comments, use */.

/*
some multi-line
comment
*/

Identifiers and keywords

Identifiers are names that identify globals, locals, functions, classes, modules, and other objects in Toit.

  • Identifier names can contain only letters, numbers, and underscores (_), but cannot contain spaces.
  • Identifiers need to start with a letter or an underscore (_), but not with a number. The following characters can be alphanumeric or underscore.
  • Toit identifiers are case-sensitive.
  • Identifier names cannot be the same as keywords and built-in functions in Toit.

The following Toit keywords cannot be used for naming identifiers:

as          abstract       assert
break       class          continue
else        false          finally
for         if             import
export      null           return
static      true           try
while       or     and     not

In addition to these keywords, there are some "pseudo keywords" - like constructor - that cannot be used in certain contexts. Trying these pseudo-keywords as globals name will generally give an error message in the IDE.

Another example of pseudo-keyword is operator that is allowed as a local but cannot be the name of a function. Similarly, string can be used like a normal global but would shadow the string type.

String and character literals

Single quotes for are used for character literals and are equivalent to integer literals of the corresponding Unicode code point, or rune.

  print 'A' // Prints "65", the ASCII code of 'A'.
  print '*' // Prints "42", the ASCII code of '*'.
  print 'æ' // Prints "230", the Unicode code point of 'æ'
  print '€' // Prints "8364", the Unicode code point of the Euro symbol.
  a /int := 'a'  // a now contains the integer 97.

Toit uses only double quotes " to denote a string literal:

  print "Hello, World!"
  str := "a string"  // str has type string.

If a string contains a double quote, a backslash, or a dollar sign, you must use the backslash \ to escape it. The backslash can also be used for characters that are difficult to type For example:

message := "This is a \"valid\" string"

The unescaped operator $ is used for string interpolation.

The methods available on string objects are documented in the string class. See also the guide to Toit strings.

Toit globals and constants

The := operator introduces a new global variable with the name of the given identifier (here "global_name").

global_name := 103

If the global is final (that is, can't be assigned to), then you can use ::= as in:

global_number ::= 42

Note that this in this case, global_number is actually a constant, and should thus be written in capital characters:

GLOBAL_NUMBER ::= 42

A final global that isn't a constant could be something that is mutable itself, such as

some_global ::= []

In this case the global is initialized with an empty list. The content of the list can change, but it will always be the same list.

When you name a global, you should adhere to rules defined earlier about identifiers.

Global variables (and static class variables) are initialized when they are first used. This is noticeable if their value is determined by some function call, for example Time.now

TIME ::= Time.now  // Time.now is called on first use of TIME.

main:
  print "time: $Time.now"  // Print time at program start.
  sleep --ms=2000          // Sleep for two seconds.
  print "TIME: $TIME"      // Print the global constant.
  sleep  --ms=100
  print "TIME: $TIME"      // The global constant remains unchanged.

This program will print two different times, the second one twice.

Indexing syntax

Indexing into lists, maps, byte arrays, and strings can be done with square brackets, []. The first element has an index of zero.

For example, an ASCII string is a sequence of characters, and you can access can access its elements using square brackets [] and indexes.

str := "Toit String"
byte_array := #[1, 42, 103]
list := [1, 10, 100, 1000]
map := {
  "foo": 42,
  "bar": 103,
}

main:
  print str[0]         // Prints "84", the ASCII code of 'T'.
  print byte_array[1]  // Prints "42", the byte at position 1.
  print list[3]        // Prints "1000", the integer at position 3.
  print map["foo"]     // Prints "42", the value with the key "foo".

Indexing into strings is done by UTF-8 byte offset, so the story is a little more complex for non-ASCII strings.

Toit also has support for slices. These are views into part of a list, byte array, or string:

  print str[0..4] // Prints "Toit", the first 4 bytes of "Toit String".
  print str[..4]  // Prints "Toit", the first 4 bytes of "Toit String".
  print str[5..]  // Prints "String", the last part of "Toit String".

See also the description of slice syntax in the string guide.

A slice may keep the underlying object alive, preventing a garbage collection. To avoid that you can use copy to create a new object containing only the elements in the slice:

long_lived_global/ByteArray := #[]

main:
  huge := ByteArray 1000000
  // The slice keeps huge alive:
  long_lived_global = huge[1000..1050]
  // Makes a copy, so huge can be garbage collected:
  long_lived_global = long_lived_global.copy

Continuation of statements

Toit uses a newline character to separate statements. It places each statement on one line. However you can break very long statements into multiple lines.

Line breaks in function arguments

In a long list of arguments for a function call you can use a new line for each argument.

A long statement can also span multiple lines by using the backslash () character.

The following example illustrates how to use per-argument newlines, or the backslash () character to continue a statement in the second line:

// A method with many parameters.
foo a b c d e f g h i j: ...

main:
  some_long_local1 := 1
  some_long_local2 := 2
  some_long_local3 := 3
  some_long_local4 := 4
  some_long_local5 := 5
  some_long_local6 := 6
  some_long_local7 := 7
  some_long_local8 := 8
  some_long_local9 := 9
  some_long_local10 := 10
  // Once there is a newline, all further arguments must be
  // on their own line:
  foo some_long_local1 some_long_local2 some_long_local3
    some_long_local4
    some_long_local5
    some_long_local6
    some_long_local7
    some_long_local8
    some_long_local9
    some_long_local10
  // We can also write all of them in one line:
  foo some_long_local1 some_long_local2 some_long_local3 some_long_local4 some_long_local5 some_long_local6 some_long_local7 some_long_local8 some_long_local9 some_long_local10
  // But that becomes hard to read fast.
  // If we want to cut the line at some point we need a `\` to conceptually continue the first line:
  foo some_long_local1 some_long_local2 some_long_local3 \
      some_long_local4 some_long_local5 some_long_local6 \
      some_long_local7 some_long_local8 some_long_local9 \
      some_long_local10

Putting a line break between function arguments also has the effect of grouping the arguments, which can make parentheses unneccessary:

two_arg_function a b:
  return a + b

three_arg_function a b c:
  return a + b + c

my_func a/int b/int c/int -> int:
  // Long line with many parentheses.
  x := three_arg_function (two_arg_function a b) (two_arg_function b c) (two_arg_function c a)

  // One line per argument looks better:
  y := three_arg_function
    (two_arg_function a b)
    (two_arg_function b c)
    (two_arg_function c a)

  // For a cleaner look we don't need parentheses around
  // the arguments that have their own line.
  z := three_arg_function
    two_arg_function a b
    two_arg_function b c
    two_arg_function c a

  return x + y + z

Line breaks in operator expressions

If you need to break up a large expression, the second and subsequent lines should be indented more than the first:

long_expr_function long_argument_name other_argument_name:
  return long_argument_name
    + 2 * other_argument_name
    - long_argument_name.abs

As seen here, the preferred style is to put binary operators after the line break, not before.

Line breaks in multi-line or very long string literals

See the guide to strings on how to format multi-line strings and how to line-break very long string literals.

Precedence

Operators and other syntactic elements have precedence, like in other languages. From highest to lowest:

Postfix operators:

  ++    --

Math operators:

  -    ~    (unary operators)
  *    /    %
  +    -
  <<    >>>    >>
  &
  ^
  |

Type operators

  is    is not    as

Comparison operators

  ==    !=    <    <=    >    >=

Assignment operators

  =  :=  ::=  +=  -=  *=  /=  %=  |=  ^=  &=  <<=  >>=  >>>=

Calls and arguments

Logical operators

  not
  and
  or
  ?:

Postfix operators

The postfix operators, ++ and -- have the highest precedence. Thus the following two lines are equivalent:

  z1 := x + y++
  z2 := x + (y++)

Toit does not have the prefix forms of ++ and --:

  for i :=0; i < 10; ++i:  // Error!  There is no prefix ++.
    // ...

Math operators

The math operators have the next highest precedence. In order of descending precedence the order is:

  • Unary operators, - ~
  • Multiplicative operators, * / %
  • Additive operators, + -
  • Bit-shift operators, << >>> >>
  • Bit-and operator, &
  • Bit-xor operator, ^
  • Bit-or operator, |

Thus the following pairs of lines are equivalent:

  a1 := ~x * 3
  a2 := (~x) * 3

  a3 := 4 + 2 * 3
  a4 := 4 + (2 * 3)

  a5 := x >> y + 1
  a6 := x >> (y + 1)

  a7 := x & mask << distance
  a8 := x & (mask << distance)

  b1 := x ^ y & z
  b2 := x ^ (y & z)

  b3 := x | y ^ z
  b4 := x | (y ^ z)

The math operators group from left to right. Thus the following pairs of lines are equivalent

  b5 := x / y / z
  b6 := (x / y) / z

  b7 := x * y / z
  b8 := (x * y) / z

Note that the bitwise operators have higher precedence than the comparison operators in Toit. This is like Dart, but unlike C and Java. Thus the following are equivalent in Toit, but not in C:

  c1 := x & 1 == 0
  c2 := (x & 1) == 0

See the full documentation of operators on int and float.

Type operators

The next operators in the precedence order are the type operators, is, is not, and as.

is and is not return a boolean depending on whether the runtime type of the left hand side has the type on the right.

as is a cast, which dynamically checks the type and returns a value with the correct static type:

my_function x y:
  if x is int:
    // Code that can use x as an integer
  if y is not string:
    // y must be something other than string
  // The programmer knows somehow that y is a number.  This checks
  // that the programmer is right and allows the program to pass the
  // compiler's type checker:
  return (y as num) + 1

Comparison operators

The next operators in the precedence order are the comparison operators, == != < <= > >=.

Thus the following pairs of lines are equivalent:

  c3 := x + 1 <= y * 2
  c4 := (x + 1) >= (y * 2)

  c5 := x + 1 <= y & -2
  c6 := (x + 1) >= (y & -2)  // Equivalent in Toit, not in C.

The comparison operators form a group with ternary and other n-ary forms so that they can be used in the mathematical way. Thus the following pairs are equivalent:

  if 0 <= x < y:
    // ...
  if 0 <= x and x < y:
    // ...

  if 0 <= from <= to < size:
    // ...
  if 0 <= from and from <= to and to < size:
    // ...

  if 0 == base <= level < top:
    // ...
  if 0 == base and base <= level and level < top:
    // ...

Calls and arguments

Calls and arguments are done with spaces in Toit, not with parentheses and commas. Thus we write:

import math show *

my_function x y z:
  foo x y z          // In C: foo(x, y, z)
  c7 := 2 * (sin x)  // In C: 2 * sin(x)

The mathematical operators all have higher precedence than calls, so the following are equivalent:

  foo x + 1 y + 2 z + 3
  foo (x + 1) (y + 2) (z + 3)

  cos x + PI / 4    // In C: cos(x + PI / 4)
  cos (x + PI / 4)  // In C: cos(x + PI / 4)
  cos(x + PI / 4)   // Looks like C call syntax.

The third form, with the redundant parentheses and no space, looks like a C-style call syntax. However this is an illusion:

import math show *

my_function x/float y/float:
  acos(x) + 100   // Not what it looks like!
  acos (x + 100)  // Equivalent!

  atan2 x y       // Correct.
  atan2(x, y)     // Syntax error!

Logical operators

In descending precedence order:

  • not
  • and
  • or
  • ?: (the ternary operator)

The first three match the names used in Python and C++. In C++ these alphabetical forms are rarely used, and it is more common to see the symbolic forms, &&, ||, and !. The symbolic forms are not available in Toit.

The following pairs are equivalent:

my_function a b c:
  if not a and b == 0:
    // ...
  if (not a) and (b == 0):
    // ...

  if a == 0 or b == 0 and c == 0:
    // ...
  if (a == 0) or ((b == 0) and (c == 0)):
    // ...

Since the logical operators are lower than the calls and arguments we have to group them:

my_unary_function x/bool:
  // ...

is_odd x/int -> bool:
  return x % 2 == 1

my_function condition_a/bool condition_b/bool x/int y/int:
  my_unary_function (condition_a and condition_b)  // Parentheses needed.

  // The following two forms are equivalent.
  if is_odd x and is_odd y:
    // ...
  if (is_odd x) and (is_odd y):
    // ...

Assignment operators

The lowest operators in the precedence order are the assignment operators, = := ::= += -= *= /= %= |= ^= &= <<= >>= >>>=.

Since they are below the logical operators and calls, we can write equivalently:

  angle_1 := atan2 x y
  angle_2 := (atan2 x y)

  condition_1 := condition_a and condition_b
  condition_2 := (condition_1 and condition_b)

Assignment operators are only allowed at the start of statements and in the first clause of a for, if, or while statement.

  while data := reader.read:
    // data was not null.

  if parsed := (int.parse str --on_error=: null):
    // parsed is not null and thus a valid number.