TON DocsTON Docs
OnboardingNodesApplicationsAPIsContractsSmart contractsTolkTolk languageTVMTON Virtual MachineFoundationsBlockchain foundations

Compiler optimizations

The Tolk compiler generates bitcode from clear, idiomatic code. Extracting variables or simple methods should not increase gas consumption.

Constant folding

Tolk compiler evaluates constant variables and conditions at compile-time:

fun calcSecondsInAYear() {
    val days = 365;
    val minutes = 60 * 24 * days;
    return minutes * 60;
}

All these computations are done statically, resulting in:

31536000 PUSHINT

It works for conditions as well.

  • If an if condition is statically known to be false, only the else body remains.
  • If an assert is statically proven to fail, the corresponding throw remains.
fun demo(s: slice) {
    var flags = s.loadUint(32);   // definitely >= 0
    if (flags < 0) {              // always false
        // ...
    }
    return s.remainingBitsCount();
}

The compiler removes the entire IF construct — both the condition evaluation and its bodies — when the branch is provably unreachable.

During compile-time evaluation, arithmetic operations are emulated as they would be at runtime. The compiler also tracks flags such as "this value is even or non-positive", which allows it to remove unreachable code.

This applies not only to plain variables but also to struct fields, tensor items, and across inlining. It runs after the high-level syntax tree is transformed to a low-level intermediate representation.

Merging constant builder.storeInt

When building cells manually, there is no need to group the constant storeUint into a single number.

// no need for manual grouping anymore
b.storeUint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1);

builder.storeInt are merged automatically:

b.storeUint(0, 1)  // prefix
 .storeUint(1, 1)  // ihr_disabled
 .storeUint(1, 1)  // bounce
 .storeUint(0, 1)  // bounced
 .storeUint(0, 2)  // addr_none

Compiles to:

b{011000} STSLICECONST

It works together with constant folding — with variables and conditions — when they turn out to be constant:

fun demo() {
    var x = 0;
    var b = beginCell();
    b.storeUint(x, 4);
    x += 12;
    if (x > 0) {
        x += x;
    }
    b.storeUint(x + 2, 8);
    return b;
}

Compiles to:

NEWC
x{01a} STSLICECONST

The same applies to structures and their fields:

struct Point {
    x: uint32
    y: uint32
}

fun demo() {
    var p: Point = { x: 10, y: 20 };
    return p.toCell();
}

Compiles to:

NEWC
x{0000000a00000014} STSLICECONST
ENDC

For unions, createMessage is lightweight. The compiler generates all IF-ELSE and STU, but during compile-time analysis, these instructions resolve to constants because all types are known at compile time. The resulting code flattens into PUSHINT and STSLICECONST.

Auto-inline functions

Tolk inlines functions at the compiler level:

fun Point.create(x: int, y: int): Point {
    return {x, y}
}

fun Point.getX(self) {
    return self.x
}

fun sum(a: int, b: int) {
    return a + b
}

fun main() {
    var p = Point.create(10, 20);
    return sum(p.getX(), p.y);
}

Compiles to:

main PROC:<{
    30 PUSHINT
}>

The compiler automatically determines which functions to inline and also provides manual control.

How does auto-inline work?

  • Simple, small functions are always inlined.
  • Functions called only once are always inlined.

For every function, the compiler calculates a "weight" and the number of usages:

  • if weight < THRESHOLD, the function is always inlined.
  • if usages == 1, the function is always inlined.
  • otherwise, an empirical formula determines inlining.

Utility methods can be created without affecting gas consumption, they are zero-cost.

How to control inlining manually?

  • @inline forces inlining. If inlining is impossible, compilation fails.
  • @noinline prevents inlining.
  • @inline_ref preserves an inline reference, suitable for rarely executed paths.

What cannot be auto-inlined?

A function is NOT inlined, even if marked with @inline, if:

  • it participates in a recursive call chain, such as f -> g -> f;
  • it is used as a value rather than called, for example val callback = f;
  • a return sits where the compiler cannot route it into the caller, e.g., inside a loop or inside try/catch.

@inline that cannot be applied is a compilation error. The message names the reason, for example "because a function recursively calls itself" or "because of return inside a loop".

@inline
fun factorial(x: int): int {
    if (x <= 1) { return 1 }
    return x * factorial(x - 1);   // error: recursive
}

This function is inlined. The early return has one continuation, the rest of the body:

fun checkPositive(x: int) {
    if (x <= 0) {
        return;
    }
    // ...
}

Peephole and stack optimizations

After the code is analyzed and transformed into IR, the compiler repeatedly replaces some assembler combinations with equivalent, cheaper ones. Examples include:

  • stack permutations: DUP + DUP -> 2DUP, SWAP + OVER -> TUCK;
  • N LDU + NIP -> N PLDU;
  • SWAP + N STU -> N STUR, SWAP + STSLICE -> STSLICER;
  • SWAP + EQUAL -> EQUAL and other symmetric like MUL, OR;
  • 0 EQINT + N THROWIF -> N THROWIFNOT and vice versa;
  • N EQINT + NOT -> N NEQINT and other xxx + NOT.

Other transformations occur semantically in advance when safe:

  • replace a ternary operator to CONDSEL;
  • evaluate arguments of asm functions in the desired stack order;
  • evaluate struct fields of a shuffled object literal to fit stack order.

lazy loading

The lazy keyword loads only the required fields from a cell or slice:

struct Storage {
    // ...
}

get fun publicKey() {
    val st = lazy Storage.load();
    // fields before are skipped; publicKey preloaded
    return st.publicKey
}

The compiler tracks exactly which fields are accessed and unpacks only those fields, skipping the rest.

Manual optimizations

The compiler does substantial work automatically, but the gas usage can be reduced.

To do it, change the evaluation order to minimize stack manipulations. The compiler keeps the order of statements written in source. It may reorder @pure assembler calls when it needs a particular stack order, and it folds constant expressions.

Example:

fun demo() {
    // variable initialization, grouped
    val v1 = someFormula1();
    val v2 = someFormula2();
    val v3 = someFormula3();

    // use them in calls, assertions, etc.
    someUsage(v1);
    anotherUsage(v2);
    assert(v3) throw 123;
}

After the first block, the stack is (v1 v2 v3). Since v1 is used first, the stack must be rearranged with SWAP, ROT, XCPU, etc. Reordering assignments or usages — for example, moving assert(v3) up — pops the topmost element and can remove those shuffles. The compiler preserves the written order, because those calls may throw or modify state.

Another option is using bitwise & and | instead of logical && and ||. Logical operators are short-circuit: the right operand is evaluated only if required. They are implemented using runtime conditional branches. In some cases, evaluating both operands directly uses fewer runtime instructions than a dynamic IF.

The last option is using low-level Fift code for certain independent tasks that cannot be expressed imperatively. This includes using TVM instructions such as NULLROTRIFNOT or IFBITJMP, and overriding the top-level Fift dictionary for method_id routing. These techniques are applicable only in a limited set of scenarios, primarily for specialized exercises rather than for real-world use.

Avoid micro-optimizations. Small manual attempts to reduce gas typically yield minimal gains and can reduce code readability. Use Tolk as intended.

Fift assembler

The Tolk compiler outputs the Fift assembler. Fift generates the bitcode. Projects built on Blueprint use tolk-js, which invokes Tolk and then Fift.

  • For command-line users, the Fift assembler is the compiler output.

  • For Blueprint users, it is an intermediate result that can be accessed in the build directory.

    To view Fift assembler in Blueprint, run npx blueprint build in the project. After compilation, the build/ directory is created, containing a folder build/ContractName/ with a .fif file.

  • For Acton users, it is an intermediate result that can be accessed in the build directory.

On this page