// SPDX-License-Identifier: MIT OR Apache-2.0

Field-cache optimization — user guide

Plan 123 umbrella (V1-V5 active). Last updated 2026-06-02.

What it does

Nova compiler automatically caches @field reads and @<pure_method>() calls in method bodies, eliminating redundant self->X pointer dereferences in the generated .c output. Hot-path methods (ReadBuffer, StringBuilder, HashMap iterators) typically see a 15-30% reduction in pointer derefs under -O0 debug builds.

The optimization is transparent — semantic equivalence guaranteed. You can disable it any time with environment variables (see Escape hatches below).

What gets cached

Four layers operate together:

D217 V1 — direct field cache (Plan 123.1)

For ro fields accessed 2+ times → cached at method-body start:

fn Point @sum_squared() -> int {
    @x * @x + @y * @y      // Before: 4 pointer derefs
}
// After D217 V1:
//   ro _at_x = @x; ro _at_y = @y; _at_x * _at_x + _at_y * _at_y

D218 V2 — LICM loop hoist (Plan 123.2)

For invariant field reads inside loops → hoisted immediately before loop:

fn Buf @sum_n(n int) -> int {
    mut total = 0
    for i in 0..n {
        total = total + @data[i] + @size   // @size invariant
    }
    total
}
// D218 hoists @size immediately before for-loop.

D219 V3 — pure-call cache (Plan 123.3)

For @<pure_method>() calls 2+ times → cached:

#pure
fn Vec3 @magnitude_sq() -> int => @x * @x + @y * @y + @z * @z

fn Vec3 @double() -> int {
    @magnitude_sq() + @magnitude_sq()   // Cached single call.
}

D217 V4 — chain cache (Plan 123.4)

For nested @a.b.c accesses 2+ times → cached:

fn Outer @check() -> int {
    @inner.cfg.limit + @inner.cfg.limit + @inner.cfg.limit
    // D217 V4 caches @inner.cfg.limit once.
}

How to inspect cache decisions

CLI flag --explain-cache on nova check:

nova check src/buffer.nv --explain-cache

Sample output:

=== src/buffer.nv ===
  fn ReadBuffer @try_read_u32_le — 4 cache(s):
    D217 field cache: data, pos
    D219 pure-call cache: len
    D217 V4 chain cache: @header.signature

field-cache total: 1 method(s) affected, 4 cache(s) inserted

Escape hatches

Disable all caching:

NOVA_FIELD_CACHE=0 nova build

Disable individual layers:

NOVA_FIELD_CACHE_LICM=0    # disable D218 LICM
NOVA_FIELD_CACHE_PURE=0    # disable D219 pure-call
NOVA_FIELD_CACHE_CHAIN=0   # disable D217 V4 chain

Tune thresholds:

NOVA_FIELD_CACHE_THRESHOLD=3        # default 2 (D217 V1)
NOVA_FIELD_CACHE_LICM_THRESHOLD=3   # default 2 (D218)
NOVA_FIELD_CACHE_PURE_THRESHOLD=3   # default 2 (D219)
NOVA_FIELD_CACHE_CHAIN_THRESHOLD=3  # default 2 (D217 V4)

Cap caches per fn (stack-frame budget):

NOVA_FIELD_CACHE_MAX=12   # default 8 — total across all 4 layers

Performance expectations

  • -O0 builds: 15-30% reduction in pointer derefs on hot paths.
  • -O2 builds: smaller gain (C compiler already does NoAlias-based CSE). Still measurable due to Nova’s deterministic emission.
  • Cross-platform: identical AST output on Windows MSVC / Linux clang / macOS clang.
  • Stack-frame impact: ≤ 8 cache locals per fn × 8 bytes ≈ 64 bytes.

Semantic equivalence

All 4 layers are pure AST→AST transformations. Disabling any layer (or all layers) produces identical observable behavior:

  • stdout / stderr identical.
  • Panics raised in same conditions.
  • File system / network effects identical.
  • GC behavior identical.

Verified via differential testing (umbrella nova_tests/plan123_* fixtures all PASS identically with the optimization enabled and disabled).

Spec references

  • D217 (Plan 123.1) — baseline field cache + V4 chain extension.
  • D218 (Plan 123.2) — LICM semantics.
  • D219 (Plan 123.3) — pure-call cache.
  • D24 (Plan 33.1+33.2) — #pure Purity infrastructure.

Followups + future versions

  • V5 (Plan 123.5, this) — LSP code-lens (deferred) + CLI flag.
  • V6 (Plan 123.6) — telemetry + production rollout + full CLI flags.
  • V7 (Plan 123.7) — inter-procedural analysis (IPA) for precise invalidation.
Last updated August 3, 2026