Skip to main content

Mutability in Tolk vs tilda functions in FunC

TLDR
  • no ~ tilda methods
  • cs.loadInt(32) modifies a slice and returns an integer
  • b.storeInt(x, 32) modifies a builder
  • b = b.storeInt() also works since it not only modifies but returns
  • chained methods work identically to JS; they return self
  • everything works exactly as expected, similar to JS
  • no runtime overhead, the same Fift instructions
  • custom methods are created with ease
  • tilda ~ does not exist in Tolk at all

This is a drastic change. If FunC has .methods() and ~methods(), Tolk has only a dot, and the only way to call a method is .method(). The method may or may not mutate an object. Also, there is a behavioral and semantic difference between FunC and the list.

The goal is to have calls identical to JS and other languages:

FunCTolk
int flags = cs~load_uint(32);var flags = cs.loadUint(32);
(cs, int flags) = cs.load_uint(32);var flags = cs.loadUint(32);
(slice cs2, int flags) = cs.load_uint(32);var cs2 = cs;
var flags = cs2.loadUint(32);
slice data = get_data()
             .begin_parse();
int flag = data~load_uint(32);
val flag = getContractData()
           .beginParse()
           .loadUint(32);
dict~udict_set(...);dict.uDictSet(...);
b~store_uint(x, 32);b.storeInt(x, 32);
b = b.store_int(x, 32);b.storeInt(x, 32);

// also works
b = b.storeUint(32);
b = b.store_int(x, 32)
     .store_int(y, 32);
b.storeInt(x, 32)
 .storeInt(y, 32);

// b = ...; also works

Tolk offers a mutability conception to make this available, a generalization of what a ~ tilda means in FunC.

By default, all arguments are copied by value (identical to FunC)

fun someFn(x: int) {
x += 1;
}

var origX = 0;
someFn(origX); // origX remains 0
someFn(10); // ok, just int
origX.someFn(); // still allowed (but not recommended), origX remains 0

The same goes for cells, slices, whatever:

fun readFlags(cs: slice) {
return cs.loadInt(32);
}

var flags = readFlags(msgBody); // msgBody is not modified
// msgBody.loadInt(32) will read the same flags

This means that when you call a function, you are sure that the original data has not been modified.

mutate keyword and mutating functions

But if you add the mutate keyword to a parameter, it will make a passed argument mutable. To avoid unexpected mutations, you must specify mutate when calling it, also:

fun increment(mutate x: int) {
x += 1;
}

// it's correct, simple and straightforward
var origX = 0;
increment(mutate origX); // origX becomes 1

// these are compiler errors
increment(origX); // error, unexpected mutation
increment(10); // error, not lvalue
origX.increment(); // error, not a method, unexpected mutation
val constX = getSome();
increment(mutate constX); // error, it's immutable since `val`

Same for slices and any other types:

fun readFlags(mutate cs: slice) {
return cs.loadInt(32);
}

val flags = readFlags(mutate msgBody);
// msgBody.loadInt(32) will read the next integer

It's a generalization. A function may have several mutate parameters:

fun incrementXY(mutate x: int, mutate y: int, byValue: int) {
x += byValue;
y += byValue;
}

incrementXY(mutate origX, mutate origY, 10); // both += 10

You may ask — is it just passing by reference? It effectively is, but since "ref" is an overloaded term in TON (cells and slices have refs), the keyword mutate was chosen.

self parameter turning a function into a method

When you name the first parameter self, it emphasizes that the function, though still global, is a method and should be called via a dot.

fun assertNotEq(self: int, throwIfEq: int) {
if (self == throwIfEq) {
throw 100;
}
}

someN.assertNotEq(10);
10.assertNotEq(10); // also ok, since self is not mutating
assertNotEq(someN, 10); // still allowed (but not recommended)

self, without mutate, is immutable (unlike all other parameters). Think of it like a read-only method.

fun readFlags(self: slice) {
return self.loadInt(32); // error, modifying immutable variable
}

fun preloadInt32(self: slice) {
return self.preloadInt(32); // ok, it's a read-only method
}

Combining mutate and self, we get mutating methods.

mutate self is a method, called via dot, mutating an object

As follows:

fun readFlags(mutate self: slice) {
return self.loadInt(32);
}

val flags = msgBody.readFlags(); // pretty obvious

fun increment(mutate self: int) {
self += 1;
}

var origX = 10;
origX.increment(); // 11
10.increment(); // error, not lvalue

// even this is possible
fun incrementWithY(mutate self: int, mutate y: int, byValue: int) {
self += byValue;
y += byValue;
}

origX.incrementWithY(mutate origY, 10); // both += 10

If you look into stdlib, you'll notice that many functions are actually mutate self, meaning they are methods of modifying an object. Tuples, dictionaries, and so on. In FunC, they were usually called via tilda.

@pure
fun tuplePush<X>(mutate self: tuple, value: X): void
asm "TPUSH";

t.tuplePush(1);

return self makes a method chainable

It is precisely like return self in Python or return this in JavaScript. That makes methods like storeInt() and others chainable.

fun storeInt32(mutate self: builder, x: int): self {
self.storeInt(x, 32);
return self;

// this would also work as expected (the same Fift code)
// return self.storeInt(x, 32);
}

var b = beginCell().storeInt(1, 32).storeInt32(2).storeInt(3, 32);
b.storeInt32(4); // works without assignment, since mutates b
b = b.storeInt32(5); // and works with assignment, since also returns

Pay attention to the return type, it's self. You should specify it; otherwise, the compilation will fail if left empty. Probably, in the future, it will be correct.

mutate self and asm functions

While it's evident for user-defined functions, one could be interested in how to make an asm function with such behavior. To answer this question, we should look under the hood at how mutation works inside the compiler.

When a function has mutate parameters, it actually implicitly returns them, and they are implicitly assigned to arguments. It's better by example:

// actually returns (int, void)
fun increment(mutate x: int): void { ... }

// actually does: (x', _) = increment(x); x = x'
increment(mutate x);

// actually returns (int, int, (slice, cell))
fun f2(mutate x: int, mutate y: int): (slice, cell) { ... }

// actually does: (x', y', r) = f2(x, y); x = x'; y = y'; someF(r)
someF(f2(mutate x, mutate y));

// when `self`, it's the same
// actually does: (cs', r) = loadInt(cs, 32); cs = cs'; flags = r
flags = cs.loadInt(32);

So, an asm function should place self' onto a stack before its return value:

// "TPUSH" pops (tuple) and pushes (tuple')
// so, self' = tuple', and return an empty tensor
// `void` is a synonym for an empty tensor
fun tuplePush<X>(mutate self: tuple, value: X): void
asm "TPUSH";

// "LDU" pops (slice) and pushes (int, slice')
// with asm(-> 1 0), we make it (slice', int)
// so, self' = slice', and return int
fun loadMessageFlags(mutate self: slice): int
asm(-> 1 0) "4 LDU";

Note, that to return self, you don't have to do anything special, just specify a return type. The compiler will do the rest.

// "STU" pops (int, builder) and pushes (builder')
// with asm(op self), we put arguments to correct order
// so, self' = builder', and return an empty tensor
// but to make it chainable, `self` instead of `void`
fun storeMessageOp(mutate self: builder, op: int): self
asm(op self) "32 STU";

It's doubtful you'll have to do such tricks. Most likely, you'll write wrappers around existing functions:

// just do it like this, without asm; it's the same effective

@inline
fun myLoadMessageFlags(mutate self: slice): int {
return self.loadUint(4);
}

@inline
fun myStoreMessageOp(mutate self: builder, flags: int): self {
return self.storeUint(32, flags);
}

Do I need @inline for simple functions/methods?

For now, better do it, yes. In most examples above, @inline was omitted for clarity. Without @inline, it will be a separate TVM continuation with jumps in/out. With @inline, a function will be generated, but inlined by Fift, like the inline specifier in FunC.

In the future, Tolk will automatically detect simple functions and perform true inlining on the AST level. Such functions won't be even codegenerated to Fift. The compiler would decide, better than a human, whether to inline, to make a ref, etc. But it will take some time for Tolk to become so smart :)

For now, please specify the @inline attribute.

But self is not a method. It's still a function! I feel like I've been cheated

Absolutely. Like FunC, Tolk has only global functions (as of v0.6). There are no classes or structures with methods. There are no methods hash() for slice and hash() for cell. Instead, there are functions sliceHash() and cellHash(), which can be called either like functions or by dot (preferred):

fun f(s: slice, c: cell) {
// not like this
s.hash();
c.hash();
// but like this
s.sliceHash();
c.cellHash();
// since it's the same as
sliceHash(s);
cellHash(s);
}

In the future, Tolk might be able to declare structures with actual methods that are generalized enough to cover built-in types. But it will take a long journey to follow and will be delivered at least after a giant amount of work on the type system, having fully refactored the FunC kernel inside.