Statements
The following statements can appear anywhere in a function body.
let
statement
The let
statement allows local and block-scoped variable declaration.
In Tact, declaring a local variable always requires an initial value. However, the type ascription can be omitted, and Tact will try to infer it from the initial value:
let value: Int = 123; // full declaration with type and valuelet vInferred = 123; // inferred type Int
let vExplicitCtx: Context = context(); // explicit type Context, a built-in Structlet vCtx = context(); // inferred type Context
Note that the initial value of null
can mean either an empty map<K, V>
with arbitrary K
and V
types or the intentional absence of any other value for the optional type. Therefore, whenever you declare an optional or a map<K, V>
, you must explicitly specify the type, as it cannot be inferred:
let vOptional: Int? = null; // explicit type Int or nulllet vOptInt = 42; // implicit type IntvOptInt = null; // COMPILATION ERROR!
let vMap: map<Int, Int> = emptyMap(); // explicit type map<Int, Int>let vMapWithSerialization: map<Int as uint8, Int as uint8> = emptyMap();
Naming a local variable with an underscore _
causes its value to be considered unused and discarded. This is useful if you do not need the return value of a function with side effects and want to explicitly mark the variable as unused. Note that such wildcard variable name _
cannot be accessed:
let _ = someFunctionWithSideEffects(); // with type inferencelet _: map<Int, Int> = emptyMap(); // with explicit type
dump(_); // COMPILATION ERROR! Cannot access _
return
statement
The return
statement ends function execution and specifies a value to be returned to the function caller.
// Simple wrapper over stdlib function now()fun getTimeFromNow(offset: Int): Int { return now() + offset;}
Block
A block statement is used to group zero or more statements. The block is delimited by a pair of braces (“curly braces”, {}
) and contains a list of zero or more statements and declarations.
Some statements, such as let
or return
, must end with a terminating semicolon ;
. However, the semicolon of the last statement in the block is optional and may be omitted.
{ // <- start of the block // arbitrary statements: let value: Int = 2 + 2; dump(value);} // <- end of the block
{ dump(2 + 2) } // a block with only one statement, // omitting the last and only semicolon
{ let nah = 3 * 3 * 3; // a block with two statements, let yay = nah + 42 // but without the last semicolon}
Expression
An expression statement is an expression used in a place where a statement is expected. The expression is evaluated, and its result is discarded. Therefore, it makes sense only for expressions that have side effects, such as executing a function or updating a variable.
dump(2 + 2); // stdlib function
Assignment
Assignment statements use an assignment operator (=
) or augmented assignment operators (assignments combined with an operation):
let value: Int; // declarationvalue = 5; // assignmentvalue += 5; // augmented assignment (one of many — see below)
Destructuring assignment
Available since Tact 1.6The destructuring assignment is a concise way to unpack Structs and Messages into distinct variables. It mirrors the instantiation syntax, but instead of creating a new Struct or Message, it binds every field or some of the fields to their respective variables.
The syntax is derived from the let
statement, and instead of specifying the variable name directly, it involves specifying the structure type on the left side of the assignment operator =
, which corresponds to the structure type of the value on the right side.
// Definition of Examplestruct Example { number: Int }
// An arbitrary helper functionfun get42(): Example { return Example { number: 42 } }
fun basic() { // Basic syntax of destructuring assignment (to the left of "="): let Example { number } = get42(); // ------- ------ ------- // ↑ ↑ ↑ // | | gives the Example Struct // | definition of "number" variable, derived // | from the field "number" in Example Struct // target structure type "Example" // to destructure fields from
// Same as above, but with an instantiation // to showcase how destructuring syntax mirrors it: let Example { number } = Example { number: 42 }; // ---------------------- // ↑ // instantiation of Example Struct
// Above examples of syntax are roughly equivalent // to the following series of statements: let example = Example { number: 42 }; let number = example.number;}
Just like in instantiation, a trailing comma is allowed.
struct Example { number: Int }
fun trailblazing() { let Example { number, // trailing comma inside variable list } = Example { number: 42, // trailing comma inside field list };}
To create a binding under a different variable name, specify it after the semicolon :
.
// Similar definition, but this time the field is called "field", not "number"struct Example { field: Int }
fun naming(s: Example) { let Example { field: varFromField } = s; // ------------ ↑ // ↑ | // | instance of Example Struct, received // | as a parameter of the function "naming" // definition of "varFromField" variable, derived // from the field "field" in Example Struct}
Note that the order of bindings doesn’t matter — all fields retain their values and types under their names regardless of the order in which they appear in their definition in the respective Struct or Message.
// "first" goes first, then goes "second"struct Two { first: Int; second: String }
fun order(s: Two) { let Two { second, first } = s; // ------ ----- // ↑ ↑ // | this variable will be of type Int, // | same as the "first" field in Struct Two // this variable will be of type String, // same as the "second" field in Struct Two}
Destructuring assignment is exhaustive and requires specifying all the fields as variables. To deliberately ignore some of the fields, use an underscore _
, which discards the relevant field’s value. Note that such wildcard variable name _
cannot be accessed:
// "first" goes first, then goes "second"struct Two { first: Int; second: String }
fun discard(s: Two) { let Two { second: _, first } = s; // --- // ↑ // discards the "second" field, only taking the "first"}
To completely ignore the rest of the fields, use ..
at the end of the list:
struct Many { one: Int; two: Int; three: Int; fans: Int }
fun ignore(s: Many) { let Many { fans, .. } = s; // -- // ↑ // ignores all the unspecified fields, // defining only "fans"}
Branches
Control the flow of the code.
if...else
When executing an if...else
statement, first, the specified condition is evaluated. If the resulting value is true
, the following statement block is executed. Otherwise, if the condition evaluates to false
, the optional else
block is executed. If the else
block is missing, nothing happens, and execution continues further.
Regular if
statement:
// condition// ↓if (true) { // consequence, when condition is true dump(2 + 2);}
With else
block:
// condition// ↓if (2 + 2 == 4) { // consequence, when condition is true dump(true);} else { // alternative, when condition is false dump(false);}
With nested if...else
:
// condition// ↓if (2 + 2 == 3) { // consequence, when condition is true dump("3?");// condition2// ↓} else if (2 + 2 == 4) { // another consequence, when condition2 is true dump(true);} else { // alternative, when both condition and condition2 are false dump(false);}
try...catch
The try...catch
statement consists of a try
block and an optional catch
block, which receives an Int
exit code as its only argument. The code in the try
block is executed first, and if it fails, the code in the catch
block will be executed, and changes made in the try
block will be rolled back, if possible.
Regular try
statement:
fun braveAndTrue() { // Let's try and do something erroneous try { throw(1042); // throwing with exit code 1042 }
// The following will be executed as the erroneous code above was wrapped in a try block dump(1042);}
With catch (e)
block:
fun niceCatch() { // Let's try and do something erroneous try { throw(1042); // throwing with exit code 1042 } catch (err) { dump(err); // this will dump the exit code caught, which is 1042 }}
With nested try...catch
:
try { // Preparing an x equal to 0, in such a way that the Tact compiler won't realize it (yet!) let xs: Slice = beginCell().storeUint(0, 1).endCell().beginParse(); let x: Int = xs.loadUint(1); // 0
try { throw(101); // 1. throws with exit code 101 } catch (err) { // 2. catches the error and captures its exit code (101) as err return err / x; // 3. divides err by x, which is zero, throwing with exit code 4 }
} catch (err) { // 4. catches the new error and captures its exit code (4) as err // ^^^ this works without name collisions because the previous err // has a different scope and is only visible inside the previous catch block
dump(err); // 5. dumps the last caught exit code (4)}
Note that similar to the let
statement, the captured exit code in the catch ()
clause can be discarded by specifying an underscore _
in its place:
try { throw(42);} catch (_) { dump("I don't know the exit code anymore");}
Loops
Conditionally repeat certain blocks of code multiple times.
repeat
The repeat
loop executes a block of code a specified number of times. The number of repetitions should be given as a positive 32-bit Int
in the inclusive range from to . If the value is greater, an error with exit code 5, Integer out of expected range
, will be thrown.
If the specified number of repetitions is equal to or any negative number in the inclusive range from to , it is ignored, and the code block is not executed at all.
let twoPow: Int = 1;
// Repeat exactly 10 timesrepeat (10) { twoPow *= 2;}
// Skippedrepeat (-1) { twoPow *= 3333;}
twoPow; // 1024
while
The while
loop continues executing the block of code as long as the given condition is true
.
In the following example, the value of x
is decremented by 1 on each iteration, so the loop will run 10 times:
let x: Int = 10;while (x > 0) { x -= 1;}
do...until
The do...until
loop is a post-test loop that executes the block of code at least once and then continues to execute it until the given condition becomes true
.
In the following example, the value of x
is decremented by 1 on each iteration, so the loop will run 10 times:
let x: Int = 10;do { x -= 1; // executes this code block at least once} until (x <= 0);
foreach
The foreach
loop operates on key-value pairs (entries) of the map<K, V>
type in sequential order: from the smallest keys of the map to the biggest ones.
This loop executes a block of code for each entry in the given map, capturing the key and value on each iteration. This is handy when you don’t know in advance how many items there are in the map or don’t want to explicitly look for each of the entries using the .get()
method of maps.
Note that the names of captured keys and values in each iteration are arbitrary and can be any valid Tact identifier, provided they are new to the current scope. The most common options are: k
and v
, or key
and value
.
In the following example, the map cells
has entries, so the loop will run times:
// Empty maplet cells: map<Int, Cell> = emptyMap();
// Setting four entriescells.set(1, beginCell().storeUint(100, 16).endCell());cells.set(2, beginCell().storeUint(200, 16).endCell());cells.set(3, beginCell().storeUint(300, 16).endCell());cells.set(4, beginCell().storeUint(400, 16).endCell());
// A variable for summing up the valueslet sum: Int = 0;
// For each key and value pair in the cells map, do:foreach (key, value in cells) { // or just k, v let s: Slice = value.beginParse(); // convert Cell to Slice sum += s.loadUint(16); // sum the Slice values}dump(sum); // 1000
It’s also possible to iterate over a map in contract storage, and over maps as members of instances of Struct or Message types:
import "@stdlib/deploy";
struct Fizz { oh_my: map<Int, Int> }message Buzz { oh_my: map<Int, Int> }
contract Iterated { oh_my: map<Int, Int>;
receive("call to iterate!") { let oh_my: map<Int, Int> = emptyMap(); oh_my.set(0, 42); oh_my.set(1, 27);
self.oh_my = oh_my; // assigning local map to the storage one let fizz = Fizz{ oh_my }; // field punning let buzz = Buzz{ oh_my }; // field punning
// Iterating over map in contract storage foreach (key, value in self.oh_my) { // ... }
// Iterating over map member of a Struct Fizz instance foreach (key, value in fizz.oh_my) { // ... }
// Iterating over map member of a Message Buzz instance foreach (key, value in buzz.oh_my) { // ... } }}
Similar to the let
statement, either of the captured key or value (or both) can be discarded by specifying an underscore _
in their place:
// Empty maplet quartiles: map<Int, Int> = emptyMap();
// Setting some entriesquartiles.set(1, 25);quartiles.set(2, 50);quartiles.set(3, 75);
// Discarding captured keys// without modifying them in the map itselfforeach (_, value in quartiles) {}
// Discarding captured values// without modifying them in the map itselfforeach (key, _ in quartiles) {}
// Discarding both keys and values// without modifying them in the map itselfforeach (_, _ in quartiles) { // Can't access via _, but can do desired operations // n times, where n is the current length of the map}