Skip to content

The Lazy Slot

Lazy fields feel like ordinary fields, but under the hood each one is replaced by a cleverly-packed u64 — a single word that encodes three different states without a separate flag. This page unpacks it.

What a lazy field lowers to

When the transform sees @lazy owner: Owner, it removes the field and generates:

  • a slot__owner_lz: u64 — the encoded state,
  • a memo__owner_val: Owner | null — where a materialized reference value is cached,
  • a get/set accessor pair named owner, so your code still just writes repo.owner,

plus one shared __src: string per object — an anchor to the original source string (more on that below).

Three states in one u64

The slot is read like a tagged union (a NaN-box, in spirit):

Slot valueMeaning
0absent — the key wasn't in the input; return the field's default
u64.MAX_VALUEmaterialized — the value lives in the memo (__owner_val)
anything elsea slice range, packed as `(start << 32)

The trick that makes this unambiguous: start is a live heap pointer, which on wasm32 is never anywhere near 4 GB, so its high 32 bits can never be all-ones. That frees u64.MAX_VALUE (0xFFFFFFFF_FFFFFFFF) to mean "materialized" with zero chance of colliding with a real range.

Reading: parse once, then cache

The generated getter is the whole lazy mechanism:

ts
get owner(): Owner {
  const lz = this.__owner_lz;
  if (lz != 0 && lz != u64.MAX_VALUE) {
    // first read of a range -> parse the slice once, flip to materialized
    this.__owner_val = JSON.__deserialize<Owner>(<usize>(lz >>> 32), <usize>(<u32>lz));
    this.__owner_lz = u64.MAX_VALUE;
  }
  return this.__owner_val as Owner; // (0 -> default lives here too)
}

So a field you never read is never parsed; a field you read once is parsed once and cached as materialized.

Packed scalars: no memo at all

For a value that fits in 32 bits — i8i32, bool, f32 — there's no need for a separate memo field, because the value itself fits in the slot. These pack directly:

SlotMeaning
0absent
high 32 == 0xFFFFFFFFmaterialized scalar — value sits in the low 32 bits
anything elsea slice range

On first read the range is parsed and the result is re-packed into the slot's low half — so the memo field is elided entirely for these types.

Serializing: zero-copy passthrough

Serialization branches on the same three states, which is why lazy is so fast for forwarding:

  • rangememory.copy the original source bytes straight into the output (never parsed, never re-formatted),
  • materialized → serialize the cached value normally,
  • absent → emit the default (or null).

That passthrough is the heart of the proxy / filter / forward win: an untouched field round-trips as a raw byte copy.

Why __src exists

The slice ranges are absolute pointers into the source string. If that string were collected while the object still held ranges, those pointers would dangle. So each lazy object keeps a __src reference to the source, set at parse time — the GC keeps the bytes alive exactly as long as some object might still need to materialize from them. It's the one ongoing cost of zero-copy, and the reason the lazy-fields guide suggests dropping long-lived objects over huge payloads once you've read what you need.