Skip to main content
Expressions in FunC combine literals, variables, operators, and function calls to produce a value when evaluated. An expression in FunC can be: We focus on variable declarations and function calls, since the other kind of expressions are explained in their respective articles. As a general rule, all sub-expressions inside an expression 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. Since variable declarations are expressions, the result of evaluating a declaration like:
type iden = expr
returns the value produced by expr, in addition to defining variable iden with value expr. For instance: (int x = 3) + x; declares x and assigns to it the value 3. The result of the expression (int x = 3) is therefore 3, which means that (int x = 3) + x evaluates to 6, since x has value 3 after the declaration. Here are further examples of variable declarations, where each line is independent from the other ones. It is possible to use the keyword var to let the type checker infer the type (see hole types).
int x = 2;
var x = 2;               ;; Equivalent to previous, but with type inference
(int, int) p = (1, 2);
(int, var) p = (1, 2);   ;; Equivalent to previous, but with type inference
[int, var, int] t = [1, 2, 3];
In the previous examples, p and t store the entire tensor and tuple, respectively. But it is possible to deconstruct tensors and tuples and assign each component to different variables. Here are some examples that showcase different ways of deconstructing tensors and tuples:
(int x, int y, int z) = (1, 2, 3);       ;; Assign each tensor component to x, y, and z.
(int, int, int) (x, y, z) = (1, 2, 3);   ;; Equivalent to previous
var (x, y, z) = (1, 2, 3);               ;; Equivalent to previous, but with type inference 
(int x = 1, int y = 2, int z = 3);       ;; Assigning each component directly
[int x, int y, int z] = [1, 2, 3];       ;; Assign each tuple component to x, y, and z
[int, int, int] [x, y, z] = [1, 2, 3];   ;; Equivalent to previous
var [x, y, z] = [1, 2, 3];               ;; Equivalent to previous, but with type inference 
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 example, 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;. The following example, which redeclares x with type (int, int) at the third line, is also valid:
int x = 2;
int y = x + 1;
(int, int) x = (y, y + 1);
After the third line, variable x has type (int, int).

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, global variables cannot be redeclared. See Global variables.

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 call

A function call in FunC follows a conventional syntax: the function name is followed by its arguments, separated by commas. However, unlike many conventional languages, FunC also treats functions as taking a single tensor argument. For example, suppose foo is a function of type (int, int, int) -> int. The following two lines are equivalent ways of calling foo:
int x = foo(1, 2, 3);    ;; Three arguments separated by ,
int x = foo((1, 2, 3));  ;; The tensor (1, 2, 3) passed as a single argument
Equivalently, we could also assign tensor (1, 2, 3) to a variable, and then call foo:
(int, int, int) t = (1, 2, 3);
int x = foo(t);    ;; Pass the tensor as a single argument

Function composition

To illustrate how function composition works in FunC, suppose that together with the previous foo function, there is also a bar function of type int -> (int, int, int). Since foo expects a single tensor 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, which decomposes the result tensor of bar(42) and then calls foo by passing all arguments separated by commas:
(int a, int b, int c) = bar(42);
int x = foo(a, b, c);

Functions as first-class objects

In FunC, functions are first-class objects: they can be assigned to variables, passed as arguments to other functions, and returned from functions. For example, the following function apply, receives a function f of type int -> int, and a value v of type int as arguments. Function apply invokes f with argument v and returns the result of the application, i.e., apply computes the expression f(v).
int apply(int -> int f, int v) {
  return f(v);
}
Let us suppose we have an increment function:
int inc(int x) {
  return x + 1;
}
We can then invoke apply by passing the increment function:
apply(inc, 2);   ;; produces 3, or equivalently, inc(2)
It is also possible to assign the increment function to variables:
var f = inc;
or return it from functions:
int -> int return_inc() {
  return inc;
}
FunC does not support lambda expressions. This means that it is not possible to create anonymous functions.

Special function call notation

In addition to the standard syntax for calling a function, FunC supports two function call notations for specific situations, the non-modifying notation, and the modifying notation, which are explained next.

Non-modifying notation

In FunC, a function with at least one argument can be called using the dot . notation, also called non-modifying notation. For example, the function store_uint, which stores an unsigned integer into a cell builder and returns the modified builder, has type (builder, int, int) -> builder, where:
  • The first argument is the builder object.
  • The second argument is the value to store.
  • The third argument is the unsigned integer bit length.
Missing link to store_uint function in built-ins page.
The following way of calling store_uint (here, begin_cell creates a new builder and has type () -> builder):
builder b = begin_cell();
b = store_uint(b, 239, 8);
is equivalent to:
builder b = begin_cell();
b = b.store_uint(239, 8);   ;; Uses non-modifying notation
The dot . notation 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);
which is equivalent to the standard syntax for calling a function:
builder b = store_uint(begin_cell(), 239, 8);
Using the . notation it is possible to chain many function calls together:
builder b = begin_cell().store_uint(239, 8)
                        .store_int(-1, 16)
                        .store_uint(0xff, 10);
which is equivalent to the longer form:
builder b = begin_cell();
b = b.store_uint(239, 8);
b = b.store_int(-1, 16);
b = b.store_uint(0xff, 10);
or to the more difficult to read form, which nests all the calls:
builder b = store_uint(
                store_int(
                    store_uint(
                         begin_cell(), 
                         239, 
                         8), 
                    -1, 
                    16), 
                0xff, 
                10
            );

Modifying notation

If a function’s first argument is of type A and its return type follows the structure (A, B), where B is an arbitrary type, the function can be called using the ~ notation, also called modifying notation. The primary purpose of the ~ notation is to automatically update the first argument in a function call. More concretely, suppose foo is a function of type (builder, int, int) -> (builder, int), then the call v = b~foo(2, 3), which uses the ~ notation, is equivalent to the standard call (b, v) = foo(b, 2, 3). The statement (b, v) = foo(b, 2, 3) reassigns (or updates) the first argument b after the call to foo finishes. The ~ notation serves as a shortcut to express this reassignment of the first argument. One possible application of the ~ notation is for working with cell slices. For example, consider a cell slice cs and the function load_uint, which has type: (slice, int) -> (slice, int). The function load_uint takes a cell slice and a number of bits to load, returning the remaining slice and the loaded unsigned integer value. The following three calls are equivalent:
(cs, int x) = load_uint(cs, 8);     ;; Standard function call
(cs, int x) = cs.load_uint(8);      ;; Call using non-modifying notation (i.e., `.`)
int x = cs~load_uint(8);            ;; Call using modifying notation (i.e., `~`)

Adapting functions to use ~

When a function type is of the form (A, ...) -> A, it is possible to adapt the function so that the ~ notation can be used on such a function. This can be achieved using unit types, by redefining the function type to (A, ...) -> (A, ()). For example, consider an increment function inc of type int -> int:
int inc(int x) {
  return x + 1;
}
To increment a variable y using inc, the function should be used as follows:
y = inc(y);
Attempting to use the ~ notation on inc would fail:
y~inc();    ;; DOES NOT COMPILE
because inc does not have a return type of the form (int, B), where B is some type. To use the ~ notation on inc, first redefine the function so that it now has type int -> (int, ()) as follows:
(int, ()) inc(int x) {
  return (x + 1, ());
}
Now, the following code increments y:
y~inc();

. and ~ in function names

Previously, we redefined inc to have type int -> (int, ()) so that it was possible to use the ~ notation on it. However, it would be bothersome to use inc in cases where we do not want to increment a variable, but we just want to store the increment in a different variable:
(int y, _) = inc(x);
In other words, we would also like to use inc as if it was the original function with type int -> int:
int y = inc(x);
In FunC, it is possible to also keep the original inc of type int -> int so that we can use inc in different ways, like:
x~inc(); ;; Increments x, using modifying notation
int y = inc(x); ;; Doesn't modify x, but stores the increment in y
int z = x.inc(); ;; Equivalent to previous, but using non-modifying notation
This is achieved by declaring a function ~inc alongside the original inc:
int inc(int x) {    ;; Original inc function
  return x + 1;
}
(int, ()) ~inc(int x) {  ;; inc version to be able to use ~ notation
  return (x + 1, ());
}
This is possible because of the way 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 regular foo definition.
I