Skip to content

runtime/codegen: one exact int32 predicate; top-16-only tests misread 0x7FFE markers as Numbers - #14353

Merged
proggeramlug merged 5 commits into
mainfrom
fix/exact-int32-predicate
Nov 9, 2026
Merged

proggeramlug merged 5 commits into
mainfrom
fix/exact-int32-predicate

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Correctness: one exact "is this an int32 Number?" predicate everywhere. About 40 runtime, codegen and perry-ffi sites tested only the top 16 bits (bits & TAG_MASK == INT32_TAG, >> 48 == 0x7FFE, & 0xFFFF_0000_0000_0000). So they read any other 0x7FFE word with bits 32..47 set as an int32 Number whose value is the low 32 bits. The JS-visible case is the C.prototype reference word (CLASS_PROTOTYPE_REF_FLAG, bit 32): as an index it read element class_id, it compared equal to the Number class_id, and with class id 0 it was falsy. It is latent: the P3 review tried about 30 shapes and could not reach that word from JS. The proof is the unit tests below, which fail on main.

The one predicate

  • Runtime: value::is_int32_bits(bits), bits & !INT32_MASK == INT32_TAG (value/tags.rs). JSValue::is_int32 calls it.
  • Codegen: codegen::emit_int32_bits_test(blk, bits) (codegen/typed_entry.rs). emit_int32_box_test calls it, and the duplicated exact sequences in typed_abi (4) and switch_stmt now call it.
  • perry-ffi: a separate crate, so its own JSValue::is_int32 uses the same mask.

Sites

(A) Number consumers → the predicate (32). A non-Number 0x7FFE word is now an object at each one: truthy, not an index, NaN as a number.

  • Runtime:
    • truthiness: thread.rs:179, value/truthy.rs:73;
    • dynamic, polymorphic and keyed indexing: value/dyn_index.rs:109,122, object/polymorphic_index.rs:99,112, array/indexing_keyed.rs:178,281,452, array/indexing_read_cold.rs:190, object/field_get_set/runtime_read_site.rs:238, typed_feedback.rs:1207,1220;
    • Buffer and typed arrays: object/buffer_dispatch.rs:1151,1189, buffer/cmp.rs:235,304, byte_cell_support/mod.rs:511, typedarray/construct.rs:53;
    • JSON: json/stringify_scalars.rs:33, json/stringify_flat.rs:175;
    • date.rs:549 (now NaN, never the tagged word);
    • array/generic.rs:104 (ToObject);
    • object/object_ops/define_property.rs:327;
    • gc/barrier_store.rs:89 (raw-slot canonicalization);
    • pty/mod.rs:288, object/native_module_dispatch.rs:143;
    • node_submodules/diagnostics_tail.rs:77;
    • closure/dispatch/value_call.rs:410.
  • perry-ffi: jsvalue.rs:266.
  • Codegen (emitted):
    • expr/index_key.rs:119 (the dynamic index-key normalizer);
    • expr/compare.rs:771,774,832,887,955 (strict equality, relational and normalization against a Number);
    • expr/compare_chain.rs:227;
    • lower_call/property_get/builtin_kind_guard.rs:569.

(C) Kind classifiers → the predicate (5). A non-Number 0x7FFE word is no longer the Number/int32 kind:

  • wpfacts_check.rs:63 (check-mode kind_of: K_OBJ);
  • contracts.rs:361 (contract kind_of: K_OBJ);
  • typed_feedback.rs:494 (stable_value_kind);
  • gc/census.rs:284 and gc/census_field_repr.rs:44.

(B) Left alone: marker decoders and producers, which read their own marker correctly.

  • Class refs: object/native_module/class_ref_values.rs:22,27,34,51, object/class_value.rs:126,400,412.
  • Bound closures: closure/dispatch/bound.rs:359,442,448.
  • Class-ref routing: closure/dispatch/value_call.rs:113, proxy.rs:151, object/property_ops/callable.rs:12, object/polymorphic_index.rs:179, object/field_get_set/ic_miss/private_member_access.rs:142, object/field_get_set/get_slow.rs:159, object/field_get_set/get_field_by_name.rs:107, object/native_call_method/primitive_methods.rs:202, object/native_call_method/common_methods.rs:412,469.
  • Constructor-ref decode: proxy/metadata/storage.rs:21, object/property_ops/metadata/storage.rs:25.
  • Producers: object/overflow.rs, object/key_attrs.rs:225, box/scope.rs:26.

Other sites, unchanged:

  • Bit-preserving: thread/clone_write.rs:460 (Inline(bits)).
  • Whole-tag exclusion, sound: map.rs:675.
  • Diagnostics: snap_census/runner.rs:3329, gc/fromspace_scan.rs:438, typed_feedback/trace.rs:54.
  • Already exact: object/field_get_set/field_ops.rs:110, json/replacer.rs:1340.
  • Boundary bytes are left to the NANB lane (fix/canonical-nan-at-boundaries): node_v8.rs:592 (as_u32).

What no bit test can separate

The bound-closure inline-args marker, the overflow route words, the class-id capture and the legacy constructor-ref key (INT32_TAG | class_id) keep their payload in the low 32 bits. They are bit-identical to an int32 Number. They stay internal, and moving them to their own tag is RFC §10 P3 phase B2.

Tests

They fail on main: branch probe/int32-tests-on-main is main plus only these tests.

  • Runtime: one test per (A)/(C) helper feeds a C.prototype ref word, and a 0x7FFE word with high payload bits, and expects "not int32":
    • value/int32_exact_tests.rs (the predicate, JSValue::is_int32, js_is_truthy, integer_key);
    • typed_feedback/tests.rs, date/tests.rs, value/dyn_index_uint8array_tests.rs;
    • object/polymorphic_index_int32_exact_tests.rs, thread_int32_exact_tests.rs;
    • inside contracts.rs, wpfacts_check.rs, gc/census.rs and gc/census_field_repr.rs.
  • Codegen: dynamic_strict_eq_against_number_compares_both_encodings_without_js_eq asserts the emitted test uses the identity mask and never icmp eq … 32766.

Gates

Gates were run on the merged head (main dda8f1e merged in, no rebase), on qb9 (x86-64 Linux); the tests-on-main runs were on the Mac.

gate result
merged head 3e5223d (main c5cd323, which has NANB and #14347) check -D warnings (codegen, ffi, perry) clean; codegen lib 2837/0; perry-runtime 6601/0; perry-ffi 55/0 and 5/0; file size OK
unit tests on main (main plus only the tests) fail: runtime 9 of 10 fail (the predicate test passes, since JSValue::is_int32 was already exact); contracts (under --cfg perry_contract_checks) 1 fails; the codegen IR test fails
unit tests on the branch pass
check -D warnings codegen, ffi and perry are clean. -p perry-runtime alone reports 4 dead-code/unused errors in files this PR does not touch (webassembly_wrapper_state.rs, cell_meta.rs, gc/mark.rs, shapes.rs). They are pre-existing on main: main reports the same 4
codegen lib 2836/0
perry-runtime suite 6601/0 (48 ignored); contracts under --cfg perry_contract_checks 6/0
perry-ffi 55/0 and 5/0
vf (1538) json=0 abort=0 rc86=0; crc!=0=2 (the main baseline)
hello / hello2 startup Ir 127738 / 633344, identical to main. hello .text 1238, identical; hello2 .text 5926142 → 5926014 (−128 B)
chess (Linux instructions:u, main → branch) search 37965.7 → 37979.6 M (+0.04%), perft 907.3 → 907.3, evaluate 3262.1 → 3262.5 M (+0.01%), legal 1293.2 → 1293.2; fingerprints identical
file size typed_feedback.rs re-asserted 2436 → 2437, typed_feedback/tests.rs 2289 → 2304. snap_census/runner.rs (3572) is over the cap on main itself and is handled separately

Changelog: runtime-exact-int32-predicate.md.

@proggeramlug
proggeramlug merged commit 369605c into main Nov 9, 2026
94 of 100 checks passed
@proggeramlug
proggeramlug deleted the fix/exact-int32-predicate branch November 9, 2026 06:04
@coderabbitai

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