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 Toit | Consequence for your code |
| Calls use spaces; zero-argument methods need no parentheses | sensor.read calls a method. Wrap a call in parentheses when passing its result to another call. |
| Optional types are enforced at runtime | An annotation is a contract, not just editor metadata. Untyped code still runs, and type errors can still occur at runtime. |
| Blocks and lambdas are distinct | Use 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 classes | Unlike Python and JavaScript, assigning a new property cannot change an object's shape. Use maps for dynamic keys. |
| Tasks have their own stacks and cooperate | Waiting code reads sequentially, but calls can yield and shared state can change while you wait. |
| Memory is garbage collected | You 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.
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.
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.
Types are optional, but checked
Type annotations specify checks on values. Parameters use /Type; return
values use -> Type:
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.
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.
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.
| Value | Literal | Common 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.
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.
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.
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 .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.
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
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.