Storage struct in a wallet:
Storage.load() do? It unpacks a cell, populates all struct fields, checks consistency, and so on.
The magic of lazy Storage.load() is that it does not load the entire cell upfront. Instead, the compiler tracks exactly which fields you access and automatically loads only those, skipping the rest.
lazy keyword, loading is deferred until the data is accessed. The compiler tracks all control flow paths, inserts loading points as needed, groups unused fields to skip, and performs other optimizations as necessary. Best of all, this works with any type and any combination of fields used anywhere in your code — the compiler tracks everything.
Even deeper than you might think
Suppose you have an NFT collection:content field from the storage—and then extract commonKey from it:
contentCell. How do we get commonKey from it? Since content is a cell, you need to load it… lazily:
Cell<T>: these typed cells are commonly used to represent nested references. When you have p: Cell<Point>, you can’t directly access p.x — you need to load the cell first, either with Point.fromCell(p) or, preferably, p.load(). Both can be used with lazy.
Lazy matching
Similarly, when reading a union type such as an incoming message, you uselazy:
lazy applied to unions:
- No union is allocated on the stack upfront; matching and loading are deferred until needed.
matchoperates naturally by inspecting the slice prefix (opcode).- Within each branch, the compiler inserts loading points and skips unused fields — just like it does for structs.
if (op == OP_RESET) commonly used in FunC. From a type system perspective, it aligns perfectly with the TVM execution model, eliminating unnecessary stack operations.
Lazy matching and else
Since lazymatch for a union is done by inspecting the prefix (opcode), you can handle unmatched cases using an else branch.
In FunC contracts, a common pattern was to ignore empty messages:
loadUint.
With lazy match, you no longer need to pay gas upfront for these checks. You can handle all cases in the else branch:
else, unpacking throws error 63 by default, which is controlled by the throwIfOpcodeDoesNotMatch option in fromCell/fromSlice. Adding else allows you to override this behavior.
Note that else in match by type is only allowed with lazy because it matches on prefixes. Without lazy, it’s just a regular union, matched by a union tag (typeid) on a stack.
Partial updating
The magic doesn’t stop at reading. Thelazy keyword also works seamlessly when writing data back.
Imagine you load a storage, use its fields for assertions, modify one field, and save it back:
toCell(), it does not save all fields of the storage since only seqno was modified. Instead, during loading, after loading seqno, it saved an immutable tail and reuses it when writing back:
lazy will fall back to regular loading.
Q: What are the disadvantages of lazy?
In terms of gas usage,lazy fromSlice is always equal to or cheaper than regular fromSlice because, in the worst case—when you access all fields—it loads everything one by one, just like the regular method.
However, there is another difference unrelated to gas consumption:
-
When you do
T.fromSlice(s), it unpacks all fields ofTand then insertss.assertEnd(), which can be turned off using an option. So, if the slice is corrupted or contains extra data,fromSlicewill throw an error. -
The
lazykeyword, of course, selectively picks only the requested fields and handles partially invalid input gracefully. For example, given:
lazy Point and access only p.x, then an input of FF (8 bits) is acceptable even though y is missing. Similarly, FFFF0000, which includes 16 bits of extra data, is also fine, as lazy ignores any data that is not requested.
In most cases, this isn’t an issue. For storage, you have guarantees regarding the data shape, as your contract controls it. For incoming messages, you typically use all fields (otherwise, why include them in the struct?). If there is extra data in the input—who cares? The message can still be deserialized correctly, and I don’t see any problem here.
Perhaps someday, lazy will become the default. For now, it remains a distinct keyword highlighting the lazy-loading capability—a killer feature of Tolk.