T
TOLK lang
@tolk_lang
7 453
// before: 50000000 nanotons
val amount = ton("0.05");
// now: 50000000 nanograms
val amount = grams("0.05");
array<T> — dynamically sized arrays backed by TVM tuples.unknown — a TVM primitive with unknown contents.lisp_list<T> — nested two-element tuples (FunC-style).string — text chunks backed by snaked cells, with StringBuilder for concatenation."str".crc32(), "str".sha256(), etc.?? like in TypeScript.import "@third_party/utils".@stdlib/reflection.array<T> — a dynamically sized container:
// array<int>
var numbers = [1, 2, 3];
// array<Point?>
var optPoints = [
Point { x: 10, y: 20 },
Point { x: 30, y: 40 },
null,
];
push, get(idx), etc.T, including sub-arrays like array<array<int>>tuple exists, but it's no longer built-in. It's just an array... of something unknown:
type tuple = array<unknown>
unknown gives access to the untyped TVM stack, fully integrated into the type system.
// string
val str = "hello";
calculateLength, equalTo, etc.StringBuilder encapsulates cell manipulation:
StringBuilder.create()
.append(content.commonContent)
.append(individualNftContent)
.build()
import statement now accepts @aliases:
import "@common/jettons"
import "@third_party/math-lib"
reflect features:
fun log(msg: string, loc: SourceLocation = reflect.sourceLocation()) {
debug.print(loc.lineNo);
}
fun demo() {
log("a"); // prints K — current line no
log("b"); // prints K+1
}
expect(...) in tests — by carrying source location at compile time.tolk compiler is now thread-safe and re-invokable within a single process. It will be embedded into an external toolchain written in Rust, communicating via FFI.address is now "internal only"address meant internal/external/noneaddress — internal onlyaddress? (nullable) — internal/none, exactly like "maybe address" in @ton/coreany_address — internal/external/none
struct Storage {
// internal, checked automatically
owner: address
}
isInternal() checks.
createMessage({
bounce: BounceMode.RichBounce,
...
})
onBouncedMessage, you get access to the original body, exit code, gas used, and more.BTOS (builder-to-slice) — without intermediate cell creation.b.endCell().beginParse() is now cheap: auto-optimized to BTOSBTOS; hacks around "return a builder with a valid address" can be removedStateInit hashing and address calculations
fun customRead(reader: (slice) -> int) { ... }
customRead(fun(s) {
return s.loadUint(32)
})
map<K, V> — a convenient zero-overhead wrapper over TVM dictionariesenum — group numeric constants into a distinct typeprivate and readonly fields in structuresmap<K, V> now fully replaces them.
var m: map<int8, int32> = createEmptyMap();
m.set(1, 10);
m.addIfNotExists(9, -90);
m.delete(9); // now: [ 1 => 10 ]
m.exists(1); // true
m.isEmpty(); // false
val r = m.get(1);
if (r.isFound) { // true
val v = r.loadValue(); // 10
}
// or if the key 100% exists
val v = m.mustGet(1); // 10
var r = m.findFirst();
while (r.isFound) {
// use r.getKey() and r.loadValue()
r = m.iterateNext(r);
}
map<address, Point>
map<Point, Cell<Extra>>
map<int32, map<int64, bool>>
...
// will be 0 1 2
enum Color {
Red
Green
Blue
}
struct Gradient {
from: Color
to: Color? = null
}
var g: Gradient = { from: Color.Blue };
g.from == Color.Red; // false
private — accessible only within methodsreadonly — immutable after object creation
struct PosInTuple {
private readonly t: tuple
curIndex: int
}
fun PosInTuple.last(mutate self) {
// `t` is visible only in methods
// and cannot be modified
self.curIndex = self.t.size() - 1;
}
// general implementation
fun Iterator<T>.next(self) { ... }
// a more specific one
fun Iterator<Cell<T>>.next(self) { ... }
lazy keyword.
val st = lazy Storage.load();
// the compiler skips everything and loads only what you access
return st.publicKey;
lazy keyword to rule them all.createMessage – control body, extra currency, stateInit, and morecreateExternalLogMessage — emit logs efficiently
val reply = createMessage({
bounce: false,
value: ton("0.05"),
dest: senderAddress,
body: RequestedInfo { ... }
});
reply.send(SEND_MODE_REGULAR);
stateInit with automatic address computationbody
// value: either tons, or tons + extra currencies
value: ton("1.5")
value: (ton("1.5"), extraDict)
// destination: various forms
dest: someAddress
dest: (workchain, hash)
// any serializable struct — no toCell!
body: RequestedInfo { ... }
dest: {
workchain: BASECHAIN,
stateInit: { code, data },
}
dest: {
...
toShard: {
fixedPrefixLength: 8,
closeTo: ownerAddress,
}
}
createMessage is not bloated. The compiler unfolds all union branches and constant conditions at compile time, producing flat, minimal code — often more optimal than hand-crafted cell composition.address
struct Point {
x: int8;
y: int8;
}
var value: Point = { x: 10, y: 20 };
// makes a cell containing "0A14"
var c = value.toCell();
// back to { x: 10, y: 20 }
var p = Point.fromCell(c);
T.fromCell, T.fromSlice, slice.loadAny<T>, builder.storeAny<T>, and other methods give both high-level and low-level API.
// low-level is allowed:
beginCell()
.storeUint(1, 32) // mix manual
.storeAny(myStruct) // with auto
struct (0x7362d09c) TransferNotification {
queryId: uint64;
...
}
struct (0b001) AssetSimple { ... }
struct (0b100) AssetBooking { ... }
type Asset = AssetSimple | AssetBooking | ...;
struct A {
ref1: cell; // untyped ref
ref2: Cell<Inner>; // typed ref
ref3: Cell<int256>?; // maybe ref
}
a.ref2.field // error
a.ref2.load().field // ok
point.toCell() really gives you Cell<Point>
Point.fromSlice(s, {
assertEndAfterReading: false
})
struct Wallet {
owner: address;
}
if (senderAddress == wallet.owner)
struct Point {
x: int;
y: int;
}
fun calcMaxCoord(p: Point) {
return p.x > p.y ? p.x : p.y;
}
// declared like a JS object
var p: Point = { x: 10, y: 20 };
// called like a JS object
calcMaxCoord({ x: 10, y: 20 });
Point a just named tensor — identical to (int, int) at the TVM level.
fun loadData(): StoredInfo {
return {
counterValue: ...,
ownerAddress: ...,
}
}
{ x, y }
struct Nullable<T> {
item: T? = null;
}
struct Ok<TResult> { result: TResult }
struct Err<TError> { err: TError }
type Response<R, E> = Ok<R> | Err<E>;
match (r) {
Ok => { r.result }
Err => { r.err }
}
fun Point.getX(self) {
return self.x
}
fun Point.create(x: int, y: int): Point {
return { x, y }
}
fun slice.load32(mutate self): int32 {
return self.loadInt(32);
}
// before
someCell.cellHash();
someTuple.tupleSize();
someBuilder.getBuilderBitsCount();
// now
someCell.hash();
someTuple.size();
someBuilder.bitsCount();
// before
getMyAddress();
setContractData(c);
getLogicalTime();
// now
contract.getAddress();
contract.setData(c);
blockchain.logicalTime();
type NewName = <existing type>T1 | T2 | ...is and !is
type UserId = int32;
type MaybeOwnerHash = bytes32?;
fun whatFor(a: bits8 | bits256): slice | UserId { ... }
var result = whatFor(...); // slice | UserId
T? are now formally T | null.
match (result) {
slice => { /* result is slice here */ }
UserId => { /* result is UserId here */ }
}
type Pair2 = (int, int);
type Pair3 = (int, int, int);
fun getLast(tensor: Pair2 | Pair3) {
return match (tensor) {
Pair2 => tensor.1,
Pair3 => tensor.2,
}
}
val nextValue = match (curValue) {
1 => 0,
0 => 1,
else => -1
};
T1 | T2 will be directly mapped to TL-B (Either T1 T2). (Either SmallPayload LargePayload) becomes
struct StoragePart {
data: SmallPayload | LargePayload;
// (de)serialized as '0' + ... or '1' + ...
}
match (s.data) {
SmallPayload => ...
LargePayload => ...
}
T1 | T2 | ... is a typed way to describe multiple constructors from TL/B. Generally, they can be used anywhere inside a storage or a message.
// don't mind about opcodes yet
struct CounterIncBy { byValue: int32 }
struct CounterReset {}
struct ... other messages
type IncomingMessage = CounterIncBy | CounterReset | ...;
// ... after parsing a message
match (msg) {
CounterIncBy => {
newCounter = curCounter + msg.byValue
}
CounterReset => {
newCounter = 0
}
...
}
int32, uint64, etc.coins and function ton("0.05")bytesN and bitsN (backed by slices at TVM)"..."c postfixes with stringCrc32("...") functions
struct CounterIncrement {
counter_id: int;
inc_by: int;
}
counterIncrement
counter_id:int32
inc_by:int64
= CounterIncrement;
// type casting?
counter_id: int as int32;
inc_by: int as int64;
// inline annotations?
counter_id: int @int32;
inc_by: int @int64;
// annotations above fields?
@serialize(int32)
counter_id: int;
@serialize(int64)
inc_by: int;
Maybe int32? Would we write:
// this?
inc_by: (int as int32)?;
// or this?
inc_by: int? as int32?;
// or this?
inc_by: Maybe<int> as Maybe<int32>;
Both (Maybe int32) int64?
// this?
my_data: Both<Maybe<int as int32>, int as int64>;
// or this?
my_data: Both<Maybe<int>, int> as Both<Maybe<int32>, int64>;
// or how??
struct CounterIncrement {
counter_id: int32;
inc_by: int64;
}
as syntax. No ambiguity.
struct MyMsg {
inc_by: int32?;
my_data: (int32?, int64);
}
int32 and similar:
var op: int32 = ...;
var query_id: uint64 = ...;
var v: uint8 = 255;
v += 1; // ???
struct Resp {
outValue: uint8;
}
resp.outValue = v; // 256
resp.toCell(); // a runtime "overflow" error
intN (fixed integers), bytesN (definite slices), coins (variadic integers), and some more additions. Read the details in the PR.Either L R and even more complex TL/B structures be expressed? int?, cell?, and T?, bringing null safety to your code. The compiler prevents using nullable values without checks, but thanks to smart casts, this feels smooth and natural.int?, cell?, etc.; null safety! (non-null assertion)throw is treated unreachablenever typenull was implicitly assignable to any primitive type — too permissive. A variable declared as int could still hold null at runtime, leading to TVM exceptions if used incorrectly.T | null and Kotlin, preventing unintended null usage.
value = x > 0 ? 1 : null; // int?
value + 5; // error
s.storeInt(value); // error
if (value != null) {
value + 5; // ok
s.storeInt(value); // ok
}
cell?, [int, slice]?, (int, cell)?int? and cell? occupy just one stack slot — zero overhead
if (lastCell != null) {
// here lastCell is `cell`, not `cell?`
}
if (lastCell == null || prevCell == null) {
return;
}
// both lastCell and prevCell are `cell`
var x: int? = ...;
if (x == null) {
x = random();
}
// here x is `int`
! operator to bypass nullability checks:
// this key 100% exists, make it `cell`, not `cell?`
validators = getBlockchainConfigParam(16)!;
fun alwaysThrows(): never {
throw 123;
}
fun f() {
...
alwaysThrows(); // no return needed after this
}
never also occurs implicitly when a condition is impossible:
var v = 0;
// compiler warning: `int` can never be `null`
if (v == null) {
// v is `never`
}
tensorVar.0 and tupleVar.0, both for reading and writingcell, slice, etc. to be valid identifiers
var t = (5, someSlice, someBuilder); // 3 stack slots
t.0 // 5
t.0 = 10; // t is now (10, ...)
t.0 += 1; // t is now (11, ...)
increment(mutate t.0); // t is now (12, ...)
t.0.increment(); // t is now (13, ...)
t.1 // slice
t.100500 // compilation error
var t = [5, someSlice, someBuilder]; // 1 tuple on a stack with 3 items
t.0 // "0 INDEX", reads 5
t.0 = 10; // "0 SETINDEX", t is now [10, ...]
t.0 += 1; // "0 INDEX" to read 10, "0 SETINDEX" to write 11
increment(mutate t.0); // also, the same way
t.0.increment(); // also, the same way
t.1 // "1 INDEX", it's slice
t.100500 // compilation error
var.{i}.{j}. It works for nested tensors, nested tuples, tuples nested into tensors. It works for mutate. It works for globals.
struct User {
id: int;
name: slice;
}
var u: User = { id: 5, name: "" };
// u is actually 2 slots on a stack, the same as
var u: (int, slice) = (5, "");
fun getUser(): User { ... }
// on a stack, the same as
fun getUser(): (int, slice) { ... }
var u: User = ...; // u: (int, slice) = ...
u.id; // u.0
u.id = 10; // u.0 = 10
struct Storage {
lastUpdated: int;
owner: User;
}
s.lastUpdated // s.0
s.owner.id // s.1.0
~ tilda. "Functional" is mostly about the Hindley-Milner type system.
() f(a, b) {
return a + b; // a and b now int, since `+` (int, int)
}
() f(slice s) {}
var s = null;
f(s); // infer s as slice, since f accepts slice
int f(x) {
(a, b) = (0, x);
return a + b; // x becomes int, since x and b edge
}
int (not nullable) and int? (nullable), so that we can assign null only to int?. What would Hindley-Milner think about this?
var x = 0; // unify(Hole, Int) = Int
...
x = null; // unify(Int, Nullable<Hole>) = Nullable<Int>
x: int?. Not as we wanted to, right? (while it can be "fixed", it would step away from HM's nature)
error: function return type (int, int) cannot be unified with implicit end-of-block return type (int, ()): cannot unify type () with int
1) can not assign `(int, slice)` to variable of type `(int, int)`
2) can not call method for `builder` with object of type `int`
3) missing `return`
fun f<T>(...) and instantiations like f<int>(...)bool typevalue as T