Skip to content

repsel: element-shape proofs through arrays — measured 6.2× vs node; route = invariant bit → versioned-loop consumer → element Ptr<Shape> #7480

Description

@proggeramlug

Scoping for the repsel stack's next gap after 3b/4a: shape proofs do not survive an array element read. keep[j] yields an untyped JSValue, so .v re-enters the field-read diamond that 3b deleted for proven locals. This is the churn-probe access shape and the blocker named in the phase sequencing ("element-shape proof through arrays").

Measured (2026-08-06, release build @ main, checksums equal everywhere)

Kernel: keep: {v,w}[] of 200k, sum += keep[j].v × 50 sweeps:

runtime ms
node 15
bun 23
perry 93

6.2× vs node / 4× vs bun on the pure shape. IR inspection: the loop body carries no out-of-line guard calls (4a's inline element tier + the #5093-family inline field precheck are doing their job) — the cost is the stacked inline diamonds: element-read tier (tag/handle-band/header/layout tests) feeding a field-read precheck (gate load + tag + handle-band + 7 dependent header loads), per iteration. Exactly the 3b recon's "the guard is the cost", now twice per access.

The route decision: both candidate routes share one prerequisite

Route A — element representation (phase-4c style): prove keep : Array<Ptr<__AnonShape_…>> from construction flow, so element reads yield proven Ptr<Shape> and field reads emit bare gep+load. Full fix; helps non-loop access too.

Route B — extend the #5093 versioned-loop clone (machinery just revived by #7425; 3b's recon calls the clone "the only unguarded form today"): hoist ONE whole-array element-shape guard into the preheader, clone the body with bare reads.

The scoping finding is that B is not actually cheaper on the load-bearing part: a preheader "all elements are shape X" test needs a per-array homogeneous-element-shape invariant that does not exist today (the only array-level invariants are the numeric-layout bits, GC_ARRAY_RAW_F64_LAYOUT/_HOLES). Maintaining that invariant — set at construction, updated/cleared at every store site, self-healing like 4a's dense bit — is the same core mechanism Route A needs. Once it exists:

  1. B consumes it first (small codegen change: preheader guard + cloned body via the existing versioned-loop machinery) — fast win on loop-shaped access, which is where the churn probes live.
  2. A consumes it fully (element reads yield Ptr<Shape>, composing with 3b's proven-local machinery) — the durable fix.

So the sequencing is: invariant bit → B → A, not "A vs B".

Constraints carried from the stack

  • Construction-flow proofs only — TS annotations are hints, never layout (docs/src/internals/…, RFC §9's "typing an array makes it slower today" trap is the cautionary case).
  • Anon-shape literals are already classes (3b recon: closed-shape {k:v} lowers to __AnonShape_<hash> with a synthesized ctor), so the invariant's shape id exists for free at the allocation site.
  • The perf(codegen): keep the numeric-array specialization when the array is captured (#6369) #6377 gate: every added proof un-gates latent fast paths its own microbench never exercises. Acceptance is the full gap suite green against a same-session main baseline, plus the churn/ratchet probes stdout-identical to the node oracle — not the kernel above.
  • Arrays are movable and growth installs forwarding pointers; the invariant bit lives in the header the collector already maintains, and any cached raw base/length stays region-local SSA, re-derived after safepoints (same rule 4a followed).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions