Skip to content

tests: 21 stale codegen IR assertions and the product element-identity test true again (exact-int32 guard, call-site stack checks, counter-versioned loops, P3 console.log) - #14599

Merged
proggeramlug merged 9 commits into
mainfrom
fix/stale-codegen-tests
Nov 13, 2026

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

No lowering, fact or runtime code changes. Every test below was red on main e147126 (and all but the perry test on
base 4283f71). For each: what the assertion looked for, what the IR has now and why, and how the adapted test
states the same property; the negative controls patch the LOWERING in a scratch tree (never committed) and show the
adapted test fails.

1. invalidation::inclusive_local_length_bound_does_not_use_local_length_bound_fact, invalidation::negative_loop_counter_does_not_use_local_length_bound_fact (#7505 class; "the invalidated proof leaked through")

Verdict: stale assertion, no leak. The fixtures are const n = 8; const buf = Buffer.alloc(n); for (i = 0; i <= n; i++) buf[i] = 1 and the same loop from i = -1 with i < n. The local length source n cannot certify these stores
(i = n, i = -1 are out of range). The tests asserted "no native buffer element address anywhere in probe". Since
fa6faf8 / 97ce0b7 (11-08, counter-versioned loops) the loop has two copies: the CHECKED copy keeps
js_buffer_set (with its own bound check), and the FAST copy takes getelementptr inbounds stores, entered only when
counter_entry_check passes. Its conjuncts, in the IR of both fixtures: counter's entry value >= 0, step >= 0,
last + step <= i32::MAX, and last + 1 <= length, where last = bound for <= (bound - 1 for <) and length is
the slot stored from js_buffer_length(buf) at the view's birth (the runtime length, not the local source). For the
fixtures that is 9 <= 8 (false) resp. -1 >= 0 (false): the fast copy is never entered.

As real programs (qb10 fx/inv_buffer_bounds.ts: the two fixtures, both with a runtime-unknown n, start -5, a
Uint8Array variant, an in-bounds positive control, canary buffers allocated before and after each buffer, and a
2 000-round stress of out-of-range stores between allocations): output = node in default mode, in check mode
(PERRY_VERIFY_FACTS=1, rc 0) and under GC schedule stress (instrumented runtime, seeds 1 / 7 / 42, rate 1, conservative
scan auto and off); every canary intact, buf[-1] undefined, no element past the end written.

The tests now use assert_out_of_range_counter_store_is_entry_guarded: either the store is the checked
js_buffer_set alone, or 8326a00's assert_stale_length_store_is_checked_or_runtime_versioned holds (every native
GEP in the fast copy, a guard conjunct end <= zext(load(slot written only from js_buffer_length))) AND the guard has
a conjunct icmp sge i32 (load counter slot), 0 (the counter slot is the one the fast copy's condition compares) AND,
for <=, end = add (sext bound), 1 (not bound - 1).

Negative controls: counter_nonneg replaced by true in counter_entry_check -> both tests FAIL ("the fast copy's
guard must test the counter's entry value >= 0"); the <= case lowered as < (last = bound - 1) -> the inclusive
test FAILS. (The helper of 8326a00 alone would have passed the first sabotage on the negative-counter fixture.)

2. 13 entry-guard tests + typed_i32_closure_clone_accepts_immutable_i32_capture (Int32 lane)

Stale. The predicates guards_i32_arg / unboxes_i32_arg looked for the INT32-tag identity test
(icmp eq i64 %, 9222809086901354496 + range fcmps) and the tag/double select; two tests also looked for the INT32
tag constant as the boxing of the raw result. P3 (#14353 369605c, P3 B3 09454cd, both before base) made a Number
one encoding: the guard is emit_typed_i32_plain_guard_and_raw (%raw = fptosi.sat(%v), sitofp %raw == %v,
bits(%v) != -0), the unbox one fptosi behind it, the result boxed with sitofp i32 to double. The IR of every
failing test has exactly that form and still routes to $typed_i32 / $spec_* and $generic.

  • guards_i32_arg now follows the data flow: a fptosi.sat of %v, whose sitofp is fcmp oeq with the SAME
    %v, and an icmp ne i64 (bitcast %v), -9223372036854775808.
  • unboxes_i32_arg: fptosi double % to i32 or the guard's saturating conversion.
  • New boxes_i32_result(ir, callee): the clone's call i32 result goes through sitofp i32 %r to double.
  • The predicate self-test gains a positive int32 case and two negatives (round trip against another value; no -0 test).
    Negative control: the -0 conjunct dropped from emit_typed_i32_plain_guard_and_raw -> the representative tests
    FAIL.

3. static_method_shape_guard_retains_canonical_layout_clone, typed_f64_receiver_method_clone_raw_loads_after_composed_guards ("raw-f64 receiver clone", undetermined)

Stale. Both read the targets of a guard's branch as block names. Since 36a74ff (11-07, chess profile: the
stack check moves from the entry to the calling blocks) an edge into a calling block enters stack_guard.call.N
first, whose overflow arm calls js_stack_overflow and which rejoins the edge's own target. In the failing IR the
method-direct guard branches to stack_guard.call.16 / .19, and the raw-f64 field guard's success edge to a stack
check in front of the block that calls $typed_f64_recv. New helper through_stack_check follows a
stack_guard.call.* edge to the one target it rejoins (it asserts there is exactly one); the tests compare that
target as before. The static test's last assertion (public wrapper noinline) is also stale for the same commit:
a small spec trampoline is preinlined (alwaysinline, PERRY_SPEC_INLINE); it now asserts the public entry is the
routing trampoline (guard, $spec_*, $generic) with no copy of the method-direct guard, out of line or preinlined.
Negative control: the method-direct deref branch's fast / fallback labels swapped -> the static test FAILS.

4. large_object_barriers::large_local_array_push_inbounds_store_emits_precise_slot_barrier ("missing slot-layout note", GC write-barrier class)

Stale; the note and the barrier are emitted. The test walks the CFG region from apush.inbounds.* through
apush.* blocks. In the IR the in-bounds block stores the element, then branches: the fast arm (value is a pointer
AND the array header's layout state already makes the collector scan its slots, flags & 0xF880 == 0xA000) skips the
note; otherwise apush.pointer_layout.bookkeeping calls js_gc_note_slot_layout and js_array_note_numeric_write;
both rejoin apush.pointer_layout.done, then apush.barrier calls js_write_barrier_slot_validated_parent for an
old parent (or an incremental mark). The edge into the bookkeeping block goes through a stack_guard.call.N check
(36a74ff, as in 3), so the apush.*-only walk never reached it. The walk now passes through stack_guard.*
blocks. As a real program (qb10 fx/large_push_barrier.ts: a 2 050-element literal Array (outside the small-literal
path) receiving young objects through the optimized push, 64 pushes per array with allocation churn between them,
some arrays kept across 5 000 more allocations, every child checked): output = node in default and check mode and
under GC schedule stress (seeds 1 / 7 / 42, rate 1, scan auto and off: 1 496 forced copying minors, 241 926 objects
moved per run). Negative control: the bookkeeping block's js_gc_note_slot_layout not emitted -> the test FAILS.

5. scalar_replaced_slot_roots::numeric_only_scalar_replaced_{object_emits_no_rooting,literal_emits_no_entry_rooting}

Stale premise (vacuity guard fired). The witness that the module binds at all was console.log(p.x, p.y)'s
pooled argument accumulator. Since P3 those two arguments are proven Numbers and the call passes them in a plain
stack array: no bind anywhere, so the == 0 claims had no subject. The fixtures now add rooting_anchor()
(console.log({k: 1}, {k: 1}): the first argument is held in a pooled temp root while the second allocates; it
touches no scalar-replaced local); the zero value-slot binds / barriers and the non-vacuity witness hold. Negative
control: Expr::Number no longer a known non-pointer in expr_is_known_non_pointer_shadow_value -> both FAIL.

6. perry bin product_element_identity_preserves_codegen_facts

Stale expectation of this effort's facts. The test compiles a program with and without element identity
(element_offset cleared) and asserts equal elision facts. Two fields differ: number_element_reads {} vs {75: false} (P3 B1, 59925ca, 11-09: first bad by reading; the long gate saw it red at 1343403) and
primitive_elements {} vs {75} (#14537, 91a26fd). Both are keyed by element_offset by design: a site with offset
0 has no key, so without identity the fact cannot be recorded. The test now empties every element-keyed map
(element_reads, number_element_reads, int32_element_reads, element_floors, element_keys, key_signs,
wide_keys, inbounds_stores, rest_reads, rest_writes, quiet_elements, primitive_elements), asserts they
are all empty without identity and that every key with identity is an element site of the program, and asserts every
other fact equal (sites, work, pops, constraints and digest as before).

Also verified, no change needed

Run (qb10)

  • Base (main e147126): native_proof_regressions 275 / 22, large_object_barriers 2 / 1, scalar_replaced_slot_roots
    9 / 2, perry bin product_element_identity_preserves_codegen_facts FAILED.
  • PR head (1e18f37, before the merge of main): native_proof_regressions 293 / 4, large_object_barriers 3 / 0,
    scalar_replaced_slot_roots 11 / 0, perry bin product_element_identity 1 / 0, scripts/check_file_size.sh OK
    (native_proof_regressions.rs re-recorded 15688 -> 15809: the helpers above).
  • The 4 left in native_proof_regressions are NOT stale tests of this PR and stay red: invalidation::loop_length_effect_artifact_records_consumed_preservation_fact,
    invalidation::non_mutating_array_alias_preserves_length_and_bounds_proofs, packed_f64_loop_store_update_versions_with_side_exit,
    packed_f64_loop_unary_math_store_versions_with_side_exit. First bad commit by build: 91a26fd (wpfacts, codegen: a loop without calls keeps a length only when its sites run no program code (quiet sites) #14537, quiet
    sites; its parent passes all four), the same commit turns typed_shape_descriptors::bounded_integer_array_store_omits_layout_note_and_barrier
    and 7 codegen lib tests (bounded_array_hole_tests, bounded_array_index_tests, 5 barrier_stem_census_tests) red.
    Reported to its owner, not changed here.
  • Negative controls (scratch tree, each restored): counter_nonneg = true -> both invalidation tests FAIL; <= as <
    -> the inclusive test FAILS; -0 conjunct dropped -> the 14 Int32 tests FAIL; method-direct deref edges swapped ->
    the static test FAILS; Expr::Number not a known non-pointer -> the 3 numeric scalar tests FAIL; no layout note in
    either push bookkeeping block -> large_local_array_push_inbounds_store_emits_precise_slot_barrier FAILS.
  • Fixtures as real programs (release build of main e147126 + instrumented runtime): fx/inv_buffer_bounds.ts and
    fx/large_push_barrier.ts = node in default, check mode and GC schedule stress (seeds 1 / 7 / 42, scan auto / off).
  • Mac: cargo check -p perry-codegen -p perry --lib --tests --bins on the merged head.

No lowering, fact or runtime change: no switch; chess not measured. Not run: IR census, tssrc, Supabase, skin size.

Ralph Küpper added 9 commits November 13, 2026 01:45
…), a stack check rejoins its edge, out-of-range counter stores sit behind the versioned loop's entry guard
…nchor (their console.log of two proven Numbers binds nothing since P3)
…ack check (the layout note's block sits behind one since 36a74ff)
… element sites (number_element_reads since 59925ca, primitive_elements since #14537); every other codegen fact is equal
@proggeramlug
proggeramlug merged commit 1a631b6 into main Nov 13, 2026
6 checks passed
@proggeramlug
proggeramlug deleted the fix/stale-codegen-tests branch November 13, 2026 02:39
@coderabbitai

coderabbitai Bot commented Nov 13, 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

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