Tolk vs FunC: in detail
A very huge list below. Will anyone have enough patience to read it up to the end?..
Here: Tolk vs FunC: in short
✅ Traditional comments :)
FunC | Tolk |
---|---|
;; comment | // comment |
{- multiline comment -} | /* multiline comment */ |
✅ 2+2
is 4, not an identifier. Identifiers can only be alpha-numeric
In FunC, almost any character can be a part of identifier.
For example, 2+2
(without a space) is an identifier.
You can even declare a variable with such a name.
In Tolk, spaces are not mandatory. 2+2
is 4, as expected. 3+~x
is 3 + (~ x)
, and so on.
FunC | Tolk |
---|---|
return 2+2; ;; undefined function `2+2` | return 2+2; // 4 |
More precisely, an identifier can start from [a-zA-Z$_]
and be continued with [a-zA-Z0-9$_]
. Note, that ?
, :
, and others are not valid symbols, found?
and op::increase
are not valid identifiers.
You can use backticks to surround an identifier, and then it can contain any symbols (similar to Kotlin and some other langs). Its potential usage is to allow keywords be used as identifiers, in case of code generation by a scheme, for example.
FunC | Tolk |
---|---|
const op::increase = 0x1234; | const OP_INCREASE = 0x1234; |
;; even 2%&!2 is valid | // don't do like this :) |
✅ Impure by default, compiler won't drop user function calls
FunC has an impure
function specifier. When absent, a function is treated as pure. If its result is unused, its call was deleted by the compiler.
Though this behavior is documented, it is very unexpected to newcomers. For instance, various functions that don't return anything (throw an exception on mismatch, for example), are silently deleted. This situation is spoilt by the fact that FunC doesn't check and validate function body, allowing impure operations inside pure functions.
In Tolk, all functions are impure by default. You can mark a function pure with annotation, and then impure operations are forbidden in its body (exceptions, globals modification, calling non-pure functions, etc.).
✅ New functions syntax: fun
keyword, @
attributes, types on the right (like in TypeScript, Kotlin, Python, etc.)
FunC | Tolk |
---|---|
cell parse_data(slice cs) { } | fun parse_data(cs: slice): cell { } |
(cell, int) load_storage() { } | fun load_storage(): (cell, int) { } |
() main() { ... } | fun main() { ... } |
Types of variables — also to the right:
FunC | Tolk |
---|---|
slice cs = ...; | var cs: slice = ...; |
(cell c, int n) = parse_data(cs); | var (c: cell, n: int) = parse_data(cs); |
global int stake_at; | global stake_at: int; |
Modifiers inline
and others — with annotations:
FunC | Tolk |
---|---|
| @inline |
| @inline_ref |
global int stake_at; | global stake_at: int; |
forall
— this way:
FunC | Tolk |
---|---|
forall X -> tuple cons(X head, tuple tail) | fun cons<X>(head: X, tail: tuple): tuple |
asm
implementation — like in FunC, but being properly aligned, it looks nicer:
@pure
fun third<X>(t: tuple): X
asm "THIRD";
@pure
fun iDictDeleteGet(dict: cell, keyLen: int, index: int): (cell, slice, int)
asm(index dict keyLen) "DICTIDELGET NULLSWAPIFNOT";
@pure
fun mulDivFloor(x: int, y: int, z: int): int
builtin;
There is also a @deprecated
attribute, not affecting compilation, but for a human and IDE.
✅ get
instead of method_id
In FunC, method_id
(without arguments) actually declared a get method. In Tolk, you use a straightforward syntax:
FunC | Tolk |
---|---|
int seqno() method_id { ... } | get seqno(): int { ... } |
Both get methodName()
and get fun methodName()
are acceptable.
For method_id(xxx)
(uncommon in practice, but valid), there is an attribute:
FunC | Tolk |
---|---|
| @method_id(1666) |
✅ It's essential to declare types of parameters (though optional for locals)
// not allowed
fun do_smth(c, n)
// types are mandatory
fun do_smth(c: cell, n: int)
There is an auto
type, so fun f(a: auto)
is valid, though not recommended.
If parameter types are mandatory, return type is not (it's often obvious of verbose). If omitted, it means auto
:
fun x() { ... } // auto infer return
For local variables, types are also optional:
var i = 10; // ok, int
var b = beginCell(); // ok, builder
var (i, b) = (10, beginCell()); // ok, two variables, int and builder
// types can be specified manually, of course:
var b: builder = beginCell();
var (i: int, b: builder) = (10, beginCell());
✅ Variables are not allowed to be redeclared in the same scope
var a = 10;
...
var a = 20; // error, correct is just `a = 20`
if (1) {
var a = 30; // it's okay, it's another scope
}
As a consequence, partial reassignment is not allowed:
var a = 10;
...
var (a, b) = (20, 30); // error, releclaration of a
Note, that it's not a problem for loadUint()
and other methods. In FunC, they returned a modified object, so a pattern var (cs, int value) = cs.load_int(32)
was quite common. In Tolk, such methods mutate an object: var value = cs.loadInt(32)
, so redeclaration is unlikely to be needed.
fun send(msg: cell) {
var msg = ...; // error, redeclaration of msg
// solution 1: intruduce a new variable
var msgWrapped = ...;
// solution 2: use `redef`, though not recommended
var msg redef = ...;
✅ Changes in the type system
Type system in the first Tolk release is the same as in FunC, with the following modifications:
void
is effectively an empty tensor (more canonical to be namedunit
, butvoid
is more reliable); btw,return
(without expression) is actuallyreturn ()
, a convenient way to return from void functions
fun setContractData(c: cell): void
asm "c4 POP";
auto
mean "auto infer"; in FunC,_
was used for that purpose; note, that if a function doesn't specify return type, it'sauto
, notvoid
self
, to make chainable methods, described below; actually it's not a type, it can only occur instead of return type of a functioncont
renamed tocontinuation
✅ Another naming for recv_internal / recv_external
fun onInternalMessage
fun onExternalMessage
fun onTickTock
fun onSplitPrepare
fun onSplitInstall
All parameter types and their order rename the same, only naming is changed. fun main
is also available.
✅ #include → import. Strict imports
FunC | Tolk |
---|---|
#include "another.fc"; | import "another.tolk" |
In Tolk, you can not used a symbol from a.tolk
without importing this file. In other words, "import what you use".
All stdlib functions are available out of the box, downloading stdlib and #include "stdlib.fc"
is not needed. See below about embedded stdlib.
There is still a global scope of naming. If f
is declared in two different files, it's an error. We "import" a whole file, no per-file visibility and export
keyword is now supported, but probably will be in the future.
✅ #pragma → compiler options
In FunC, "experimental" features like allow-post-modifications
were turned on by a pragma in .fc files (leading to problems when some files contain it, some don't). Indeed, it's not a pragma for a file, it's a compilation option.
In Tolk, all pragmas were removed. allow-post-modification
and compute-asm-ltr
were merged into Tolk sources (as if they were always on in FunC). Instead of pragmas, there is now an ability to pass experimental options.
As for now, there is one experimental option introduced — remove-unused-functions
, which doesn't include unused symbols to Fift output.
#pragma version xxx
was replaced by tolk xxx
(no >=, just a strict version). It's good practice to annotate compiler version you are using. If it doesn't match, Tolk will show a warning.
tolk 0.6
✅ Late symbols resolving. AST representation
In FunC (like in С) you can not access a function declared below:
int b() { a(); } ;; error
int a() { ... } ;; since it's declared below
To avoid an error, a programmer should create a forward declaration at first. The reason is that symbols resolving is performed right at the time of parsing.
Tolk compiler separates these two steps. At first it does parsing, and then it does symbol resolving. Hence, a snippet above would not be erroneous.
Sounds simple, but internally, it's a very huge job. To make this available, I've introduced an intermediate AST representation, completely missed in FunC. That's an essential point of future modifications and performing semantic code analisys.
✅ null
keyword
Creating null values and checking variables on null looks very pretty now.
FunC | Tolk |
---|---|
a = null() | a = null |
if (null?(a)) | if (a == null) |
if (~ null?(b)) | if (b != null) |
if (~ cell_null?(c)) | if (c != null) |
Note, that it does NOT mean that Tolk language has nullability. No, you can still assign null
to an integer variable — like in FunC, just syntactically pleasant. A true nullability will be available someday, after hard work on the type system.
✅ throw
and assert
keywords
Tolk greatly simplifies working with exceptions.
If FunC has throw()
, throw_if()
, throw_arg_if()
, and the same for unless, Tolk has only two primitives: throw
and assert
.
FunC | Tolk |
---|---|
throw(excNo) | throw excNo |
throw_arg(arg, excNo) | throw (excNo, arg) |
throw_unless(excNo, condition) | assert(condition, excNo) |
throw_if(excNo, condition) | assert(!condition, excNo) |
Note, that !condition
is possible since logical NOT is available, see below.
There is a long (verbose) syntax of assert(condition, excNo)
:
assert(condition) throw excNo;
// with possibility to include arg to throw
Also, Tolk swaps catch
arguments: it's catch (excNo, arg)
, both optional (since arg is most likely empty).
FunC | Tolk |
---|---|
try { } catch (_, _) { } | try { } catch { } |
try { } catch (_, excNo) { } | try { } catch(excNo) { } |
try { } catch (arg, excNo) { } | try { } catch(excNo, arg) { } |
✅ do ... until
→ do ... while
FunC | Tolk |
---|---|
do { ... } until (~ condition); | do { ... } while (condition); |
do { ... } until (condition); | do { ... } while (!condition); |
Note, that !condition
is possible since logical NOT is available, see below.
✅ Operator precedence became identical to C++ / JavaScript
In FunC, such code if (slices_equal() & status == 1)
is parsed as if( (slices_equal()&status) == 1 )
. This is a reason of various errors in real-world contracts.
In Tolk, &
has lower priority, identical to C++ and JavaScript.
Moreover, Tolk fires errors on potentially wrong operators usage to completely eliminate such errors:
if (flags & 0xFF != 0)
will lead to a compilation error (similar to gcc/clang):
& has lower precedence than ==, probably this code won't work as you expected. Use parenthesis: either (... & ...) to evaluate it first, or (... == ...) to suppress this error.
Hence, the code should be rewritten:
// either to evaluate it first (our case)
if ((flags & 0xFF) != 0)
// or to emphasize the behavior (not our case here)
if (flags & (0xFF != 0))
I've also added a diagnostic for a common mistake in bitshift operators: a << 8 + 1
is equivalent to a << 9
, probably unexpected.
int result = a << 8 + low_mask;
error: << has lower precedence than +, probably this code won't work as you expected. Use parenthesis: either (... << ...) to evaluate it first, or (... + ...) to suppress this error.
Operators ~% ^% /% ~/= ^/= ~%= ^%= ~>>= ^>>=
no longer exist.
✅ Immutable variables, declared via val
Like in Kotlin: var
for mutable, val
for immutable, optionally followed by a type. FunC has no analogue of val
.
val flags = msgBody.loadMessageFlags();
flags &= 1; // error, modifying an immutable variable
val cs: slice = c.beginParse();
cs.loadInt(32); // error, since loadInt() mutates an object
cs.preloadInt(32); // ok, it's a read-only method
Parameters of a function are mutable, but since they are copied by value, called arguments aren't changed. Exactly like in FunC, just to clarify.
fun some(x: int) {
x += 1;
}
val origX = 0;
some(origX); // origX remains 0
fun processOpIncrease(msgBody: slice) {
val flags = msgBody.loadInt(32);
...
}
processOpIncrease(msgBody); // by value, not modified
In Tolk, a function can declare mutate
parameters. It's a generalization of FunC ~
tilda functions, read below.
✅ Deprecated command-line options removed
Command-line flags -A
, -P
, and others, were removed. Default behavior
/path/to/tolk {inputFile}
is more than enough. Use -v
to print version and exit. Use -h
for all available command-line flags.
Only one input file can be passed, others should be import
'ed.
✅ stdlib functions renamed to verbose clear names, camelCase style
All naming in standard library was reconsidered. Now, functions are named using longer, but clear names.
FunC | Tolk |
---|---|
cur_lt() | getLogicalTime() |
A former "stdlib.fc" was split into multiple files: common.tolk, tvm-dicts.tolk, and others.
Continue here: Tolk vs FunC: standard library.
✅ stdlib is now embedded, not downloaded from GitHub
FunC | Tolk |
---|---|
|
|
In Tolk, stdlib a part of distribution. Standard library is inseparable, since keeping a triple "language, compiler, stdlib" together is the only correct way to maintain release cycle.
It works in such a way. Tolk compiler knows how to locate a standard library. If a user has installed an apt package, stdlib sources were also downloaded and exist on a hard disk, so the compiler locates them by system paths. If a user uses a WASM wrapper, they are provided by tolk-js. And so on.
Standard library is split into multiple files: common.tolk
(most common functions), gas-payments.tolk
(calculating gas fees), tvm-dicts.tolk
, and others. Functions from common.tolk
are available always (a compiler implicitly imports it). Other files are needed to be explicitly imported:
import "@stdlib/tvm-dicts" // ".tolk" optional
...
var dict = createEmptyDict();
dict.iDictSet(...);
Mind the rule "import what you use", it's applied to @stdlib/...
files also (with the only exception of "common.tolk").
JetBrains IDE plugin automatically discovers stdlib folder and inserts necessary imports as you type.
✅ Logical operators && ||
, logical not !
In FunC, there are only bitwise operators ~ & | ^
. Developers making first steps, thinking "okay, no logical, I'll use bitwise in the same manner", often do errors, since operator behavior is completely different:
a & b | a && b |
---|---|
sometimes, identical: | |
0 & X = 0 | 0 & X = 0 |
-1 & X = -1 | -1 & X = -1 |
but generally, not: | |
1 & 2 = 0 | 1 && 2 = -1 (true) |
~ found | !found |
---|---|
sometimes, identical: | |
true (-1) → false (0) | -1 → 0 |
false (0) → true (-1) | 0 → -1 |
but generally, not: | |
1 → -2 | 1 → 0 (false) |
condition & f() | condition && f() |
---|---|
f() is called always | f() is called only if condition |
condition | f() | condition || f() |
---|---|
f() is called always | f() is called only if condition is false |
Tolk supports logical operators. They behave exactly as you get used to (right column). For now, &&
and ||
sometimes produce not optimal Fift code, but in the future, Tolk compiler will become smarter in this case. It's negligible, just use them like in other languages.
FunC | Tolk |
---|---|
if (~ found?) | if (!found) |
if (~ found?) { | if (!found && cs.loadInt(32) == 0) { |
ifnot (cell_null?(signatures)) | if (signatures != null) |
elseifnot (eq_checksum) | else if (!eqChecksum) |
Keywords ifnot
and elseifnot
were removed, since now we have logical not (for optimization, Tolk compiler generates IFNOTJMP
, btw). Keyword elseif
was replaced by traditional else if
.
Note, that it does NOT mean that Tolk language has bool
type. No, comparison operators still return an integer. A bool
type support will be available someday, after hard work on the type system.
Remember, that true
is -1, not 1. Both in FunC and Tolk. It's a TVM representation.
✅ No tilda ~
methods, mutate
keyword instead
This change is so huge that it's described on a separate page: Tolk mutability.
Tolk vs FunC gas consumption
Tolk gas consumption could be a bit higher, because it fixes unexpected arguments shuffling in FunC. It's negligible in practice.
In the future, Tolk compiler will become smart enough to reorder arguments targeting less stack manipulations,
but still avoiding a shuffling problem.
FunC compiler could unexpectedly shuffle arguments when calling an assembly function:
some_asm_function(f1(), f2());
Sometimes, f2()
could be called before f1()
, and it's unexpected.
To fix this behavior, one could specify #pragma compute-asm-ltr
, forcing arguments to be always evaluated in ltr-order.
This was experimental, and therefore turned off by default.
This pragma reorders arguments on a stack, often leading to more stack manipulations than without it. In other words, in fixes unexpected behavior, but increases gas consumption.
Tolk puts arguments onto a stack exactly the same as if this pragma turned on. So, its gas consumption is sometimes higher than in FunC if you didn't use this pragma. Of course, there is no shuffling problem in Tolk.
In the future, Tolk compiler will become smart enough to reorder arguments targeting less stack manipulations, but still avoiding a shuffling problem.