Statements
This section briefly overviews FunC statements, which form the core of function bodies.
Expression statements
The most common type of statement is the expression statement—an expression followed by ;
. As a rule, all sub-expressions are evaluated from left to right,
except in cases where asm stack rearrangement explicitly defines the order.
Variable declaration
Local variables must be initialized at the time of declaration. Here are some examples:
int x = 2;
var x = 2;
(int, int) p = (1, 2);
(int, var) p = (1, 2);
(int, int, int) (x, y, z) = (1, 2, 3);
(int x, int y, int z) = (1, 2, 3);
var (x, y, z) = (1, 2, 3);
(int x = 1, int y = 2, int z = 3);
[int, int, int] [x, y, z] = [1, 2, 3];
[int x, int y, int z] = [1, 2, 3];
var [x, y, z] = [1, 2, 3];
A variable can be redeclared in the same scope. For example, the following code is valid:
int x = 2;
int y = x + 1;
int x = 3;
In this case,
the second occurrence of int x
is not a new declaration but a compile-time check ensuring that x
has type int
. The third line is equivalent to x = 3;
.
Variable redeclaration in nested scopes
In nested scopes, a new variable with the same name can be declared, just like in C:
int x = 0;
int i = 0;
while (i < 10) {
(int, int) x = (i, i + 1);
;; Here x is a variable of type (int, int)
i += 1;
}
;; Here, x refers to the original variable of type int declared above
However, as mentioned in the Global variables section, global variables cannot be redeclared.
Since variable declarations are expression statements, constructs like int x = 2;
are valid expressions.
For instance:
int y = (int x = 3) + 1;
Here, x
is declared and assigned 3
, and y
is assigned 4
.
Underscore
The underscore _
is used when a value is not needed.
For example, if foo
is a function of type int -> (int, int, int)
,
you can retrieve only the first return value while ignoring the rest:
(int fst, _, _) = foo(42);
Function application
A function call in FunC follows a conventional syntax: the function name is followed by its arguments, separated by commas.
;; Suppose foo has type (int, int, int) -> int
int x = foo(1, 2, 3);
However, unlike many conventional languages, FunC treats functions as taking a single argument.
In the example above, foo
is a function that takes one tuple argument of type (int, int, int)
.
Function composition
To illustrate how function arguments work in FunC, consider a function bar
of type int -> (int, int, int)
. Since foo
expects a single tuple argument, you can pass the entire result of bar(42)
directly into foo
:
int x = foo(bar(42));
This is equivalent to the longer form:
(int a, int b, int c) = bar(42);
int x = foo(a, b, c);
Also Haskell-style calls are possible, but not always (to be fixed later):
FunC also supports Haskell-style function application, but with some limitations:
;; Suppose foo has type int -> int -> int -> int
;; i.e., it is curried
(int a, int b, int c) = (1, 2, 3);
int x = foo a b c; ;; Valid syntax
However, direct application with literals does not compile:
;; int y = foo 1 2 3; ERROR: won't compile
Instead, parentheses are required:
int y = foo (1) (2) (3); ;; Valid syntax
Lambda expressions
Lambda expressions are not yet supported in FunC.
Methods calls
Non-modifying methods
In FunC, a function with at least one argument can be called a non-modifying method using the dot .
syntax.
For example, the function store_uint
has the type (builder, int, int) → builder
, where:
- The first argument is a builder object.
- The second argument is the value to store.
- The third argument is the bit length.
The function begin_cell()
creates a new builder.
These two ways of calling store_uint
are equivalent:
builder b = begin_cell();
b = store_uint(b, 239, 8);
builder b = begin_cell();
b = b.store_uint(239, 8);
The dot .
syntax allows the first argument of a function to be placed before the function name,
simplifying the code further:
builder b = begin_cell().store_uint(239, 8);
Multiple non-modifying methods can be chained together:
builder b = begin_cell().store_uint(239, 8)
.store_int(-1, 16)
.store_uint(0xff, 10);
Modifying methods
If a function’s first argument is of type A
and its return value follows the structure (A, B)
,
where B
is an arbitrary type, the function can be used as a modifying method.
A modifying method modifies its first argument by assigning the first component of the returned value to the original variable. These methods may take additional arguments and return extra values, but their primary purpose is to update the first argument.
For example, consider a cell slice cs
and the function load_uint
, which has the type: load_uint(slice, int) → (slice, int)
.
This function takes a cell slice and a number of bits to load, returning the remaining slice and the loaded value. The following three calls are equivalent:
(cs, int x) = load_uint(cs, 8);
(cs, int x) = cs.load_uint(8);
int x = cs~load_uint(8);
Without return values Sometimes, a function can be used as a modifying method even when it doesn’t return a meaningful value—only modifying its first argument. This can be achieved using unit types.
For example, consider an increment function inc
of type int -> int
. It should be redefined as a function of type int -> (int, ())
to use it as a modifying method:
(int, ()) inc(int x) {
return (x + 1, ());
}
Now, the following code increments x
:
x~inc();
.
and ~
in function names
Suppose we want to use inc
as a non-modifying method. We can write:
(int y, _) = inc(x);
However, we can also define inc
as a modifying method:
int inc(int x) {
return x + 1;
}
(int, ()) ~inc(int x) {
return (x + 1, ());
}
Now, we can call it in different ways:
x~inc(); ;; Modifies x
int y = inc(x); ;; Doesn't modify x
int z = x.inc(); ;; Also doesn't modify x
How FunC resolves function calls
- If a function is called with
.
(e.g.,x.foo()
), the compiler looks for a.foo
definition. - If a function is called with
~
(e.g.,x~foo()
), the compiler looks for a~foo
definition. - If neither
.foo
nor~foo
is defined, the compiler falls back to the regularfoo
definition.
Operators
Note that all the unary and binary operators are currently integer operators. Logical operators are bitwise integer operators (cf. absence of boolean type).
Unary operators
FunC supports two unary operators:
~
is bitwise not (priority 75)-
is integer negation (priority 20)
These operators must be separated from the arguments:
- x
- Negates x.-x
- Interpreted as a single identifier, not an operation.
Binary operators
With priority 30 (left-associative):
*
is integer multiplication/
is integer division (floor)~/
is integer division (round)^/
is integer division (ceil)%
is integer reduction by modulo (floor)~%
is integer reduction by modulo (round)^%
is integer reduction by modulo (ceil)/%
returns the quotient and the remainder&
is bitwise AND
With priority 20 (left-associative):
+
is integer addition-
is integer subtraction|
is bitwise OR^
is bitwise XOR
With priority 17 (left-associative):
<<
is bitwise left shift>>
is bitwise right shift~>>
is bitwise right shift (round)^>>
is bitwise right shift (ceil)
With priority 15 (left-associative):
==
is integer equality check!=
is integer inequality check<
is integer comparison<=
is integer comparison>
is integer comparison>=
is integer comparison<=>
is integer comparison (returns -1, 0 or 1)
They also should be separated from the argument:
x + y
- Proper spacing between operands.x+y
- Interpreted as a single identifier, not an operation.
Conditional operator
FunC supports the standard conditional (ternary) operator with the following syntax:
<condition> ? <consequence> : <alternative>
For example:
x > 0 ? x * fac(x - 1) : 1;
Priority 13.
Assignments
Priority 10.
Supports simple assignment =
and compound assignment operators: +=
, -=
, *=
, /=
, ~/=
, ^/=
, %=
, ~%=
, ^%=
, <<=
, >>=
, ~>>=
, ^>>=
, &=
, |=
, ^=
.
Loops
FunC supports repeat
, while
, and do { ... } until
loops. The for
loop is not supported.
Repeat loop
The repeat
loop uses the repeat
keyword followed by an int
expression. It executes the code a specified number of times.
Examples:
int x = 1;
repeat(10) { ;;Repeats the block 10 times
x *= 2;
}
;; x = 1024
int x = 1, y = 10;
repeat(y + 6) { ;;Repeats the block 16 times
x *= 2;
}
;; x = 65536
If the repetition count is negative, the loop does not execute:
int x = 1;
repeat(-1) {
x *= 2;
}
;; x = 1
A range check exception is thrown if the repetition count is less than -2³¹
or greater than 2³¹ - 1
.
While loop
The while
loop follows standard syntax:
int x = 2;
while (x < 100) {
x = x * x;
}
;; x = 256
Note that the truth value of condition x < 100
is of type int
(cf. absence of boolean type).
Until loop
The do { ... } until
loop has the following syntax:
int x = 0;
do {
x += 3;
} until (x % 17 == 0);
;; x = 51
If statements
Examples
Standard if
statement:
if (flag) {
do_something();
}
Negated condition, which is equivalent to if
(~flag
):
ifnot (flag) {
do_something();
}
If-else
statement:
if (flag) {
do_something();
} else {
do_alternative();
}
;; Some specific features
if (flag1) {
do_something1();
} else {
do_alternative4();
}
Curly brackets {}
are required for if
statements. The following code will not compile:
if (flag1)
do_something();
Try-catch statements
Available in FunC since v0.4.0
The try
block executes a section of code.
If an error occurs, all changes made within the try
block are completely rolled back, and the catch
block is executed instead. The catch
block receives two arguments:
x
: the exception parameter, which can be of any typen
: the error code, an integer
Unlike many other languages, in FunC, all changes are undone if an error occurs inside the try
block. These modifications include updates to local and global variables and changes to storage registers (c4
for storage, c5
for action/messages, c7
for context, etc.).
Any contract storage updates and outgoing messages are also reverted.
However, certain TVM state parameters are not rolled back, such as:
- Codepage settings
- Gas counters
As a result, all gas consumed within the
try
block is still accounted for, and any operations that modify gas limits (e.g.,accept_message
orset_gas_limit
) will remain in effect.
Exception parameter handling
Since the exception parameter can be of any type, which may vary depending on the exception, FunC cannot determine its type at compile time. This requires the developer to manually cast the exception parameter when necessary, as shown in the type-casting example below.
Examples
Basic try-catch
usage:
try {
do_something();
} catch (x, n) {
handle_exception();
}
Casting the exception parameter:
forall X -> int cast_to_int(X x) asm "NOP";
...
try {
throw_arg(-1, 100);
} catch (x, n) {
x.cast_to_int();
;; x = -1, n = 100
return x + 1;
}
Variable reset on exception:
int x = 0;
try {
x += 1;
throw(100);
} catch (_, _) {
}
In this last example, although x
is incremented inside the try
block, the modification is rolled back due to the exception, so x
remains 0
.
Block statements
Block statements are supported as well, creating a new nested scope:
int x = 1;
builder b = begin_cell();
{
builder x = begin_cell().store_uint(0, 8);
b = x;
}
x += 1;
In this example, the inner block introduces a new builder
variable named x
, which exists only within that scope.
The outer x
remains unchanged and can be used after the block ends.