Skip to main content
Tolk uses the lazy keyword – the compiler tracks which fields are accessed and loads only those fields, skipping the rest. In practice, prefer lazy T.fromCell() over T.fromCell().

lazy usage

Consider a Storage struct in a wallet:
Storage.load() unpacks the cell, loads all fields, performs consistency checks, etc. lazy Storage.load() does not load all fields. Unused fields are skipped:
The compiler tracks all control-flow paths, inserts load points when required, and groups unused fields to skip. This works for any type and any combination of fields.

lazy usage of referenced cells

Consider the NFT collection:
To read content and then get commonKey from it:
  1. Skipping address and uint64 is unnecessary. Accessing a reference does not require skipping preceding fields.
  2. To read commonKey from content, load the cell with lazy.
p: Cell<Point> does not allow direct access to p.x. The cell must be loaded first using either Point.fromCell(p) or p.load(). Both work with lazy.

lazy matching

A union type such as an incoming message can be read with lazy:
With lazy applied to unions:
  1. No union is allocated on the stack upfront; matching and loading are deferred until needed.
  2. match operates by inspecting the slice prefix (opcode).
  3. Within each branch, the compiler inserts loading points and skips unused fields, as it does for structs.
lazy matching avoids unnecessary stack operations.

lazy matching and else

match with lazy on a union operates by inspecting the prefix. Any unmatched case falls into the else branch.
Without an explicit else, unpacking throws error 63 by default, which is controlled by the throwIfOpcodeDoesNotMatch option in fromSlice. The else branch allows inserting any custom logic. else in a type-based match is allowed only with lazy because matching uses prefixes. Without lazy, the union is matched normally and an else branch is not allowed.

Partial updating

The lazy keyword also applies when writing data back. Example: Load a storage, use its fields for assertions, update one field, and save it back:
toCell() does not save all fields of the storage since only seqno is modified. Instead, after loading seqno, the compiler saves an immutable tail and reuses it when writing back:
The compiler can also group unmodified fields located in the middle, load them as a slice, and preserve that slice on write-back.

How does lazy skip unused fields?

When several consecutive fields are unused, the compiler tries to group them. This works for fixed-size types such as intN or bitsN:
In Fift assembler, “skip 97 bits” becomes:
Variable-width fields, such as coins, cannot be grouped and cannot be skipped with one instruction – TVM has no instruction for that. The only option is to load the value and ignore it. The same applies to address. Even though it occupies 267 bits, the value should be validated even when unused; otherwise, binary data could be decoded incorrectly. For these types, lazy does only “load and ignore”. In practice, intN types are common, so grouping has an effect. The trick “access a ref without skipping any data” also works.

What are the disadvantages of lazy?

In terms of gas consumption, lazy fromSlice is equal to or cheaper than regular fromSlice. When all fields are accessed, it loads them one by one, the same way as the non-lazy version. There is a difference unrelated to gas consumption:
  • If a slice is small or contains extra data, fromSlice throws.
  • lazy picks only the requested fields and handles partially invalid input. For example:
Since only p.x is accessed, an input of FF (8 bits) is acceptable even though y is missing. Similarly, FFFF0000 (16 bits of extra data) is also acceptable, as lazy ignores any data that is not requested.