Skip to main content
FunC is the first language for writing smart contracts in TON. For years, it was the only option. Lots of production code was written in FunC, and it will always be alive on-chain and in developers’ hearts. Tolk replaces FunC with modern syntax, a robust type system, and built-in serialization — while generating efficient assembly code. Released in 2025, now it is considered the primary language for the TON ecosystem.

How to migrate from FunC to Tolk

  1. Scan the list below to get the overall picture.
  2. Explore the tolk-bench repo as a source of reference contracts.
  3. Use the FunC-to-Tolk converter to migrate existing projects.

Gas benchmarks

The tolk-bench repository compares FunC and Tolk on several TEPs. For every metric measured, gas consumption reduced 30–50%. Primarily it’s a result of the language design.

What Tolk and FunC have in common

Both languages target into Fift assembler. Tolk is not “a wrapper” that transpiles to FunC — it has its own semantic and optimization kernel. Both languages work on TVM after being compiled to bytecode. TVM is a stack machine, imposing architectural and runtime restrictions. Both languages have IDE plugins, although support for Tolk is way better. JetBrains IDEs, VS Code, and LSP-based editors: Cursor, Windsurf, etc. Both languages are available in blueprint and other client-side tooling. Command-line mode is also supported. But all language aspects are completely different — a huge list below.

List of “Tolk vs FunC” differences

Tolk and FunC are completely different. It’s even inaccurate to compare them — the difference lies in the design, not in syntax. Nevertheless, let’s try to summarize the details.

Tolk reminds TypeScript and Rust

  • FunC: resembles C and Lisp (“FunC” stands for “functional C”)
  • Tolk: resembles TypeScript, Rust, and Kotlin
fun sum(a: int, b: int): int {
    return a + b;
}
See: basic syntax.

Tolk has structures

  • FunC: return long unnamed tensors such as (int, slice, int, int)
  • Tolk: declare a struct, it’s the same efficient
struct Demo {
    previousValue: int256
    ownerAddress: address
    effectsCount: uint32
    totalAmount: coins
}
See: structures.

Automatic serialization

  • FunC: manual bit-level work with builders and slices
  • Tolk: declare a struct and call fromCell and toCell
struct Point {
    x: int8
    y: int8
}

fun demo() {
    var value: Point = { x: 10, y: 20 };

    // makes a cell containing "0A14" (hex)
    var c = value.toCell();
    // back to { x: 10, y: 20 }
    var p = Point.fromCell(c);
}
Pay attention to the use of int8, uint64, coins — all of them are TVM integers (see numbers). See: automatic serialization.

Lazy loading

  • FunC: for optimization, manual juggling with preloads and skips
  • Tolk: the lazy keyword loads only requested fields skipping the rest
get fun publicKey() {
    val st = lazy Storage.load();
    // <-- here "skip 65 bits, preload uint256" is inserted
    return st.publicKey
}
See: lazy loading.

The bool type

  • FunC: only integers, ‘true’ is -1, ‘false’ is 0; ifnot
  • Tolk: type bool and logical operators && || ! are supported
if (trustInput || validate(input)) {
    // ...
}
See: booleans.

The address type

  • FunC: only slices (binary data); parse and compare bits
  • Tolk: type address with convenient methods and operator ==
if (in.senderAddress == storage.ownerAddress) {
    val workchain = storage.ownerAddress.getWorkchain();
    // ...
}
See: address.

Null safety

  • FunC: any variable can hold null, which may lead to runtime errors
  • Tolk: provides nullable types T?, null safety, and smart casts
fun checkWithOptional(a: int, b: int?): bool {
    if (b == null) {
        return checkSingle(a);
    }
    return b >= 0 && checkDouble(a, b);
}
See: nullability.

Everything else in the type system

  • FunC: several types, the Hindley-Milner type system
  • Tolk: a wide range of types, including unions, generics, and enums
struct Container<T> {
    element: T?
}

struct Nothing

type Wrapper<T> = Nothing | Container<T>
See: type system overview.

Methods for any types

  • FunC: global-scope functions only
  • Tolk: both functions and methods — for structures and even primitives
// no `self` — static method
fun Point.createZero(): Point {
    return { x: 0, y: 0 }
}

// has `self` — instance method
fun Point.sumCoords(self) {
    return self.x + self.y
}

// even for primitives: cells, integers, tuples, etc.
fun tuple.isEmpty(self) {
    return self.size() == 0
}
See: functions and methods.

No impure keyword

  • FunC: once impure is forgotten, a call may be dropped
  • Tolk: the compiler does not remove user function calls
fun validate(input: SomeStruct) {
    // ...
}

No ~tilde methods

  • FunC: x~f() and x.f() are different (mutating and not)
  • Tolk: only the dot — a single, consistent way to call methods
val delta = someSlice.loadUint(32);   // mutates someSlice
val owner = someSlice.loadAddress();
See: mutability.

Native maps over TVM dictionaries

  • FunC: m~idict_set_builder(1,32,begin_cell().store_uint(10,32))
  • Tolk: m.set(1, 10)
var m: map<int8, int32> = createEmptyMap();
m.set(1, 10);
m.addIfNotExists(2, -20);
m.delete(2);   // now: [ 1 => 10 ]
See: maps.

Modern message handling

  • FunC: () recv_internal(4 params) and parse a message cell
  • Tolk: fun onInternalMessage(in) and use in.senderAddress, etc.
fun onInternalMessage(in: InMessage) {
    // internal non-bounced messages arrive here
    in.senderAddress;
    in.originalForwardFee;
    // and other fields
}

fun onBouncedMessage(in: InMessageBounced) {
    // bounced messages arrive here
}
See: message handling.

No if (op == OP_TRANSFER) for opcodes

  • FunC: if-else to route an incoming message based on opcode
  • Tolk: use union types and pattern matching
type MyMessage =
    | CounterIncBy
    | CounterReset
    // ...

fun onInternalMessage(in: InMessage) {
    val msg = lazy MyMessage.fromSlice(in.body);
    match (msg) {
        CounterIncBy => {
            // ...
        }
        CounterReset => {
            // ...
        }
        // ...
    }
}
See: pattern matching.

No “ignore empty messages” pattern

  • FunC: recv_internal() starts with if (slice_empty?(...))
  • Tolk: just use else in match
fun onInternalMessage(in: InMessage) {
    val msg = lazy MyMessage.fromSlice(in.body);
    match (msg) {
        CounterReset => { /* ... */ }
        // ... handle all variants of the union

        else => {
            // for example: ignore empty messages
            if (in.body.isEmpty()) {
                return
            }
            throw 0xFFFF
        }
    }
}
See: lazy matching.

Native message composition

  • FunC: store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) etc.
  • Tolk: createMessage that auto-detects body ref or not
val reply = createMessage({
    bounce: BounceMode.NoBounce,
    value: ton("0.05"),
    dest: senderAddress,
    body: RequestedInfo { ... }
});
reply.send(SEND_MODE_REGULAR);
See: message sending.

Native deployment and StateInit

  • FunC: manually pack contract’s code and data according to TL-B
  • Tolk: createMessage auto-computes destination
val deployMsg = createMessage({
    // address auto-calculated, code+data auto-attached
    dest: {
        stateInit: {
            code: contractCodeCell,
            data: emptyStorage.toCell(),
        },
        // optionally control workchains and sharding
    }
});
See: message sending.

op::increase is not a valid identifier

  • FunC: allows any symbols in identifiers, even var 2+2 = ... is ok
  • Tolk: alphanumeric identifiers, 2+2 is 4, as expected
const OP_INCREASE = 0x12345678
See: variables.

Small functions are inlined automatically

  • FunC: prefer larger functions for reduced gas consumption
  • Tolk: the compiler auto-inlines functions with zero overhead
fun int.zero() {
    return 0
}

fun int.inc(mutate self, byValue: int = 1): self {
    self += byValue;
    return self;
}

fun main() {
    return int.zero().inc().inc()
}
is reduced to “return 2” in assembler:
main() PROC:<{
    2 PUSHINT
}>
Note: inline modifier in FunC works at the Fift level, it’s sub-optimal due to extra stack permutations. In Tolk, inlining works at the compiler level and is combined with constant folding. See: compiler optimizations.

Consecutive builder.storeUint are merged

  • FunC: manually combine constant stores into b.storeUint(0x18,6)
  • Tolk: merges b.storeUint(...).storeUint(...) if constant
b.storeUint(0, 1)
 .storeUint(1, 1)
 .storeUint(1, 1)
 .storeUint(0, 1)
 .storeUint(0, 2)
is translated to just
b{011000} STSLICECONST
See: compiler optimizations.

Standard library redesigned

Functions from stdlib.fc now use longer, descriptive naming:
FunCTolk
cur_lt()blockchain.logicalTime()
car(l)listGetHead(l)
raw_reserve(coins)reserveToncoinsOnBalance(coins)
~dump(x)debug.print(x)
Many global-scope functions became methods for primitives:
FunCTolk
s.slice_hash()s.hash()
equal_slices_bits(a, b)a.bitsEqual(b)
t.tuple_len()t.size()
t~tpush(triple(x, y, z))t.push([x, y, z])
String postfixes like "..."c became built-in functions:
FunCTolk
"..."cstringCrc32("...")
"..."HstringSha256("...")
"..."hstringSha256_32("...")
"..."aaddress("...")
"..."sstringHexToSlice("...")
"..."ustringToBase256("...")
See: differences in a standard library.

… and of course — assembler functions

Regardless of being a high-level language, Tolk provides all low-level capabilities. The code can still be written in a “FunC-style” with manual builders and slices, exotic TVM instructions can still be used.
@pure
fun incThenNegate(v: int): int
    asm "INC" "NEGATE"
See: assembler functions.