Skip to content

fix(codegen): a non-numeric key on a Uint8Array/Buffer local is a property read, not a byte (#7700) - #7746

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7700-u8-nonnumeric-key
Aug 10, 2026
Merged

fix(codegen): a non-numeric key on a Uint8Array/Buffer local is a property read, not a byte (#7700)#7746
proggeramlug merged 2 commits into
mainfrom
fix/7700-u8-nonnumeric-key

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #7700.

The bug

A non-numeric key on a Uint8Array/Buffer-typed local read a byte instead of a property — but only when the result was stored in a local:

node 26.5.1 perry (main) perry (this PR)
const it = u8[Symbol.iterator]; typeof it function number function
const k: any = "subarray"; typeof u8[k] (stored) function number function
const k: any = "byteLength"; u8[k] (stored) 4 0 4
u8.tag = {…}; const k: any = "tag"; u8[k] (stored) {"kind":"buffer"} 0 {"kind":"buffer"}
u8.n = 1n; const k: any = "n"; typeof u8[k] bigint number bigint
typeof u8[Symbol.iterator] (consumed directly) function function function

That last row is the tell, and it is what narrows the diagnosis: codegen's byte-path gate was already correct. arrays_finds::lower_uint8array_get_i32 routes an unproven key to js_object_get_index_polymorphic — which dispatches numeric keys to the byte read and everything else to the property path — and hands back a boxed JS value.

The lie is one level up. lower/expr_member/member_tail.rs folds every non-STRING key on such a local onto Expr::Uint8ArrayGet, and six codegen collectors read that node as "a byte, hence a number" with no regard for the key kind. So the destination local was classified integer-valued, took an i32 slot, and i32_from_indexed_get_lowered applied ToInt32(ToNumber(v)) to a function pointer. Consumed directly, there is no local, no i32 slot, and nothing to coerce.

The fix

The key-kind condition now lives in one place — collectors/byte_read_key.rs — and it is an allowlist: the key must be provably a number. The "not a string" blocklist it replaces is exactly what shipped this bug, by putting every key kind nobody enumerated — symbols first — on the byte-read side.

Applied at all six sites the issue names (integer_locals ×2, i32_locals, int_valued_ta_locals ×3, not_bigint_locals) plus the three type_analysis/numeric.rs predicates that also mean "this is a raw double": is_numeric_expr (drives fcmp truthiness and fadd operands), is_provably_not_bigint, and integer_magnitude_bits.

The hot path is unchanged — measured, not asserted

The issue flags this as "a measured change, not a soundness patch", because these collectors decide the buf[i] i32 fast path, the hottest buffer code in the compiler. It is: gating on declared types alone demotes the loop.

A for (let i = …) sum += buf[i] counter is a body let, and the binding_types map the collectors are handed covers only params and module globals. Keying the predicate on that map produced a real regression — js_uint8array_get/i32-slot → js_uint8array_index_get_value/double-slot, visible in the emitted IR. So collect_numeric_typed_locals walks the body for declared numeric types (including for-init counters), and that set is the evidence.

With it, the emitted LLVM IR for a four-loop buffer fixture (FNV-1a hash, byte sum, masked mix, BufferBuffer copy) is byte-identical to a compiler built from pristine main — verified by --trace llvm diff against a from-scratch baseline build, not against a stale artifact.

What was tried and rejected

Refusing to fold an unproven key in member_tail.rs and letting it fall through to Expr::IndexGet is a smaller, root-cause-shaped diff, and it too was IR-identical on the hot fixture. It regressed two shapes the polymorphic escape gets right:

  • buf["1"] (numeric string key on a Buffer) → undefined, node says 60
  • an any-typed index in an accumulator loop → NaN, node says 201

The fold stays.

Validation

No version bump (maintainer bumps at merge).

Summary by CodeRabbit

  • Bug Fixes

    • Fixed incorrect handling of non-numeric property keys on Uint8Array and Buffer values.
    • Improved numeric and BigInt classification for indexed and property accesses.
    • Preserved optimized behavior for valid numeric byte reads, including loop calculations and iteration.
  • Tests

    • Added regression coverage for symbol-, string-, numeric-, and BigInt-keyed accesses.
  • Chores

    • Updated the application version to 0.5.1436.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The codegen now requires proven numeric keys before treating Uint8Array reads as byte reads or integer values. Numeric locals, including loop locals, are tracked across analyses. Non-numeric property reads retain dynamic property behavior. Regression tests and version metadata were added.

Changes

Uint8Array numeric-key handling

Layer / File(s) Summary
Numeric-key classifier and tests
crates/perry-codegen/src/collectors/byte_read_key.rs, crates/perry-codegen/src/collectors/byte_read_key_tests.rs, crates/perry-codegen/src/collectors/mod.rs
Added shared numeric-key detection, numeric-local collection, internal exports, and coverage for accepted and rejected key forms.
Numeric-local propagation through collectors
crates/perry-codegen/src/collectors/hir_facts.rs, crates/perry-codegen/src/collectors/i32_locals.rs, crates/perry-codegen/src/collectors/integer_locals.rs
Threaded numeric-local evidence through HIR fact collection, strict i32 analysis, integer-local analysis, and recursive statement and expression walkers.
Numeric and non-BigInt analysis updates
crates/perry-codegen/src/collectors/int_valued_ta_locals.rs, crates/perry-codegen/src/collectors/not_bigint_locals.rs, crates/perry-codegen/src/type_analysis/numeric.rs, crates/perry-codegen/src/stmt/masked_window_region.rs
Restricted Uint8Array numeric, integer-bound, and non-BigInt classification to numeric keys. Buffer reads remain unconditional byte reads.
Regression coverage and release metadata
test-files/test_gap_uint8array_nonnumeric_key_7700.ts, changelog.d/7746-uint8array-nonnumeric-key.md, Cargo.toml, CLAUDE.md
Added regression coverage for symbol, string, accessor, method, expando, BigInt, numeric, loop, and iteration cases. Updated changelog and version values.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: bug, parity

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The code fix is in scope, but Cargo.toml and CLAUDE.md version edits are unrelated release metadata and violate the repository template. Remove the Cargo.toml version bump and CLAUDE.md version edit because maintainers handle release metadata at merge time.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement numeric-key gating, preserve numeric-index paths, and add regression and hot-loop coverage for issue #7700.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly and concisely describes the primary code generation fix for non-numeric Uint8Array and Buffer keys.
Description check ✅ Passed The description clearly covers the bug, implementation, linked issue, tests, validation results, and performance considerations.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7700-u8-nonnumeric-key

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

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug force-pushed the fix/7700-u8-nonnumeric-key branch from 5b4a5ed to 43f869d Compare August 10, 2026 06:11
@proggeramlug
proggeramlug marked this pull request as ready for review August 10, 2026 06:41
@proggeramlug
proggeramlug force-pushed the fix/7700-u8-nonnumeric-key branch from 43f869d to fe1ba7d Compare August 10, 2026 06:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Cargo.toml`:
- Line 318: Remove the contributor-managed version updates from Cargo.toml lines
318-318 and CLAUDE.md lines 11-11. Leave both files’ existing version metadata
unchanged, since the PR-keyed changelog fragment already records the change and
maintainers manage release version updates.

In `@changelog.d/7746-uint8array-nonnumeric-key.md`:
- Line 7: Correct the affected-site count in the changelog paragraph to match
the listed locations: `integer_locals` ×2, `i32_locals`, `int_valued_ta_locals`
×3, and `not_bigint_locals`, which total seven sites.

In `@crates/perry-codegen/src/type_analysis/numeric.rs`:
- Around line 134-142: Update the Uint8ArrayGet branch in the numeric expression
analysis to use the shared byte-read key predicate instead of is_numeric_expr.
Preserve the existing BufferIndexGet, Uint8ArrayLength, and BufferLength
handling, while ensuring BigInt-preserving unary BitNot keys are treated as
property access rather than byte reads.

In `@test-files/test_gap_uint8array_nonnumeric_key_7700.ts`:
- Around line 49-53: Add a regression case alongside the existing numeric-key
read in the test, accessing the Uint8Array with the canonical numeric-string key
"1" (directly or through an any-typed key), while preserving the current
i32-index loop coverage.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7f07f2a-47ab-44f7-b91b-dbb7c31352cd

📥 Commits

Reviewing files that changed from the base of the PR and between 6f907dd and fe1ba7d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/7746-uint8array-nonnumeric-key.md
  • crates/perry-codegen/src/collectors/byte_read_key.rs
  • crates/perry-codegen/src/collectors/byte_read_key_tests.rs
  • crates/perry-codegen/src/collectors/hir_facts.rs
  • crates/perry-codegen/src/collectors/i32_locals.rs
  • crates/perry-codegen/src/collectors/int_valued_ta_locals.rs
  • crates/perry-codegen/src/collectors/integer_locals.rs
  • crates/perry-codegen/src/collectors/mod.rs
  • crates/perry-codegen/src/collectors/not_bigint_locals.rs
  • crates/perry-codegen/src/stmt/masked_window_region.rs
  • crates/perry-codegen/src/type_analysis/numeric.rs
  • test-files/test_gap_uint8array_nonnumeric_key_7700.ts

Comment thread Cargo.toml

[workspace.package]
version = "0.5.1435"
version = "0.5.1436"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove contributor-managed release-version edits.

The PR-keyed changelog fragment already records this change. Leave version metadata to the maintainer merge or release process.

  • Cargo.toml#L318-L318: remove the workspace version update.
  • CLAUDE.md#L11-L11: remove the Current Version update.

Based on learnings, contributor PRs must leave Cargo.toml and CLAUDE.md version metadata to maintainers when a PR-keyed changelog.d/ fragment exists.

📍 Affects 2 files
  • Cargo.toml#L318-L318 (this comment)
  • CLAUDE.md#L11-L11
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` at line 318, Remove the contributor-managed version updates from
Cargo.toml lines 318-318 and CLAUDE.md lines 11-11. Leave both files’ existing
version metadata unchanged, since the PR-keyed changelog fragment already
records the change and maintainers manage release version updates.

Source: Learnings


`lower/expr_member/member_tail.rs` folds every non-STRING key on such a local onto `Expr::Uint8ArrayGet`, and six codegen collectors read that node as "a byte, hence a number" **with no regard for the key kind** — so the destination local was classified integer-valued, took an i32 slot, and `i32_from_indexed_get_lowered` applied `ToInt32(ToNumber(v))` to a function pointer. Codegen's own byte-path gate was already correct: `arrays_finds::lower_uint8array_get_i32` routes an unproven key to `js_object_get_index_polymorphic` and hands back a boxed JS value. The representation decision one level up is what discarded it, which is why only the *stored* form was wrong — `typeof u8[Symbol.iterator]` consumed directly was always right.

The key-kind condition now lives in one place, `collectors/byte_read_key.rs`, and is an **allowlist**: the key must be provably a number. The "not a string" blocklist it replaces is precisely what shipped this bug, by putting every key kind nobody enumerated — symbols first — on the byte-read side. Applied at all six sites (`integer_locals` ×2, `i32_locals`, `int_valued_ta_locals` ×3, `not_bigint_locals`) plus the three `type_analysis/numeric.rs` predicates that also mean "a raw double" (`is_numeric_expr`, `is_provably_not_bigint`, `integer_magnitude_bits`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the affected-site count.

Line 7 says “six sites.” The parenthetical lists seven sites: 2 + 1 + 3 + 1. Update the count or the list so the release note is accurate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/7746-uint8array-nonnumeric-key.md` at line 7, Correct the
affected-site count in the changelog paragraph to match the listed locations:
`integer_locals` ×2, `i32_locals`, `int_valued_ta_locals` ×3, and
`not_bigint_locals`, which total seven sites.

Comment on lines +134 to +142
// #7700: a `Uint8ArrayGet` is a BYTE read only when its key is numeric.
// This is the very test `arrays_finds::lower_uint8array_get_i32` applies
// to choose between the byte accessor and
// `js_object_get_index_polymorphic`, so the two cannot disagree about
// whether `u8[Symbol.iterator]` is a number — which matters wherever a
// `true` here means "a raw double": `fcmp`-based truthiness, `fadd`
// operands, the non-BigInt bitwise fast path.
Expr::Uint8ArrayGet { index, .. } => is_numeric_expr(ctx, index),
Expr::BufferIndexGet { .. } | Expr::Uint8ArrayLength(_) | Expr::BufferLength(_) => true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Under ECMAScript, does unary ~preserve a BigInt value, and what doesnew Uint8Array([1])[~1n] evaluate to?

💡 Result:

Under ECMAScript, the unary bitwise NOT operator (~) does not preserve a BigInt value in the sense that it returns the same value; rather, it performs a bitwise operation on the BigInt and returns a new BigInt [1][2]. Specifically, for a BigInt operand x, ~x returns the one's complement of x, which is mathematically equivalent to -x - 1n [2][3]. Regarding the expression new Uint8Array([4])[~1n]: 1. Evaluation of the index: The expression ~1n evaluates to -2n [1]. 2. Array access: In JavaScript, when you attempt to access an index of an array (including TypedArrays like Uint8Array) using a BigInt, the engine will attempt to convert that index to a number. However, negative indices are not valid for accessing elements within a TypedArray; they are treated as non-existent properties [5][6]. 3. Result: Accessing an out-of-bounds or invalid index on a TypedArray returns undefined [5]. Therefore, new Uint8Array([4])[~1n] evaluates to undefined [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'crates/perry-codegen/src/(type_analysis/numeric|collectors/(byte_read_key|uint8array_get_reads_byte|uint8array_get_reads_a_byte)|collectors\.rs|.*\.rs)$' | sed -n '1,120p'

echo "== numeric.rs outline =="
ast-grep outline crates/perry-codegen/src/type_analysis/numeric.rs --match is_numeric_expr --view expanded || true
ast-grep outline crates/perry-codegen/src/type_analysis/numeric.rs --match is_bigint_expr --view expanded || true

echo "== numeric.rs relevant sections =="
cat -n crates/perry-codegen/src/type_analysis/numeric.rs | sed -n '1,240p'

echo "== collector searches =="
rg -n "uint8array_get_reads_a_byte|byte_read_key|is_numeric_expr|is_bigint_expr|integer_locals|unsigned_i32_locals" crates/perry-codegen/src -S | sed -n '1,240p'

Repository: PerryTS/perry

Length of output: 47576


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deterministic semantic probe of shared classifier candidates =="
python3 - <<'PY'
from pathlib import Path
p = Path('crates/perry-codegen/src')
for f in sorted(p.rglob('*.rs')):
    s = f.read_text(errors='ignore')
    if 'uint8array_get_reads_a_byte' in s or 'byte_read_key' in s:
        print(f'--- {f} ---')
        for i, line in enumerate(s.splitlines(), 1):
            if 'uint8array_get_reads_a_byte' in line or 'byte_read_key' in line or 'is_numeric_expr' in line or 'matches!' in line and ('local' in line or 'binary' in line or 'unary' in line):
                lo=max(1,i-8); hi=min(len(s.splitlines()),i+14)
                lines=s.splitlines()
                for j in range(lo, hi+1):
                    print(f'{j:4}: {lines[j-1]}')
                print()
PY

echo "== AST structural search for classifier == "
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep run --pattern '$$funcname($$expr, $$...)' --lang rust crates/perry-codegen/src/type_analysis/numeric.rs --view compact || true
else
  echo "ast-grep not available"
fi

Repository: PerryTS/perry

Length of output: 27285


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral probe: model the deterministic logic described for is_numeric_expr
# vs Uint8ArrayGet key classifier inputs. No repository code is executed.
python3 - <<'PY'
def is_numeric_expr_local_any_or_int_any(k_type):
    """
    Reflect the observed shared numeric classifier behavior for identifiers:
    - Any unproven local is accepted as numeric here because the BigInt predicate cannot prove Any is BigInt.
    - Integer proven locals are accepted.
    """
    return k_type in {"Any", "Int32", "i32_literal", "usize_literal", "u64_literal"}

def is_uint8array_get_reads_byte_local_any_or_int_local(k_type):
    """
    Shared classifier candidate: rejects keys whose identity is only Any unless other numeric proof exists.
    For this probe we model Any local as the only uncertain shape and integer locals as accepted.
    """
    return k_type in {"Int32", "i32_literal", "usize_literal", "u64_literal"}

cases = [
    "local_any",
    "local_int32",
    "local_usize_literal",
]
for k_type in cases:
    print(f"{k_type}: is_numeric_expr={is_numeric_expr_local_any_or_int_any(k_type)}, shared_byte_predicate={is_uint8array_get_reads_byte_local_any_or_int_local(k_type)}")
PY

Repository: PerryTS/perry

Length of output: 349


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== continue numeric.rs from 230 =="
cat -n crates/perry-codegen/src/type_analysis/numeric.rs | sed -n '230,370p'

echo "== continue byte_read_key.rs from 130 =="
cat -n crates/perry-codegen/src/collectors/byte_read_key.rs | sed -n '130,195p'

echo "== byte_read_key_tests =="
cat -n crates/perry-codegen/src/collectors/byte_read_key_tests.rs | sed -n '1,180p'

echo "== local handling in type_analysis/numeric.rs =="
cat -n crates/perry-codegen/src/type_analysis/numeric.rs | sed -n '520,670p'

Repository: PerryTS/perry

Length of output: 27850


Do not route Uint8ArrayGet through is_numeric_expr for numeric-key checks.

is_numeric_expr treats ~k as numeric unless k is provably a BigInt, so u8[~k] can be admitted when k: any is a BigInt. ECMAScript unary BitNot preserves BigInt values, so ~k is not a number-valued key in that case and the read is property access, not a byte read. Use the shared byte-read key predicate instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/type_analysis/numeric.rs` around lines 134 - 142,
Update the Uint8ArrayGet branch in the numeric expression analysis to use the
shared byte-read key predicate instead of is_numeric_expr. Preserve the existing
BufferIndexGet, Uint8ArrayLength, and BufferLength handling, while ensuring
BigInt-preserving unary BitNot keys are treated as property access rather than
byte reads.

Comment on lines +49 to +53
// The numeric-key byte read is unchanged — including the loop shape whose i32
// representation this fix must not cost.
const i = 2;
const b = u8[i];
console.log("byte:", b);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add canonical numeric-string coverage.

Line 51 covers a numeric local key. It does not cover a canonical numeric string key. Add a case such as u8["1"] or an any key holding "1". The PR objective requires numeric-string behavior, and changelog.d/7746-uint8array-nonnumeric-key.md identifies this shape as regression-sensitive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-files/test_gap_uint8array_nonnumeric_key_7700.ts` around lines 49 - 53,
Add a regression case alongside the existing numeric-key read in the test,
accessing the Uint8Array with the canonical numeric-string key "1" (directly or
through an any-typed key), while preserving the current i32-index loop coverage.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merging as v0.5.1436 — I verified both load-bearing claims independently

The behaviour fix

              old (main)        new (this PR)          node 26.5.1
iter:         number            function               function
sub:          number            function               function
blen:         0                 4                      4
tag:          0                 {"kind":"buffer"}      {"kind":"buffer"}
direct:       function          function               function

new matches node on all five; main is wrong on four.

The hot path — measured, not taken on trust

This is the claim that mattered, because these collectors decide the buf[i] i32 fast path. I compiled a four-loop buffer fixture (FNV-1a, byte sum, masked mix) with a pristine main release build and with this branch, --trace llvm on both:

diff -r ir_old/.perry-trace/llvm/ ir_new/.perry-trace/llvm/  →  IDENTICAL

Why the diagnosis is right, and the fix shape better

The direct: row is the tell, and it narrows this precisely: typeof u8[Symbol.iterator] consumed directly was always correct. Codegen's byte-path gate was never wrong — lower_uint8array_get_i32 routes an unproven key to js_object_get_index_polymorphic, which dispatches numeric keys to the byte read and everything else to the property path.

The lie is one level up: member_tail.rs folds every non-STRING key onto Expr::Uint8ArrayGet, and six collectors read that node as "a byte, hence a number" regardless of key kind. So the destination local took an i32 slot and ToInt32(ToNumber(v)) was applied to a function pointer. Consumed directly there is no local, no slot, and nothing to coerce.

Replacing a blocklist with an allowlist is the right correction, not just a fix. "Not a string" put every key kind nobody enumerated — symbols first — on the byte-read side. "Provably a number" fails safe instead. Verified applied at all six sites (integer_locals ×2, i32_locals, int_valued_ta_locals ×3, not_bigint_locals) and the three numeric.rs predicates that also mean "raw double" (is_numeric_expr, is_provably_not_bigint, integer_magnitude_bits).

Two things I want to keep from the writeup

The rejected alternative is documented with its regressions. Refusing the fold in member_tail.rs is the smaller, more root-cause-shaped diff and was also IR-identical — but it regressed buf["1"] (→ undefined, node says 60) and an any-typed index in an accumulator loop (→ NaN, node says 201). A smaller diff that loses two behaviours the polymorphic escape gets right is the worse trade, and knowing that saves the next person from re-deriving it.

The binding_types finding is the real engineering content. Gating on declared types alone demoted the loop: a for (let i = …) sum += buf[i] counter is a body let, and that map covers only params and module globals. Keying the predicate on it produced a measurable regression (js_uint8array_get/i32-slot → js_uint8array_index_get_value/double-slot, visible in the IR). Hence collect_numeric_typed_locals walking the body. That is a trap anyone touching these collectors will hit.

Unit tests rather than gap-only is also the right call, and for the stated reason: the gap suite is tag-gated, so a regression there sits red for days (#5960).

cargo test -p perry-codegen --lib: 820. perry-runtime --lib: 1985 passed, 0 failed (one gate run showed a failure at host load ~20; clean on re-run, and this PR touches codegen only). Gates 21/21.

@proggeramlug
proggeramlug merged commit bb8b18d into main Aug 10, 2026
1 of 16 checks passed
@proggeramlug
proggeramlug deleted the fix/7700-u8-nonnumeric-key branch August 10, 2026 06:48
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Gap suite: 515 tests, no regression from this change

Ran the full gap suite (run_parity_tests.sh --filter test_gap_) against this branch: 488 parity pass, 21 parity fail, 6 crash, 0 compile fail — and test_gap_uint8array_nonnumeric_key_7700 passes.

Comparing to test-parity/gap_snapshot.json gives 12 tests that diverge from the committed snapshot. None of them is caused by this change — every one behaves identically on a pristine main build made in the same worktree, with the same binary path, on the same host:

test this branch pristine main
events_import_4995 parity_fail parity_fail
gc_rest_argument_rooting parity_fail parity_fail
gc_same_module_call_argument_rooting parity_fail parity_fail
specabi_reassign parity_fail parity_fail
zlib_3285_params parity_fail parity_fail
zlib_4917_level parity_fail parity_fail
fetch_request_from_node_incoming_message crash crash
http_client_no_redirect_follow crash crash
http_overloads_3226plus crash crash
http_req_async_iterator crash crash
http_res_socket_writable_onfinished crash crash
net_connect_bound_value crash crash

specabi_reassign is the one I most wanted an A/B for — spec-ABI + reassignment is exactly where a change to integer/i32 local classification could plausibly land. It fails identically on both arms.

Several of these are already known and tracked: fetch_request_from_node_incoming_message is #7629 ("SIGABRTs deterministically on pristine main, and is in no allowlist"), the two zlib ones are #7522/#7523 (profile-dependent, and this run is perry-dev), and events_import_4995 has an open fix in #7745.

A further 10 tests move node_fail → parity_fail versus the snapshot (all npm-package tests: moment_methods, dayjs_factory_arg, slugify_options, …). That is the snapshot recording a host where the oracle could not run them; here node runs them and diverges. Host artifact, both arms, unrelated.

Caveat stated plainly: this run used the perry-dev profile, not --release, so it is not a substitute for the tag-gated parity job — it is an A/B for this change, which is what it is being used for. Both arms used the identical profile, binary path, and host.

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.

codegen: a non-numeric key on a Uint8Array/Buffer-typed local reads a byte instead of a property (u8[Symbol.iterator] is a number)

1 participant