Skip to content

runtime: a JS Number has one encoding; delete the INT32 Number consumers (P3 B4b) - #14658

Merged
proggeramlug merged 5 commits into
mainfrom
perf/p3-b4b-delete-int32-runtime
Nov 14, 2026
Merged

proggeramlug merged 5 commits into
mainfrom
perf/p3-b4b-delete-int32-runtime

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

What

The last phase of the canonical-Number plan (representation RFC §10 P3; after B1, B2, B4a #14372 and B3 #14395).
No code can make an INT32-tagged Number since B4a, and codegen stopped reading one in B3. This PR deletes the
runtime side: every place that still decoded a 0x7FFE_0000_xxxx_xxxx word as a Number.

  • JSValue::is_int32 / as_int32, value::is_int32_bits and INT32_MASK are gone. About 330 consumer arms go
    with them in perry-runtime (equality and relational compare, loose equality, ToNumber / ToString / radix,
    truthiness, typeof, JSON writers, console and util.format, key and index decoding for Arrays, typed arrays,
    Buffers, Maps and Sets, argument validation in fs / net / dgram / tls / zlib / child_process / bun:ffi / sqlite),
    perry-stdlib (crypto, webcrypto, sqlite, streams, events, querystring) and the http, net, sharp and
    better-sqlite3 extensions.
  • The tag constant is INTERNAL_WORD_TAG in perry-runtime: the 0x7FFE band holds runtime-internal words only
    and never a JS value.
  • perry-ffi: JsValue::is_number, to_number and to_int32 read a plain double only. JsValue::is_int32 stays,
    deprecated and always false, so an out-of-tree extension that calls it keeps compiling.
  • Two helpers that existed only for the second encoding are deleted: the store barrier's
    canonicalize_typed_slot_store_bits (and slot_holds_raw_f64), and js_closure_call_array's defensive
    argument decode with its scratch buffer; numify_arith_operand and int32_or_class_repr likewise.

One thing the design did not know: I32 at rest

DESIGN-p3 was written before I32 at rest (N1). Since then the 0x7FFE band also holds the I32 slot words of an
at-rest Array and of an I32 field lane (layout_slot.rs); they are decoded by the layout accessor before a value
leaves the slot. So the sweep separates two kinds of reader:

  • a consumer of a JS value (an argument, a result, a value read through the accessor): its INT32 arm is deleted;
  • a classifier of raw slot words keeps the I32 slot word, now stated as layout_slot::is_i32_word (the same
    bits the old predicate tested, so nothing changes for it): array::value_bits_to_number, the check-mode kind
    tests (wpfacts_check::kind_of, contracts::kind_of), the GC census, typed feedback's stable kind and the
    structured-clone writer.

A generic reader that wpfacts admits never meets an at-rest slot (that is the at-rest admission rule), so no
value-side arm was reachable through one. The language parity corpus and check mode below are the evidence.

Not in this PR

  • perry-codegen's INT32_TAG constants (the at-rest word and the control cells) keep their names.
  • perry-ui-android / perry-ui-windows handle decoders and wasm_runtime.js (not checkable here).
  • The dead "class reference in the 0x7FFE band" tests on JS values (dead since B2), the now unused _slot_index
    parameter of runtime_store_jsvalue_slot_layout_deferred, and codegen's calls of the now-identity
    js_typed_f64_arg_to_raw / js_typed_i32_arg_to_raw. Listed in FOLLOWUPS.

Switch, probe

None: a pure deletion with no fact and no lowering change, like B3. The off arm is main dd083f3. The chess facts dump is byte-identical to main's (facts_sha below).

Chess (qb10, instructions:u, each arm x2, same session; off = main dd083f3, on = this branch 9f48f8a = main dd083f3 merged in)

mode main PR delta checksum peak RSS main / PR (KiB)
search 250000 11 848.132 M 11 845.507 M −0.022% OK 23 720 / 23 720
perft 4 258.929 M 258.911 M −0.007% OK 15 360 / 15 616
evaluate 100000 1 191.813 M 1 190.589 M −0.103% OK 15 360 / 15 360
legal 10000 354.792 M 354.774 M −0.005% OK 15 604 / 15 604

Second runs: search 11 848.183 / 11 845.511, perft 258.931 / 258.911, evaluate 1 191.814 / 1 190.590, legal
354.793 / 354.774. Binary 9 320 576 -> 9 308 288 B (−12 288). Facts dump identical (facts_sha 69df1bea5e8af5b7 in
both). Chess check mode: all four modes rc 0, 0 misses, in both arms.

Gates run

All on the merged head 9f48f8a (qb10). Earlier runs on the pre-merge heads 75baabc and 5c46e43 (base
b44f944) gave the same picture and are superseded by these.

  • Fixture tests/parity/language/number/test_p3_b4b_one_number_encoding.ts: = node in default, DCE off, check mode,
    check mode with DCE off.
  • Chess, x2, with checksums, RSS, size and check mode (above).
  • cargo test -p perry-runtime --lib: 6819 passed, 0 failed. -p perry-ffi --lib: 55 passed.
  • cargo test -p perry-stdlib --lib: 342 passed, 1 failed:
    sqlite::bind::tests::typed_arrays_are_not_named_parameter_objects. It fails the same way on main (checked on
    b44f944: the test binary has no typed-array family hooks); not from this PR.
  • Language parity (2732 programs, default mode): 2472 match, 0 crashes, 0 timeouts. No main baseline exists at
    dd083f3; against the newest one (main 12cace2) no program that matched there fails here, and the
    number-births branch on the same base gives the identical per-file result.
  • Language check mode (2732 programs): 0 rc 86, 0 aborts.
  • S0 with a canonnum runtime built from this branch: 203 executable controls passed
    (canonical-number-negative: 0 / 86 / 86 for the array-rep arm).
  • scripts/check_file_size.sh: OK (four allowlist counts re-recorded, all smaller).

The merge of main

Main moved by 46 commits in the same files while the gates ran; it is merged in (not rebased). Two conflicts:
typedarray/bigint.rs (main moved the ToBigInt label into bigint_coerce.rs; its INT32 arm is deleted there) and
the check_file_size.sh counts. The S0 hunk this branch first carried (accept array rep in
canonical-number-negative) is dropped: main fixed that arm itself, and the branch has main's script unchanged.

instanceof and internal words

On main x instanceof C answers false when x is a 0x7FFE word that names no class, through the INT32 arms of
instanceof_lhs_is_primitive and of the prototype walk. Deleting those arms made the walk throw for such a word.
Both are restored as "an internal word is no object" (unit tests
ordinary_has_instance_walk_is_false_for_primitive_lhs, lhs_primitive_classification_is_tag_only). No fixture
line: a JS program cannot reach the case, since no code makes a 0x7FFE word as a value (B4a) and an I32 slot word
is decoded before it leaves its slot.

Not run

Full suites other than the three lib suites above, parity IR census, node-suite check mode, tssrc, Supabase, skin
size, hello / hello2 startup Ir, the ext crates' ABI tests.

Ralph Küpper added 4 commits November 13, 2026 20:47
…ers (P3 B4b)

No producer of an INT32-tagged Number exists since B4a and codegen stopped
reading one in B3. The runtime, stdlib and extension consumers go: equality
and compare, ToString / ToNumber, JSON, console formatting, key and index
decoding, argument validation. JSValue::is_int32 / as_int32 and
is_int32_bits are deleted; the 0x7FFE tag is INTERNAL_WORD_TAG (runtime
internal words only). The classifiers that read raw slot words keep the I32
slot word of an at-rest Array or I32 field lane (layout_slot).
@coderabbitai

coderabbitai Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Explain Change →

Walkthrough

The change removes INT32-tagged Number decoding across the runtime and perry-ffi. It renames the 0x7FFE tag to INTERNAL_WORD_TAG for runtime-internal words, updates numeric consumers to use shared Number paths, and adds parity coverage for Number behavior.

Changes: One Number Encoding

Layer / File(s) Summary
Number and internal-word representation
crates/perry-runtime/src/value/*, crates/perry-ffi/src/jsvalue.rs, crates/perry-runtime/src/layout_slot.rs, crates/perry-runtime/src/gc/*
The 0x7FFE tag is named INTERNAL_WORD_TAG. I32 slot words continue to be identified through layout metadata. perry-ffi deprecates is_int32 and makes it always return false; runtime JSValue removes is_int32 and as_int32.
Runtime Number semantics and storage consumers
crates/perry-runtime/src/array/*, crates/perry-runtime/src/buffer/*, crates/perry-runtime/src/builtins/*, crates/perry-runtime/src/value/*, crates/perry-runtime/src/object/*
Numeric paths no longer decode 0x7FFE-tagged values as Numbers. Equality, coercion, indexing, formatting, and storage paths use their remaining Number checks or conversions.
Runtime and extension API conversions
crates/perry-runtime/src/{bun_compat,bun_ffi,child_process,fs,node_submodules,process}/*, crates/perry-ext-*/*, crates/perry-stdlib/src/*
Numeric argument handling removes dedicated int32 conversions and uses shared numeric validation. Existing validation and fallback paths remain in many of these consumers.
Parity and supporting updates
tests/parity/language/number/*, scripts/*, changelog.d/*, crates/perry-runtime/src/**/*tests*
The parity test logs Number behavior across language operations and runtime containers. Tests and size-check counts are updated. The changelog records the encoding changes and states that no program behavior changes because no INT32 Number producer existed since B4a.

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

Change: Refactor

Merge Risk: Low · up to 5c46e

The change removes INT32-tagged Number decoding from the runtime. No incorrect program results were found in review. Rename the changelog fragment to include PR 14658 before merge. The parameter type guard may skip its fast path for numeric fields stored in I32 lanes, which costs speed only. A stale code comment in the proxy dense-array code should be corrected.

Pre-merge checks: Passed 2

Check name Status Explanation
Title check Passed The title names the main change: the runtime moves to one encoding for JS Numbers and deletes the INT32 Number consumers. The diff matches this. The title is short and specific, though the phrase 'P3 …
Description check Passed The description follows the template's main intent. It has a clear summary of what changed and why, a detailed list of changes, a related-work reference, and a test plan with measured results and gate…
Full details: Title check

Explanation

The title names the main change: the runtime moves to one encoding for JS Numbers and deletes the INT32 Number consumers. The diff matches this. The title is short and specific, though the phrase 'P3 B4b' refers to an internal plan stage a reader may not know.

Full details: Description check

Explanation

The description follows the template's main intent. It has a clear summary of what changed and why, a detailed list of changes, a related-work reference, and a test plan with measured results and gates. It does not follow the template's section headings exactly: it has no 'Related issue' section (it cites PR #14372 and #14395 instead), and the test plan does not use the template checklist. The template's required checklist items about not bumping the version and not editing CHANGELOG.md are not shown as checked. The raw summary also lists a changelog entry, which the author may have added as a template-compliant item. The description is largely complete.

  • 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

Conflicts: typedarray/bigint.rs (main moved the ToBigInt label into bigint_coerce.rs; its INT32 arm is deleted there), check_file_size.sh (counts re-recorded). The sweep's own S0 hunk is dropped: main fixed the canonical-number arm.
@proggeramlug
proggeramlug merged commit d0f763f into main Nov 14, 2026
95 of 101 checks passed
@proggeramlug
proggeramlug deleted the perf/p3-b4b-delete-int32-runtime branch November 14, 2026 02:22
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