Toit for C and C++ programmers
This introduction covers Toit from a C/C++ starting point. Familiar operations get a short explanation; calls, blocks, managed objects, and cooperative tasks get more space. The examples run on your computer with the Toit SDK.
Each example is a complete program unless a filename is given. Replace the previous example when running the next one. For a quick syntax lookup, use the comparison cheat sheet.
A program starts at main
Toit uses indentation instead of braces. Functions can be defined directly in a file, and statements do not end with semicolons. Here is a complete program:
Use two spaces per indentation level. Comments use // or /* ... */.
Variables and functions use kebab-case, classes use UpperCamelCase, and
constants use UPPER-KEBAB-CASE. A hyphen can belong to a name: item-count is
one variable, while item - count is subtraction.
Variables and scope
:= declares a mutable binding; = assigns a new value. ::= declares a final
binding, which cannot be reassigned. Normally use := inside functions. Prefer
final fields and globals when their bindings will not change.
Finality applies to the binding, not to the referenced object's contents. It is
not C++ const access to an object:
Local names belong to their enclosing scope. Top-level variables are globals; their initializers run on first access. Objects are managed references. Passing an object to a function does not copy it, and assignment does not invoke a user-defined copy constructor.
Calling functions and methods
Spaces separate call arguments: add 1 2. Parentheses group an expression
when its result is needed by another call: print (add 1 2).
Arithmetic binds more tightly than calls. Although add 1 2 * 3 means
add 1 (2 * 3), prefer the latter spelling, especially with multiple arguments.
A zero-argument call needs no parentheses. name.trim invokes trim; it does
not produce a member-function pointer. Public fields use the same access
syntax, so a field can later become a computed getter without changing readers.
Named and default arguments
Both positional and named parameters can have default values. A named parameter
uses -- in its definition and calls:
greet name="World" --greeting="Hello" --loud=false: text := "$greeting $name" print (loud ? "$text!" : text) main: greet greet "Ada" --greeting="Hi" --loud greet "Grace" --no-loud
This prints Hello World, Hi Ada!, then Hello Grace. A bare boolean flag
passes true; its --no- form passes false.
Toit overloads by argument count and names, not by parameter types. Use return
to supply a function's result. Type annotations can document and check the
arguments and result, as we will see next.
Types and null
Types are optional. Parameters and variables use /Type; return annotations
use -> Type. The annotations check actual values at runtime:
add a/int b/int -> int:
return a + b
main:
print (add 2 3)
value/any := "3"
error := catch: add 2 value
if error:
print "Expected integers"This prints 5, then Expected integers. The any annotation permits any
value in value, but add still requires integers. Its second call fails a parameter
check; we will cover catch below. Unlike C++, not every type mismatch prevents
compilation. Analysis catches some mistakes, while the runtime enforces the
annotations.
null denotes absence. Types exclude it by default; string? accepts a string
or null. any accepts any value, including null, and none marks a function
with no result. A nullable reference does not support pointer arithmetic or
manual dereferencing.
Toit permits assigning a nullable type to its non-nullable counterpart
statically, with a runtime check on the value. The type Null itself is the
exception. See the nullable assignment example.
Use value is Type for a type test and value as Type for a checked cast.
A cast does not convert a value; conversions use methods such as to-float
or functions such as int.parse.
Numbers, booleans, and operators
Integers are signed 64-bit values; floats are double precision. Integer division
truncates toward zero, as in C/C++: -5 / 2 is -2. A float operand preserves
the fractional part: 5 / 2.0 is 2.5.
Arithmetic and comparison operators are familiar: +, -, *, /, %,
==, !=, <, <=, >, and >=. Use and, or, and not for logic.
Only false and null are falsy; unlike C/C++, zero is truthy.
main: print (0 or 10) // Prints 0. print (null or 10) // Prints 10. print (5 / 2) // Prints 2. print (5 / 2.0) // Prints 2.5.
and and or short-circuit and return an operand, rather than always producing
a boolean. An empty list or string is also truthy; check its size or use a
collection's is-empty getter when you mean emptiness.
Bitwise operators are &, |, ^, ~, <<, >>, and >>>. The last is
unsigned right shift. Hexadecimal and binary literals use 0x and 0b;
underscores can group digits, as in 0xffff_ffff.
Strings and formatting
Strings are immutable UTF-8 text. Double quotes create a string; single quotes
create an integer code point, so 'A' is 65. Use triple double quotes for
multiline strings, \n for a newline, and \$ for a literal dollar sign.
Insert values with $name or $(expression). Formatting puts the specifier
next to the value, instead of in a separate printf argument list:
main: name := "Ada" print "Hello $name" print "Name length: $name.size" print "Next: $(21 + 1)" print "Pi: $(%.2f 3.14159)"
This prints Hello Ada, Name length: 3, Next: 22, and Pi: 3.14.
Member access and indexing can be part of interpolation. Use $(name)-sensor
when a literal hyphen should follow the name.
String offsets count UTF-8 bytes: "é".size is 2. Indexing at a character
boundary returns a code point; indexing at a continuation byte returns null.
Use text.do --runes: to iterate over code points. See
strings for slicing and formatting details.
Conditions and loops
if, else if, else, while, break, and continue work much as in C/C++,
with indentation in place of braces. The conditional expression uses
condition ? yes : no. A C-style for loop is available:
main:
for i := 0; i < 4; i++:
if i == 1: continue
print i // Prints 0, 2, then 3.
remaining := 3
while remaining > 0:
remaining--
print remaining // Prints 0.For a fixed number of repetitions, normally use 3.repeat:. To iterate over
a collection, use values.do:. These are ordinary methods taking blocks,
which let library code provide control structures.
Blocks provide scoped control flow
A block starts with :. Parameters appear between | characters. The block
can read and update locals in the surrounding function:
A return in a block returns from the function in which the block was written.
This is a non-local return, unlike returning from a C++ lambda:
first-positive values:
values.do: |value|
if value > 0:
return value
return null
main:
print (first-positive [-1, 0, 4, 8]) // Prints 4.The return leaves both the block and first-positive. Use continue.do to
skip one do iteration, or continue.repeat for repeat. Those exits leave
the enclosing function running.
Accepting a block
A block parameter is written in square brackets. Invoke it with .call.
The block's last expression supplies its result; return would instead leave
the caller's enclosing function:
with-offset offset/int [action]:
return action.call offset
main:
base := 10
result := with-offset 5: |offset|
base + offset
print result // Prints 15.Lifetime and cost
Block references encode stack-relative positions as small integers. Passing a block pushes a reference; it does not allocate a heap closure to copy captured locals. The language enforces the lifetime that makes this safe: blocks cannot escape into fields, globals, collections, or return values. A lambda cannot capture a block either.
C++ lambdas can also avoid heap allocation, but a lambda capturing references can outlive the referenced locals. C++ does not enforce Toit's block lifetime restriction. Use blocks for scoped callbacks such as iteration and resource management.
Lambdas for stored callbacks
A lambda starts with :: and can outlive the call that created it. Its last
expression is 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) // Prints 8.
The lambda keeps access to amount. It can be stored in a field or returned,
as here. Use the form the API expects: [action] accepts a block; a stored
callback uses a lambda. See blocks and lambdas.
Collections and binary data
Lists, maps, sets, and byte arrays have literal syntax:
| Collection | Literal | Typical operation |
| List | [1, 2, 3]; empty [] | values.add 4 |
| Map | {"name": "Ada"}; empty {:} | labels["name"] |
| Set | {1, 2, 3}; empty {} | seen.contains 2 |
| Byte array | #[0, 127, 255]; empty #[] | bytes[0] |
Collections can hold mixed types. List does not take a template argument for
its elements; use annotations or checks where element types matter.
Use map to transform, filter to select, and reduce to combine elements.
Short one-parameter blocks can use the implicit name it:
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 // Prints 72.
A map lookup with brackets throws if the key is absent; labels.get "name"
returns null when absent. Use --if-absent to compute a different fallback.
A slice such as values[1..3] includes index 1 and excludes index 3. List slices
are views into the original list: writes affect the shared elements, and the
slice keeps the entire original list alive. Use .copy for a shallow copy
with independent element slots.
Use a ByteArray and encoding APIs for binary data. Do not depend on object
layout or C struct packing:
import io main: bytes := ByteArray 4 io.LITTLE-ENDIAN.put-uint32 bytes 0 0x1234 print (io.LITTLE-ENDIAN.uint32 bytes 0) // Prints 4660.
Classes, fields, and methods
Call a class to construct an object, without new. Declare fields in the class;
objects cannot gain additional fields dynamically. Methods have fields and
other methods in scope, so this. is usually unnecessary:
class Counter:
value_/int := ?
constructor .value_=0:
value -> int: return value_
increment -> none:
value_++
main:
counter := Counter 4
counter.increment
print counter.value // Prints 5.:= ? requires the constructor to initialize the field. The .value_ parameter
stores its argument directly in the field. A field declared as name/string
without := is final and must also be initialized by the constructor.
A trailing underscore marks a private member. The public value getter uses
the same call syntax as a public field. A setter is written value= new-value:
and called with counter.value = new-value.
Inheritance and interfaces
A class can extend one superclass and implement multiple interfaces. Interfaces
must be implemented explicitly. Methods do not need a virtual declaration
to be overridden:
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. This is not the
C++ spelling Base::method(...).
Use abstract class and abstract methods for partially implemented classes.
Use static for class-level methods and fields. Named constructors, such as
Counter.from-reading, give different construction paths descriptive names.
Mixins and factory constructors
Mixins share implementation across classes without adding another superclass. For example, a mixin can provide printing while requiring the class to supply a description:
abstract mixin Printable:
abstract description -> string
print-description -> none:
print description
class Device extends Object with Printable:
description -> string: return "Device"
main:
device := Device
device.print-description // Prints Device.A factory constructor returns an object instead of initializing a new instance of its own class. Interfaces and abstract classes can expose factories that choose a concrete implementation:
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.The return makes this a factory constructor. An interface can only have
factories; an abstract class can also have regular constructors for subclasses
to call. See the abstract-class factory example
and the class reference.
Memory is managed; resources still need cleanup
You do not use malloc, free, new, delete, or pointer arithmetic. Garbage
collection reclaims unreachable objects. It does not provide C++ destructor
semantics for closing files, sockets, or peripherals at a particular time.
Use try: with finally: for explicit cleanup.
throw raises an exception. catch: runs a block and returns the thrown value,
or null when nothing was thrown. Exceptions are values; strings are common:
main:
error := catch:
try:
print "Using resource"
throw "Operation failed"
finally:
print "Cleanup runs here"
if error:
print "Handled: $error"This prints the resource message, the cleanup message, then Handled: Operation
failed. Replace the cleanup message with the resource's close operation in
real code. There is no combined try/catch syntax: nest the cleanup inside
catch when both are needed. Throw a truthy value when using if error to
detect a failure. See exception handling.
Libraries replace headers
Each .toit file is an importable library; declarations do not need separate
headers. Put imports at the top of the file. Core names such as print and
List are available automatically:
import .helpers imports a sibling helpers.toit and exposes its members
without a prefix by default. import .helpers as helpers chooses an explicit
prefix. SDK and package imports normally use a prefix; show selects names.
See the two-file example.
Dependencies on separately distributed libraries are declared in package.yaml;
package.lock records resolved versions. Imports are resolved before execution.
Use the package quick start to add a dependency.
Tasks cooperate within a program
Each task has a call stack. Tasks share objects and take turns running, switching at yielding operations such as waiting for I/O or sleeping. Ordinary functions can wait without a special declaration or a callback at every call:
report label/string delay/int -> none:
2.repeat:
sleep --ms=delay
print label
main:
task:: report "Fast" 10
task:: report "Slow" 25Each task prints twice. While one is waiting, another can run. The timing is not a real-time guarantee.
Unlike preemptive threads, another task in the same program cannot interrupt a computation that does not yield. A loop that never yields starves other tasks. Conversely, shared state can change across a waiting call, so operations spanning a wait may need synchronization. A helper can yield even though its call looks ordinary. See tasks and synchronization.
Continue with a project
You can use Toit for host scripts, command-line tools, network applications, and device programs. Use the language reference to look up details, the package quick start to add libraries, or Run on your device to work with an ESP32.