Skip to content

codegen: a contents proof only for a contents-stable binding (F13) - #14059

Merged
proggeramlug merged 2 commits into
mainfrom
thr11-contents-stable
Nov 5, 2026
Merged

proggeramlug merged 2 commits into
mainfrom
thr11-contents-stable

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Bug on main

A proof about an object's contents (its property types) is recorded once, at the binding, and kept for the binding's whole life. Any later write that changes a property without touching the binding leaves a stale proof behind, and the read is lowered as a number slot load:

function f4() {
  const o = { x: 1, y: 2 };
  delete (o as any).y;
  return o.y * 10;   // node: NaN, main: a number
}

The same holds for a store of another type, a write through an alias (const t = o; t.y = "s"), a write in a callee or a closure, a store through a held reference, Object.assign, Object.defineProperty, an array alias and a child alias (t = o.a; t.b = ...). The guarded parameter clones had the same gap: guard_blocked used has_any_mutation || (body calls && reference-like), so a body that stores through another parameter that aliases this one (f5(z, z)) kept the clone. The new gap fixture differs from node on main; probe p1.ts differed on 13 of its 18 lines.

The rule

collectors::contents_stable_bindings gives one module-wide definition of a binding whose object no code can write for its whole life:

  • aliases are joined by union-find (const t = o, t = o);
  • a store, update, delete, push-like op or reassignment rooted at a member makes the class unstable;
  • a member used as a value (call or new argument, method receiver, stored value, thrown, closure value use, and every other unlisted expression) escapes. A property read, an arithmetic or compare operand and return o from its own frame do not;
  • a non-primitive child that is handed out (g(o.a), const t = o.a, a callee's return p.a) escapes its parent;
  • a call argument to a known function is harmless when that function's parameter is itself contents-stable and is not returned;
  • a class that is not built from literals (a parameter, a call result) can be aliased before the binding, so it also needs a body that runs no unknown code and stores through no root other than a fresh member (the perf(codegen): guarded ordinary-parameter specialization #8094 predicate).

Both consumers read it: the let proof and the inlined-parameter proofs through FnCtx::admits_contents_proof (it gates only proofs that claim contents: Object, an alias or interface of one, a union, tuple or array whose element claims one; a class identity and an array of primitives are not gated), and the clone guard_blocked of a reference-like parameter. A primitive parameter keeps has_any_mutation.

Proofs this drops, and what they cost

  • A literal handed to an unknown call (console.log(o), a callee outside the module) keeps no contents proof.
  • A guarded parameter clone is dropped when the parameter escapes or when the body stores through another object that is not a fresh local.

Measured on qb9 against main a119084 (load 13, memcg counters unchanged):

check main head
m.js field micros (28 rows) instr/op equal on all rows equal on all rows
m.js .text / perry_js_text 5,483,902 / 25,520 byte-identical
literal micros (param / local) ns 2.893 / 0.679 2.907 / 0.674 (byte-identical .text)
hello size / callgrind Ir 14,464 B / 126,930 14,464 B / 126,930
gap subset (757) 635 pass 636 pass, 0 regressions (the new fixture is fixed)
perry-codegen lib tests (release) 2748 passed, 0 failed

Binaries that grow are only the fixtures main got wrong: the reads that lost their proof now go through js_dynamic_mul, js_dynamic_neg and to_numeric_pair (fixture +2,768 B, F13 +1,595 B, p1 +529 B, all in those symbols and the call sites in main, f3 and f4).

Tests

  • test-files/test_gap_property_value_contents_stable.ts (19 lines, = node).
  • collectors/contents_stable_tests.rs: 8 unit tests (delete and store, alias, child, call arguments, a callee returning a child, parameter hazards, closure write).
  • stmt/contents_stable_ir_tests.rs: a literal handed to a read-only callee multiplies its slots as a bare fmul double; after a delete it goes through js_dynamic_mul.
  • Sabotage, each alone: ignoring delete turns the delete/store unit test, the IR test and 3 fixture lines red; ignoring aliases turns the alias unit test and 2 fixture lines red.

A binding-level proof about an object's contents (an Object, an alias or
interface of one, a union or an array whose element claims one) was
recorded once at the binding, so a later delete, a store of another type,
a write through an alias, a callee, a closure, Object.assign or
defineProperty left a stale number proof behind (`delete o.y; o.y * 10`
printed a number instead of NaN).

collectors::contents_stable_bindings is one module-wide definition of a
binding whose object no code can write for its whole life (aliases by
union-find; writes, escapes, handed-out non-primitive children, a known
callee whose parameter is itself stable; an entry-aliased class also needs
a body with no unknown code and no store through a foreign root). The let
and inlined-parameter proofs and the guard_blocked choice of guarded
parameter clones both read it. Class identities and primitive arrays
claim no contents and are not gated.
@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Explain Change →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@proggeramlug
proggeramlug marked this pull request as ready for review November 5, 2026 08:19
@proggeramlug
proggeramlug merged commit b3477ab into main Nov 5, 2026
95 of 102 checks passed
@proggeramlug
proggeramlug deleted the thr11-contents-stable branch November 5, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant