​

Toit for programmers

This is a complete first tour for readers who already program in another language. It introduces Toit's syntax, values, functions, objects, libraries, and concurrency. Familiar concepts get a short explanation; features such as blocks and cooperative tasks get more space.

For the side-by-side syntax tables, use the language comparison cheat sheet.

Read this tour in order, or use the table of contents to skip familiar topics. We use C/C++, JavaScript, Python, and Java/Kotlin as points of comparison. You do not need to know all of them: each section explains the Toit behavior directly. Optional example collections explore variations without interrupting the main explanation.

You can run the complete examples on your computer using the local setup; no hardware is required. If you are new to programming, use the beginner lessons. The JavaScript and Python guides focus on differences from those languages. The C/C++ introduction is an alternative complete tour using native programming comparisons.

The differences that matter

Choice in ToitConsequence for your code
Calls use spaces; zero-argument methods need no parenthesessensor.read calls a method. Wrap a call in parentheses when passing its result to another call.
Optional types are enforced at runtimeAn annotation is a contract, not just editor metadata. Untyped code still runs, and type errors can still occur at runtime.
Blocks and lambdas are distinctUse blocks for scoped iteration and control flow; use lambdas when code must be stored for later.
Objects keep the fields and methods declared by their classesUnlike Python and JavaScript, assigning a new property cannot change an object's shape. Use maps for dynamic keys.
Tasks have their own stacks and cooperateWaiting code reads sequentially, but calls can yield and shared state can change while you wait.
Memory is garbage collectedYou do not free objects; you still explicitly release resources such as open files.

Calling functions and methods

A program starts in main. Functions and classes can be declared at the top level; there is no need for a wrapper class. Indentation groups statements, with two spaces per level. Comments start with //, or use /* ... */ for multiple lines.

Unlike Java, Toit allows main and other functions directly in a file. The first example defines add and calls it from main. We will add type annotations later.

add a b:
  return a + b

main:
  reading-count := 3
  reading-count++
  print (add reading-count 2)

This prints 6. := introduces reading-count, and ++ increases it by one. Use = for reassignment, for example reading-count = 10.

Toit names can contain hyphens: reading-count is one name. Put spaces around subtraction, as in reading - count. Variables and functions use kebab-case; classes use UpperCamelCase, and constants use UPPER-KEBAB-CASE.

Pass arguments with spaces

add reading-count 2 calls add with two arguments. Spaces separate arguments; commas belong in collection literals. To pass the result of one call to another, group the inner call: print (add reading-count 2).

Arithmetic binds more tightly than calls, so add 1 2 * 3 means add 1 (2 * 3). Prefer the explicit parentheses, especially in calls with several arguments. They make it easier to see which calculation supplies each argument.

Call a method without arguments

Writing clock.read invokes read. There is no () after a zero-argument call. A public field uses the same syntax, so it can become a computed getter without changing the code that reads it.

Name an argument

A call can label an argument: sleep --ms=100 waits for 100 milliseconds. The function definition also marks that parameter with --, as the examples below show. See syntax for the full call rules.

More examples: call grouping, continuation lines, and named arguments

Functions and scope

Define parameters after the function name. Positional parameters can have default values; named parameters have a -- prefix in both the definition and the call. Use return for a function's result. A function without a result can be annotated -> none.

greet name/string="World" --greeting/string="Hello" -> string:
  return "$greeting $name"

main:
  print (greet)                         // Prints Hello World.
  print (greet "Ada" --greeting="Hi")    // Prints Hi Ada.

C/C++ and JavaScript programmers should notice that named arguments are part of the call's syntax, not fields in an options object. A boolean named argument can be passed as --verbose for true or --no-verbose for false. Toit supports overloading by argument count and names, rather than selecting overloads by parameter types.

A local name belongs to its enclosing scope. A top-level variable is a global; its initializer runs on first access. Inside functions, normally use := for local variables. For globals and fields, prefer final bindings when the value will not be reassigned. MAX-READINGS ::= 100 declares a constant; its uppercase name is a convention. Finality prevents reassignment of the binding, but does not prevent changes to an object it refers to. We will see the common final-field syntax, name/string, in the classes section.

String interpolation

Double-quoted strings support $name and $(expression) interpolation:

main:
  name := "Ada"
  print "Hello $name"                  // Prints Hello Ada.
  print "Next reading: $(21 + 1) C"    // Prints Next reading: 22 C.

Interpolation includes member access and indexing: "$name.size" inserts the size, while "$(name).size" inserts the name followed by literal .size. Hyphens can belong to identifiers, so use "$(name)-sensor" when you want a literal suffix. Formatting goes inside the parentheses, as in "$(%.2f 3.14159)".

Strings are immutable UTF-8 text. Unlike Python's character indexing or JavaScript's UTF-16 code-unit indexing, Toit's string offsets are byte offsets: "é".size is 2. Indexing at a character boundary returns its Unicode code point; indexing at a continuation byte returns null. Slices must end at character boundaries. Use text.do --runes: to iterate over code points instead of treating every byte offset as a character.

Use triple double quotes for multiline strings, \n for a newline, and \$ for a literal dollar sign. Single quotes denote a code point: 'A' is the integer 65, not a one-character string.

12 more interpolation examples: boundaries, expressions, and formatting

Types are optional, but checked

Type annotations specify checks on values. Parameters use /Type; return values use -> Type:

add a/int b/int -> int:
  return a + b

main:
  print (add 2 3)  // Prints 5.

The arguments and the result must be integers. These checks happen at runtime, unlike TypeScript's erased annotations or ordinary Python type hints. Analysis can catch some mistakes before execution, but not every type error. Omitting an annotation permits dynamic values.

Use annotations on function signatures and fields to document and enforce expectations. Local types are often inferred by the editor and need not be written out. any explicitly accepts any value, including null; none marks a function with no result.

Types exclude null by default. Add ? to allow it: string? accepts a string or null. Kotlin uses a similar suffix. A nullable reference is still an object reference; it does not enable C++ pointer operations.

Nullable types and assignments

A nullable type such as string? is statically assignable to its non-nullable counterpart, string. Toit accepts the assignment and checks the actual value at runtime. The type Null itself is the exception: it is not statically assignable to a non-nullable type.

print-name name/string? -> none:
  required/string := name
  print required

main:
  print-name "Ada"  // Prints Ada.
  error := catch: print-name null
  if error:
    print "A name is required"

Both calls are accepted by the compiler. The second fails the runtime check on required, and the handler prints A name is required. A nullable annotation does not promise that every later use accepts null.

Test or cast a value

Use value is Type to test a type, and value as Type for a checked cast. The cast does not convert the value; use conversion methods such as to-float or parsing functions such as int.parse for that. See types.

Numbers, booleans, and operators

Only false and null are falsy. Zero, empty strings, and empty collections are truthy. Integers are signed 64-bit values; integer division truncates toward zero. 5 / 2 is 2, while 5 / 2.0 is 2.5. These rules matter when porting default expressions, emptiness checks, and calculations.

Arithmetic uses +, -, *, /, and %; comparisons use ==, !=, <, <=, >, and >=. and, or, and not are the logical operators. Unlike Java and Kotlin, a condition does not require a boolean value: the truthiness rules above apply. and and or short-circuit and return an operand, which makes name or "World" a useful default when name is nullable. Check .size == 0 for emptiness.

Integers have a fixed range, unlike Python integers. Floats are double precision. Hexadecimal and binary literals use 0x and 0b; separate digits with _, as in 0xffff_ffff. Bitwise operations use &, |, ^, ~, <<, >>, and >>>; the last is an unsigned right shift. See numbers and bitwise operations for the detailed rules.

More examples: defaults, guarded access, and integer division

Blocks are part of control flow

Blocks let ordinary methods implement control structures. The : following values.do passes a block, and |value| names its parameter. The block can read and update variables in the surrounding function. For a short block with one parameter, it is an implicit parameter name, as in values.do: print it.

Unlike a JavaScript forEach callback or a C++ lambda, a Toit block can return from the function in which it was written:

first-positive values:
  values.do: |value|
    if value > 0:
      return value
  return null

main:
  print (first-positive [-2, 0, 7, 9])

This prints 7: return inside the block returns from first-positive. This is a non-local return: it exits the enclosing function, across the call to do. A block's last expression supplies its result to its caller; it does not need return. This makes library methods such as do, map, and repeat work like control structures. Blocks cannot be saved in fields or returned for later use, allowing their lifetime to stay within the enclosing call.

Write a method that accepts a block

A block parameter is written in square brackets. Invoke it with .call, passing its arguments in the usual way. The caller's local variables remain accessible while the called method runs:

with-offset offset/int [action]:
  return action.call offset

main:
  base := 10
  result := with-offset 5: |offset|
    base + offset
  print result  // Prints 15.

The final expression base + offset supplies the block's result to action.call. The return in with-offset then returns that result to main. These are different from putting return inside the block, which would leave main itself.

Lifetime explains the low cost

Blocks can be passed down through calls, but cannot escape into fields, collections, globals, or return values. A lambda cannot capture a block. Those restrictions keep the referenced locals alive for every use of the block.

For C/C++ readers, the VM representation helps explain the design: a block reference is a small integer encoding a position relative to the stack base. Loading that reference pushes a value; it does not allocate a heap closure to copy the captured locals. C++ lambdas can also avoid heap allocation, but a lambda capturing references can outlive the referenced locals. C++ does not enforce the lifetime restriction that Toit enforces for blocks.

That makes blocks a practical default for scoped callbacks, including iteration and resource-management helpers. Use the parameter form required by the API: [action] expects a block, whereas a stored callback needs a lambda.

Lambdas can outlive the call

A lambda starts with :: and can outlive the call that creates it. Its final expression supplies its result; it cannot contain an explicit return:

make-adder amount/int -> Lambda:
  return :: |value| value + amount

main:
  add-five := make-adder 5
  print (add-five.call 3)

This prints 8. Use this form for stored callbacks. To pass a method for later execution, wrap its call in a lambda. Read blocks and lambdas for block parameters and local exits such as continue.do.

More examples: block results, skipping iterations, and lambda returns

Conditions and loops

if, else if, else, and while work as you would expect, with indentation in place of braces. A conditional expression uses condition ? yes : no. Python readers should note that Toit also has a C-style for loop.

main:
  for i := 0; i < 4; i++:
    if i == 1: continue
    print i  // Prints 0, 2, and 3.

  remaining := 3
  while remaining > 0:
    remaining -= 1
    if remaining == 1: break
  print remaining  // Prints 1.

break and continue apply to for and while. For a fixed number of repetitions, prefer 3.repeat:; for collections, prefer values.do:. These are methods taking blocks, so skip an iteration with continue.repeat or continue.do. To exit a search early, use a block's non-local return, as in first-positive above. See control flow.

Collections and binary data

Lists, maps, and sets are built in. Their elements can have different types; List itself does not declare a type parameter for its elements. Use typed block parameters or checks where element types matter.

ValueLiteralCommon operations
List[1, 2, 3]; empty []values[0], values.add 4, values.size
Map{"name": "Ada"}; empty {:}labels["name"], labels["name"] = "Grace"
Set{1, 2, 3}; empty {}seen.add 4, seen.contains 4
Byte array#[0, 127, 255]; empty #[]bytes[0], bytes.size

Python readers should note the empty map/set distinction. JavaScript readers should use maps for dynamic keys and classes for structured objects. C/C++ readers should use byte arrays and encoding APIs for binary data, rather than assuming an object has a C struct's memory layout.

Use slices such as values[1..3] for a range with an inclusive start and an exclusive end. List slices are views: they share elements with the original list, and keep that list alive. Updating an element through either view is visible through the other. Use .copy for an independent list of those elements:

main:
  values := [10, 20, 30]
  middle := values[1..2]
  middle[0] = 99
  print values  // Prints [10, 99, 30].
  separate := middle.copy
  separate[0] = 42
  print values  // Still prints [10, 99, 30].

The copy is shallow: if elements are themselves objects, both lists still refer to those objects. A small slice can retain a large original list, so copying also helps when you only need to keep a few elements.

A map lookup using brackets throws if the key is missing; labels.get "name" returns null when absent. Use --if-absent when you need a different fallback.

Transform and combine values with blocks

map transforms each element; filter selects elements; reduce combines elements into one value. Their blocks supply results through their final expressions. do is for performing an action on each element.

main:
  readings := [21, 26, 25]
  warm := readings.filter: it >= 25
  labels := readings.map: "$it C"
  total := readings.reduce --initial=0: |sum reading| sum + reading
  print warm                            // Prints [26, 25].
  print (labels.join ", ")               // Prints 21 C, 26 C, 25 C.
  print (total.to-float / readings.size)  // Prints 24.0.

The initial value makes this reduction valid for an empty list, but computing an average still requires an explicit policy for empty input. reduce without an initial value requires a nonempty collection. Other useful operations are any and every, which test predicates without constructing a filtered list.

More examples: map defaults, mutation, and byte encoding

Classes define the shape of objects

A class declares fields and methods. Construct an instance by calling its class, without new. Objects have a fixed shape: you cannot add a field by assigning to a new property, as you can in Python or JavaScript.

In a constructor, .value_ stores the corresponding argument in the field. Within methods, fields and methods are already in scope; you do not need a this. prefix. Use this when you need to name the receiver explicitly.

class Counter:
  value_/int := ?

  constructor .value_=0:

  value -> int: return value_

  increment -> none:
    value_ += 1

main:
  counter := Counter 4
  counter.increment
  print counter.value  // Prints 5.

:= ? declares a mutable field that the constructor must initialize. A typed field without :=, such as name/string, is final and must also be initialized by the constructor. A trailing underscore marks a private member. Use private fields when callers should go through methods instead of updating the representation directly.

counter.value invokes a zero-argument getter. Public fields have accessors with the same call syntax, so you can replace a field with a computed getter without changing readers. A setter is defined as value= new-value: and called with counter.value = new-value. Kotlin readers will recognize the property-like call syntax. There is no getValue() naming convention as in Java.

Inheritance, interfaces, and constructors

A class can extend one superclass and implement multiple interfaces. Interfaces must be implemented explicitly: a class does not acquire an interface type just because it has matching methods. This differs from TypeScript's structural typing and Python's usual duck typing, and will be familiar from Java/Kotlin.

interface Described:
  description -> string

class Device implements Described:
  name_/string

  constructor .name_:

  description -> string: return name_

class Sensor extends Device:
  constructor name/string:
    super name

  description -> string:
    return "Sensor: $(super)"

main:
  device/Described := Sensor "Kitchen"
  print device.description  // Prints Sensor: Kitchen.

In a constructor, super calls the superclass constructor. In an overriding method, it calls the superclass's method with the same name; do not translate a JavaScript super.description() into super.description.

Use abstract class when you want shared implementation with abstract methods that subclasses must provide. Use static for methods and fields on the class rather than its instances. Named constructors, such as Counter.from-reading, give creation paths meaningful names. A factory constructor returns an existing object or an object of another implementation rather than initializing a new instance of its own class.

More examples: a setter and a named constructor

Share implementation with mixins

A class extends one superclass, but it can add reusable behavior with with. A mixin contributes methods and fields without becoming another superclass. An abstract mixin can require methods that the applying class must provide:

abstract mixin Printable:
  abstract description -> string

  print-description -> none:
    print description

class Device extends Object with Printable:
  name/string

  constructor .name:

  description -> string: return name

main:
  device := Device "Kitchen"
  device.print-description  // Prints Kitchen.

Printable supplies print-description; Device supplies the required description. Multiple mixins are listed after with, separated by spaces. This offers shared implementation while keeping a single superclass chain.

Construct an implementation through its interface

Interfaces and abstract classes can have factory constructors. Calling the factory returns a concrete implementation, so callers do not need to name that implementation. A return in a constructor makes it a factory:

interface Named:
  constructor name/string:
    return Person name

  name -> string

class Person implements Named:
  name/string

  constructor .name:

main:
  person := Named "Ada"
  print person.name  // Prints Ada.

An interface can only have factory constructors. An abstract class can also have regular constructors used by its subclasses. For example:

abstract class Message:
  constructor:

  constructor.text text/string:
    return TextMessage text

  abstract text -> string

class TextMessage extends Message:
  text/string

  constructor .text:

main:
  message := Message.text "Hello"
  print message.text  // Prints Hello.

The regular Message constructor initializes the base part when a subclass is created; it does not make the abstract class directly instantiable. Message.text is a named factory that returns a concrete TextMessage. See factory constructors for more uses, including returning an existing object.

Errors and resources need explicit handling

throw raises an exception. Exceptions are values; strings are common, and objects are useful when a caller needs to distinguish kinds of failure. catch: runs a block and returns the thrown value, or null on success. The result is the exception, not the block's final expression.

main:
  value := 0
  error := catch:
    value = int.parse "invalid"
  if error:
    print "Expected an integer"
  else:
    print "Read $value"

This prints Expected an integer. Throw a truthy value when using if error to detect failure. try: with finally: performs cleanup, including when execution leaves through an exception or a return. There is no combined try/catch syntax; put a try/finally inside a catch when both are needed.

Garbage collection reclaims unreachable memory. Unlike C++ destructors, it does not provide deterministic resource cleanup. Put a resource's close operation in finally, or use a library's scoped helper when one is available.

More examples: cleanup before handling a failure

Files are libraries

Each .toit file is a library. Imports go at the top of the file. import math imports an SDK library and makes its members available through the math prefix. The core library, including print, List, and int, is available automatically.

import math

main:
  print (math.sqrt 9.0)  // Prints 3.0.

import .helpers imports a sibling helpers.toit. Local imports make names available without a prefix by default, whereas SDK and package imports use a prefix. Use as to choose one explicitly, or show to select names. Unlike C/C++, declarations do not need separate header files.

Example: split a program across two files

Libraries distributed separately from the SDK are packages. Dependencies are declared in package.yaml, and package.lock records resolved versions. Imports are resolved before execution. Follow the package quick start to add a dependency, and imports for namespace and path rules.

Waiting suspends a task

main:
  task::
    sleep --ms=10
    print "Background task finished"
  print "Main task continues"

Toit tasks have independent stacks and share the program's objects. Waiting operations such as sleep suspend the current task so another can run. Ordinary functions can wait without an async declaration or an await at each caller. This lets device and network code use ordinary loops and calls. JavaScript and Python programmers can think of the sequential style of async/await, with the difference that waiting works through ordinary functions and callers. Kotlin readers do not need a suspend marker on each waiting function either.

Scheduling within a program is cooperative. A CPU loop that never yields starves other tasks. Conversely, a call that waits can allow another task to change shared state; inspect library behavior when reasoning about an update spanning calls. Tasks are not parallel threads and are distinct from independently running containers. See tasks and synchronization.

Continue with a project

You now have the language tools to read and write a Toit program: calls, values, blocks, collections, objects, errors, imports, and tasks. Use the language reference for rules and APIs as you need them. For a host script, continue running your .toit files with the SDK and use the package quick start to add libraries. For hardware, follow Run on your device and then a hardware or networking tutorial.