fix(codegen): stop typing a symbol-keyed element read as a number (#7796) - #7810
Conversation
…rryTS#7796) Reading a[Symbol.iterator] off a number[] is a property read on the array object and answers with a function, but both element-type inference sites took the array's element type without ever looking at the index. The local was then recorded as a number, and a value believed numeric is tested for truthiness with a floating-point compare against zero — which is false for every NaN-boxed pointer, so if (f) took the false branch on a function. Both sites now require the index to be provably numeric.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthrough
ChangesIndex truthiness correction
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Covers the 32 PRs admin-merged in one pass (audited in principle at the maintainer's direction): PerryTS#7768 PerryTS#7772 PerryTS#7779 PerryTS#7784 PerryTS#7785 PerryTS#7786 PerryTS#7788 PerryTS#7789 PerryTS#7797 PerryTS#7798 PerryTS#7801 PerryTS#7802 PerryTS#7804 PerryTS#7805 PerryTS#7806 PerryTS#7807 PerryTS#7808 PerryTS#7810 PerryTS#7811 PerryTS#7815 PerryTS#7816 PerryTS#7818 PerryTS#7819 PerryTS#7820 PerryTS#7821 PerryTS#7822 PerryTS#7823 PerryTS#7824 PerryTS#7825 PerryTS#7826 PerryTS#7827 PerryTS#7828. (PerryTS#7787 closed as already-landed via the PerryTS#7786 stack.) Per-change history lives in each PR's changelog.d fragment as usual. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
…push guard Two sabotage-verified IR gates, prompted by review of the #7831/#7837 family against #7839's guard. `a_declared_type_lie_is_routed_to_the_runtime_tier_not_the_guard` — a `number[]` really can hold heap strings at runtime, and `is_numeric_expr` admits an element read off one (#7810). What keeps that value off the inline guard is `expr_produces_canonical_raw_f64` excluding every READ, which routes it to the pre-existing runtime numeric tier instead. Widening that predicate to admit a read fails this test. `the_guard_branches_on_the_live_bits_not_on_a_constant` — pins the guard's condition to a computed register and its predicate to the full heap-tag set. Hard-wiring the branch to `false` fails this test; it is invisible to every output-equality probe, because the elided bookkeeping is a GC-liveness fact rather than an arithmetic one.
… live test (push_num 0.149 -> 0.069) (#7839) * perf(codegen): put the numeric array push's GC bookkeeping behind one live test The inline array-append tier emitted `js_string_addref_if_heap_string`, `js_gc_note_slot_layout` and a seq_cst load of `PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT` on EVERY element. On `bench/push_num.ts` — 20,000,000 pushes of a double into a `number[]` — all three are dead on all 20M of them. The static proof that retires them cannot be made for the shape that matters: `keep.push(base + j)` is an `Expr::Binary { Add }`, and `expr_produces_non_pointer_bits_by_construction` answers `false` there unconditionally, because `+` is string concatenation for non-numeric operands. This is #7511's answer to the identical problem on class-field stores, applied to the array append: ask the question ONCE inline, on the live bits, and branch over all three calls. The array's half of the proof rides the header test the `nofwd` block already performs — the integrity mask widens from 0x0407 to 0x3C07, so reaching the inline store additionally proves ELEMENT_SHAPE, TYPED_LAYOUT_INTACT and ALL_POINTERS clear, the three states in which `js_gc_note_slot_layout` does real work for a non-pointer value. A guard, not an elision: Perry does not validate declared types, so a `number`-annotated value that is a heap string at runtime takes the guarded arm and records the slot exactly as it always did. * test(codegen): pin that a declared-type lie cannot reach the numeric push guard Two sabotage-verified IR gates, prompted by review of the #7831/#7837 family against #7839's guard. `a_declared_type_lie_is_routed_to_the_runtime_tier_not_the_guard` — a `number[]` really can hold heap strings at runtime, and `is_numeric_expr` admits an element read off one (#7810). What keeps that value off the inline guard is `expr_produces_canonical_raw_f64` excluding every READ, which routes it to the pre-existing runtime numeric tier instead. Widening that predicate to admit a read fails this test. `the_guard_branches_on_the_live_bits_not_on_a_constant` — pins the guard's condition to a computed register and its predicate to the full heap-tag set. Hard-wiring the branch to `false` fails this test; it is invisible to every output-equality probe, because the elided bookkeeping is a GC-liveness fact rather than an arithmetic one. --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Closes #7796.
A symbol-keyed property read off an array produced a value that every test agreed was a function —
typeofsaid"function",Boolean()saidtrue,=== undefinedsaidfalse— but that a plainiftreated as falsy. Only when the value was stored in a local first, which is what made it look so strange.Why it happened
Perry types an element read of a
number[]as a number, and it was doing that regardless of what the index actually was. Two places do this inference —refine_type_from_init(which decides the type ofconst f = ...) andis_numeric_expr(which answers "is this expression a number?") — and both destructured the index and then ignored it.But
a[Symbol.iterator]is not an element read. It reads a property of the array object, and the answer isArray.prototype[Symbol.iterator], a function. So the local holding it was recorded as a number.That is where it turns from imprecise into wrong. A value believed to be a number gets its truthiness tested with a floating-point comparison:
Perry stores objects, strings and functions as NaN-boxed doubles, which really are NaN — and every comparison against a NaN is false. So the test answered "falsy" for every function, object and string that reached it. The inline form was correct only because it never went through a local, so the bad type was never recorded.
The fix, and why it asks for proof
Both inference sites now require the index to be provably numeric before taking the array's element type.
Requiring proof, rather than just checking for a known-bad index like a symbol, is deliberate. An index the compiler cannot type may hold anything at runtime, so "I have no evidence this is a symbol" is not evidence that it is a number. The two possible mistakes are not equal either: answering "not a number" costs one missed fast path, while answering "number" costs a branch that silently goes the wrong way.
The masked-window fast-copy case keeps its early exit, since that fact already proves the index is an integer.
Tests
Two unit tests in
crates/perry-codegen/src/type_analysis/numeric/tests.rs, both asserting on the emitted IR for the function under test rather than the whole module:a_symbol_indexed_element_is_tested_with_js_is_truthyfcmp onefast patha_numeric_index_keeps_the_inline_fast_patha[i]still gets its inline comparisonReverting just the two source changes and keeping the tests turns the first one red, so it fails for the reason it claims.
Also checked by hand that a hot loop is unaffected:
for (let i = 0; ...) { const v = a[i]; if (v) acc += v; }still emits twofcmp oneand zerojs_is_truthycalls in the loop body, and returns the right answer.cargo test -p perry-codegen --lib: 853 passing.cargo test -p perry-hir --lib: 293 passing. The issue's reproducer now matchesnode --experimental-strip-typesline for line.Summary by CodeRabbit