Characters such as ? or : are invalid, so names like found? or op::increase are not allowed.
Identifiers such as cell or slice are valid.
Example:
var cell = ...var cell: cell = ...
It is similar to how number is a valid identifier in TypeScript.FunC vs TolkIn FunC, almost any character can be part of an identifier.
For example, 2+2 without spaces is treated as a single identifier, and a variable can be declared with such a name.In Tolk, spaces are not required. 2+2 is 4, not an identifier. Identifiers can only contain alphanumeric characters.
2+2 evaluates to 4, and 3+~x is interpreted as 3 + (~x), and so on.
FunC
Tolk
return 2+2; ;; undefined function 2+2
return 2+2; // 4
Backticks can be used to enclose an identifier, allowing any symbols to be included.
This feature is intended primarily for code generation, where keywords may need to appear as identifiers.
FunC has an impure function specifier. When absent, a function is treated as pure. If its result is unused, its call is deleted by the compiler.For example, functions that do not return a value, such as those that throw an exception on a mismatch, are removed. This issue is spoiled by FunC not validating the function body, allowing impure operations to be executed within pure functions.In Tolk, all functions are impure by default. A function can be marked as pure using an annotation. In pure functions, impure operations such as throwing exceptions, modifying globals, or calling non-pure functions are disallowed.
Parameter types are required, local types are optional
// not allowedfun do_smth(c, n)// types are mandatoryfun do_smth(c: cell, n: int)
Parameter types are mandatory, but the return type is optional when it can be inferred.
If omitted, it’s auto-inferred:
fun x() { ... } // auto infer from return statements
Local variable types are optional:
var i = 10; // ok, intvar b = beginCell(); // ok, buildervar (i, b) = (10, beginCell()); // ok, two variables, int and builder// types can be specified manually, of course:var b: builder = beginCell();var (i: int, b: builder) = (10, beginCell());
Default values for parameters are supported:
fun increment(x: int, by: int = 1) { return x + by}
var a = 10;...var a = 20; // error, correct is `a = 20`if (1) { var a = 30; // ok, it's another scope}
As a consequence, partial reassignment is not allowed:
var a = 10;...var (a, b) = (20, 30); // error, releclaration of a
This is not an issue for methods like loadUint().In FunC, such methods returned a modified object, so a pattern like
var (cs, int value) = cs.load_int(32) is common.In Tolk, such methods mutate the object: var value = cs.loadInt(32), so redeclaration is rarely needed:
fun send(msg: cell) { var msg = ...; // error, redeclaration of msg // solution 1: intruduce a new variable var msgWrapped = ...; // solution 2: use `redef`, though not recommended var msg redef = ...;
Tolk removes FunC-style string postfixes like "..."c and replaces them with compile-time functions.
FunC
Tolk
"..."c
stringCrc32("...")
—
stringCrc16("...")
"..."H
stringSha256("...")
"..."h
stringSha256_32("...")
"..."a
address("...")
"..."s
stringHexToSlice("...")
"..."u
stringToBase256("...")
These functions are:
compile-time only
for constant strings only
usable in constant initialization
// type will be `address`const BASIC_ADDR = address("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF")// return type will be `int`fun minihashDemo() { return stringSha256_32("transfer(slice, int)");}
The naming highlights that these functions arrived from string postfixes and operate on string values.
At runtime, there are no strings, only slices.
Optional semicolon for the last statement in a block
In Tolk, the semicolon after the final statement in a block can be omitted.
While semicolons are still required between statements, the trailing semicolon on the last statement is now optional.
fun f(...) { doSomething(); return result // <-- valid without semicolon}// orif (smth) { return 1} else { return 2}
The function ton() only accepts constant values. For example, ton(some_var) is invalid.
Its type is coins, not int, although it’s treated as a regular int by the TVM.
Arithmetic operations on coins degrade to int — for example, cost << 1 or cost + ton("0.02") are both valid.
In Tolk v0.7, the type system was rewritten from scratch.
To introduce booleans, fixed-width integers, nullability, structures, and generics, Tolk required a static type system similar to TypeScript or Rust.The types are:
int, bool, cell, slice, builder, untyped tuple
typed tuple [T1, T2, ...]
tensor (T1, T2, ...)
callables (TArgs) -> TResult
nullable types T?, compile-time null safety
union types T1 | T2 | ..., handled with pattern matching
coins and function ton("0.05")
int32, uint64, and other fixed-width integers — int at TVM — details
bytesN and bitsN — similar to intN — backed by slices at TVM
address — internal (standard) address, still a slice at TVM
any_address — internal/external/none
void — more canonical to be named unit, but void is more reliable
self, to make chainable methods, described below; it’s not a type, it can only occur instead of return type of a function
never — an always-throwing function returns never, for example; an impossible type is also never
structures and generics
The type system obeys the following rules:
Variable types can be specified manually or are inferred from declarations, and never change after being declared.
Function parameters must be strictly typed.
Function return types, if unspecified, are inferred from return statements similar to TypeScript. In the case of recursion, direct or indirect, the return type must be explicitly declared.
In FunC, type mismatch errors are hard to interpret:
error: previous function return type (int, int)cannot be unified with implicit end-of-block return type (int, ()):cannot unify type () with int
In Tolk, errors are human-readable:
1) can not assign `(int, slice)` to variable of type `(int, int)`2) can not call method for `builder` with object of type `int`3) can not use `builder` as a boolean condition4) missing `return`...
At the TVM level, bool is represented as -1 or 0, but in the type system, bool and int are distinct types.
Comparison operators == / >= /... return bool.
Logical operators && || return bool.
Constants true and false have the bool type.
Many standard library functions now return bool, not int:
var valid = isSignatureValid(...); // boolvar end = cs.isEnd(); // bool
Operator !x supports both int and bool.
if conditions and similar statements accept both int values that are not equal to zero and bool.
Logical operators && and || accept both bool and int, preserving compatibility with constructs like a && b where a and b are nonzero integers.
Arithmetic operators are restricted to integers. Only bitwise and logical operations are allowed for bool.
valid && end; // okvalid & end; // ok, bitwise & | ^ also work if both are boolsif (!end) // okif (~end) // error, use !endvalid + end; // error8 & valid; // error, int & bool not allowed
Logical operators && and ||, which are absent in FunC, use the if/elseasm representation.
In the future, for optimization, they could be automatically replaced with & or | when safe to do so, for example, a > 0 && a < 10.To manually optimize gas consumption, & and | can be used for bool, but they are not short-circuited.
bool can be cast to int using as operator:
var i = boolValue as int; // -1 / 0
There are no runtime transformations. bool is guaranteed to be -1 or 0 at the TVM level, so this is a type-only cast.
Such casts are rarely necessary, except for tricky bitwise optimizations.
Although the generic type T is usually inferred from the arguments, there are edge cases where T cannot be inferred because it does not depend on them.
fun tupleLast<T>(t: tuple): T asm "LAST"var last = tupleLast(t); // error, can not deduce T
To make this valid, T must be specified externally:
var last: int = tupleLast(t); // ok, T=intvar last = tupleLast<int>(t); // ok, T=intvar last = tupleLast(t) as int; // ok, T=intsomeF(tupleLast(t)); // ok, T=(paremeter's declared type)return tupleLast(t); // ok if function specifies return type
For asm functions, T must occupy exactly one stack slot.
For user-defined functions, T may represent any structure.
Otherwise, the asm body cannot handle it properly.
As first-class functions, lambdas can even be returned:
fun createFinalizer() { return fun(b: builder) { b.storeUint(0xFFFFFFFF, 32); return b.toSlice(); }}val f = createFinalizer(); // (builder) -> slicef(beginCell()); // slice with 32 bits
While lambdas are not common in smart contracts, they become useful in general purpose tools.
They can easily be combined with generics of any level, nested into each other, and so on.Note that lambdas are not closures: capturing outer variables not supported.
fun outer(x: int) { return fun(y: int) { return x + y; // error: undefined symbol `x` }}
Capturing variables is nearly impossible to implement on a stack machine, just like inheritance (conceptually equivalent).
In Tolk, symbols from another file cannot be used without explicitly importing it — import what is used.All standard library functions are available by default. Downloading the stdlib and including it manually #include "stdlib.fc" is unnecessary. See embedded stdlib.There is a global naming scope. If the same symbol is declared in multiple files, it results in an error.
import brings all file-level symbols into scope. The export keyword is reserved for future use.
In FunC, experimental features such as allow-post-modifications were enabled with #pragma directives inside .fc files, which caused inconsistencies across files. These flags are compiler options, not file-level pragmas.In Tolk, all pragmas were removed. allow-post-modification and compute-asm-ltr are merged into Tolk sources and behave as if they were always enabled in FunC. Instead of pragmas, experimental behavior is set through compiler options.There is one experimental option: remove-unused-functions, which excludes unused symbols from the Fift output.#pragma version xxx is replaced with tolk xxx, no >=, only strict versioning. If the version does not match, Tolk shows a warning.
In FunC, as in C, a function cannot be accessed before its declaration:
int b() { a(); } ;; errorint a() { ... } ;; since it's declared below
To avoid an error, a forward declaration is required because symbol resolution occurs during the parsing process.Tolk compiler separates parsing and symbol resolution into two distinct steps. The code above is valid, since symbols are resolved after parsing.This required introducing an intermediate AST representation, which is absent in FunC. The AST enables future language extensions and semantic code analysis.
Operator precedence aligned with C++ and JavaScript
In FunC, the code if (slices_equal() & status == 1) is parsed as if ((slices_equal() & status) == 1). This causes errors in real-world contracts.In Tolk, & has a lower priority, identical to C++ and JavaScript.Tolk generates errors on potentially incorrect operator usage to prevent such mistakes:
if (flags & 0xFF != 0)
Produces a compilation error:
& has lower precedence than ==, probably this code won't work as you expected. Use parenthesis: either (... & ...) to evaluate it first, or (... == ...) to suppress this error.
Code should be rewritten as:
// Evaluate it first (this case)if ((flags & 0xFF) != 0)// Or emphasize the behavior (not used here)if (flags & (0xFF != 0))
Tolk detects a common mistake in bitshift operators: a << 8 + 1 is equivalent to a << 9, which may be unexpected.
int result = a << 8 + low_mask;error: << has lower precedence than +, probably this code won't work as you expected. Use parenthesis: either (... << ...) to evaluate it first, or (... + ...) to suppress this error.
Operators ~% ^% /% ~/= ^/= ~%= ^%= ~>>= ^>>= are no longer supported.
stdlib is now embedded, not downloaded from GitHub
FunC
Tolk
1. Download stdlib.fc from GitHub
1. Use standard functions
2. Save into the project
–
3. `#include “stdlib.fc”;“
–
4.Use standard functions
–
In Tolk, the standard library is part of the distribution. It is inseparable, as maintaining the language, compiler, and standard library together is required for proper release management.The compiler automatically locates the standard library. If Tolk is installed using an apt package, stdlib sources are downloaded and stored on disk, so the compiler locates them by system paths. When using the WASM wrapper, stdlib is provided by tolk-js.The standard library is split into multiple files:
common.tolk for most common functions,
gas-payments.tolk for gas calculations,
tvm-dicts.tolk, and others.
Functions from common.tolk are available and implicitly imported by the compiler. Other files must be explicitly imported.
The rule import what is used applies to @stdlib/... files as well, with the only exception of common.tolk.IDE plugins automatically detect the stdlib folder and insert required imports while typing.
In FunC, only bitwise operators ~ & | ^ exist. Using them as logical operators leads to errors because their behavior is different:
a & b
a && b
note
0 & X = 0
0 & X = 0
sometimes identical
-1 & X = -1
-1 & X = -1
sometimes identical
1 & 2 = 0
1 && 2 = -1 (true)
generally not
~ found
!found
note
true (-1) → false (0)
-1 → 0
sometimes identical
false (0) → true (-1)
0 → -1
sometimes identical
1 → -2
1 → 0 (false)
generally not
condition & f()
condition && f()
f() is called always
f() is called only if condition
condition | f()
condition || f()
f() is called always
f() is called only if condition is false
Tolk supports logical operators. They behave as expected, as shown in the right column.. && and || may produce suboptimal Fift code, but the effect is negligible. Use them as in other languages.
FunC
Tolk
if (~ found?)
if (!found)
if (~ found?) {if (cs~load_int(32) == 0) {...}
if (!found && cs.loadInt(32) == 0) {...}
ifnot (cell_null?(signatures))
if (signatures != null)
elseifnot (eq_checksum)
else if (!eqChecksum)
Keywords ifnot and elseifnot are removed because logical NOT is now available. For optimization, Tolk compiler generates IFNOTJMP. The elseif keyword is replaced by the standard else if.A boolean true transformed as int is -1, not 1. This reflects TVM representation.
Use tensorVar.{i} to access i-th component of a tensor. Modifying it changes the tensor.
var t = (5, someSlice, someBuilder); // 3 stack slotst.0 // 5t.0 = 10; // t is now (10, ...)t.0 += 1; // t is now (11, ...)increment(mutate t.0); // t is now (12, ...)t.0.increment(); // t is now (13, ...)t.1 // slicet.100500 // compilation error
Use tupleVar.{i} to access the i-th element of a tuple, uses INDEX internally. Modifying it changes the tuple, SETINDEX internally.
var t = [5, someSlice, someBuilder]; // 1 tuple on a stack with 3 itemst.0 // "0 INDEX", reads 5t.0 = 10; // "0 SETINDEX", t is now [10, ...]t.0 += 1; // also works: "0 INDEX" to read 10, "0 SETINDEX" to write 11increment(mutate t.0); // also, the same wayt.0.increment(); // also, the same wayt.1 // "1 INDEX", it's slicet.100500 // compilation error
It also works for untyped tuples, though the compiler does not guarantee index correctness.
var t = createEmptyTuple();t.tuplePush(5);t.0 // will head 5t.0 = 10 // t will be [10]t.100500 // will fail at runtime
Supports nesting var.{i}.{j}
Supports nested tensors, nested tuples, and tuples inside tensors
In TVM, all binary data is represented as a slice. The same applies to addresses: even though TL-B describes the MsgAddress, at the TVM level, it’s just a slice.
Thus, in FunC’s standard library, loadAddress returns slice and storeAddress accepts slice.Tolk introduces a dedicated address type meaning “internal address”. It remains a TVM slice at runtime, but differs from an abstract slice in terms of the type system:
Integrated with auto-serialization: the compiler knows how to pack and unpack it using LDSTDADDR and STSTDADDR.
Comparable: operators == and != supported for addresses.
if (senderAddress == msg.owner)
Introspectable: address.getWorkchain() and address.getWorkchainAndHash().
Passing a slice instead leads to an error:
var a: slice = s.loadAddress(); // error, can not assign `address` to `slice`
There is also a type any_address to store internal, external, or none address.Embedding a const address into a contractUse the built-in address() function. In FunC, this was done using the postfix "..."a, which returned a slice.
Casting slice to address and vice versaA raw slice that represents an address can be cast using the as operator.This occurs when an address is manually constructed in a builder using its binary representation:
var b = beginCell() .storeUint(0b01) // addr_extern ...;var s = b.endCell().beginParse();return s as address; // `slice` as `address`
A reversed cast is also valid: someAddr as slice.Different types of addressesThere are different types of addresses. The most frequently used is an internal address — the address of a smart contract. But also, there are external and none addresses. In a binary TL-B representation:
01 (external prefix) + len (9 bits) + len bits — external addresses
00 (none prefix) — address none, 2 bits
address is “internal only” (90% use cases).
address? (nullable) is “internal/none” (9% use cases).
any_address is “internal/external/none” (1% use cases).Remember that address is “workchain + hash”. Validate untrusted input:
val newOwner = msg.nextOwnerAddress;assert(newOwner.getWorkchain() == BASECHAIN) throw 403;
Tolk supports nullable types: int?, cell?, and T? in general, including tensors.
Non-nullable types, such as int and cell, cannot hold null values.The compiler enforces null safety: nullable types cannot be accessed without a null check.
Checks are applied through smart casts. Smart casts exist only at compile time and do not affect gas or stack usage.
var value = x > 0 ? 1 : null; // int?value + 5; // errors.storeInt(value); // errorif (value != null) { value + 5; // ok, smart cast s.storeInt(value); // ok, smart cast}
When a variable’s type is not declared, it is inferred from the initial assignment and never changes:
var i = 0;i = null; // error, can't assign `null` to `int`i = maybeInt; // error, can't assign `int?` to `int`
Variables that may hold null must be explicitly declared as nullable:
// incorrectvar i = null;if (...) { i = 0; // error}// correctvar i: int? = null;// orvar i = null as int?;
Smart casts handle nullable types automatically, enabling code such as:
if (lastCell != null) { // here lastCell is `cell`, not `cell?`}
if (lastCell == null || prevCell == null) { return;}// both lastCell and prevCell are `cell`
var x: int? = ...;if (x == null) { x = random();}// here x is `int`
while (lastCell != null) { lastCell = lastCell.beginParse().loadMaybeRef();}// here lastCell is 100% null
Smart casts do not apply to global variables; they operate only on local variables.The ! operator in Tolk provides a compile-time non-null assertion, similar to ! in TypeScript and !!in Kotlin.
It bypasses the compiler’s check for variables that are guaranteed to be non-null.
fun doSmth(c: cell);fun analyzeStorage(nCells: int, lastCell: cell?) { if (nCells) { // then lastCell 100% not null doSmth(lastCell!); // use ! for this fact }}
Functions that always throw can be declared with the return type never:
fun alwaysThrows(): never { throw 123;}fun f(x: int) { if (x > 0) { return x; } alwaysThrows(); // no `return` statement needed}
The never type occurs implicitly when a condition is impossible to satisfy:
var v = 0;// prints a warningif (v == null) { // v is `never` v + 10; // error, can not apply `+` `never` and `int`}// v is `int` again
Encountering never in compilation errors usually indicates a warning in the preceding code.Non-atomic nullable types are supported, e.g., (int, int)?, (int?, int?)?, or ()?.
A special value presence stack slot is added automatically.It stores 0 for null values and -1 for non-null values.
Union types allow a variable to hold multiple types, similar to TypeScript.
fun whatFor(a: bits8 | bits256): slice | UserId { ... }var result = whatFor(...); // slice | UserId
Nullable types T? are equivalent to T | null.
Union types support intersection properties. For example, B | C can be passed and assigned to A | B | C | D.The only way to handle union types in code is through pattern matching:
match (result) { slice => { /* result is smart-casted to slice */ } UserId => { /* result is smart-casted to UserId */ }}
match (val v = getPair2Or3()) { Pair2 => { // use v.0 and v.1 } Pair3 => { // use v.0, v.1, and v.2 }}
How are union types represented on the stack, at the TVM level?
At the TVM level, union types are stored as tagged unions, similar to enums in Rust:
Each type is assigned a unique type ID, stored alongside the value.
The union occupies N + 1 stack slots, where N is the maximum size of any type in the union.
A nullable type T? is a union with null (type ID = 0). Atomic types like int? use a single stack slot.
var v: int | slice; // 2 stack slots: value and typeID// - int: (100, 0xF831)// - slice: (CS{...}, 0x29BC)match (v) {int => // IF TOP == 0xF831 { ... } // v.slot1 contains int, can be used in arithmetics slice => // ELSE { IF TOP == 0x29BC { ... } }// v.slot1 contains slice, can be used to loadInt()}fun complex(v: int | slice | (int, int)) {// Stack representation:// - int: (null, 100, 0xF831)// - slice: (null, CS{...}, 0x29BC)// - (int, int): (200, 300, 0xA119)}complex(v); // passes (null, v.slot1, v.typeid)complex(5); // passes (null, 5, 0xF831)
Union types can also be tested using is. Smart casts behave as follows:
fun f(v: cell | slice | builder) { if (v is cell) { v.cellHash(); } else { // v is `slice | builder` if (v !is builder) { return } // v is `slice` v.sliceHash(); } // v is `cell | slice` if (v is int) { // v is `never` // a warning is also printed, condition is always false }}
Similar to TypeScript, but executed at the TVM level.
struct Point { x: int y: int}fun calcMaxCoord(p: Point) { return p.x > p.y ? p.x : p.y;}// declared like a JS objectvar p: Point = { x: 10, y: 20 };calcMaxCoord(p);// called like a JS objectcalcMaxCoord({ x: 10, y: 20 });// works with shorthand syntaxfun createPoint(x: int, y: int): Point { return { x, y }}
A struct is a named tensor.
Point is equivalent to (int, int) at the TVM level.
Field access p.x corresponds to tensor element access t.0 for reading and writing.
There is no bytecode overhead; tensors can be replaced with structured types.Fields can be separated by newlines, which is recommended, or by ; or ,,. Both of which are valid, similar to TypeScript.When creating an object, either StructName { ... } or { ... } can be used if the type is clear from context, such as return type or assignment:
var s: StoredInfo = { counterValue, ... };var s: (int, StoredInfo) = (0, { counterValue, ... });// also validvar s = StoredInfo { counterValue, ... };
Structs can include methods as extension functions.Fields support the following modifiers:
private — accessible only within methods.
readonly — immutable after object creation.
struct PositionInTuple { private readonly t: tuple currentIndex: int}fun PositionInTuple.create(t: tuple): PositionInTuple { // the only way to create an object with a private field // is from a static method (or asm function) return { t, currentIndex: 0 }}fun PositionInTuple.next(mutate self) { // self.t can not be modified: it's readonly self.currentIndex += 1;}var p = PositionInTuple.create(someTuple);// p.t is unavailable here: it's private
Methods are declared as extension functions, similar to Kotlin.
A method that accepts the first self parameter acts as an instance method; without self, it is a static method.
fun Point.getX(self) { return self.x}fun Point.create(x: int, y: int): Point { return { x, y }}
Methods can be defined for any type, including aliases, unions, and built-in types:
fun int.isZero(self) { return self == 0}type MyMessage = CounterIncrement | ...fun MyMessage.parse(self) { ... }// this is identical to// fun (CounterIncrement | ...).parse(self)
Methods work with asm, as self is treated like a regular variable:
@purefun tuple.size(self): int asm "TLEN"
By default, self is immutable, preventing modification or calls to mutating methods.
To make self mutable, declare mutate self explicitly:
fun Point.assignX(mutate self, x: int) { self.x = x; // without mutate, an error "modifying immutable object"}fun builder.storeInt32(mutate self, v: int32): self { return self.storeInt(v, 32);}
Methods for generic structs can be created without specifying <T>. The compiler interprets unknown symbols in the receiver type as generic arguments during the parsing process.
struct Container<T> { item: T}// compiler treats T (unknown symbol) as a generic parameterfun Container<T>.getItem(self) { return self.item;}// and this is a specialization for integer containersfun Container<int>.getItem(self) { ...}
Example:
struct Pair<T1, T2> { first: T1 second: T2}// both <T1,T2>, <A,B>, etc. work: any unknown symbolsfun Pair<A, B>.create(f: A, s: B): Pair<A, B> { return { first: f, second: s, }}
Similarly, any unknown symbol, typically T, can be used to define a method that accepts any type:
// any receiverfun T.copy(self): T { return self;}// any nullable receiverfun T?.isNull(self): bool { return self == null;}
When multiple methods match a call to someObj.method(), the compiler selects the most specific one:
fun int.copy(self) { ... }fun T.copy(self) { ... }6.copy() // int.copy(6 as int32).copy() // T.copy with T=int32(6 as int32?).copy() // T.copy with T=int?type MyMessage = CounterIncrement | CounterResetfun MyMessage.check() { ... }fun CounterIncrement.check() { ... }MyMessage{...}.check() // firstCounterIncrement{...}.check() // secondCounterReset{...}.check() // first
A generic function can be assigned to a variable, but type arguments must be specified explicitly.
fun genericFn<T>(v: T) { ... }fun Container<T>.getItem(self) { ... }var callable1 = genericFn<slice>;var callable2 = Container<int32>.getItem;callable2(someContainer32); // pass it as self
Enum syntaxEnum members can be separated by , , ;, or a newline, similar to struct fields.
Values can be specified manually; unspecified members are auto-calculated.
enum Mode { Foo = 256, Bar, // implicitly 257}
Enums are distinct types, not integersColor.Red is Color, not int, although it holds the value 0 at runtime.
fun isRed(c: Color) { return c == Color.Red}isRed(Color.Blue) // okisRed(1) // error, can not pass `int` to `Color`
Since enums are types, they can be:
Used as variable and parameters
Extended with methods an enum
Used in struct fields, unions, generics, and other type contexts
Enums are integers under the hoodAt the TVM level, an enum such as Color is represented as int. Casting between the enum and int is allowed:
Color.Blue as int evaluates to 2
2 as Color evaluates to Color.Blue
Using as can produce invalid enum values. This is undefined behavior: for example, 100 as Color is syntactically valid, but program behavior is unpredictable after this pointDuring deserialization using fromCell(), the compiler performs checks to ensure that encoded integers correspond to valid enum values.Enums in Tolk differ from Rust. In Rust, each enum member can have a distinct structure. In Tolk, union types provide that capability, so enums are integer constants.match for enums is exhaustivePattern matching on enums requires coverage of all cases:
match (someColor) { Color.Red => {} Color.Green => {} // error: Color.Blue is missing}
All enum cases must be covered, or else can be used to handle remaining values:
match (someColor) { Color.Red => {} else => {}}
The == operator can be used to compare integers and addresses:
if (someColor == Color.Red) {}else {}
The expression someColor is Color.Red is invalid syntax.
The is operator is used for type checks.
Given var union: Color | A, u is Color is valid.
Use == to compare enum values.Enums are allowed in throw and assert
Enums and serializationEnums can be packed to and unpacked from cells like intN or uintN, where N is:
Specified manually, e.g., enum Role: int8 { ... }
Calculated automatically as the minimal N to fit all values
The serialization type can be specified manually:
// `Role` will be (un)packed as `int8`enum Role: int8 { Admin, User, Guest,}struct ChangeRoleMsg { ownerAddress: address newRole: Role // int8: -128 <= V <= 127}
Or it will be calculated automatically. For Role above, uint2 is sufficient to fit values 0, 1, 2:
// `Role` will (un)packed as `uint2`enum Role { Admin, User, Guest,}
During deserialization, the input value is checked for correctness. For enum Role: int8 with values 0, 1, 2, any input<0 or input>2 triggers exception 5, integer out of range.This check applies to both value ranges and manually specified enum values:
enum OwnerHashes: uint256 { id1 = 0x1234, id2 = 0x2345, ...}// on serialization, just "store uint256"// on deserialization, "load uint256" + throw 5 if v not in [0x1234, 0x2345, ...]
Tolk can inline functions at the compiler level without using PROCINLINE as defined by Fift.
fun Point.create(x: int, y: int): Point { return {x, y}}fun Point.getX(self) { return self.x}fun sum(a: int, b: int) { return a + b;}fun main() { var p = Point.create(10, 20); return sum(p.getX(), p.y);}
is compiled to:
main PROC:<{ 30 PUSHINT}>
The compiler automatically determines which functions to inline.
@inline attribute forces inlining.
@noinline prevents a function from being inlined.
@inline_ref preserves an inline reference, suitable for rarely executed paths.
Compiler inlining:
Efficient for stack manipulation.
Supports arguments of any stack width.
Works with any functions or methods, except:
Recursive functions
Functions containing return statements in the middle
Supports mutate and self.
Simple getters, such as fun Point.getX(self) { return self.x }, do not require stack reordering.
Small functions can be extracted without runtime cost.
The compiler handles inlining; no inlining is deferred to Fift.How does auto-inline work?
Simple, small functions are always inlined
Functions called only once are always inlined
For every function, the compiler calculates a weight, a heuristic AST-based metric, and the usages count.
If weight < THRESHOLD, the function is always inlined
If usages == 1, the function is always inlined
Otherwise, an empirical formula determines inlining
The @inline annotation can be applied to large functions when all usages correspond to hot paths.
Inlining can also be disabled with @inline_ref, even for functions called once. For example, in unlikely execution paths.
For optimization, use gas benchmarks and experiment with inlining and branch reordering.What can NOT be auto-inlined?A function is NOT inlined, even if marked with @inline, in the following cases:
The function contains return in the middle. Multiple return points are unsupported for inlining.
The function participates in a recursive call chain f -> g -> f.
The function is used as a non-call. For example, when a reference is taken: val callback = f.
In FunC, both .methods() and ~methods() exist.
In Tolk, only the dot syntax is used, and methods are called as .method().
Tolk follows expected behavior:
b.storeUint(x, 32); // modifies a builder, can be chainables.loadUint(32); // modifies a slice, returns integer
Any struct can be automatically packed into a cell or unpacked from one:
struct Point { x: int8 y: int8}var value: Point = { x: 10, y: 20 }// makes a cell containing "0A14"var c = value.toCell();// back to { x: 10, y: 20 }var p = Point.fromCell(c);
m.get(key) returns not an “optional value”, but isFound + loadValue()
// NOT like thisvar v = m.get(key);if (v != null) { // "then v is the value" — NO, not like this}// BUTvar r = m.get(key);if (r.isFound) { val v = r.loadValue(); // this is the value}
m.get(key) returns a struct, NOT V?.
m.mustGet(key) returns V and throws if the key is missing.
Why “isFound” but not “optional value”?
Gas consumption; zero overhead.
Nullable values can be supported, such as map<int32, address?> or map<K, Point?>.
Returning V?, makes it impossible to distinguish between “key exists but value is null” and “key does not exist”.
Iterating forward and backwardThere is no syntax like foreach. Iteration follows this pattern:
define the starting key: r = m.findFirst() or r = m.findLast()
while r.isFound:
use r.getKey() and r.loadValue()
move the cursor: r = m.iterateNext(r) or r = m.iteratePrev(r)
Example: iterate all keys forward
// suppose there is a map [ 1 => 10, 2 => 20, 3 => 30 ]// this function will print "1 10 2 20 3 30"fun iterateAndPrint<K, V>(m: map<K, V>) { var r = m.findFirst(); while (r.isFound) { debug.print(r.getKey()); debug.print(r.loadValue()); r = m.iterateNext(r); }}
Example: iterate from key<=2 backward
// suppose `m` is `[ int => address ]`, already filled// for every key<=2, print addr.workchainfun printWorkchainsBackwards(m: map<int32, address>) { var r = m.findKeyLessOrEqual(2); while (r.isFound) { val a = r.loadValue(); // it's address debug.print(a.getWorkchain()); r = m.iteratePrev(r); }}
Iteration over maps uses existing syntax.Use while (r.isFound), not while (r == null).
As with m.get(key), existence is checked through isFound.
// this is a cursor, it has "isFound" + "getKey()" + "loadValue()"// (methods are applicable only if isFound)var r = m.findFirst();while (r.isFound) { // ... use r.getKey() and r.loadValue() r = m.iterateNext(r);}// similar to map.get() with "isFound" + "loadValue()"var f = m.get(key);if (f.isFound) { // ... use f.loadValue()}
The reason is the same — zero overhead and no hidden runtime instructions or stack manipulations.Use m.isEmpty(), not m == null. Since map is a dedicated type, it must be checked with isEmpty(), because m == null does not work.Suppose a wrapper over dictionaries is implemented:
Given var m: MyMap, calling m.isEmpty() works. The expression m == null is invalid. The compiler issues the following warning:
variable `m` of type `map<int32, int64>` can never be `null`, this condition is always false
The same rule applies to built-in maps. When transitioning code from low-level dicts to high-level maps, pay attention to compiler warnings in the console.A nullable map is valid: var m: map<...>?. This variable can be null and not null. When not null, it can contain an empty map or a non-empty map. The expression m == null only makes sense for nullable maps.Allowed types for K and VAll the following key and value types are valid:
// all these types are validmap<int32, Point?>map<address, address>map<Point, map<int3, bool>>map<uint256, Cell<SnakeData>>map<bits18, slice>
Some types are NOT allowed. General rules:
Keys must be fixed-width and contain zero references
Valid: int32, uint64, address, bits256, Point
Invalid: int, coins, cell
Values must be serializable
Valid: int32, coins, AnyStruct, Cell<AnyStruct>
Invalid: int, builder
In practice, keys are typically intN, uintN, or address. Values can be any serializable type.At the TVM level, keys can be numbers or slices. Complex keys, such as Point, are automatically serialized and deserialized by the compiler.
struct Point { x: int8 y: int8}// the compiler automatically packs Point to a 16-bit slice keyvar m: map<Point, V>
If a key is a struct with a single intN field, it behaves like a number.
struct UserId { v: int32}// works equally to K=int32 without extra serializationvar m: map<UserId, V>
Converts a low-level TVM dictionary to a typed map. Accepts an optional cell and returns the same optional cell. Incorrect key and value types cause failure at map.get or similar methods.
m.toLowLevelDict(): dict
Converts a high-level map to a low-level TVM dictionary. Returns the same optional cell.
m.isEmpty(): bool
Checks whether a map is empty. Use m.isEmpty() instead of m == null.
m.exists(key: K): bool
Checks whether a key exists in a map.
m.get(key: K): MapLookupResult<V>
Gets an element by key. Returns isFound = false if key does not exist.
m.mustGet(key: K, throwIfNotFound: int = 9): V
Gets an element by key and throws if it does not exist.
m.set(key: K, value: V): self
Sets an element by key. Since it returns self, calls may be chained.
Sets an element only if the key does not exist. If exists, returns an old value.
m.delete(key: K): bool
Deletes an element by key. Returns true if deleted.
m.deleteAndGetDeleted(key: K): MapLookupResult<V>
Deletes an element by key and returns the deleted element. If not found, isFound = false.
m.findFirst(): MapEntry<K, V>
Finds the first (minimal) element. For integer keys, returns minimal integer. For addresses or complex keys, represented as slices, returns lexicographically smallest key. Returns isFound = false for an empty map.
m.findLast(): MapEntry<K, V>
Finds the last (maximal) element. For integer keys, returns maximal integer. For addresses or complex keys (represented as slices), returns lexicographically largest key. Returns isFound = false for an empty map.
Iterates over a map in descending order.Augmented hashmaps and prefix dictionariesThese structures are rarely used and are not part of the type system.
Prefix dictionaries: import @stdlib/tvm-dicts and use assembly functions.
Augmented hashmaps and Merkle proofs: implement interaction manually.
In Tolk, msg_cell does not require manual parsing to retrieve sender_address or fwd_fee. Fields are accessed directly:
fun onInternalMessage(in: InMessage) { in.senderAddress in.originalForwardFee in.valueCoins // typically called "msg value" in.| // IDE shows completions}
The legacy approach of accepting 4 parameters, as recv_internal, works but is less efficient. InMessage fields are directly mapped to TVM-11 instructions.Recommended pattern:
Define each message as a struct, typically including a 32-bit opcode.
Define a union of all allowed messages.
Use val msg = lazy MyUnion.fromSlice(in.body).
Match on msg, handling each branch and possibly an else.
Avoid manually extracting fwd_fee or other fields at the start of the function. Access them on demand through the in.smth.
type AllowedMessageToMinter = | MintNewJettons | BurnNotificationForMinter | RequestWalletAddressfun onInternalMessage(in: InMessage) { val msg = lazy AllowedMessageToMinter.fromSlice(in.body); match (msg) { BurnNotificationForMinter => { var storage = lazy MinterStorage.load(); ... storage.save(); ... } RequestWalletAddress => ... MintNewJettons => ... else => { // for example: // ignore empty messages, "wrong opcode" for others assert (in.body.isEmpty()) throw 0xFFFF } }}
Separate onBouncedMessageIn FunC, msg_cell required parsing, reading 4-bit flags, and testing flags & 1 to detect a bounced message.In Tolk, bounced messages are handled through a separate entry point:
fun onBouncedMessage(in: InMessageBounced) {}
The compiler automatically routes bounced messages:
fun onInternalMessage(in: InMessage) { // the compiler inserts this automatically: if (MSG_IS_BOUNCED) { onBouncedMessage(...); return; } ... // contract logic}
If onBouncedMessage is not declared, bounced messages are filtered out:
fun onInternalMessage(in: InMessage) { // the compiler inserts this automatically: if (MSG_IS_BOUNCED) { return; } ... // contract logic}
Bounced body is either “first 256 bits” or “the entire body”When you use createMessage, its parameter bounce is an enum:
BounceMode.NoBounce
BounceMode.Only256BitsOfBody — in.bouncedBody will be “0xFFFFFFFF” + first 256 bits of original body (cheapest)
BounceMode.RichBounce — parse in.bouncedBody with RichBounceBody.fromSlice
BounceMode.RichBounceOnlyRootCell — same, but originalBody will contain only a root cell
// demo for "old-fashioned" 256 bits (if you send all with Only256BitsOfBody)fun onBouncedMessage(in: InMessageBounced) { in.bouncedBody // 32-bit prefix + 256 bits in.bouncedBody.skipBouncedPrefix(); // skips 0xFFFFFFFF // handle rest of body, probably with lazy match}
// demo for "rich bounces" to access not 256 bits, but the entire body// (if you send ALL outgoing messages with BounceMode.RichBounce)fun onBouncedMessage(in: InMessageBounced) { val rich = lazy RichBounceBody.fromSlice(in.bouncedBody); // handle rich.originalBody, probably with lazy match // use rich.xxx to get exitCode, gasUsed, and so on}
Explore the Tolk vs FunC benchmarks —real Jetton, NFT, and Wallet contracts migrated from FunC with the same logic.Use the FunC-to-Tolk converter for incremental migration.Run npm create ton@latest to experiment.