Continuation
The primary computational primitive of TVM: a 'continuation' — pointer to a code segment plus execution context. Replaces conventional procedures and functions — every TVM control-flow primitive is built on continuations.
Aliases: tvm continuation
Continuation is the fundamental computational primitive of TVM, replacing the conventional notions of “function”, “procedure”, and “return address”. It’s a structure holding a pointer to a code segment plus execution context (stack, registers).
Why it matters
TVM has no CALL instruction in the Z80/x86 sense — only continuation manipulations. IFJMPREF { ... }, CALLREF, RETALT, and others all operate at the continuation level. This gives you:
- Higher-order functions in FunC/Tact/Tolk for free: a continuation is a first-class value.
- Try/catch through the
c2alternative continuation. - Loops via recursion or
WHILE { c1 } { c2 }combinators.
Why it’s strange coming from EVM
For developers arriving from EVM, this is the most unusual TVM concept. It’s worth reading TVM Whitepaper §4 (“Control flow, continuations, exceptions”) before writing your first non-trivial contract. Without understanding continuations you can’t really read a bridge or DEX’s TASM.
Registers c0-c5: where continuations live
TVM keeps continuations in dedicated control registers:
| Register | Purpose |
|---|---|
| c0 | Return continuation — where RET/RETALT jump after completion |
| c1 | Alternative continuation — used by RETALT, handles exceptions |
| c2 | Exception handler — THROW jumps here |
| c3 | Dictionary of procedure IDs — acts as the contract’s “function table” |
| c4 | Persistent storage cell — written back to state on commit |
| c5 | Actions output — list of outgoing messages |
When a get-method or receive function starts, the runtime pre-populates c0 so that RET correctly terminates execution and returns the result tuple.
Example: try/catch via c2
In FunC, error handling looks like two continuations — the normal path and a handler:
try {
throw_unless(error_code::not_owner, equal_slices(sender, owner));
do_owner_only_thing();
} catch (_, exno) {
send_text_message(sender, "Access denied");
}
The compiler unrolls this into TVM instructions: PUSHCONT { ... } PUSHCONT { ... } TRY — two continuations land on the stack, TRY installs the second one as c2 and executes the first.
Example: get-method ABI built on continuations
c3 is a dictionary keyed by method-ID (CRC16 of the name), with method bodies as continuations. When tonapi calls runGetMethod, TVM does:
PUSHINT <method_id>
PUSH c3
DICTPUSHCONT ; look up in the dictionary, push the continuation
EXECUTE ; call it
To add a custom method to your contract you register an entry in the c3 dictionary at compile time (global_method_id(...) in FunC, get fun in Tact).
Comparison with EVM/Move
| Concept | EVM | Move | TVM |
|---|---|---|---|
| Function call | CALL opcode, stack frame | Call bytecode, frame | PUSHCONT + CALLREF — continuation as value |
| Return | RETURN | Ret | RET → c0 |
| Exceptions | REVERT | abort | THROW → c2 |
| Higher-order | Not supported until Cancun (function-pointer hacks) | Generics + closures | Built-in: continuation == first-class |
An EVM developer is used to “a method at an address in bytecode” — on TVM the relevant chunk of code is passed in as a parameter.
Where to inspect continuations when debugging
- TON Sandbox (
@ton/sandbox) — step-by-step execution shows c0/c1/c2 after every instruction. - tonscan execution trace — surfaces the final c4 value (storage delta) and c5 (out_msgs).
- func-js compiler —debug-info — annotates which
PUSHCONTcorresponds to which source function.
Related
- TVM — overall virtual-machine architecture.
- Get-method — practical use of the c3 dictionary.
- Opcode — list of instructions that manipulate continuations.