Functions
FunC program is essentially a list of function declarations/definitions and global variable declarations. This section covers the first topic.
Any function declaration or definition starts with a common pattern and one of the three things goes next:
-
single
;
, which means that the function is declared but not defined yet. It may be defined later in the same file or in some other file which is passed before the current one to the FunC compiler. For example,int add(int x, int y);
is a simple declaration of a function named
add
of type(int, int) -> int
. -
assembler function body definition. It is the way to define functions by low-level TVM primitives for later use in FunC program. For example,
int add(int x, int y) asm "ADD";
is an assembler definition of the same function
add
of type(int, int) -> int
which will translate to the TVM opcodeADD
. -
ordinary block statement function body definition. It is the usual way to define functions. For example,
int add(int x, int y) {
return x + y;
}is an ordinary definition of the
add
function.
Function declaration
As said before, any function declaration or definition starts with a common pattern. The following is the pattern:
[<forall declarator>] <return_type> <function_name>(<comma_separated_function_args>) <specifiers>
where [ ... ]
corresponds to an optional entry.
Function name
Function name can be any identifier and also it can start with .
or ~
symbols. The meaning of those symbols is explained in the statements section.
For example, udict_add_builder?
, dict_set
and ~dict_set
are valid and different function names. (They are defined in stdlib.fc.)
Special function names
FunC (actually Fift assembler) has several reserved function names with predefined ids.
main
andrecv_internal
have id = 0recv_external
has id = -1run_ticktock
has id = -2
Every program must have a function with id 0, that is, main
or recv_internal
function.
run_ticktock
is called in ticktock transactions of special smart contracts.
Receive internal
recv_internal
is called when a smart contract receives an inbound internal message.
There are some variables at the stack when TVM initiates, by setting arguments in recv_internal
we give smart-contract code awareness about some of them. Those arguments about which code will not know, will just lie at the bottom of the stack never touched.
So each of the following recv_internal
declarations is correct, but those with less variables will spend slightly less gas (each unused argument adds additional DROP
instructions)
() recv_internal(int balance, int msg_value, cell in_msg_cell, slice in_msg) {}
() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) {}
() recv_internal(cell in_msg_cell, slice in_msg) {}
() recv_internal(slice in_msg) {}
Receive external
recv_external
is for inbound external messages.
Return type
Return type can be any atomic or composite type as described in the types section. For example,
int foo();
(int, int) foo'();
[int, int] foo''();
(int -> int) foo'''();
() foo''''();
are valid function declarations.
Type inference is also allowed. For example,
_ pyth(int m, int n) {
return (m * m - n * n, 2 * m * n, m * m + n * n);
}
is a valid definition of function pyth
of type (int, int) -> (int, int, int)
, which computes Pythagorean triples.
Function arguments
Function arguments are separated by commas. The valid declarations of an argument are following:
- Ordinary declaration: type + name. For example,
int x
is a declaration of argument of typeint
and namex
in the function declaration() foo(int x);
- Unused argument declaration: only type. For example,
is a valid function definition of type
int first(int x, int) {
return x;
}(int, int) -> int
- Argument with an inferred type declaration: only name.
For example,
is a valid function definition of type
int inc(x) {
return x + 1;
}int -> int
. Theint
type ofx
is inferred by the type-checker.
Note that although a function may look like a function of several arguments, it's actually a function of one tensor-type argument. To see the difference, please refer to function application. Nevertheless, the components of the argument tensor are conventionally called function arguments.
Function calls
Non-modifying methods
Non-modifying function supports short function call form with .
example(a);
a.example();
If a function has at least one argument, it can be called as a non-modifying method. For example, store_uint
has type (builder, int, int) -> builder
(the second argument is the value to store, and the third is the bit length). begin_cell
is a function that creates a new builder. The following codes are equivalent:
builder b = begin_cell();
b = store_uint(b, 239, 8);
builder b = begin_cell();
b = b.store_uint(239, 8);
So the first argument of a function can be passed to it being located before the function name, if separated by .
. The code can be further simplified:
builder b = begin_cell().store_uint(239, 8);
Multiple calls of methods are also possible:
builder b = begin_cell().store_uint(239, 8)
.store_int(-1, 16)
.store_uint(0xff, 10);
Modifying functions
Modifying function supports short form with ~
and .
operators.
If the first argument of a function has type A
and the return value of the function has the shape of (A, B)
where B
is some arbitrary type, then the function can be called as a modifying method.
Modifying function calls may take some arguments and return some values, but they modify their first argument, that is, assign the first component of the returned value to the variable from the first argument.
a~example();
a = example(a);
For example, suppose cs
is a cell slice and load_uint
has type (slice, int) -> (slice, int)
: it takes a cell slice and number of bits to load and returns the remainder of the slice and the loaded value. The following codes are equivalent:
(cs, int x) = load_uint(cs, 8);
(cs, int x) = cs.load_uint(8);
int x = cs~load_uint(8);
In some cases we want to use a function as a modifying method that doesn't return any value and only modifies the first argument. It can be done using unit types as follows: Suppose we want to define function inc
of type int -> int
, which increments an integer, and use it as a modifying method. Then we should define inc
as a function of type int -> (int, ())
:
(int, ()) inc(int x) {
return (x + 1, ());
}
When defined like that, it can be used as a modifying method. The following will increment x
.
x~inc();
.
and ~
in function names
Suppose we want to use inc
as a non-modifying method too. We can write something like that:
(int y, _) = inc(x);
But it is possible to override the definition of inc
as a modifying method.
int inc(int x) {
return x + 1;
}
(int, ()) ~inc(int x) {
return (x + 1, ());
}
And then call it like that:
x~inc();
int y = inc(x);
int z = x.inc();
The first call will modify x; the second and third won't.
In summary, when a function with the name foo
is called as a non-modifying or modifying method (i.e. with .foo
or ~foo
syntax), the FunC compiler uses the definition of .foo
or ~foo
correspondingly if such a definition is presented, and if not, it uses the definition of foo
.
Specifiers
There are three types of specifiers: impure
, inline
/inline_ref
, and method_id
. One, several, or none of them can be put in a function declaration but currently they must be presented in the right order. For example, it is not allowed to put impure
after inline
.
Impure specifier
impure
specifier means that the function can have some side effects which can't be ignored. For example, we should put impure
specifier if the function can modify contract storage, send messages, or throw an exception when some data is invalid and the function is intended to validate this data.
If impure
is not specified and the result of the function call is not used, then the FunC compiler may and will delete this function call.
For example, in the stdlib.fc function
int random() impure asm "RANDU256";
is defined. impure
is used because RANDU256
changes the internal state of the random number generator.
Inline specifier
If a function has inline
specifier, its code is actually substituted in every place where the function is called. It goes without saying that recursive calls to inlined functions are not possible.
For example,
(int) add(int a, int b) inline {
return a + b;
}
as the add
function is marked with the inline
specifier. The compiler will try to replace calls to add
with the actual code a + b
, avoiding the function call overhead.
Here is another example of how you can use inline, taken from ICO-Minter.fc:
() save_data(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) impure inline {
set_data(begin_cell()
.store_coins(total_supply)
.store_slice(admin_address)
.store_ref(content)
.store_ref(jetton_wallet_code)
.end_cell()
);
}