- A number literal
- An identifier
- A compile-time builtin
- An operator
- A variable declaration
- A function call
Variable declaration
Local variables must be initialized at the time of declaration. Since variable declarations are expressions, the result of evaluating a declaration like: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).
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
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:
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: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:
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, supposefoo
is a function of type (int, int, int) -> int
. The following two lines are equivalent ways of calling foo
:
(1, 2, 3)
to a variable, and then call foo
:
Function composition
To illustrate how function composition works in FunC, suppose that together with the previousfoo
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
:
bar(42)
and then calls foo
by passing all arguments separated by commas:
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 functionapply
, 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)
.
apply
by passing the increment function:
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.store_uint
(here, begin_cell
creates a new builder and has type () -> builder
):
.
notation allows the first argument of a function to be placed before the function name,
simplifying the code further:
.
notation it is possible to chain many function calls together:
Modifying notation
If a function’s first argument is of typeA
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:
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
:
y
using inc
, the function should be used as follows:
~
notation on inc
would fail:
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:
y
:
.
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:
inc
as if it was the original function with type int -> int
:
inc
of type int -> int
so that we can use inc
in different ways, like:
~inc
alongside the original inc
:
- 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.