From d9e78dce1b116f85cc5e895f80ba8630b033f5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 30 Sep 2026 08:24:01 +0200 Subject: [PATCH] =?UTF-8?q?shapes:=20true=20field=20representations=20and?= =?UTF-8?q?=20one=20birth=20shape=20(step=205=20P2b=E2=80=93P2d,=20T1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carry field representations through key-add transitions, every store path, class and literal birth mints, outlined allocators, and link-time seeds. Equivalent keys/prototype/representation births now share one ShapeId. Reject boxed loop-region stores into F64 lanes and guard typed-receiver clones by both class and shape. Keep worker installs and exact fallbacks representation-aware, and initialize F64 birth lanes as canonical doubles. --- changelog.d/11674-shape-class-birth-f64.md | 1 + .../11674-shape-field-rep-class-instances.md | 5 + ...11674-shape-field-rep-delete-no-release.md | 3 + ...1674-shape-field-rep-inline-store-check.md | 8 + ...674-shape-field-rep-key-add-convergence.md | 4 + ...674-shape-field-rep-key-add-one-publish.md | 4 + changelog.d/11674-shape-field-rep-key-add.md | 8 + .../11674-shape-field-rep-region-store.md | 1 + ...674-shape-field-rep-store-check-fixture.md | 4 + ...1674-shape-field-rep-verify-and-migrate.md | 1 + .../11674-typed-recv-clone-shape-guard.md | 1 + crates/perry-codegen/src/codegen/artifacts.rs | 1 + crates/perry-codegen/src/codegen/closure.rs | 1 + crates/perry-codegen/src/codegen/entry.rs | 2 + crates/perry-codegen/src/codegen/function.rs | 1 + crates/perry-codegen/src/codegen/method.rs | 1 + .../src/codegen/method_static.rs | 1 + crates/perry-codegen/src/codegen/mod.rs | 25 ++ crates/perry-codegen/src/codegen/opts.rs | 4 + .../src/codegen/static_shape_ids.rs | 90 ++++- .../src/codegen/static_shape_ids_tests.rs | 102 ++++- .../perry-codegen/src/codegen/string_pool.rs | 24 +- .../src/expr/class_field_inline_guard.rs | 22 +- .../src/expr/literal_descriptor.rs | 6 +- crates/perry-codegen/src/expr/mod.rs | 2 + crates/perry-codegen/src/expr/property_set.rs | 1 + .../expr/property_set/sloppy_class_field.rs | 2 + .../src/expr/put_value_store_ic.rs | 46 ++- .../src/expr/region_loop_tests.rs | 46 +++ .../src/expr/write_pic_barrier_tests.rs | 29 +- crates/perry-codegen/src/lib.rs | 2 +- .../src/lower_call/class_birth_rep_tests.rs | 66 ++++ crates/perry-codegen/src/lower_call/mod.rs | 2 + .../perry-codegen/src/lower_call/new_alloc.rs | 32 +- .../property_get/dynamic_dispatch.rs | 187 ++------- .../property_get/dynamic_dispatch_collapse.rs | 156 ++++++++ .../src/lower_call/scalar_method.rs | 10 +- .../src/lower_call/typed_shape_bake_tests.rs | 98 +++-- .../src/runtime_decls/strings.rs | 20 +- .../src/stmt/region_loop/guard.rs | 3 +- .../perry-codegen/src/stmt/region_loop/mod.rs | 9 +- .../src/stmt/region_loop/plan.rs | 39 +- crates/perry-codegen/src/stubs.rs | 24 +- crates/perry-codegen/src/typed_shape.rs | 69 ++++ .../perry-codegen/src/wasm32/runtime_abi.tsv | 18 +- crates/perry-runtime/Cargo.toml | 4 + .../src/array/index_get_exit_tests.rs | 1 + .../src/array/literal_descriptor.rs | 9 +- .../perry-runtime/src/array/subclass_tests.rs | 36 +- crates/perry-runtime/src/gc/instruments.rs | 1 + crates/perry-runtime/src/gc/layout.rs | 23 ++ .../src/gc/layout/typed_shape.rs | 31 +- .../src/gc/layout/typed_shape_static_tests.rs | 12 +- .../src/gc/tests/canonical_keys_holders.rs | 7 +- .../layout_trace/declared_at_allocation.rs | 12 +- .../tests/layout_trace/per_object_tables.rs | 9 +- .../tests/layout_trace/shape_install_memo.rs | 2 +- .../src/gc/tests/layout_trace/typed_shape.rs | 5 + crates/perry-runtime/src/hot_diag.rs | 10 +- crates/perry-runtime/src/json/parse_api.rs | 1 + crates/perry-runtime/src/object/alloc.rs | 69 ++-- .../perry-runtime/src/object/alloc_plain.rs | 100 ++++- .../src/object/class_birth_rep_tests.rs | 297 ++++++++++++++ .../src/object/field_get_set/field_ops.rs | 11 +- .../field_get_set/ic_miss/outline_split.rs | 2 + crates/perry-runtime/src/object/field_rep.rs | 26 ++ .../src/object/field_rep_store.rs | 365 +++++++++++++++++- .../src/object/field_rep_store_tests.rs | 272 ++++++++++++- .../src/object/field_set_by_name.rs | 17 +- .../object/field_set_by_name/fast_paths.rs | 2 +- .../src/object/field_set_by_name/tail.rs | 93 +++-- crates/perry-runtime/src/object/gc_slots.rs | 8 + .../src/object/json_construction.rs | 5 +- .../src/object/literal_constructor.rs | 10 +- crates/perry-runtime/src/object/mod.rs | 63 ++- .../src/object/object_ops/keys_array.rs | 8 +- .../src/object/shape_mint_census.rs | 12 +- crates/perry-runtime/src/object/shapes.rs | 281 ++++++++++++-- .../src/object/shapes_slot_list.rs | 11 +- .../src/object/shapes_store_kind_tests.rs | 2 +- .../src/object/shapes_test_support.rs | 1 + .../perry-runtime/src/object/shapes_tests.rs | 49 ++- .../perry-runtime/src/object/static_shapes.rs | 42 +- .../src/object/static_shapes_tests.rs | 101 ++++- crates/perry-runtime/src/proxy.rs | 4 + crates/perry-runtime/src/proxy/put_value.rs | 7 + .../src/proxy/put_value/packed_add.rs | 38 +- .../src/proxy/put_value/packed_add_tests.rs | 27 ++ .../src/proxy/put_value/packed_set.rs | 18 +- .../src/proxy/put_value/packed_set_tests.rs | 30 +- .../src/thread_static_shape_tests.rs | 1 + .../src/typed_feedback/guards.rs | 23 +- .../src/commands/compile/object_cache.rs | 2 + .../object_cache/object_cache_tests.rs | 5 +- .../compile/optimized_libs/freshness.rs | 1 + scripts/shape_descriptor_census.py | 25 +- test-files/test_gap_class_birth_f64.ts | 101 +++++ test-files/test_gap_field_rep_converge.ts | 34 ++ test-files/test_gap_field_rep_store_check.ts | 218 +++++++++++ test-files/test_gap_region_store_f64_lane.ts | 63 +++ test-files/test_gap_typed_recv_clone_alias.ts | 61 +++ 101 files changed, 3365 insertions(+), 484 deletions(-) create mode 100644 changelog.d/11674-shape-class-birth-f64.md create mode 100644 changelog.d/11674-shape-field-rep-class-instances.md create mode 100644 changelog.d/11674-shape-field-rep-delete-no-release.md create mode 100644 changelog.d/11674-shape-field-rep-inline-store-check.md create mode 100644 changelog.d/11674-shape-field-rep-key-add-convergence.md create mode 100644 changelog.d/11674-shape-field-rep-key-add-one-publish.md create mode 100644 changelog.d/11674-shape-field-rep-key-add.md create mode 100644 changelog.d/11674-shape-field-rep-region-store.md create mode 100644 changelog.d/11674-shape-field-rep-store-check-fixture.md create mode 100644 changelog.d/11674-shape-field-rep-verify-and-migrate.md create mode 100644 changelog.d/11674-typed-recv-clone-shape-guard.md create mode 100644 crates/perry-codegen/src/lower_call/class_birth_rep_tests.rs create mode 100644 crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch_collapse.rs create mode 100644 crates/perry-runtime/src/object/class_birth_rep_tests.rs create mode 100644 test-files/test_gap_class_birth_f64.ts create mode 100644 test-files/test_gap_field_rep_converge.ts create mode 100644 test-files/test_gap_field_rep_store_check.ts create mode 100644 test-files/test_gap_region_store_f64_lane.ts create mode 100644 test-files/test_gap_typed_recv_clone_alias.ts diff --git a/changelog.d/11674-shape-class-birth-f64.md b/changelog.d/11674-shape-class-birth-f64.md new file mode 100644 index 0000000000..71d72d1fa8 --- /dev/null +++ b/changelog.d/11674-shape-class-birth-f64.md @@ -0,0 +1 @@ +- Charter step 5 (T1): a class whose constructor prologue writes every `number` field from a parameter before anything can read it (the #7510 "declared at allocation" proof) is now born with a shape whose representation is `F64` for exactly those fields. Codegen makes the decision once (`typed_shape::class_birth_rep_in`) and passes the word to the module-init mint (`js_object_shape_id_for_class_keys{,_live}` and `js_gc_typed_shape_id_for_keys` take a trailing `rep: u64`); the inline allocation and the runtime stamped allocator birth-fill those lanes with `+0.0` instead of `undefined`; the class-field store precheck finite-tests every value bound for an `F64` birth lane, so a non-Number or non-finite value takes the checked, generalizing path. The rep is part of a birth shape's static-id content (`static_shape_ids::BirthShape::rep`; `js_object_shape_id_for_class_keys_static` takes it too), so an importing module's all-`Any` stub never adopts an `F64` birth id. `PERRY_FIELD_REPR_VERIFY` gains the reverse typed-layout cross-check: a compiled birth id that declares an `F64` lane may not leave any raw-f64 slot of its intact layout `Any`. diff --git a/changelog.d/11674-shape-field-rep-class-instances.md b/changelog.d/11674-shape-field-rep-class-instances.md new file mode 100644 index 0000000000..b964bbf4d3 --- /dev/null +++ b/changelog.d/11674-shape-field-rep-class-instances.md @@ -0,0 +1,5 @@ +Class instances can now keep a Number-only field in its unboxed form too. The +restriction that kept every class instance on the boxed representation is +gone: every class-field fast path already matches the instance by its exact +shape, and a shape that marks a field Number-only sends other values through +the checked path. diff --git a/changelog.d/11674-shape-field-rep-delete-no-release.md b/changelog.d/11674-shape-field-rep-delete-no-release.md new file mode 100644 index 0000000000..a8860de9f7 --- /dev/null +++ b/changelog.d/11674-shape-field-rep-delete-no-release.md @@ -0,0 +1,3 @@ +Deleting a property from an object whose Number-only fields are stored +unboxed no longer runs an extra release step first; the delete already moves +the object to a general shape before it shifts any value. diff --git a/changelog.d/11674-shape-field-rep-inline-store-check.md b/changelog.d/11674-shape-field-rep-inline-store-check.md new file mode 100644 index 0000000000..481515e167 --- /dev/null +++ b/changelog.d/11674-shape-field-rep-inline-store-check.md @@ -0,0 +1,8 @@ +Every compiled fast path that stores into an object's field now respects the +field's recorded representation: when a shape says a slot holds a Number, the +inline store and the inline property-add accept only a plain double there and +send anything else (an object, a string, an integer box, NaN, Infinity) to the +runtime, which re-describes the field before storing. Deleting a property +drops the representation before it moves values between slots. A new +`field-rep-assert` runtime feature (always on in debug builds) checks at every +collection that each such slot holds a double. diff --git a/changelog.d/11674-shape-field-rep-key-add-convergence.md b/changelog.d/11674-shape-field-rep-key-add-convergence.md new file mode 100644 index 0000000000..cce12575e5 --- /dev/null +++ b/changelog.d/11674-shape-field-rep-key-add-convergence.md @@ -0,0 +1,4 @@ +When a property is sometimes added with a non-number value, objects built the +same way now settle on one shape for it instead of splitting between a +number-only shape and a general one, so code that reads or writes that +property keeps one fast path. diff --git a/changelog.d/11674-shape-field-rep-key-add-one-publish.md b/changelog.d/11674-shape-field-rep-key-add-one-publish.md new file mode 100644 index 0000000000..080cd1bc3f --- /dev/null +++ b/changelog.d/11674-shape-field-rep-key-add-one-publish.md @@ -0,0 +1,4 @@ +A property add now publishes its field representation in the same shape +publish that installs the key (or the grown slot bound), instead of minting an +all-`Any` shape first and the representation-carrying one after it. Shape +mints on tsc return to their pre-P2b count (14,345 vs 14,335; P2b had 19,438). diff --git a/changelog.d/11674-shape-field-rep-key-add.md b/changelog.d/11674-shape-field-rep-key-add.md new file mode 100644 index 0000000000..564ba5acac --- /dev/null +++ b/changelog.d/11674-shape-field-rep-key-add.md @@ -0,0 +1,8 @@ +Adding a property now records its field representation in the shape: a key-add +of a Number into an inline slot gives the new shape an `F64` lane, any other +value (or an overflow slot) an `Any` lane, and the predecessor's lanes carry. +The transition cache needs no new key bit: a cached edge serves a value only +when its target's lane admits it (an `F64` lane refuses a non-Number, a target +with a deprecated lane never serves, so new objects converge on the normalized +shape). The by-name cache-hit writers and `js_object_set_field` now store +through the checked funnel, so an `F64` slot always holds a canonical double. diff --git a/changelog.d/11674-shape-field-rep-region-store.md b/changelog.d/11674-shape-field-rep-region-store.md new file mode 100644 index 0000000000..b22fc6839b --- /dev/null +++ b/changelog.d/11674-shape-field-rep-region-store.md @@ -0,0 +1 @@ +- Charter step 5: a loop region's bare store runs no field-representation check, so the region now tells the runtime which keys it may store a value not proven a canonical double into (`js_region_loop_prime` / `js_region_loop_pack` take a trailing `boxed_mask`), and the pack refuses a word whose shape has a non-`Any` lane at such a key (census route `rt_rloop_refuse_f64_stored`); the static supplier (`static_region_slots`) refuses the same keys against the birth rep. A proven double still stores bare into any lane. diff --git a/changelog.d/11674-shape-field-rep-store-check-fixture.md b/changelog.d/11674-shape-field-rep-store-check-fixture.md new file mode 100644 index 0000000000..aa02be34c2 --- /dev/null +++ b/changelog.d/11674-shape-field-rep-store-check-fixture.md @@ -0,0 +1,4 @@ +The field-representation store-check test now reaches the compiled store and +property-add fast paths with a non-Number after they were primed on a Number +field, and forces a collection after each case, so a fast path that skipped +the check fails the test instead of passing unnoticed. diff --git a/changelog.d/11674-shape-field-rep-verify-and-migrate.md b/changelog.d/11674-shape-field-rep-verify-and-migrate.md new file mode 100644 index 0000000000..fdd69fbf53 --- /dev/null +++ b/changelog.d/11674-shape-field-rep-verify-and-migrate.md @@ -0,0 +1 @@ +- Charter step 5 (P2d): `PERRY_FIELD_REPR_VERIFY=1` checks the field-representation invariant at every object trace in a runtime built with `gc-instruments` (always on in debug and with `field-rep-assert`), plus a cross-check that an intact typed layout never calls an F64 lane a pointer slot; a binary without the feature aborts at startup when the knob is set. Every property-IC miss entry (generic get fast miss, put-value set and dynamic set misses, class-field get/set fast misses) now migrates a receiver whose shape has a deprecated lane before it learns anything from it. diff --git a/changelog.d/11674-typed-recv-clone-shape-guard.md b/changelog.d/11674-typed-recv-clone-shape-guard.md new file mode 100644 index 0000000000..f4296a7cb6 --- /dev/null +++ b/changelog.d/11674-typed-recv-clone-shape-guard.md @@ -0,0 +1 @@ +- A method call on a receiver whose class codegen proved (the `$typed_f64_recv` clone route in `try_lower_instance_method_call`) now runs the clone only while the receiver's (class id, ShapeId) pair is the class's own (`emit_class_field_read_precheck`, raw): an alias the object escaped to may store a non-Number into a field, which moves the object off its birth shape, and the clone reads its fields as raw doubles. Otherwise the call takes the generic target. diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 0be569cbfe..0e4c386bd9 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1002,6 +1002,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { output_type, class_keys_init_data, class_header_image_inits, + &cross_module.class_birth_reps, class_ids, class_table, &hir.classes, diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index 7ea14a5cdf..aab5c27969 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -1120,6 +1120,7 @@ pub(super) fn compile_closure( class_field_counts: &cross_module.class_field_counts, class_init_chains: &cross_module.class_init_chains, class_header_image_globals: &cross_module.class_header_images, + class_birth_reps: &cross_module.class_birth_reps, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, func_synthetic_arguments, diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index 3c68a580dd..34ef5ca9bd 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -709,6 +709,7 @@ pub(super) fn compile_module_entry( class_field_counts: &cross_module.class_field_counts, class_init_chains: &cross_module.class_init_chains, class_header_image_globals: &cross_module.class_header_images, + class_birth_reps: &cross_module.class_birth_reps, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, func_synthetic_arguments, @@ -1571,6 +1572,7 @@ pub(super) fn compile_module_entry( class_field_counts: &cross_module.class_field_counts, class_init_chains: &cross_module.class_init_chains, class_header_image_globals: &cross_module.class_header_images, + class_birth_reps: &cross_module.class_birth_reps, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, func_synthetic_arguments, diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index d904070e7b..b750b41613 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -1247,6 +1247,7 @@ pub(super) fn compile_function( class_field_counts: &cross_module.class_field_counts, class_init_chains: &cross_module.class_init_chains, class_header_image_globals: &cross_module.class_header_images, + class_birth_reps: &cross_module.class_birth_reps, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, func_synthetic_arguments, diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index 795237cc12..e58828a4a8 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -601,6 +601,7 @@ pub(super) fn compile_method( class_field_counts: &cross_module.class_field_counts, class_init_chains: &cross_module.class_init_chains, class_header_image_globals: &cross_module.class_header_images, + class_birth_reps: &cross_module.class_birth_reps, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, func_synthetic_arguments, diff --git a/crates/perry-codegen/src/codegen/method_static.rs b/crates/perry-codegen/src/codegen/method_static.rs index 457c08ddf7..235f6e2ff1 100644 --- a/crates/perry-codegen/src/codegen/method_static.rs +++ b/crates/perry-codegen/src/codegen/method_static.rs @@ -276,6 +276,7 @@ pub(in crate::codegen) fn compile_static_method( class_field_counts: &cross_module.class_field_counts, class_init_chains: &cross_module.class_init_chains, class_header_image_globals: &cross_module.class_header_images, + class_birth_reps: &cross_module.class_birth_reps, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, func_synthetic_arguments, diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index dbd775044d..05e9c72549 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -251,6 +251,7 @@ mod static_shape_ids; pub use static_shape_ids::{ assign_static_shape_ids, decode_static_seed, encode_static_seed, take_module_static_seeds, BirthProto, BirthShape, DefinedClassShape, ModuleBirth, ProgramClassShapeIds, TypedMasks, + STATIC_SEED_FORMAT, }; pub(crate) use static_shape_ids::{ static_region_slots, static_shape_id_for_foreign_global, static_shape_id_for_keys_global, @@ -2443,12 +2444,34 @@ fn compile_module_impl( inits.retain(|_, (class_id, _, _)| *class_id != u32::MAX); inits }; + // Charter step 5, T1: each class's birth rep (`typed_shape::class_birth_rep_in`), + // keyed like the header-image inits by keys global. Two names sharing a + // keys global that disagree get `Any` (0): every consumer reads this map, + // so they still agree with each other. + let class_birth_reps_map: std::collections::HashMap = { + let mut reps: std::collections::HashMap = std::collections::HashMap::new(); + for (class_name, keys_global) in &class_keys_globals_map { + let rep = crate::typed_shape::class_birth_rep_in( + &class_table, + &class_keys_globals_map, + &class_init_chains_map, + imported_stub_names.contains(class_name.as_str()), + class_name, + ); + let entry = reps.entry(keys_global.clone()).or_insert(rep); + if *entry != rep { + *entry = 0; + } + } + reps + }; if let Some(births) = births { *births = static_shape_ids::module_births( &module_prefix, &class_keys_init_data, defined_class_keys_len, &class_header_image_inits, + &class_birth_reps_map, &class_ids, ); return Ok(Vec::new()); @@ -2457,6 +2480,7 @@ fn compile_module_impl( &module_prefix, &class_keys_init_data, &class_header_image_inits, + &class_birth_reps_map, &class_ids, &opts.static_shape_ids, &opts.program_class_shape_ids, @@ -2568,6 +2592,7 @@ fn compile_module_impl( class_field_counts: class_field_counts_map, class_init_chains: class_init_chains_map, class_header_images: class_header_images_map, + class_birth_reps: class_birth_reps_map, imported_class_ctors: opts .imported_classes .iter() diff --git a/crates/perry-codegen/src/codegen/opts.rs b/crates/perry-codegen/src/codegen/opts.rs index 027a9aafcf..f2867d74dc 100644 --- a/crates/perry-codegen/src/codegen/opts.rs +++ b/crates/perry-codegen/src/codegen/opts.rs @@ -988,6 +988,10 @@ pub(crate) struct CrossModuleCtx { /// both come from `target_layout::inline_alloc_gc_packed`, but a header /// word is not something to trust by argument. pub class_header_images: std::collections::HashMap, + /// Keys global -> the class's birth rep word (charter step 5, T1; + /// `typed_shape::class_birth_rep_in`): what module init mints and what + /// every inline allocation and class-field store of the class obeys. + pub class_birth_reps: std::collections::HashMap, /// Imported class constructor function names. Maps class_name → /// full constructor symbol (e.g. "Editor" → "hone_editor_...__Editor_constructor"). /// Populated from `opts.imported_classes`. diff --git a/crates/perry-codegen/src/codegen/static_shape_ids.rs b/crates/perry-codegen/src/codegen/static_shape_ids.rs index 777cf37a97..5972500450 100644 --- a/crates/perry-codegen/src/codegen/static_shape_ids.rs +++ b/crates/perry-codegen/src/codegen/static_shape_ids.rs @@ -54,20 +54,29 @@ pub struct BirthShape { pub live: u32, pub proto: BirthProto, pub typed: Option, + /// The birth representation word (charter step 5, T1; + /// `typed_shape::class_birth_rep_in`): `F64` lanes are shape identity at + /// runtime, so they are content here. A class born with an `F64` lane and + /// an importer's all-`Any` stub of the same keys are two contents, and + /// the stub never adopts the definer's id. + pub rep: u64, } impl BirthShape { /// A literal content without a typed layout: the runtime seed mints it - /// from its key names alone (`js_shape_seed_plain`). Class contents are - /// seeded by their class registration, typed ones by their typed install. + /// from its key names and its birth rep (`js_shape_seed_plain`), the + /// same facts the literal's own mint names, so the seed and a lazy mint + /// are one ShapeId. Class contents are seeded by their class + /// registration, typed ones by their typed install. pub fn is_seedable(&self) -> bool { self.proto == BirthProto::Literal && self.typed.is_none() } /// The facts the runtime mints for this content, without the masks: a - /// typed layout and a structural mint of the same class share them. - pub(crate) fn structure(&self) -> (&[u8], u32, u32, &BirthProto) { - (&self.keys, self.key_count, self.live, &self.proto) + /// typed layout and a structural mint of the same class share them. The + /// rep is a runtime fact, so it is part of them. + pub(crate) fn structure(&self) -> (&[u8], u32, u32, &BirthProto, u64) { + (&self.keys, self.key_count, self.live, &self.proto, self.rep) } /// A stable 64-bit FNV-1a over the content (never `RandomState`: the id @@ -100,6 +109,12 @@ impl BirthShape { } } } + // An all-`Any` rep adds nothing, so contents without an `F64` lane + // keep the ids they had before the rep was content. + if self.rep != 0 { + eat(&[3]); + eat(&self.rep.to_le_bytes()); + } h } @@ -298,6 +313,7 @@ pub(crate) fn class_birth( module_prefix: &str, entry: &ClassKeysInit, class_header_image_inits: &HashMap, + class_birth_reps: &HashMap, class_ids: &HashMap, ) -> ClassBirth { let (global_name, packed, field_count, raw_mask_words, pointer_mask_words) = entry; @@ -344,6 +360,7 @@ pub(crate) fn class_birth( raw_f64_words: raw_mask_words.clone(), pointer_words: pointer_mask_words.clone(), }), + rep: class_birth_reps.get(global_name).copied().unwrap_or(0), }); ClassBirth { class_id, @@ -378,6 +395,7 @@ pub(crate) fn set_module_static_ids( module_prefix: &str, class_keys_init_data: &[ClassKeysInit], class_header_image_inits: &HashMap, + class_birth_reps: &HashMap, class_ids: &HashMap, assigned: &[(BirthShape, u32)], program: &ProgramClassShapeIds, @@ -389,7 +407,13 @@ pub(crate) fn set_module_static_ids( class_keys_init_data .iter() .filter_map(|entry| { - let birth = class_birth(module_prefix, entry, class_header_image_inits, class_ids); + let birth = class_birth( + module_prefix, + entry, + class_header_image_inits, + class_birth_reps, + class_ids, + ); let shape = birth.shape.as_ref()?; let own = *by_content.get(shape)?; let id = program.resolved_id(&entry.0, birth.class_id, shape, own); @@ -420,21 +444,34 @@ pub fn take_module_static_seeds() -> Vec<(u32, BirthShape)> { MODULE_SEEDS.with(|s| std::mem::take(&mut *s.borrow_mut()).into_iter().collect()) } +/// The version of the seed sidecar's line format ([`encode_static_seed`]), +/// part of the object-cache key: an entry written in another format is a +/// miss, never a line this decoder reads as other facts (a pinned +/// `PERRY_OBJECT_CACHE_BUILD_ID` keeps the build id across compilers). +pub const STATIC_SEED_FORMAT: &str = "2"; + /// One seed as a line of the object cache's seed sidecar: -/// ` `. +/// ` `, +/// the rep as `0x`-prefixed hex. Every field the seed mints from is in the +/// line: a warm link seeds exactly the facts the cold one did. pub fn encode_static_seed(id: u32, shape: &BirthShape) -> String { let hex: String = shape.keys.iter().map(|b| format!("{b:02x}")).collect(); - format!("{id} {} {} {hex}", shape.key_count, shape.live) + format!( + "{id} {} {} {hex} {:#x}", + shape.key_count, shape.live, shape.rep + ) } -/// The inverse of [`encode_static_seed`]; `None` for a malformed line. +/// The inverse of [`encode_static_seed`]; `None` for a malformed line +/// (including a line of another format, which lacks the rep field). pub fn decode_static_seed(line: &str) -> Option<(u32, BirthShape)> { let mut it = line.split_ascii_whitespace(); let id = it.next()?.parse().ok()?; let key_count = it.next()?.parse().ok()?; let live = it.next()?.parse().ok()?; - let hex = it.next().unwrap_or(""); - if it.next().is_some() || hex.len() % 2 != 0 { + let hex = it.next()?; + let rep = u64::from_str_radix(it.next()?.strip_prefix("0x")?, 16).ok()?; + if it.next().is_some() || hex.is_empty() || hex.len() % 2 != 0 { return None; } let keys = (0..hex.len()) @@ -449,6 +486,7 @@ pub fn decode_static_seed(line: &str) -> Option<(u32, BirthShape)> { live, proto: BirthProto::Literal, typed: None, + rep, }, )) } @@ -474,9 +512,16 @@ pub(crate) fn static_shape_id_for_keys_global(keys_global: &str) -> Option /// data summary, no holes and generation 0, so the slots follow from the /// keys alone and this is the word the runtime would publish for the id. /// `None` (the region keeps its learned supplier alone) when a key is not an -/// inline key of the birth shape. A returned id is a guard immediate: it -/// joins the module's seed set like any other. -pub(crate) fn static_region_slots(keys_global: &str, keys: &[String]) -> Option<(u32, Vec)> { +/// inline key of the birth shape, or when a key in `boxed_mask` (a bare +/// store of a value not proven a canonical double) sits on a non-`Any` lane +/// of the birth rep: the runtime's pack refuses that word too (charter step +/// 5). A returned id is a guard immediate: it joins the module's seed set +/// like any other. +pub(crate) fn static_region_slots( + keys_global: &str, + keys: &[String], + boxed_mask: u32, +) -> Option<(u32, Vec)> { MODULE_STATIC_IDS.with(|m| { let m = m.borrow(); let (id, shape) = m.get(keys_global)?; @@ -496,6 +541,14 @@ pub(crate) fn static_region_slots(keys_global: &str, keys: &[String]) -> Option< (at < 32).then_some(at as u32) }) .collect::>>()?; + let lane_is_any = |slot: u32| (shape.rep >> (2 * slot)) & 0b11 == 0; + if slots + .iter() + .enumerate() + .any(|(i, &slot)| boxed_mask & (1 << i) != 0 && !lane_is_any(slot)) + { + return None; + } note_guard_id(*id, Some(shape)); Some((*id, slots)) }) @@ -535,13 +588,20 @@ pub(crate) fn module_births( class_keys_init_data: &[ClassKeysInit], defined_len: usize, class_header_image_inits: &HashMap, + class_birth_reps: &HashMap, class_ids: &HashMap, ) -> Vec { class_keys_init_data .iter() .enumerate() .filter_map(|(i, entry)| { - let birth = class_birth(module_prefix, entry, class_header_image_inits, class_ids); + let birth = class_birth( + module_prefix, + entry, + class_header_image_inits, + class_birth_reps, + class_ids, + ); Some(ModuleBirth { keys_global: entry.0.clone(), class_id: birth.class_id, diff --git a/crates/perry-codegen/src/codegen/static_shape_ids_tests.rs b/crates/perry-codegen/src/codegen/static_shape_ids_tests.rs index 1b0d4426bb..ca396503f7 100644 --- a/crates/perry-codegen/src/codegen/static_shape_ids_tests.rs +++ b/crates/perry-codegen/src/codegen/static_shape_ids_tests.rs @@ -7,6 +7,7 @@ fn class(keys: &str, count: u32, cid: u32) -> BirthShape { live: count, proto: BirthProto::Class(cid), typed: None, + rep: 0, } } @@ -225,6 +226,101 @@ fn a_structural_stub_of_the_definers_facts_resolves_to_the_definers_typed_id() { assert_eq!(program.resolved_id("k_imp__C", 21, &typed_stub, 9), 9); } +/// Charter step 5, T1 (b): the birth rep is content. A definer born with an +/// `F64` lane and an importer's all-`Any` stub of the same keys are two +/// contents with two ids, and the stub never resolves to the definer's id: +/// its inline allocation fills `undefined` and cannot know the definer's +/// constructor proof, so it keeps its own structural id. +#[test] +fn an_f64_birth_rep_is_content_and_a_stub_never_adopts_it() { + let rep = 0b01 << 2; // F64 lane at slot 1 + let stub = class("next\0value\0", 2, 22); + let def = BirthShape { + rep, + ..typed("next\0value\0", 2, 22, 0b10, 0b01) + }; + let def_any = typed("next\0value\0", 2, 22, 0b10, 0b01); + assert_ne!(def.content_hash(), def_any.content_hash()); + assert_ne!(def.structure(), stub.structure()); + let births = [ + birth("k_def__D", 22, true, &def), + birth("k_imp__D", 22, false, &stub), + ]; + let ids = assign_static_shape_ids(births.iter().map(|b| &b.shape)); + assert_ne!(ids[&def], ids[&stub]); + let program = ProgramClassShapeIds::from_births(&births, &ids); + assert_eq!( + program.resolved_id("k_imp__D", 22, &stub, ids[&stub]), + ids[&stub], + "the stub keeps its own id" + ); + // A structural (untyped) definer with an F64 lane: same rule. + let def_plain = BirthShape { + rep, + ..class("next\0value\0", 2, 23) + }; + let stub23 = class("next\0value\0", 2, 23); + let births = [ + birth("k_def__E", 23, true, &def_plain), + birth("k_imp__E", 23, false, &stub23), + ]; + let ids = assign_static_shape_ids(births.iter().map(|b| &b.shape)); + let program = ProgramClassShapeIds::from_births(&births, &ids); + assert_eq!(program.resolved_id("k_imp__E", 23, &stub23, 11), 11); + // A literal with an F64 lane is seeded like an all-`Any` one: the seed + // carries its rep, so it mints the literal's own facts. + let lit = BirthShape { + proto: BirthProto::Literal, + rep, + ..class("a\0b\0", 2, 0) + }; + assert!(lit.is_seedable()); +} + +/// The seed sidecar carries the birth rep: a warm link replays exactly the +/// facts a cold one seeded. A line without the rep (another format) is +/// malformed, never an all-`Any` seed of the same keys. +#[test] +fn a_seed_line_round_trips_the_birth_rep() { + for rep in [0u64, 0b0101, 0b01 << 20] { + let lit = BirthShape { + proto: BirthProto::Literal, + rep, + ..class("lt_u\0lt_v\0", 2, 0) + }; + let line = encode_static_seed(0x1000_0077, &lit); + assert_eq!( + decode_static_seed(&line), + Some((0x1000_0077, lit)), + "{line}" + ); + } + assert_eq!(decode_static_seed("268435575 2 2 6c745f7500"), None); + assert_eq!(decode_static_seed("268435575 2 2 6c745f7500 5"), None); + assert_eq!(decode_static_seed("268435575 2 2 6c745f7500 0x5 x"), None); +} + +#[test] +fn class_birth_reads_the_birth_rep_of_its_keys_global() { + let prefix = "m"; + let class_ids: HashMap = [("Pair".to_string(), 57)].into_iter().collect(); + let images = HashMap::new(); + let pair: ClassKeysInit = ( + "perry_class_keys_m__Pair".into(), + "a\0b\0".into(), + 2, + vec![], + vec![], + ); + let reps: HashMap = [("perry_class_keys_m__Pair".to_string(), 0b0101u64)] + .into_iter() + .collect(); + let b = class_birth(prefix, &pair, &images, &reps, &class_ids); + assert_eq!(b.shape.unwrap().rep, 0b0101); + let b = class_birth(prefix, &pair, &images, &HashMap::new(), &class_ids); + assert_eq!(b.shape.unwrap().rep, 0); +} + #[test] fn a_class_id_defined_twice_has_no_program_entry() { let a = class("a\0", 1, 31); @@ -264,11 +360,11 @@ fn class_birth_names_anon_shapes_as_literals_and_skips_class_zero() { vec![], vec![], ); - let a = class_birth(prefix, &anon, &images, &class_ids); + let a = class_birth(prefix, &anon, &images, &HashMap::new(), &class_ids); assert_eq!(a.shape.unwrap().proto, BirthProto::Literal); - let p = class_birth(prefix, &point, &images, &class_ids); + let p = class_birth(prefix, &point, &images, &HashMap::new(), &class_ids); assert_eq!(p.shape.unwrap().proto, BirthProto::Class(56)); - let o = class_birth(prefix, &orphan, &images, &class_ids); + let o = class_birth(prefix, &orphan, &images, &HashMap::new(), &class_ids); assert_eq!(o.class_id, 0); assert!(o.shape.is_none()); } diff --git a/crates/perry-codegen/src/codegen/string_pool.rs b/crates/perry-codegen/src/codegen/string_pool.rs index d1bc18823d..69f3579d10 100644 --- a/crates/perry-codegen/src/codegen/string_pool.rs +++ b/crates/perry-codegen/src/codegen/string_pool.rs @@ -128,6 +128,7 @@ pub(super) fn emit_string_pool( output_type: &str, class_keys_init_data: &[(String, String, u32, Vec, Vec)], class_header_image_inits: &std::collections::HashMap, + class_birth_reps: &HashMap, class_ids: &HashMap, classes: &HashMap, // The classes this module defines, by identity: the registration loops @@ -654,6 +655,7 @@ pub(super) fn emit_string_pool( module_prefix, &class_keys_init_data[idx], class_header_image_inits, + class_birth_reps, class_ids, ); let class_id = birth.class_id; @@ -668,6 +670,17 @@ pub(super) fn emit_string_pool( format!("@{}", packed_global_names[idx]) }; let len_str = packed.len().to_string(); + // Charter step 5, T1: the birth rep rides every class mint + // (`typed_shape::class_birth_rep_in`, one decision for the mint, the + // inline allocation's birth fill and the store precheck), the shape + // cache's mint beside the keys included: an outlined birth from that + // entry carries it. It is part of the content, so the static id + // already names it. + let rep_str = class_birth_reps + .get(global_name) + .copied() + .unwrap_or(0) + .to_string(); let arr = blk.call( I64, "js_build_class_keys_array", @@ -676,6 +689,7 @@ pub(super) fn emit_string_pool( (I32, &fc_str), (PTR, &packed_ref), (I32, &len_str), + (I64, &rep_str), ], ); let global_ref = format!("@{}", global_name); @@ -730,6 +744,7 @@ pub(super) fn emit_string_pool( (PTR, &pointer_mask_ref), (I32, &pointer_mask_words.len().to_string()), (I32, &requested), + (I64, &rep_str), ], ) } else if requested != "0" { @@ -751,6 +766,7 @@ pub(super) fn emit_string_pool( (I32, &live.to_string()), (I32, &cid_str), (I32, &requested), + (I64, &rep_str), ], ) } else { @@ -768,12 +784,18 @@ pub(super) fn emit_string_pool( (I32, &fc_str), (I32, &birth_live.to_string()), (I32, &cid_str), + (I64, &rep_str), ], ), _ => blk.call( I32, "js_object_shape_id_for_class_keys", - &[(I64, &arr), (I32, &fc_str), (I32, &cid_str)], + &[ + (I64, &arr), + (I32, &fc_str), + (I32, &cid_str), + (I64, &rep_str), + ], ), } }; diff --git a/crates/perry-codegen/src/expr/class_field_inline_guard.rs b/crates/perry-codegen/src/expr/class_field_inline_guard.rs index 64e2996b1d..eb221c85ff 100644 --- a/crates/perry-codegen/src/expr/class_field_inline_guard.rs +++ b/crates/perry-codegen/src/expr/class_field_inline_guard.rs @@ -62,6 +62,8 @@ const F64_EXP_MASK: &str = "9218868437227405312"; // 0x7FF0_0000_0000_0000 pub(crate) struct ClassFieldSubclassArm { pub class_id: u32, pub keys_global: String, + /// The subclass's birth rep word (`CrossModuleCtx::class_birth_reps`, T1). + pub birth_rep: u64, } /// A hierarchy wider than this turns the shape check into a longer compare @@ -152,6 +154,7 @@ pub(crate) fn class_field_subclass_arms( seen_ids.push(sub_id); arms.push(ClassFieldSubclassArm { class_id: sub_id, + birth_rep: ctx.class_birth_reps.get(&keys_global).copied().unwrap_or(0), keys_global, }); if arms.len() > MAX_CLASS_FIELD_SUBCLASS_ARMS { @@ -508,7 +511,24 @@ pub(crate) fn emit_class_field_inline_precheck( fast_label: &str, subclass_arms: &[ClassFieldSubclassArm], keys_global_name: &str, + field_index: u32, ) -> String { + // Charter step 5, T1 (c): a store the shape compare admits into an `F64` + // birth lane of ANY accepted class stores only a canonical double. That is + // the same plain-finite test the raw-f64 arm emits; a non-Number or + // non-finite value takes the guard call, whose checked store generalizes. + // It is decided here from the birth rep, not inferred from the declared + // field type, so a writer cannot raw-store into an `F64` lane by passing + // `require_raw_f64 = false`. + let f64_lane = crate::typed_shape::birth_rep_slot_is_f64( + ctx.class_birth_reps + .get(keys_global_name) + .copied() + .unwrap_or(0), + field_index, + ) || subclass_arms + .iter() + .any(|arm| crate::typed_shape::birth_rep_slot_is_f64(arm.birth_rep, field_index)); let deref_idx = ctx.new_block("class_field_inline.deref"); let guardcall_idx = ctx.new_block("class_field_inline.guardcall"); let deref_label = ctx.block_label(deref_idx); @@ -584,7 +604,7 @@ pub(crate) fn emit_class_field_inline_precheck( let bits = blk.and(I16, &reserved, &(mask as i16).to_string()); let facts_ok = blk.icmp_eq(I16, &bits, &(expected as i16).to_string()); ok = blk.and(I1, &ok, &facts_ok); - if let (Some(value_bits), true) = (set_value_bits, require_raw_f64) { + if let (Some(value_bits), true) = (set_value_bits, require_raw_f64 || f64_lane) { // Only a plain finite number may be stored raw. Non-finite // (exponent all-ones: +-Inf/NaN) and every NaN-boxed tag share the // all-ones exponent, so one mask/compare routes them to the call. diff --git a/crates/perry-codegen/src/expr/literal_descriptor.rs b/crates/perry-codegen/src/expr/literal_descriptor.rs index bf53ab0dde..e308695277 100644 --- a/crates/perry-codegen/src/expr/literal_descriptor.rs +++ b/crates/perry-codegen/src/expr/literal_descriptor.rs @@ -12,7 +12,7 @@ use crate::types::{DOUBLE, I32, PTR}; const MIN_NODES: usize = 256; const MAX_DEPTH: usize = 128; // Must match runtime::array::literal_descriptor::LiteralShape (repr(C)). -const SHAPE_TYPE: &str = "{ i32, i32, ptr, ptr, ptr, i32, ptr, i32 }"; +const SHAPE_TYPE: &str = "{ i32, i32, ptr, ptr, ptr, i32, ptr, i32, i64 }"; #[derive(Default)] struct Descriptor { @@ -59,10 +59,12 @@ impl Descriptor { crate::typed_shape::mask_global_name_from_keys_global(keys), ); let shape_id = crate::typed_shape::shape_id_global_name_from_keys_global(keys); + // The birth rep the module-init mint gave that id (T1). + let rep = ctx.class_birth_reps.get(keys).copied().unwrap_or(0); let index = u32::try_from(self.shapes.len()).ok()?; self.shapes.push(format!( "{SHAPE_TYPE} {{ i32 {class_id}, i32 {argc}, ptr @{keys}, ptr @{shape_id}, \ - ptr {raw_mask}, i32 {}, ptr {pointer_mask}, i32 {} }}", + ptr {raw_mask}, i32 {}, ptr {pointer_mask}, i32 {}, i64 {rep} }}", layout.raw_f64_mask_words.len(), layout.pointer_mask_words.len(), )); diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index f9a15702fc..aa7403d7f0 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -647,6 +647,8 @@ pub(crate) struct FnCtx<'a> { /// #8122: per-class inline-`new` header-image globals, see /// `CrossModuleCtx::class_header_images`. pub class_header_image_globals: &'a std::collections::HashMap, + /// `CrossModuleCtx::class_birth_reps` (keys global -> birth rep, T1). + pub class_birth_reps: &'a std::collections::HashMap, /// Imported class constructor metadata, keyed by effective imported class name. pub imported_class_ctors: &'a std::collections::HashMap, /// Per-function param signature: `(declared_param_count, diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs index 499ed7f701..f5f0d21402 100644 --- a/crates/perry-codegen/src/expr/property_set.rs +++ b/crates/perry-codegen/src/expr/property_set.rs @@ -1423,6 +1423,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, assignment_strict: bool) - &fast_label, &subclass_arms, &keys_global_name, + field_index, ); super::store_census::bump(ctx, super::store_census::CFIELD_IC_CALL); let guard_ok = ctx.block().call( diff --git a/crates/perry-codegen/src/expr/property_set/sloppy_class_field.rs b/crates/perry-codegen/src/expr/property_set/sloppy_class_field.rs index aeb480c381..bc52c5e7ab 100644 --- a/crates/perry-codegen/src/expr/property_set/sloppy_class_field.rs +++ b/crates/perry-codegen/src/expr/property_set/sloppy_class_field.rs @@ -192,6 +192,7 @@ pub(crate) fn try_lower_sloppy_class_field_store( &fast_label, &subclass_arms, &keys_global_name, + field_index, ); // Miss: the strict-aware runtime with `strict = 0`, so a rejected write @@ -328,6 +329,7 @@ fn try_lower_sloppy_class_field_boxed_store( &fast_label, &subclass_arms, &keys_global_name, + field_index, ); { diff --git a/crates/perry-codegen/src/expr/put_value_store_ic.rs b/crates/perry-codegen/src/expr/put_value_store_ic.rs index 701f79a1ed..70dade4cf5 100644 --- a/crates/perry-codegen/src/expr/put_value_store_ic.rs +++ b/crates/perry-codegen/src/expr/put_value_store_ic.rs @@ -146,7 +146,17 @@ pub(crate) const ADD_WAY_HASH: u32 = 0x9E37_79B1; /// Ways compared from the home on: the home, then the next (mod the block), /// where the runtime places a memo whose home an earlier one holds. pub(crate) const ADD_WAY_PROBES: usize = 2; -const ADD_SLOT_MASK: u64 = (1 << ADD_SLOT_BITS) - 1; +/// Charter step 5 (P2c): the guard's slot-field bit that marks a successor +/// whose lane at the slot is not `Any`. **Must equal +/// `perry_runtime::proxy::put_value::packed_add::ADD_F64_SLOT`.** +const ADD_F64_SLOT: u64 = 1 << (ADD_SLOT_BITS - 1); +const ADD_SLOT_MASK: u64 = ADD_F64_SLOT - 1; +/// The store word's slot half without its top bit, the runtime's +/// `packed_set::PACKED_SET_F64_SLOT` (the word's sign bit). +const PACKED_SLOT_INDEX_MASK: &str = "2147483647"; +/// A double's exponent field: all ones = an INT32/tagged box, an infinity or +/// a NaN, the values an `F64` lane refuses inline (DESIGN §3.2). +const F64_EXP_MASK: &str = "9218868437227405312"; // 0x7FF0_0000_0000_0000 /// Block-name stem of the key-add hit. const ADD_STEM: &str = "put.add"; /// `GC_FLAG_TENURED` (gc_flags byte). @@ -271,11 +281,13 @@ pub(crate) fn emit_static_store_ic( let tok_idx = ctx.new_block(&format!("{STORE_IC_STEM}.token")); let kind_idx = ctx.new_block(&format!("{STORE_IC_STEM}.kind")); + let rep_idx = ctx.new_block(&format!("{STORE_IC_STEM}.hit.rep")); let store_idx = ctx.new_block(&format!("{STORE_IC_STEM}.hit.store")); let miss_idx = ctx.new_block(&format!("{STORE_IC_STEM}.miss")); let merge_idx = ctx.new_block(&format!("{STORE_IC_STEM}.merge")); let tok_label = ctx.block_label(tok_idx); let kind_label = ctx.block_label(kind_idx); + let rep_label = ctx.block_label(rep_idx); let store_label = ctx.block_label(store_idx); let miss_label = ctx.block_label(miss_idx); let merge_label = ctx.block_label(merge_idx); @@ -438,11 +450,24 @@ pub(crate) fn emit_static_store_ic( let reserved_addr = ctx.block().sub(I64, &handle, "6"); let reserved_ptr = ctx.block().inttoptr(I64, &reserved_addr); let reserved = ctx.block().load(I16, &reserved_ptr); - ctx.block().br(&store_label); + ctx.block().br(&rep_label); + + // Charter step 5 (P2c, DESIGN §3.2): the store check. A word with its sign + // bit set names an `F64` lane: a value whose exponent is not all ones is + // a finite double, stored inline as is; anything else (a box, an + // infinity, a NaN) takes the miss, whose store is the checked funnel + // (canonicalize, or generalize the lane with the shape word first). + ctx.current_block = rep_idx; + let f64_slot = ctx.block().icmp_slt(I64, &word, "0"); + let exponent = ctx.block().and(I64, value_bits, F64_EXP_MASK); + let boxed = ctx.block().icmp_eq(I64, &exponent, F64_EXP_MASK); + let refuse = ctx.block().and(I1, &f64_slot, &boxed); + ctx.block().cond_br(&refuse, &miss_label, &store_label); // The store, then the GC's obligations for the bits actually stored. ctx.current_block = store_idx; - let slot = ctx.block().lshr(I64, &word, "32"); + let slot_half = ctx.block().lshr(I64, &word, "32"); + let slot = ctx.block().and(I64, &slot_half, PACKED_SLOT_INDEX_MASK); let header_size = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string(); let fields = ctx.block().add(I64, &handle, &header_size); let fields_ptr = ctx.block().inttoptr(I64, &fields); @@ -554,11 +579,13 @@ fn emit_key_add_hit( miss_label: &str, merge_label: &str, ) -> String { + let rep_idx = ctx.new_block(&format!("{ADD_STEM}.rep")); let obj_idx = ctx.new_block(&format!("{ADD_STEM}.object")); let layout_idx = ctx.new_block(&format!("{ADD_STEM}.layout")); let slow_idx = ctx.new_block(&format!("{ADD_STEM}.layout.slow")); let forget_idx = ctx.new_block(&format!("{ADD_STEM}.layout.forget")); let store_idx = ctx.new_block(&format!("{ADD_STEM}.hit.store")); + let rep_label = ctx.block_label(rep_idx); let obj_label = ctx.block_label(obj_idx); let layout_label = ctx.block_label(layout_idx); let slow_label = ctx.block_label(slow_idx); @@ -578,7 +605,18 @@ fn emit_key_add_hit( .load_atomic_monotonic(I64, "@PERRY_PROTO_VALIDITY", 8); let recorded = ctx.block().lshr(I64, &guard, &ADD_SLOT_BITS.to_string()); let gen_eq = ctx.block().icmp_eq(I64, &now, &recorded); - ctx.block().cond_br(&gen_eq, &obj_label, miss_label); + ctx.block().cond_br(&gen_eq, &rep_label, miss_label); + + // Charter step 5 (P2c): a memo whose successor has an `F64` lane at the + // slot admits only a value whose exponent is not all ones (a finite + // double); the miss serves the rest, before anything is stamped. + ctx.current_block = rep_idx; + let flag = ctx.block().and(I64, &guard, &ADD_F64_SLOT.to_string()); + let f64_slot = ctx.block().icmp_ne(I64, &flag, "0"); + let exponent = ctx.block().and(I64, value_bits, F64_EXP_MASK); + let boxed = ctx.block().icmp_eq(I64, &exponent, F64_EXP_MASK); + let refuse = ctx.block().and(I1, &f64_slot, &boxed); + ctx.block().cond_br(&refuse, miss_label, &obj_label); // The GcHeader's first word (obj_type | gc_flags << 8 | _reserved << 16). // No receiver-kind admission: the memo's pre-shape is an `Ordinary` shape diff --git a/crates/perry-codegen/src/expr/region_loop_tests.rs b/crates/perry-codegen/src/expr/region_loop_tests.rs index 51489e5d0c..1be5005334 100644 --- a/crates/perry-codegen/src/expr/region_loop_tests.rs +++ b/crates/perry-codegen/src/expr/region_loop_tests.rs @@ -337,3 +337,49 @@ fn a_region_that_stores_every_key_it_names_has_no_spill_copy() { "a stored key is published only inline, so no spill copy is needed:\n{ir}" ); } + +/// The prime call's last argument: the boxed-store mask (charter step 5). +fn prime_boxed_masks(ir: &str) -> Vec { + ir.lines() + .filter(|l| l.contains("@js_region_loop_prime(")) + .filter_map(|l| { + let args = l.rsplit_once("i32 ")?.1; + args.trim_end_matches(')').trim().parse().ok() + }) + .collect() +} + +/// Charter step 5: a bare store runs no field-representation check, so the +/// region tells the runtime which keys it may store a value not proven a +/// canonical double into; the runtime refuses a word whose shape has a +/// non-`Any` lane at such a key (`region_loop_pack`, `F64Stored`). +#[test] +fn a_bare_store_of_a_value_not_proven_a_double_names_its_key_to_the_prime() { + let boxed = loop_ir("region_loop_boxed", vec![put("x", Expr::LocalGet(V))]); + let masks = prime_boxed_masks(&boxed); + assert!(!masks.is_empty(), "no prime call in\n{boxed}"); + assert!( + masks.iter().all(|&m| m == 1), + "the `any` store to `x` must name key 0: {masks:?}" + ); + let raw = loop_ir( + "region_loop_raw", + vec![ + put("x", Expr::Number(1.5)), + Stmt::Expr(Expr::LocalSet( + H, + Box::new(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(H)), + right: Box::new(get("x")), + }), + )), + ], + ); + let masks = prime_boxed_masks(&raw); + assert!(!masks.is_empty(), "no prime call in\n{raw}"); + assert!( + masks.iter().all(|&m| m == 0), + "a literal double is a valid value of every lane: {masks:?}" + ); +} diff --git a/crates/perry-codegen/src/expr/write_pic_barrier_tests.rs b/crates/perry-codegen/src/expr/write_pic_barrier_tests.rs index 9d1c7de1a8..98d356b64e 100644 --- a/crates/perry-codegen/src/expr/write_pic_barrier_tests.rs +++ b/crates/perry-codegen/src/expr/write_pic_barrier_tests.rs @@ -48,6 +48,8 @@ use perry_hir::{Expr, Function, Module, ModuleInitKind, Param, Stmt}; /// THE shape compare, and the block holding the slot store. const HIT: &str = "put.pic.token"; const HIT_STORE: &str = "put.pic.hit.store"; +/// Charter step 5 (P2c): the store check between the admission and the store. +const HIT_REP: &str = "put.pic.hit.rep"; /// The pointer-bearing arm. The IC passes the `"put.pic"` stem precisely so an /// assertion about THIS site cannot be satisfied by a class-field store /// elsewhere in the same module. @@ -475,8 +477,8 @@ fn store_ic_hit_path_reads_no_gc_kind_or_forwarded_byte() { /// Charter step 3: the receiver kind and the Array-subclass numeric proof are /// SHAPE facts (`perry_runtime::object::shapes::store_kind` — the runtime /// publishes a word only for an `Ordinary` shape), so nothing per object -/// stands between the shape compare and the store: the hit block loads -/// `_reserved` for the barrier only and branches straight to the store, with +/// stands between the shape compare and the store check: the hit block loads +/// `_reserved` for the barrier only and branches straight to the rep block, with /// no `class_id` read and no proof or ordinary-mark mask. Sabotage: /// re-inserting any of the old per-object tests turns this red. #[test] @@ -489,15 +491,28 @@ fn store_ic_hit_reads_no_per_object_receiver_fact() { let kind = block(&ir, "put.pic.kind").unwrap_or_else(|| panic!("hit block:\n{ir}")); let term = kind.lines().last().unwrap_or("").trim().to_string(); assert!( - term.starts_with("br label %") - && label_is(term.trim_start_matches("br label %"), HIT_STORE), - "a matched shape stores unconditionally: {term}\n{kind}" + term.starts_with("br label %") && label_is(term.trim_start_matches("br label %"), HIT_REP), + "a matched shape goes straight on to the store check: {term}\n{kind}" ); assert!( !kind.contains("load i32") && !kind.contains(", 128") && !kind.contains(", 768"), "the hit reads no class id and tests no proof / ordinary-mark bit:\n{kind}" ); - // Exactly the hit block enters the store. + // The store check (DESIGN §3.2): a word whose sign bit is set (an `F64` + // lane) refuses a value whose exponent is all ones to the miss; every + // other store goes on to the store block. + let rep = block(&ir, HIT_REP).unwrap_or_else(|| panic!("rep block:\n{ir}")); + assert!( + rep.contains("icmp slt i64") && rep.contains("9218868437227405312"), + "the rep block must test the word's F64 flag and the value's exponent:\n{rep}" + ); + let (_, r_true, r_false) = branch_targets(rep.lines().last().unwrap_or("").trim()); + assert!( + label_is(&r_true, "put.pic.miss") && label_is(&r_false, HIT_STORE), + "a refused value misses, anything else stores:\n{rep}" + ); + + // No other edge reaches the store. let into_store = ir .lines() .filter(|l| { @@ -510,7 +525,7 @@ fn store_ic_hit_reads_no_per_object_receiver_fact() { .count(); assert_eq!( into_store, 1, - "only the matched-shape hit may enter the store:\n{ir}" + "only the store check may enter the store:\n{ir}" ); } diff --git a/crates/perry-codegen/src/lib.rs b/crates/perry-codegen/src/lib.rs index 912749b500..3c8f50cf8a 100644 --- a/crates/perry-codegen/src/lib.rs +++ b/crates/perry-codegen/src/lib.rs @@ -95,7 +95,7 @@ pub use codegen::{ ExportedObjectLiteralCapability, FpContractMode, ImportedClass, ImportedObjectLiteral, ImportedObjectLiteralMethod, ModuleBirth, NamespaceEntry, NamespaceEntryKind, ObjectLiteralMethodCandidate, ProgramClassShapeIds, ResolvedConstructorContracts, - ShortSpreadMethodCandidate, TypedMasks, + ShortSpreadMethodCandidate, TypedMasks, STATIC_SEED_FORMAT, }; // #10399: whole-program Worker flag, set by the driver before module codegen. pub use codegen::{program_has_worker, set_program_has_worker}; diff --git a/crates/perry-codegen/src/lower_call/class_birth_rep_tests.rs b/crates/perry-codegen/src/lower_call/class_birth_rep_tests.rs new file mode 100644 index 0000000000..892c0e5380 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/class_birth_rep_tests.rs @@ -0,0 +1,66 @@ +//! Charter step 5, T1: the class birth rep is decided once, in codegen +//! (`typed_shape::class_birth_rep_in`), and every consumer takes it from +//! there: the module-init mint, the inline allocation's birth fill and the +//! class-field store precheck. + +use super::typed_shape_bake_tests::{emit, loop_new_module}; +use perry_hir::types::Type; +use perry_hir::Expr; + +/// The last argument of the first call to `callee` in `ir`. +fn mint_rep(ir: &str, callee: &str) -> String { + let at = ir + .find(&format!("call i32 @{callee}(")) + .unwrap_or_else(|| panic!("no {callee} call:\n{ir}")); + let line = ir[at..].lines().next().unwrap(); + let args = line.rsplit_once(')').unwrap().0; + args.rsplit_once(", ").unwrap().1.to_string() +} + +/// (a): a class declarable at allocation mints `F64` for exactly its +/// `number` fields (pointer-free and pointer-bearing mints alike); one whose +/// `number` field could be read before its constructor store (a field +/// initializer runs first) mints all-`Any`. +#[test] +fn the_birth_rep_is_f64_for_exactly_the_raw_f64_fields_declared_at_allocation() { + // `class Pair { a: number; b: number }`, both prologue-assigned. + let ir = emit(&loop_new_module("Pair", Type::Number, Expr::Integer(2))); + assert_eq!(mint_rep(&ir, "js_object_shape_id_for_class_keys"), "i64 5"); + // `class Link { a: number; b: string }`: only slot 0 is raw-f64. + let ir = emit(&loop_new_module( + "Link", + Type::String, + Expr::String("s".into()), + )); + assert_eq!(mint_rep(&ir, "js_gc_typed_shape_id_for_keys"), "i64 1"); + // `class Late { a: number; b: number = 3 }`: an initializer runs before + // the constructor body, so nothing is declared at allocation. + let mut m = loop_new_module("Late", Type::Number, Expr::Integer(2)); + m.classes[0].fields[1].init = Some(Expr::Number(3.0)); + let ir = emit(&m); + assert_eq!(mint_rep(&ir, "js_object_shape_id_for_class_keys"), "i64 0"); +} + +/// (c) + (d): the inline allocation birth-fills the `F64` lanes the mint +/// declared with +0.0 (`store i64 0`), and the undeclared twin fills them with +/// `undefined`: one decision drives the mint and the fill. +#[test] +fn the_inline_allocation_fills_exactly_the_minted_f64_lanes() { + let undefined = format!("store i64 {}, ptr", crate::nanbox::TAG_UNDEFINED_I64); + let declared = emit(&loop_new_module("Pair", Type::Number, Expr::Integer(2))); + let mut m = loop_new_module("Late", Type::Number, Expr::Integer(2)); + m.classes[0].fields[1].init = Some(Expr::Number(3.0)); + let late = emit(&m); + let fill = |ir: &str| { + ( + ir.matches("store i64 0, ptr").count(), + ir.matches(undefined.as_str()).count(), + ) + }; + let (zeros, undef) = fill(&declared); + let (late_zeros, late_undef) = fill(&late); + assert!( + zeros >= late_zeros + 2 && late_undef >= undef + 2, + "declared: {zeros} zero / {undef} undefined fills; undeclared: {late_zeros} / {late_undef}\n{declared}" + ); +} diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index f310b92b3f..2f7ac61896 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -97,6 +97,8 @@ mod new_ctor_args; mod new_error_init; mod new_helpers; pub(crate) use new_helpers::emit_ctor_return_override; +#[cfg(test)] +mod class_birth_rep_tests; mod omitted_native_params; mod options; mod private_method; diff --git a/crates/perry-codegen/src/lower_call/new_alloc.rs b/crates/perry-codegen/src/lower_call/new_alloc.rs index 91de56ecec..56de4c254a 100644 --- a/crates/perry-codegen/src/lower_call/new_alloc.rs +++ b/crates/perry-codegen/src/lower_call/new_alloc.rs @@ -565,8 +565,16 @@ fn emit_instance_alloc_inner( ctx.pending_declares.push(( "js_object_alloc_class_inline_keys_stamped".to_string(), I64, - vec![I32, I32, I32, I64, I32], + vec![I32, I32, I32, I64, I32, I64], )); + // The birth rep module init minted that id with (T1): a birth + // the runtime cannot stamp with it verbatim still carries it. + let rep = ctx + .class_birth_reps + .get(&keys_global_name) + .copied() + .unwrap_or(0) + .to_string(); ctx.block().call( I64, "js_object_alloc_class_inline_keys_stamped", @@ -576,6 +584,7 @@ fn emit_instance_alloc_inner( (I32, &field_count.to_string()), (I64, &keys_ptr), (I32, &shape_id), + (I64, &rep), ], ) } else { @@ -803,6 +812,11 @@ fn emit_instance_alloc_inner( ctx.class_header_images.insert(image_key, source.clone()); source }; + let birth_rep = ctx + .class_birth_reps + .get(&keys_global_name) + .copied() + .unwrap_or(0); let header_image = match image_source { crate::expr::HeaderImageSource::EntrySlot(slot) => { ctx.block().load("<2 x i64>", &slot) @@ -837,11 +851,23 @@ fn emit_instance_alloc_inner( // `undefined`/pointer (e.g. `marked`'s `this.defaults`), the constructor // crashed with "Cannot read properties of undefined". Slots start // at raw + GcHeader(8) + ObjectHeader(16) = raw + 24 (#8047). + // + // Charter step 5, T1: an `F64` lane of the class's birth rep starts + // as +0.0 instead (the same word module init minted the ShapeId + // with), so the representation invariant holds before the + // constructor's stores; the constructor proof behind the lane says + // nothing reads the slot first. The runtime allocator does the same + // (`field_rep_store::birth_fill_f64_lanes`). for i in 0..alloc_field_count { let slot_off = GC_HEADER_SIZE + object_header_size + i * FIELD_SLOT_SIZE; let slot_ptr = blk.gep(I8, &raw, &[(I64, &slot_off.to_string())]); - // GC_STORE_AUDIT(INIT): freshly allocated inline object slot initialized to undefined. - blk.store(I64, crate::nanbox::TAG_UNDEFINED_I64, &slot_ptr); + if crate::typed_shape::birth_rep_slot_is_f64(birth_rep, i as u32) { + // GC_STORE_AUDIT(INIT): fresh F64 birth lane initialized to +0.0 (T1). + blk.store(I64, "0", &slot_ptr); + } else { + // GC_STORE_AUDIT(INIT): freshly allocated inline object slot initialized to undefined. + blk.store(I64, crate::nanbox::TAG_UNDEFINED_I64, &slot_ptr); + } } // User pointer = raw + 8 (the ObjectHeader address — what the diff --git a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs index e90278182a..346d08feaf 100644 --- a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs +++ b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs @@ -12,10 +12,13 @@ use crate::nanbox::double_literal; use crate::type_analysis::receiver_class_name; use crate::types::{DOUBLE, I1, I32, I64}; +#[path = "dynamic_dispatch_collapse.rs"] +mod collapse; #[path = "dispatch_receiver_class.rs"] mod dispatch_receiver_class; #[path = "dynamic_dispatch_tower.rs"] mod tower; +use collapse::{emit_collapsed_instance_dispatch, method_dispatch_collapse_enabled}; use dispatch_receiver_class::*; use tower::{emit_tower_pshape_call, tower_pshape_route}; @@ -1516,15 +1519,43 @@ pub(crate) fn try_lower_instance_method_call( None => ok, }); } + let shape_idx = ctx.new_block("ptr_shape_method.shape"); let typed_idx = ctx.new_block("ptr_shape_method.typed"); let generic_idx = ctx.new_block("ptr_shape_method.generic"); let merge_idx = ctx.new_block("ptr_shape_method.merge"); + let shape_label = ctx.block_label(shape_idx); let typed_label = ctx.block_label(typed_idx); let generic_label = ctx.block_label(generic_idx); let merge_label = ctx.block_label(merge_idx); match all_plain { - Some(cond) => ctx.block().cond_br(&cond, &typed_label, &generic_label), - None => ctx.block().br(&typed_label), + Some(cond) => ctx.block().cond_br(&cond, &shape_label, &generic_label), + None => ctx.block().br(&shape_label), + } + // Provenance proves the receiver's CLASS, not its + // field representations: an alias the local escaped + // to may store a non-Number into a field, which moves + // the object off its birth shape (`field_rep_store`). + // The clone reads its fields as raw doubles, so it + // runs only while the (class id, ShapeId) pair is the + // class's own — the shape pins the `F64` lanes. + ctx.current_block = shape_idx; + let class_id = ctx.class_ids.get(&class_name).copied(); + let keys_global = ctx.class_keys_globals.get(&class_name).cloned(); + match (class_id, keys_global) { + (Some(class_id), Some(keys_global)) => { + let obj_bits = ctx.block().bitcast_double_to_i64(&recv_box); + crate::expr::class_field_inline_guard::emit_class_field_read_precheck( + ctx, + &obj_bits, + &class_id.to_string(), + true, + &typed_label, + &[], + &keys_global, + ); + ctx.block().br(&generic_label); + } + _ => ctx.block().br(&generic_label), } ctx.current_block = typed_idx; let mut typed_args: Vec<(crate::types::LlvmType, &str)> = @@ -1819,155 +1850,3 @@ pub(crate) fn try_lower_instance_method_call( } Ok(None) } - -/// Whether to collapse the instance method-dispatch tower in full-outline mode. -/// On by default whenever full-outline is active; `PERRY_OUTLINE_METHOD_DISPATCH=0` -/// / `off` / `false` keeps the inline class-id switch tower (escape hatch / -/// differential-test isolation). -fn method_dispatch_collapse_enabled() -> bool { - !matches!( - std::env::var("PERRY_OUTLINE_METHOD_DISPATCH").as_deref(), - Ok("0") | Ok("off") | Ok("false") - ) -} - -/// #5391 path 4: full-outlined collapse of the instance method-dispatch tower -/// (see the call site in `try_lower_instance_method_call`). -/// -/// Emits the own-property override probe (unchanged semantics) and, on the -/// non-override path, a SINGLE by-name `js_native_call_method` instead of the -/// per-implementor class-id switch tower. `js_native_call_method` is the tower's -/// own default arm and resolves the user-class method through its (class_id, -/// name) vtable registry, so behavior is preserved while the per-site IR shrinks -/// from ~6 + N blocks (N = implementor count) to a fixed 3 blocks. The raw user -/// args are marshalled once into an entry-block array and shared by both arms; -/// `js_native_call_method` / `js_native_call_value` apply their own arity / rest -/// adaptation at runtime (the same contract the tower's default + override arms -/// already rely on). -fn emit_collapsed_instance_dispatch( - ctx: &mut FnCtx<'_>, - recv_box: &str, - property: &str, - static_user_args: &[String], - call_byte_offset: u32, - with_override_probe: bool, -) -> Result { - let key_idx = ctx.strings.intern(property); - let entry = ctx.strings.entry(key_idx); - let bytes_global = format!("@{}", entry.bytes_global); - let name_len_str = entry.byte_len.to_string(); - - // Marshal the raw user args into an entry-block array once; both arms pass - // the same flat (ptr, len). `js_native_call_value` (override) and - // `js_native_call_method` (dispatch) each do their own rest-bundling / - // arity padding at runtime, so the un-bundled args are correct for both. - let n = static_user_args.len(); - let (args_ptr, args_len) = if n == 0 { - ("null".to_string(), "0".to_string()) - } else { - let buf = ctx.func.alloca_entry_array(DOUBLE, n); - for (i, a) in static_user_args.iter().enumerate() { - let slot = ctx.block().gep(DOUBLE, &buf, &[(I64, &format!("{}", i))]); - ctx.block().store(DOUBLE, a, &slot); - } - let ptr = ctx.block().next_reg(); - ctx.block().emit_raw(format!( - "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", - ptr, n, buf - )); - (ptr, n.to_string()) - }; - - // The virtual-dispatch switch this collapses (overriding-subclass case) has - // NO own-property override probe, so its collapse must be a bare by-name - // dispatch to stay behavior-identical; the dynamic-dispatch tower DOES probe - // first, so its collapse keeps the probe. `with_override_probe` selects. - // `js_native_call_method` handles a non-pointer (primitive) receiver at - // runtime, so no codegen POINTER_TAG guard is needed on this path. - if !with_override_probe { - crate::expr::calls::emit_call_location_at(ctx, call_byte_offset); - return Ok(ctx.block().call( - DOUBLE, - "js_native_call_method", - &[ - (DOUBLE, recv_box), - (crate::types::PTR, &bytes_global), - (I64, &name_len_str), - (crate::types::PTR, &args_ptr), - (I64, &args_len), - ], - )); - } - - // Override probe: an own-property method override (e.g. hono SmartRouter - // rebinding `this.method = X`) wins over the class method. - let own_method = ctx.block().call( - DOUBLE, - "js_object_get_own_field_or_undef", - &[ - (DOUBLE, recv_box), - (crate::types::PTR, &bytes_global), - (I64, &name_len_str), - ], - ); - let own_bits = ctx.block().bitcast_double_to_i64(&own_method); - let undef_bits_str = format!("{}", crate::nanbox::TAG_UNDEFINED as i64); - let is_undef = ctx.block().icmp_eq(I64, &own_bits, &undef_bits_str); - let override_idx = ctx.new_block("idispc.override"); - let dispatch_idx = ctx.new_block("idispc.dispatch"); - let merge_idx = ctx.new_block("idispc.merge"); - let override_label = ctx.block_label(override_idx); - let dispatch_label = ctx.block_label(dispatch_idx); - let merge_label = ctx.block_label(merge_idx); - ctx.block() - .cond_br(&is_undef, &dispatch_label, &override_label); - - // Override arm: call the stored function value with the receiver as - // `this` (#632 — a class-field non-arrow function reads `this`). - ctx.current_block = override_idx; - let this_bits = ctx.block().bitcast_double_to_i64(recv_box); - let v_override = ctx.block().call( - DOUBLE, - "js_native_call_value", - &[ - (DOUBLE, &own_method), - (I64, &this_bits), - (crate::types::PTR, &args_ptr), - (I64, &args_len), - ], - ); - let after_override = ctx.block().label.clone(); - if !ctx.block().is_terminated() { - ctx.block().br(&merge_label); - } - - // Dispatch arm: one by-name runtime dispatch (replaces the class-id tower). - ctx.current_block = dispatch_idx; - // #5247: record the call location so a runtime "X is not a function" carries - // `at :`. - crate::expr::calls::emit_call_location_at(ctx, call_byte_offset); - let v_dispatch = ctx.block().call( - DOUBLE, - "js_native_call_method", - &[ - (DOUBLE, recv_box), - (crate::types::PTR, &bytes_global), - (I64, &name_len_str), - (crate::types::PTR, &args_ptr), - (I64, &args_len), - ], - ); - let after_dispatch = ctx.block().label.clone(); - if !ctx.block().is_terminated() { - ctx.block().br(&merge_label); - } - - ctx.current_block = merge_idx; - Ok(ctx.block().phi( - DOUBLE, - &[ - (v_override.as_str(), after_override.as_str()), - (v_dispatch.as_str(), after_dispatch.as_str()), - ], - )) -} diff --git a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch_collapse.rs b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch_collapse.rs new file mode 100644 index 0000000000..19b43b7b9c --- /dev/null +++ b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch_collapse.rs @@ -0,0 +1,156 @@ +//! The full-outline collapse of the instance method-dispatch tower (#5391 +//! path 4), split out of `dynamic_dispatch.rs` for the 2000-line cap. + +use super::*; + +/// Whether to collapse the instance method-dispatch tower in full-outline mode. +/// On by default whenever full-outline is active; `PERRY_OUTLINE_METHOD_DISPATCH=0` +/// / `off` / `false` keeps the inline class-id switch tower (escape hatch / +/// differential-test isolation). +pub(super) fn method_dispatch_collapse_enabled() -> bool { + !matches!( + std::env::var("PERRY_OUTLINE_METHOD_DISPATCH").as_deref(), + Ok("0") | Ok("off") | Ok("false") + ) +} + +/// #5391 path 4: full-outlined collapse of the instance method-dispatch tower +/// (see the call site in `try_lower_instance_method_call`). +/// +/// Emits the own-property override probe (unchanged semantics) and, on the +/// non-override path, a SINGLE by-name `js_native_call_method` instead of the +/// per-implementor class-id switch tower. `js_native_call_method` is the tower's +/// own default arm and resolves the user-class method through its (class_id, +/// name) vtable registry, so behavior is preserved while the per-site IR shrinks +/// from ~6 + N blocks (N = implementor count) to a fixed 3 blocks. The raw user +/// args are marshalled once into an entry-block array and shared by both arms; +/// `js_native_call_method` / `js_native_call_value` apply their own arity / rest +/// adaptation at runtime (the same contract the tower's default + override arms +/// already rely on). +pub(super) fn emit_collapsed_instance_dispatch( + ctx: &mut FnCtx<'_>, + recv_box: &str, + property: &str, + static_user_args: &[String], + call_byte_offset: u32, + with_override_probe: bool, +) -> Result { + let key_idx = ctx.strings.intern(property); + let entry = ctx.strings.entry(key_idx); + let bytes_global = format!("@{}", entry.bytes_global); + let name_len_str = entry.byte_len.to_string(); + + // Marshal the raw user args into an entry-block array once; both arms pass + // the same flat (ptr, len). `js_native_call_value` (override) and + // `js_native_call_method` (dispatch) each do their own rest-bundling / + // arity padding at runtime, so the un-bundled args are correct for both. + let n = static_user_args.len(); + let (args_ptr, args_len) = if n == 0 { + ("null".to_string(), "0".to_string()) + } else { + let buf = ctx.func.alloca_entry_array(DOUBLE, n); + for (i, a) in static_user_args.iter().enumerate() { + let slot = ctx.block().gep(DOUBLE, &buf, &[(I64, &format!("{}", i))]); + ctx.block().store(DOUBLE, a, &slot); + } + let ptr = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr, n, buf + )); + (ptr, n.to_string()) + }; + + // The virtual-dispatch switch this collapses (overriding-subclass case) has + // NO own-property override probe, so its collapse must be a bare by-name + // dispatch to stay behavior-identical; the dynamic-dispatch tower DOES probe + // first, so its collapse keeps the probe. `with_override_probe` selects. + // `js_native_call_method` handles a non-pointer (primitive) receiver at + // runtime, so no codegen POINTER_TAG guard is needed on this path. + if !with_override_probe { + crate::expr::calls::emit_call_location_at(ctx, call_byte_offset); + return Ok(ctx.block().call( + DOUBLE, + "js_native_call_method", + &[ + (DOUBLE, recv_box), + (crate::types::PTR, &bytes_global), + (I64, &name_len_str), + (crate::types::PTR, &args_ptr), + (I64, &args_len), + ], + )); + } + + // Override probe: an own-property method override (e.g. hono SmartRouter + // rebinding `this.method = X`) wins over the class method. + let own_method = ctx.block().call( + DOUBLE, + "js_object_get_own_field_or_undef", + &[ + (DOUBLE, recv_box), + (crate::types::PTR, &bytes_global), + (I64, &name_len_str), + ], + ); + let own_bits = ctx.block().bitcast_double_to_i64(&own_method); + let undef_bits_str = format!("{}", crate::nanbox::TAG_UNDEFINED as i64); + let is_undef = ctx.block().icmp_eq(I64, &own_bits, &undef_bits_str); + let override_idx = ctx.new_block("idispc.override"); + let dispatch_idx = ctx.new_block("idispc.dispatch"); + let merge_idx = ctx.new_block("idispc.merge"); + let override_label = ctx.block_label(override_idx); + let dispatch_label = ctx.block_label(dispatch_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block() + .cond_br(&is_undef, &dispatch_label, &override_label); + + // Override arm: call the stored function value with the receiver as + // `this` (#632 — a class-field non-arrow function reads `this`). + ctx.current_block = override_idx; + let this_bits = ctx.block().bitcast_double_to_i64(recv_box); + let v_override = ctx.block().call( + DOUBLE, + "js_native_call_value", + &[ + (DOUBLE, &own_method), + (I64, &this_bits), + (crate::types::PTR, &args_ptr), + (I64, &args_len), + ], + ); + let after_override = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + // Dispatch arm: one by-name runtime dispatch (replaces the class-id tower). + ctx.current_block = dispatch_idx; + // #5247: record the call location so a runtime "X is not a function" carries + // `at :`. + crate::expr::calls::emit_call_location_at(ctx, call_byte_offset); + let v_dispatch = ctx.block().call( + DOUBLE, + "js_native_call_method", + &[ + (DOUBLE, recv_box), + (crate::types::PTR, &bytes_global), + (I64, &name_len_str), + (crate::types::PTR, &args_ptr), + (I64, &args_len), + ], + ); + let after_dispatch = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + Ok(ctx.block().phi( + DOUBLE, + &[ + (v_override.as_str(), after_override.as_str()), + (v_dispatch.as_str(), after_dispatch.as_str()), + ], + )) +} diff --git a/crates/perry-codegen/src/lower_call/scalar_method.rs b/crates/perry-codegen/src/lower_call/scalar_method.rs index 1d3b703356..9dfa48b78a 100644 --- a/crates/perry-codegen/src/lower_call/scalar_method.rs +++ b/crates/perry-codegen/src/lower_call/scalar_method.rs @@ -609,8 +609,15 @@ fn materialize_scalar_receiver( ctx.pending_declares.push(( "js_object_alloc_class_inline_keys_stamped".to_string(), I64, - vec![I32, I32, I32, I64, I32], + vec![I32, I32, I32, I64, I32, I64], )); + // The birth rep module init minted that id with (T1). + let rep = ctx + .class_birth_reps + .get(&keys_global_name) + .copied() + .unwrap_or(0) + .to_string(); let obj_handle = ctx.block().call( I64, "js_object_alloc_class_inline_keys_stamped", @@ -620,6 +627,7 @@ fn materialize_scalar_receiver( (I32, &field_count_str), (I64, &keys_ptr), (I32, &shape_id), + (I64, &rep), ], ); emit_materialized_scalar_receiver_typed_shape_init(ctx, class_name, &obj_handle); diff --git a/crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs b/crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs index 5257fc096d..aa27374078 100644 --- a/crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs +++ b/crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs @@ -649,11 +649,14 @@ fn imported_length_only_arguments_capability_uses_scalar_direct_abi() { /// A module's string pool can run before the defining module of a class it /// imports has initialized (the entry module, an import cycle). With link-time /// ids (design step 4) the stub's mint carries the static id the driver gave -/// its content — the id the definer's typed install uses when exactly one -/// typed layout matches — so either init order converges on one id without -/// the runtime keeping any module's global addresses. +/// its content, so either init order converges on one id without the runtime +/// keeping any module's global addresses. The birth rep is content (charter +/// step 5, T1): an importer's all-`Any` stub requests the definer's id only +/// when the definer is all-`Any` too; a definer born with an `F64` lane is +/// another content, and the stub keeps its own id. #[test] fn imported_stub_mints_with_the_drivers_static_id_and_registers_no_slots() { + use crate::{BirthShape, DefinedClassShape, ProgramClassShapeIds}; let module = || { let mut module = Module::new("imported_shape_slots.ts"); module.init = vec![Stmt::Let { @@ -676,27 +679,76 @@ fn imported_stub_mints_with_the_drivers_static_id_and_registers_no_slots() { let births = crate::module_birth_shapes(&module(), opts.clone()).unwrap(); assert_eq!(births.len(), 1, "the stub is this module's one class birth"); assert!(!births[0].defined, "an imported stub is not a definition"); - let ids = crate::assign_static_shape_ids(births.iter().map(|b| &b.shape)); - let id = ids[&births[0].shape]; - opts.static_shape_ids = vec![(births[0].shape.clone(), id)]; - let ir = String::from_utf8(compile_module(&module(), opts).unwrap()) - .expect("LLVM IR should be UTF-8"); - let mint = ir - .lines() - .find(|l| l.contains("call i32 @js_object_shape_id_for_class_keys_static(")) - .unwrap_or_else(|| panic!("the stub must mint with its static id:\n{ir}")); - assert!( - mint.contains(&format!("i32 {id})")) && mint.contains("i32 55,"), - "the mint must carry the class id and the driver's id {id}:\n{mint}" + let stub = births[0].shape.clone(); + assert_eq!(stub.rep, 0, "an imported stub is born all-Any"); + + // `definer_rep`: the defining module's birth rep for the same keys. Returns + // the id the stub's mint requests, after checking the mint's shape. + let mint_id = |definer_rep: u64| -> (u32, u32, u32) { + let definer = BirthShape { + rep: definer_rep, + ..stub.clone() + }; + let ids = crate::assign_static_shape_ids([&stub, &definer]); + let (own, def_id) = (ids[&stub], ids[&definer]); + let mut opts = opts.clone(); + opts.static_shape_ids = vec![(stub.clone(), own)]; + opts.program_class_shape_ids = ProgramClassShapeIds( + [( + 55, + DefinedClassShape { + keys_global: "perry_class_keys_producer_ts__Remote".to_string(), + shape: definer, + id: def_id, + }, + )] + .into_iter() + .collect(), + ); + let ir = String::from_utf8(compile_module(&module(), opts).unwrap()) + .expect("LLVM IR should be UTF-8"); + let mint = ir + .lines() + .find(|l| l.contains("call i32 @js_object_shape_id_for_class_keys_static(")) + .unwrap_or_else(|| panic!("the stub must mint with its static id:\n{ir}")); + assert!( + mint.contains("i32 55,") && mint.ends_with(", i64 0)"), + "the mint must carry the class id and the stub's all-Any rep:\n{mint}" + ); + assert!( + !ir.contains("js_register_imported_class_shape_slot"), + "no module global address is handed to the runtime any more:\n{ir}" + ); + // S6: there is no poisonable guard twin to seed or register any more; + // the class-field guards compare against the ShapeId global itself. + assert!( + !ir.contains("perry_class_guard_shape_"), + "no poisonable guard expectation may be emitted:\n{ir}" + ); + let requested = [own, def_id] + .into_iter() + .find(|id| mint.contains(&format!("i32 {id}, i64 0)"))) + .unwrap_or_else(|| panic!("the mint requests neither {own} nor {def_id}:\n{mint}")); + (requested, own, def_id) + }; + + // Both all-Any: one content, so the stub requests the definer's id. + let (requested, own, def_id) = mint_id(0); + assert_eq!(own, def_id, "equal contents get one id"); + assert_eq!( + requested, def_id, + "the all-Any stub must adopt the definer's id" ); - assert!( - !ir.contains("js_register_imported_class_shape_slot"), - "no module global address is handed to the runtime any more:\n{ir}" + + // The definer has an F64 lane (slot 0): two contents, two ids, and the + // stub keeps its own. + let (requested, own, def_id) = mint_id(0b01); + assert_ne!( + own, def_id, + "an F64 birth rep is content: one id would name two layouts" ); - // S6: there is no poisonable guard twin to seed or register any more; the - // class-field guards compare against the ShapeId global itself. - assert!( - !ir.contains("perry_class_guard_shape_"), - "no poisonable guard expectation may be emitted:\n{ir}" + assert_eq!( + requested, own, + "an all-Any stub must never adopt an F64 definer's id" ); } diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index b40a98be0d..dfc5d99043 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1107,15 +1107,19 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function( "js_object_alloc_class_inline_keys_stamped", I64, - &[I32, I32, I32, I64, I32], + &[I32, I32, I32, I64, I32, I64], ); - module.declare_function("js_build_class_keys_array", I64, &[I32, I32, PTR, I32]); + module.declare_function("js_build_class_keys_array", I64, &[I32, I32, PTR, I32, I64]); module.declare_function("js_object_shape_id_for_keys", I32, &[I64, I32]); - module.declare_function("js_object_shape_id_for_class_keys", I32, &[I64, I32, I32]); + module.declare_function( + "js_object_shape_id_for_class_keys", + I32, + &[I64, I32, I32, I64], + ); module.declare_function( "js_object_shape_id_for_class_keys_live", I32, - &[I64, I32, I32, I32], + &[I64, I32, I32, I32, I64], ); // #10123: (shape_id, NaN-boxed key) -> inline slot index, or -1. The // element-shape loop clone's shape-keyed preheader resolves each tracked @@ -1132,21 +1136,21 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function( "js_region_loop_prime", I64, - &[PTR, I32, I32, I64, I64, I64, I64, I64, I32, I32], + &[PTR, I32, I32, I64, I64, I64, I64, I64, I32, I32, I32], ); module.declare_function( "js_gc_typed_shape_id_for_keys", I32, - &[I32, I64, I32, PTR, I32, PTR, I32, I32], + &[I32, I64, I32, PTR, I32, PTR, I32, I32, I64], ); // Design step 4: the per-class mint with the driver's static id, and the // literal-shape seed. module.declare_function( "js_object_shape_id_for_class_keys_static", I32, - &[I64, I32, I32, I32, I32], + &[I64, I32, I32, I32, I32, I64], ); - module.declare_function("js_shape_seed_plain", I32, &[I32, PTR, I32, I32, I32]); + module.declare_function("js_shape_seed_plain", I32, &[I32, PTR, I32, I32, I32, I64]); module.declare_function("js_shape_register_static_seed", VOID, &[PTR]); module.declare_function("js_shape_run_static_seed", VOID, &[]); // Inline bump-allocator state accessor + slow path. Ordinary allocation diff --git a/crates/perry-codegen/src/stmt/region_loop/guard.rs b/crates/perry-codegen/src/stmt/region_loop/guard.rs index 28e6065b5a..00b9440b82 100644 --- a/crates/perry-codegen/src/stmt/region_loop/guard.rs +++ b/crates/perry-codegen/src/stmt/region_loop/guard.rs @@ -91,6 +91,7 @@ pub(super) fn emit_prime_call( (I64, &key_bits[4]), (I32, &last), (I32, &rv.stored_mask.to_string()), + (I32, &rv.boxed_mask.to_string()), ], ) } @@ -209,7 +210,7 @@ fn static_region_word(ctx: &FnCtx<'_>, rv: &Receiver) -> Option { } })?; let keys_global = ctx.class_keys_globals.get(&class_name)?; - let (id, slots) = crate::codegen::static_region_slots(keys_global, &rv.keys)?; + let (id, slots) = crate::codegen::static_region_slots(keys_global, &rv.keys, rv.boxed_mask)?; let mut word = u64::from(id); for (i, slot) in slots.iter().enumerate() { word |= u64::from(*slot) << (32 + SLOT_BITS * i as u32); diff --git a/crates/perry-codegen/src/stmt/region_loop/mod.rs b/crates/perry-codegen/src/stmt/region_loop/mod.rs index 8b44c639f0..da9dc92afd 100644 --- a/crates/perry-codegen/src/stmt/region_loop/mod.rs +++ b/crates/perry-codegen/src/stmt/region_loop/mod.rs @@ -183,6 +183,9 @@ pub(crate) struct Receiver { pub(crate) has_store: bool, /// Bit `i`: the body stores `keys[i]` (the runtime then requires it inline). stored_mask: u32, + /// Bit `i`: a bare store may write `keys[i]` a value not proven a + /// canonical double (the word must then give it an `Any` lane). + boxed_mask: u32, /// `i1`: the guard matched this receiver's SPILL word (flipped id). spill: String, sites: Option<(String, String)>, @@ -418,11 +421,12 @@ fn begin_with( let mut receivers: Vec = p .receivers .iter() - .map(|(r, k, st, sm)| Receiver { + .map(|(r, k, st, sm, bm)| Receiver { recv: *r, keys: k.clone(), has_store: *st, stored_mask: effective_stored_mask(*sm, k.len()), + boxed_mask: *bm, spill: "false".to_string(), sites: None, word: String::new(), @@ -538,11 +542,12 @@ fn body_pending(p: Plan, body: &[Stmt], split_at: usize) -> Pending { let receivers: Vec = p .receivers .iter() - .map(|(r, k, st, sm)| Receiver { + .map(|(r, k, st, sm, bm)| Receiver { recv: *r, keys: k.clone(), has_store: *st, stored_mask: effective_stored_mask(*sm, k.len()), + boxed_mask: *bm, spill: "false".to_string(), sites: None, word: String::new(), diff --git a/crates/perry-codegen/src/stmt/region_loop/plan.rs b/crates/perry-codegen/src/stmt/region_loop/plan.rs index 2b7ff8dec8..1990c10876 100644 --- a/crates/perry-codegen/src/stmt/region_loop/plan.rs +++ b/crates/perry-codegen/src/stmt/region_loop/plan.rs @@ -129,6 +129,10 @@ pub(super) struct Planner<'p, 'a> { in_inner: bool, trees: HashSet, bare_stores: HashSet, + /// Per receiver, the keys a planned-bare store may write a value the + /// compiler does not prove a canonical double into (charter step 5): the + /// runtime then refuses a word whose shape has a non-`Any` lane there. + boxed_stores: HashMap>, continues: Vec, record: bool, } @@ -147,13 +151,20 @@ impl Planner<'_, '_> { .is_some_and(|k| k.iter().any(|x| x == key)) } - fn access(&mut self, e: &Expr, r: Recv, key: &str, store: bool, st: &mut St) { + /// `boxed`: a store whose value is not proven a canonical double. + fn access(&mut self, e: &Expr, r: Recv, key: &str, store: bool, boxed: bool, st: &mut St) { let fresh = st.as_ref().is_some_and(|m| m.get(&r) == Some(&FRESH)); if fresh && self.cands.contains(&r) && self.covered(r, key) { if self.record { self.bare.insert(e as *const Expr as usize); if store { self.bare_stores.insert(r); + if boxed { + self.boxed_stores + .entry(r) + .or_default() + .insert(key.to_string()); + } } } return; @@ -193,7 +204,7 @@ impl Planner<'_, '_> { } => { st = self.expr(object, st); match Recv::of(object) { - Some(r) => self.access(e, r, property, false, &mut st), + Some(r) => self.access(e, r, property, false, false, &mut st), None => kill(&mut st), } st @@ -210,7 +221,12 @@ impl Planner<'_, '_> { st = self.expr(value, st); match (Recv::of(target), key.as_ref()) { (Some(r), Expr::String(k)) if Recv::of(receiver) == Some(r) => { - self.access(e, r, k, true, &mut st) + // The same predicate the bare store lowers with + // (`bare::try_lower_bare_put`): a proven canonical + // double is a valid value of every lane. + let boxed = + !crate::type_analysis::expr_produces_canonical_raw_f64(self.ctx, value); + self.access(e, r, k, true, boxed, &mut st) } _ => kill(&mut st), } @@ -781,7 +797,8 @@ pub(super) fn receiver_eligible(ctx: &FnCtx<'_>, r: Recv) -> bool { } pub(super) struct Plan { - pub(super) receivers: Vec<(Recv, Vec, bool, u32)>, + /// `(receiver, keys, has a bare store, stored mask, boxed-store mask)`. + pub(super) receivers: Vec<(Recv, Vec, bool, u32, u32)>, pub(super) bare: HashSet, pub(super) trees: HashSet, pub(super) recheck: Recheck, @@ -853,6 +870,7 @@ pub(super) fn plan( in_inner: false, trees: HashSet::new(), bare_stores: HashSet::new(), + boxed_stores: HashMap::new(), continues: Vec::new(), record: true, }; @@ -911,6 +929,7 @@ pub(super) fn plan( let bare = std::mem::take(&mut p.bare); let trees = std::mem::take(&mut p.trees); let bare_stores = std::mem::take(&mut p.bare_stores); + let boxed_stores = std::mem::take(&mut p.boxed_stores); let mut plan_arrays: Vec<(Recv, u32)> = std::mem::take(&mut p.bare_arrays) .into_iter() .map(|r| (r, arrays[&r])) @@ -940,18 +959,26 @@ pub(super) fn plan( } } } - let mut receivers: Vec<(Recv, Vec, bool, u32)> = used + let mut receivers: Vec<(Recv, Vec, bool, u32, u32)> = used .into_iter() .map(|r| { + let boxed = boxed_stores.get(&r).map_or(0, |ks| { + keys[&r] + .iter() + .enumerate() + .filter(|(_, k)| ks.contains(*k)) + .fold(0u32, |m, (i, _)| m | 1 << i) + }); ( r, keys[&r].clone(), bare_stores.contains(&r), stored.get(&r).copied().unwrap_or(0), + boxed, ) }) .collect(); - receivers.sort_by_key(|(r, _, _, _)| *r); + receivers.sort_by_key(|(r, _, _, _, _)| *r); Some(Plan { receivers, bare, diff --git a/crates/perry-codegen/src/stubs.rs b/crates/perry-codegen/src/stubs.rs index ceb2e73d84..0dec8dd3a1 100644 --- a/crates/perry-codegen/src/stubs.rs +++ b/crates/perry-codegen/src/stubs.rs @@ -193,7 +193,7 @@ pub fn generate_stdlib_installer_object( /// The LLVM IR of the program's static shape seed unit (design step 4): one /// `js_shape_seed_plain` per seed (literal contents the guards embed as -/// immediates), in a function a `global_ctors` entry REGISTERS with the +/// immediates, each with its birth rep), in a function a `global_ctors` entry REGISTERS with the /// runtime. The constructor only stores the pointer — it runs before /// `js_gc_init`, when no agent heap exists — and each agent runs the function /// through `js_shape_run_static_seed` (`main` after `js_gc_init`, every other @@ -201,7 +201,7 @@ pub fn generate_stdlib_installer_object( pub fn static_shape_seed_ll(seeds: &[(u32, crate::BirthShape)]) -> String { let mut ll = String::new(); ll.push_str("; Perry static shape seeds — generated by perry-codegen::stubs\n\n"); - ll.push_str("declare i32 @js_shape_seed_plain(i32, ptr, i32, i32, i32)\n"); + ll.push_str("declare i32 @js_shape_seed_plain(i32, ptr, i32, i32, i32, i64)\n"); ll.push_str("declare void @js_shape_register_static_seed(ptr)\n\n"); for (i, (_, shape)) in seeds.iter().enumerate() { let bytes: String = shape.keys.iter().map(|b| format!("\\{b:02X}")).collect(); @@ -213,10 +213,11 @@ pub fn static_shape_seed_ll(seeds: &[(u32, crate::BirthShape)]) -> String { ll.push_str("\ndefine internal void @perry_static_shape_seed() {\n"); for (i, (id, shape)) in seeds.iter().enumerate() { ll.push_str(&format!( - " %s{i} = call i32 @js_shape_seed_plain(i32 {id}, ptr @perry_static_seed_keys_{i}, i32 {}, i32 {}, i32 {})\n", + " %s{i} = call i32 @js_shape_seed_plain(i32 {id}, ptr @perry_static_seed_keys_{i}, i32 {}, i32 {}, i32 {}, i64 {})\n", shape.keys.len(), shape.key_count, - shape.live + shape.live, + shape.rep )); } ll.push_str(" ret void\n}\n\n"); @@ -251,10 +252,22 @@ mod tests { live: 3, proto: crate::BirthProto::Literal, typed: None, + rep: 0, }; let ll = static_shape_seed_ll(&[(0x1000_0042, shape)]); assert!(ll.contains("c\"\\75\\00\\76\\00\""), "{ll}"); - assert!(ll.contains("call i32 @js_shape_seed_plain(i32 268435522, ptr @perry_static_seed_keys_0, i32 4, i32 2, i32 3)"), "{ll}"); + assert!(ll.contains("call i32 @js_shape_seed_plain(i32 268435522, ptr @perry_static_seed_keys_0, i32 4, i32 2, i32 3, i64 0)"), "{ll}"); + // A literal born with F64 lanes is seeded with its birth rep. + let f64_lanes = crate::BirthShape { + keys: b"u\0v\0".to_vec(), + key_count: 2, + live: 2, + proto: crate::BirthProto::Literal, + typed: None, + rep: 0b0101, + }; + let ll = static_shape_seed_ll(&[(0x1000_0043, f64_lanes)]); + assert!(ll.contains("call i32 @js_shape_seed_plain(i32 268435523, ptr @perry_static_seed_keys_0, i32 4, i32 2, i32 2, i64 5)"), "{ll}"); assert!(ll.contains("@js_shape_register_static_seed(ptr @perry_static_shape_seed)")); let bytes = generate_static_shape_seed_object( &[( @@ -265,6 +278,7 @@ mod tests { live: 1, proto: crate::BirthProto::Literal, typed: None, + rep: 0, }, )], None, diff --git a/crates/perry-codegen/src/typed_shape.rs b/crates/perry-codegen/src/typed_shape.rs index bfcc0d06c9..fffdb13fe3 100644 --- a/crates/perry-codegen/src/typed_shape.rs +++ b/crates/perry-codegen/src/typed_shape.rs @@ -231,6 +231,75 @@ pub(crate) fn class_layout_declarable_at_allocation( worth_declaring } +/// Slots with a birth representation lane (the runtime's `field_rep::REP_SLOTS`). +pub(crate) const BIRTH_REP_SLOTS: u32 = 32; +/// The `F64` lane value (`field_rep::REP_F64`). +const BIRTH_REP_F64: u64 = 0b01; + +/// Charter step 5, T1: a class's birth representation word — `F64` for +/// exactly the slots its typed layout marks raw-f64 when that layout is +/// declared at allocation, `Any` everywhere else. +/// +/// This is THE decision. The string pool passes it to the runtime mint +/// (`js_object_shape_id_for_class_keys{,_live}`, `js_gc_typed_shape_id_for_keys`), +/// the inline allocation birth-fills its `F64` lanes with `+0.0`, and the +/// class-field store precheck finite-tests every value bound for one. The +/// runtime takes the word as given; nothing re-derives it from the +/// environment. +/// +/// Why "declared at allocation" is the per-field condition: that proof +/// (`class_layout_declarable_at_allocation` and its chain form) holds exactly +/// when every `number` field is written by the constructor prologue before +/// anything can read `this` — so no JS code observes the `+0.0` birth fill. +/// A class with even one `number` field that could be read first (a field +/// initializer, a `this` read before the store, heritage without the chain +/// proof) gets no `F64` lane at all, because its typed layout is then only +/// validated after the constructor and "requires raw f64" would not hold at +/// birth. An imported class stub has no constructor body to prove anything +/// from, so it is `Any` too. +pub(crate) fn class_birth_rep_in( + classes: &std::collections::HashMap, + class_keys_globals: &std::collections::HashMap, + class_init_chains: &std::collections::HashMap< + String, + Vec<(String, Vec)>, + >, + imported_stub: bool, + class_name: &str, +) -> u64 { + if imported_stub + || !crate::lower_call::typed_shape_init::layout_declared_at_allocation_in( + classes, + class_keys_globals, + class_name, + ) + { + return 0; + } + let Some(chain) = class_init_chains.get(class_name) else { + return 0; + }; + birth_rep_from_raw_f64_mask(&class_typed_layout_from_chain(chain).raw_f64_mask_words) +} + +/// The birth rep word for a raw-f64 mask: one `F64` lane per raw-f64 slot +/// below [`BIRTH_REP_SLOTS`] (slots past it have no lane and stay `Any`). +pub(crate) fn birth_rep_from_raw_f64_mask(raw_f64_mask_words: &[u64]) -> u64 { + let mut low = raw_f64_mask_words.first().copied().unwrap_or(0) & 0xFFFF_FFFF; + let mut rep = 0u64; + while low != 0 { + let slot = low.trailing_zeros(); + low &= low - 1; + rep |= BIRTH_REP_F64 << (2 * slot); + } + rep +} + +/// Is `slot` an `F64` lane of birth rep `rep`? +pub(crate) fn birth_rep_slot_is_f64(rep: u64, slot: u32) -> bool { + slot < BIRTH_REP_SLOTS && (rep >> (2 * slot)) & 0b11 == BIRTH_REP_F64 +} + #[derive(Clone, Debug, Default)] pub(crate) struct TypedShapeLayout { pub(crate) slot_count: u32, diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv index 7b52e81e26..2363a930ed 100644 --- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv +++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv @@ -479,7 +479,7 @@ js_buffer_write_uint32_le void f64,f64,i32s js_buffer_write_uint8 void f64,f64,i32s js_buffer_write_uint_be void f64,f64,i32s,i32s js_buffer_write_uint_le void f64,f64,i32s,i32s -js_build_class_keys_array ptr i32u,i32u,ptr,i32u +js_build_class_keys_array ptr i32u,i32u,ptr,i32u,i64 js_builtin_prototype_method_value f64 ptr,usize,ptr,usize js_builtin_subclass_construct f64 i32u,ptr,usize,ptr,usize js_bun_ant_get_peer_pid f64 f64 @@ -1494,7 +1494,7 @@ js_gc_temp_root_get i64 i32u js_gc_temp_root_push i32u i64 js_gc_temp_root_set void i32u,i64 js_gc_temp_root_truncate void i32u -js_gc_typed_shape_id_for_keys i32u i32u,i64,i32u,ptr,i32u,ptr,i32u,i32u +js_gc_typed_shape_id_for_keys i32u i32u,i64,i32u,ptr,i32u,ptr,i32u,i32u,i64 js_gc_write_barriers_emitted void i32u js_ge i64 i64,i64 js_generator_attach_closure_prototype f64 f64,ptr @@ -2547,7 +2547,7 @@ js_numeric_step f64 f64,i32s js_object_alloc ptr i32u,i32u js_object_alloc_class_dynamic_parent ptr i32u,i32u,ptr,i32u js_object_alloc_class_inline_keys ptr i32u,i32u,i32u,ptr -js_object_alloc_class_inline_keys_stamped ptr i32u,i32u,i32u,ptr,i32u +js_object_alloc_class_inline_keys_stamped ptr i32u,i32u,i32u,ptr,i32u,i64 js_object_alloc_class_with_keys ptr i32u,i32u,i32u,ptr,i32u js_object_alloc_fast ptr i32u,i32u js_object_alloc_fast_with_parent ptr i32u,i32u,i32u @@ -2646,9 +2646,9 @@ js_object_set_property_key_method f64 f64,f64,f64 js_object_set_prototype_of f64 f64,f64 js_object_set_symbol_method f64 f64,f64,f64 js_object_set_symbol_property f64 f64,f64,f64 -js_object_shape_id_for_class_keys i32u i64,i32u,i32u -js_object_shape_id_for_class_keys_live i32u i64,i32u,i32u,i32u -js_object_shape_id_for_class_keys_static i32u i64,i32u,i32u,i32u,i32u +js_object_shape_id_for_class_keys i32u i64,i32u,i32u,i64 +js_object_shape_id_for_class_keys_live i32u i64,i32u,i32u,i32u,i64 +js_object_shape_id_for_class_keys_static i32u i64,i32u,i32u,i32u,i32u,i64 js_object_shape_id_for_keys i32u i64,i32u js_object_super_call f64 f64,f64,f64,ptr,usize js_object_super_get f64 f64,f64,f64 @@ -3197,8 +3197,8 @@ js_regexp_test i32s ptr,ptr js_regexp_to_string ptr ptr js_region_guard_pack i64 i32u,i32u,i64,i64,i64,i64,i64 js_region_guard_prime i64 ptr,i32u,i32u,i64,i64,i64,i64,i64 -js_region_loop_pack i64 i32u,i32u,i64,i64,i64,i64,i64,i32u -js_region_loop_prime i64 ptr,i32u,i32u,i64,i64,i64,i64,i64,i32u,i32u +js_region_loop_pack i64 i32u,i32u,i64,i64,i64,i64,i64,i32u,i32u +js_region_loop_prime i64 ptr,i32u,i32u,i64,i64,i64,i64,i64,i32u,i32u,i32u js_register_anon_shape_class_id void i32u js_register_aux_has_active void ptr js_register_aux_pump void ptr @@ -3459,7 +3459,7 @@ js_shadow_state_addr ptr js_shape_ordinary_inline_slot_for_key i32s i32u,i64 js_shape_register_static_seed void ptr js_shape_run_static_seed void -js_shape_seed_plain i32u i32u,ptr,i32u,i32u,i32u +js_shape_seed_plain i32u i32u,ptr,i32u,i32u,i32u,i64 js_shared_array_buffer_new ptr i32s js_shared_array_buffer_new_value ptr f64 js_sharp_auto_orient i64 i64 diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index 36b971e7eb..39f597d408 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -36,6 +36,10 @@ crate-type = ["rlib"] # they look gets this backwards, so every call site is gated rather than # argued about. shape-mint-diag = [] +# Charter step 5: check at every object trace that each F64 lane holds a +# canonical double (always on in a debug build; this turns it on in release, +# for the gap subset and the tsc/Zod runs). +field-rep-assert = [] # Charter step 3: default-off `PERRY_ATTR_DIAG` census of the attribute # machinery (where attributes are written, read, and which key-list paths run). attr-census = [] diff --git a/crates/perry-runtime/src/array/index_get_exit_tests.rs b/crates/perry-runtime/src/array/index_get_exit_tests.rs index 738d3ecb47..17fad83f76 100644 --- a/crates/perry-runtime/src/array/index_get_exit_tests.rs +++ b/crates/perry-runtime/src/array/index_get_exit_tests.rs @@ -90,6 +90,7 @@ fn array_subclass( declared, packed_keys.as_ptr(), packed_keys.len() as u32, + 0, ); let obj = crate::object::js_object_alloc_class_inline_keys(class_id, CLASS_ID_ARRAY, declared, keys); diff --git a/crates/perry-runtime/src/array/literal_descriptor.rs b/crates/perry-runtime/src/array/literal_descriptor.rs index 3cc6895b04..1eac33993a 100644 --- a/crates/perry-runtime/src/array/literal_descriptor.rs +++ b/crates/perry-runtime/src/array/literal_descriptor.rs @@ -15,6 +15,8 @@ pub struct LiteralShape { raw_mask_len: u32, pointer_mask: *const u64, pointer_mask_len: u32, + /// The birth rep codegen gave the shape id (charter step 5). + rep: u64, } struct Reader<'a> { @@ -86,6 +88,7 @@ impl Reader<'_> { shape.field_count, unsafe { *shape.keys_slot } as *mut super::ArrayHeader, unsafe { *shape.shape_id_slot }, + shape.rep, ); for i in 0..shape.field_count { let value = self.value(depth + 1)?; @@ -145,7 +148,8 @@ mod tests { const POINTERS: &[u64] = &[2]; const CLASS_ID: u32 = 1017301; let keys = - crate::object::js_build_class_keys_array(CLASS_ID, 2, b"id\0name\0".as_ptr(), 8) as u64; + crate::object::js_build_class_keys_array(CLASS_ID, 2, b"id\0name\0".as_ptr(), 8, 0) + as u64; let shape_id = crate::gc::js_gc_typed_shape_id_for_keys( CLASS_ID, keys, @@ -155,6 +159,7 @@ mod tests { POINTERS.as_ptr(), 1, 0, + 0, ); let shape = LiteralShape { class_id: CLASS_ID, @@ -165,6 +170,7 @@ mod tests { raw_mask_len: 1, pointer_mask: POINTERS.as_ptr(), pointer_mask_len: 1, + rep: 0, }; // {id:-0, name:"snowman☃"}, using the public compiler/runtime ABI. let mut bytes = vec![7, 0, 0, 0, 0, 0]; @@ -245,6 +251,7 @@ mod tests { raw_mask_len: 0, pointer_mask: std::ptr::null(), pointer_mask_len: 0, + rep: 0, }; for bytes in [ &[0_u8][..], diff --git a/crates/perry-runtime/src/array/subclass_tests.rs b/crates/perry-runtime/src/array/subclass_tests.rs index 89b499b365..d2e67b207b 100644 --- a/crates/perry-runtime/src/array/subclass_tests.rs +++ b/crates/perry-runtime/src/array/subclass_tests.rs @@ -360,8 +360,13 @@ fn array_subclass_length_ic_publishes_only_scalar_exact_or_family_facts() { let class_id = 0x0074_867b; crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); let packed = b"sset\0mask\0"; - let keys = - crate::object::js_build_class_keys_array(class_id, 2, packed.as_ptr(), packed.len() as u32); + let keys = crate::object::js_build_class_keys_array( + class_id, + 2, + packed.as_ptr(), + packed.len() as u32, + 0, + ); let obj = crate::object::js_object_alloc_class_inline_keys(class_id, CLASS_ID_ARRAY, 2, keys); let receiver = crate::value::js_nanbox_pointer(obj as i64); crate::node_stream::js_array_subclass_init(receiver, 0.0); @@ -660,8 +665,13 @@ fn array_subclass_named_prefix_token_survives_only_exact_numeric_tail_transition let class_id = 0x0074_865b; crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); let packed = b"sset\0mask\0"; - let keys = - crate::object::js_build_class_keys_array(class_id, 2, packed.as_ptr(), packed.len() as u32); + let keys = crate::object::js_build_class_keys_array( + class_id, + 2, + packed.as_ptr(), + packed.len() as u32, + 0, + ); let obj = crate::object::js_object_alloc_class_inline_keys(class_id, CLASS_ID_ARRAY, 2, keys); let receiver = crate::value::js_nanbox_pointer(obj as i64); crate::node_stream::js_array_subclass_init(receiver, 0.0); @@ -783,8 +793,13 @@ fn plain_array_element_shape_consumes_array_subclass_prefix_proof() { let class_id = 0x0074_8667; crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); let packed = b"sset\0mask\0change\0"; - let keys = - crate::object::js_build_class_keys_array(class_id, 3, packed.as_ptr(), packed.len() as u32); + let keys = crate::object::js_build_class_keys_array( + class_id, + 3, + packed.as_ptr(), + packed.len() as u32, + 0, + ); let obj = crate::object::js_object_alloc_class_inline_keys(class_id, CLASS_ID_ARRAY, 3, keys); let receiver = crate::value::js_nanbox_pointer(obj as i64); crate::node_stream::js_array_subclass_init(receiver, 0.0); @@ -850,8 +865,13 @@ fn empty_array_subclass_named_prefix_token_survives_warm_tail_cycle() { let class_id = 0x0074_8659; crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); let packed = b"sset\0mask\0change\0"; - let keys = - crate::object::js_build_class_keys_array(class_id, 3, packed.as_ptr(), packed.len() as u32); + let keys = crate::object::js_build_class_keys_array( + class_id, + 3, + packed.as_ptr(), + packed.len() as u32, + 0, + ); let obj = crate::object::js_object_alloc_class_inline_keys(class_id, CLASS_ID_ARRAY, 3, keys); let receiver = crate::value::js_nanbox_pointer(obj as i64); crate::node_stream::js_array_subclass_init(receiver, 0.0); diff --git a/crates/perry-runtime/src/gc/instruments.rs b/crates/perry-runtime/src/gc/instruments.rs index 778579e39e..96165de1f3 100644 --- a/crates/perry-runtime/src/gc/instruments.rs +++ b/crates/perry-runtime/src/gc/instruments.rs @@ -507,6 +507,7 @@ pub(crate) const INSTRUMENT_KNOBS: &[&str] = &[ "PERRY_GC_VERIFY_MARK", "PERRY_GC_VERIFY_CLASSIFIER", "PERRY_STACK_SYMBOLS", + "PERRY_FIELD_REPR_VERIFY", ]; /// The first instrument knob set (non-empty) in the environment, if any. diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index 5efbc02208..26b810b0cf 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -1419,6 +1419,29 @@ pub(crate) fn layout_typed_intact_for_user(user_ptr: usize) -> bool { } } +/// `(raw_f64, pointer)` for `slot_index` of `user_ptr`'s intact typed +/// layout, or `None` without one: the field-representation cross-check +/// (`object::field_rep_store::assert_f64_lanes_hold_numbers`). +#[cfg(any( + debug_assertions, + feature = "field-rep-assert", + feature = "gc-instruments" +))] +pub(crate) fn layout_typed_slot_kinds_for_user( + user_ptr: usize, + slot_index: usize, +) -> Option<(bool, bool)> { + if !layout_typed_intact_for_user(user_ptr) { + return None; + } + with_typed_descriptor_for_query(user_ptr, |layout| { + ( + slot_index < layout.slot_count && layout.raw_f64_mask.contains_slot(slot_index), + slot_index < layout.slot_count && layout.pointer_mask.contains_slot(slot_index), + ) + }) +} + pub(crate) fn layout_typed_raw_f64_slot_for_user(user_ptr: usize, slot_index: usize) -> bool { #[cfg(test)] TYPED_RAW_F64_DESCRIPTOR_QUERIES.with(|c| c.set(c.get() + 1)); diff --git a/crates/perry-runtime/src/gc/layout/typed_shape.rs b/crates/perry-runtime/src/gc/layout/typed_shape.rs index b9e4fb76b4..03e87f5c03 100644 --- a/crates/perry-runtime/src/gc/layout/typed_shape.rs +++ b/crates/perry-runtime/src/gc/layout/typed_shape.rs @@ -69,9 +69,13 @@ unsafe fn mask_words<'a>(words: *const u64, word_count: u32) -> &'a [u64] { /// globals and init guards are per-thread), so every agent installs its own. /// /// `requested` is the driver's static id for this layout (design step 4, 0 = -/// none: a fresh counter id). Its content includes the masks; the only other -/// requester is an importer's structural view of exactly these facts (the -/// same content at runtime, so the driver hands it this id too). So the id is +/// none: a fresh counter id). Its content includes the masks and `rep`, the +/// birth representation codegen declared (charter step 5, T1: `F64` lanes are +/// shape identity); the only other requester is an importer's structural view +/// of exactly these facts (the same content at runtime, so the driver hands it +/// this id too). An importer's stub is all-`Any`, so it never requests an id +/// whose rep has an `F64` lane: its inline allocation fills `undefined` and +/// cannot know the defining constructor's proof. So the id is /// absent from this agent, present with these exact facts and no layout (the /// importer initialized first), or present with these exact facts and this /// exact descriptor (a second module deriving the same typed layout). A @@ -92,6 +96,7 @@ pub extern "C" fn js_gc_typed_shape_id_for_keys( pointer_words: *const u64, pointer_word_count: u32, requested: u32, + rep: u64, ) -> u32 { if class_id == 0 || keys == 0 || slot_count >= 16_000_000 { eprintln!("Perry internal error: invalid pre-registered typed shape"); @@ -109,6 +114,12 @@ pub extern "C" fn js_gc_typed_shape_id_for_keys( eprintln!("Perry internal error: invalid pre-registered typed shape masks"); std::process::abort(); } + // T1: an `F64` lane may name only a raw-f64 slot of this layout. Codegen + // derives both from one class layout, so a disagreement is a codegen bug. + if !birth_rep_within_raw_mask(rep, raw_f64_slice) { + eprintln!("Perry internal error: typed shape rep {rep:#x} names a non-raw-f64 slot"); + std::process::abort(); + } let proto_id = crate::object::shapes::class_proto_id(class_id); let descriptor = TypedLayoutDescriptor { slot_count: slot_count as usize, @@ -117,10 +128,10 @@ pub extern "C" fn js_gc_typed_shape_id_for_keys( }; let keys = keys as usize as *const crate::array::ArrayHeader; let shape_id = if requested == 0 { - crate::object::shapes::mint_typed_shape_id(keys, slot_count, proto_id) + crate::object::shapes::mint_typed_shape_id(keys, slot_count, proto_id, rep) } else if hot_layout_accepts(requested, &descriptor) && crate::object::shapes::install_static_typed_shape_id( - requested, keys, slot_count, proto_id, + requested, keys, slot_count, proto_id, rep, ) { requested @@ -151,6 +162,16 @@ fn hot_layout_accepts(shape_id: u32, descriptor: &TypedLayoutDescriptor) -> bool } } +/// Does every `F64` lane of `rep` name a slot of the raw-f64 mask? +fn birth_rep_within_raw_mask(rep: u64, raw_f64_words: &[u64]) -> bool { + if !crate::object::field_rep::is_valid(rep) { + return false; + } + let raw_low = raw_f64_words.first().copied().unwrap_or(0) as u32; + crate::object::field_rep::f64_lane_slots(rep) & !raw_low == 0 + && rep & !crate::object::field_rep::lanes_below(crate::object::field_rep::REP_SLOTS) == 0 +} + #[allow(clippy::too_many_arguments)] unsafe fn init_typed_shape_layout( user_ptr: usize, diff --git a/crates/perry-runtime/src/gc/layout/typed_shape_static_tests.rs b/crates/perry-runtime/src/gc/layout/typed_shape_static_tests.rs index 9a031d6f0d..b3c83635d9 100644 --- a/crates/perry-runtime/src/gc/layout/typed_shape_static_tests.rs +++ b/crates/perry-runtime/src/gc/layout/typed_shape_static_tests.rs @@ -9,7 +9,7 @@ const RAW: [u64; 1] = [0b10]; const POINTERS: [u64; 1] = [0b01]; fn keys_for(class_id: u32, packed: &[u8]) -> u64 { - crate::object::js_build_class_keys_array(class_id, 2, packed.as_ptr(), packed.len() as u32) + crate::object::js_build_class_keys_array(class_id, 2, packed.as_ptr(), packed.len() as u32, 0) as usize as u64 } @@ -23,6 +23,7 @@ fn typed(class_id: u32, keys: u64, raw: &[u64], pointers: &[u64], requested: u32 pointers.as_ptr(), pointers.len() as u32, requested, + 0, ) } @@ -49,7 +50,7 @@ fn the_definer_adopts_the_static_id_and_an_importer_resolves_to_it() { assert_eq!(typed(class_id, keys, &RAW, &POINTERS, s), s); assert!(hot(s) == Some(Some(descriptor(&RAW, &POINTERS)))); let importer = crate::object::static_shapes::js_object_shape_id_for_class_keys_static( - keys, 2, 2, class_id, s, + keys, 2, 2, class_id, s, 0, ); assert_eq!( importer, s, @@ -69,7 +70,7 @@ fn an_importer_first_adopts_the_definers_id_and_the_typed_install_accepts_it() { let s = SHAPE_ID_BASE + 0x5103; let keys = keys_for(class_id, b"lt4u_next\0lt4u_value\0"); let importer = crate::object::static_shapes::js_object_shape_id_for_class_keys_static( - keys, 2, 2, class_id, s, + keys, 2, 2, class_id, s, 0, ); assert_eq!(importer, s); assert!( @@ -80,7 +81,7 @@ fn an_importer_first_adopts_the_definers_id_and_the_typed_install_accepts_it() { assert!(hot(s) == Some(Some(descriptor(&RAW, &POINTERS)))); assert_eq!( crate::object::static_shapes::js_object_shape_id_for_class_keys_static( - keys, 2, 2, class_id, s, + keys, 2, 2, class_id, s, 0, ), s, "a later birth of those facts reaches the one id" @@ -171,7 +172,8 @@ fn a_static_id_naming_other_facts_aborts_the_typed_install() { 2, 2, other_class, - s + s, + 0 ), s ); diff --git a/crates/perry-runtime/src/gc/tests/canonical_keys_holders.rs b/crates/perry-runtime/src/gc/tests/canonical_keys_holders.rs index 0656c379f6..5da0ba2f9e 100644 --- a/crates/perry-runtime/src/gc/tests/canonical_keys_holders.rs +++ b/crates/perry-runtime/src/gc/tests/canonical_keys_holders.rs @@ -157,6 +157,7 @@ fn no_published_canonical_list_names_a_key_at_its_pre_move_address() { 3, packed.as_ptr(), packed.len() as u32, + 0, )); // Whole-list canonicalization of a caller-built list of young keys. let scratch = crate::array::js_array_alloc_with_length(2); @@ -279,6 +280,7 @@ fn class_inline_keys_birth_follows_the_move(stamped: bool) { 2, packed.as_ptr(), packed.len() as u32, + 0, )); let before = addr_of(&keys) as *mut ArrayHeader; assert!( @@ -290,7 +292,7 @@ fn class_inline_keys_birth_follows_the_move(stamped: bool) { arm_collection_on_next_block(&trigger); let obj = if stamped { crate::object::js_object_alloc_class_inline_keys_stamped( - class_id, 0, 2, before, shape_id, + class_id, 0, 2, before, shape_id, 0, ) } else { crate::object::js_object_alloc_class_inline_keys(class_id, 0, 2, before) @@ -355,6 +357,7 @@ fn dynamic_parent_birth_installs_the_live_merged_keys_when_its_allocation_collec 2, parent_packed.as_ptr(), parent_packed.len() as u32, + 0, )); crate::object::register_class(child_cid, parent_cid); let own_packed = format!("{}\0", names[2]); @@ -430,6 +433,7 @@ fn class_keys_memo_belongs_to_the_agent_that_built_it() { 2, packed.as_ptr(), packed.len() as u32, + 0, )); let current = || addr_of(&mine); let registered = || { @@ -452,6 +456,7 @@ fn class_keys_memo_belongs_to_the_agent_that_built_it() { 2, packed.as_ptr(), packed.len() as u32, + 0, ) as usize; let seen = crate::object::registered_class_keys_array(AGENT_MEMO_CLASS_ID) .map(|(a, _)| a.arr() as usize); diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/declared_at_allocation.rs b/crates/perry-runtime/src/gc/tests/layout_trace/declared_at_allocation.rs index 32a86831d4..a054071cf5 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace/declared_at_allocation.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace/declared_at_allocation.rs @@ -201,8 +201,13 @@ fn test_registered_typed_shape_traces_without_a_per_object_install() { let class_id = 17; let packed = b"peer\0payload\0"; - let keys = - crate::object::js_build_class_keys_array(class_id, 2, packed.as_ptr(), packed.len() as u32); + let keys = crate::object::js_build_class_keys_array( + class_id, + 2, + packed.as_ptr(), + packed.len() as u32, + 0, + ); let raw_mask = [0b10u64]; let pointer_mask = [0b01u64]; let shape_id = js_gc_typed_shape_id_for_keys( @@ -214,9 +219,10 @@ fn test_registered_typed_shape_traces_without_a_per_object_install() { pointer_mask.as_ptr(), 1, 0, + 0, ); let obj = - crate::object::js_object_alloc_class_inline_keys_stamped(class_id, 0, 2, keys, shape_id); + crate::object::js_object_alloc_class_inline_keys_stamped(class_id, 0, 2, keys, shape_id, 0); let child = crate::string::js_string_from_bytes(b"registered".as_ptr(), 10); unsafe { let header = header_from_user_ptr(obj as *const u8); diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs b/crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs index 8303c7685d..a1d78d685b 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs @@ -320,8 +320,13 @@ fn test_class_keys_array_declares_all_pointer_slots_instead_of_a_mask() { clear_mark_seeds(); let packed: &[u8] = b"alpha\0beta\0gamma\0"; - let keys = - crate::object::js_build_class_keys_array(0x7510, 3, packed.as_ptr(), packed.len() as u32); + let keys = crate::object::js_build_class_keys_array( + 0x7510, + 3, + packed.as_ptr(), + packed.len() as u32, + 0, + ); assert!(!keys.is_null()); assert!( diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/shape_install_memo.rs b/crates/perry-runtime/src/gc/tests/layout_trace/shape_install_memo.rs index b9baf47102..05e5af7103 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace/shape_install_memo.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace/shape_install_memo.rs @@ -50,7 +50,7 @@ static POINTER_MASK_WORDS: [u64; 1] = [0b10]; fn keys_for(class_id: u32) -> *mut crate::array::ArrayHeader { let packed: &[u8] = b"n\0s\0"; - crate::object::js_build_class_keys_array(class_id, 2, packed.as_ptr(), packed.len() as u32) + crate::object::js_build_class_keys_array(class_id, 2, packed.as_ptr(), packed.len() as u32, 0) } /// Allocate one instance of the shape and fill it: a plain double in slot 0, a diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs b/crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs index 458915d1fe..be51f074e7 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace/typed_shape.rs @@ -136,6 +136,7 @@ fn test_typed_shape_descriptor_visible_for_shape_keyed_objects() { 2, packed.as_ptr(), packed.len() as u32, + 0, ); let first = crate::object::js_object_alloc_class_inline_keys(0x6957_01, 0, 2, keys); let second = crate::object::js_object_alloc_class_inline_keys(0x6957_01, 0, 2, keys); @@ -229,6 +230,7 @@ fn test_shape_keyed_typed_layout_survives_layout_transfer() { 2, packed.as_ptr(), packed.len() as u32, + 0, ); let src = crate::object::js_object_alloc_class_inline_keys(0x6964_01, 0, 2, keys); crate::object::js_object_set_field(src, 0, crate::value::JSValue::number(1.5)); @@ -310,6 +312,7 @@ fn test_shape_keyed_typed_layout_survives_copying_minor() { 2, packed.as_ptr(), packed.len() as u32, + 0, ); let obj = crate::object::js_object_alloc_class_inline_keys(0x6964_02, 0, 2, keys); crate::object::js_object_set_field(obj, 0, crate::value::JSValue::number(10.5)); @@ -385,6 +388,7 @@ fn test_poisoned_shape_intact_and_per_object_record_survive_a_copying_minor() { 2, packed.as_ptr(), packed.len() as u32, + 0, ); // Slot 0 raw-f64, slot 1 a declared pointer: the shape's first descriptor. @@ -632,6 +636,7 @@ fn test_typed_shape_descriptor_growing_new_field_falls_back() { 1, packed_keys.as_ptr(), packed_keys.len() as u32, + 0, ); let obj = crate::object::js_object_alloc_class_inline_keys(65_001, 0, 1, keys); js_gc_init_typed_shape_layout(obj as u64, 1, std::ptr::null(), 0, std::ptr::null(), 0); diff --git a/crates/perry-runtime/src/hot_diag.rs b/crates/perry-runtime/src/hot_diag.rs index db309d92f0..2d59ebe3db 100644 --- a/crates/perry-runtime/src/hot_diag.rs +++ b/crates/perry-runtime/src/hot_diag.rs @@ -1553,7 +1553,7 @@ fn buffer_dump() { /// Receiver-route admission census names, indexed by the route number the /// emitted call passes. **Must match `receiver_range::Route` in perry-codegen.** -const RECV_ROUTE_NAMES: [&str; 32] = [ +const RECV_ROUTE_NAMES: [&str; 33] = [ "generic", "generic_mru_hit", "generic_way_hit", @@ -1603,6 +1603,9 @@ const RECV_ROUTE_NAMES: [&str; 32] = [ // Runtime-counted: a by-name overwrite of a live inline slot on an object // holding a typed layout, which keeps it (it used to declare it unknown). "rt_overwrite_kept_typed", + // Runtime-counted by `js_region_loop_prime`: refused because a key a bare + // store may write a non-double into is not an `Any` lane (charter step 5). + "rt_rloop_refuse_f64_stored", ]; /// The runtime-counted routes: see [`RECV_ROUTE_NAMES`]. @@ -1620,9 +1623,10 @@ pub(crate) const RT_ROUTE_RLOOP_REFUSE_SPILL_STORED: u32 = 24; pub(crate) const RT_ROUTE_RLOOP_REFUSE_SPILL_UNSERVABLE: u32 = 25; pub(crate) const RT_ROUTE_RLOOP_REFUSE_RANGE: u32 = 26; pub(crate) const RT_ROUTE_RLOOP_RETIRE: u32 = 27; +pub(crate) const RT_ROUTE_RLOOP_REFUSE_F64_STORED: u32 = 32; -static RECV_ROUTES: [std::sync::atomic::AtomicU64; 32] = - [const { std::sync::atomic::AtomicU64::new(0) }; 32]; +static RECV_ROUTES: [std::sync::atomic::AtomicU64; 33] = + [const { std::sync::atomic::AtomicU64::new(0) }; 33]; static RECV_ROUTES_REPORT: std::sync::Once = std::sync::Once::new(); /// Set by the first emitted `js_recv_route_note`, i.e. only in a binary /// compiled with `PERRY_RECV_ROUTE_COUNT=1`; the runtime-counted routes are a diff --git a/crates/perry-runtime/src/json/parse_api.rs b/crates/perry-runtime/src/json/parse_api.rs index 96b7479a0b..8ebc92492a 100644 --- a/crates/perry-runtime/src/json/parse_api.rs +++ b/crates/perry-runtime/src/json/parse_api.rs @@ -1046,6 +1046,7 @@ pub(crate) unsafe fn build_shape_hint( field_count, packed_keys, packed_keys_len, + 0, ); Some(ObjectShapeHint { diff --git a/crates/perry-runtime/src/object/alloc.rs b/crates/perry-runtime/src/object/alloc.rs index 3dffc4b464..c67e981299 100644 --- a/crates/perry-runtime/src/object/alloc.rs +++ b/crates/perry-runtime/src/object/alloc.rs @@ -423,6 +423,7 @@ pub extern "C" fn js_object_alloc_class_inline_keys_stamped( field_count: u32, keys_array: *mut ArrayHeader, shape_id: u32, + rep: u64, ) -> *mut ObjectHeader { // The key count comes from the shape the module-init code minted beside // this keys global: the global holds only the array, and the array can @@ -433,36 +434,11 @@ pub extern "C" fn js_object_alloc_class_inline_keys_stamped( field_count, keys_array, shape_id, + rep, false, ) } -/// A class keys global's keys, with the count its module-init ShapeId names. -/// A worker agent may not have installed that id yet, and an id that names a -/// different array is not this global's; both fall back to the array itself, -/// which module init built exact. So does an id whose count the array no -/// longer holds: the id's facts diverged from the global beside it, and a -/// count past the array's initialized slots would name keys that are not -/// there. The fallback's count then differs from the id's, so the stamp -/// declines it and publishes an exact descriptor. -#[inline] -pub(super) fn preinstalled_class_keys( - keys_array: *mut ArrayHeader, - shape_id: u32, -) -> crate::object::ObjectKeys { - // SAFETY: a module keys global is a live keys array (or null). - let owned = unsafe { crate::object::ObjectKeys::owned(keys_array) }; - match crate::object::shapes::shape_descriptor_by_id(shape_id) { - Some(descriptor) - if descriptor.keys == keys_array as u64 - && descriptor.logical_key_count <= owned.count() => - { - descriptor.keys_view() - } - _ => owned, - } -} - /// Build (or fetch from SHAPE_CACHE) the keys_array for a class. /// Called ONCE per class at module init time; the resulting pointer /// is cached in a per-class global by the codegen and then passed @@ -470,17 +446,21 @@ pub(super) fn preinstalled_class_keys( /// /// Same packed-keys format as `js_object_alloc_class_with_keys`: /// null-separated UTF-8 field names. +/// +/// `rep` is the class's birth rep (charter step 5): the shape minted beside +/// the keys carries it, so an allocator that births from this cache entry +/// (`js_object_alloc_class_with_keys`, the runtime construct path through +/// `alloc_plain::class_keys_birth_rep`) gets the birth rep, never an all-`Any` +/// twin of the class's shape. #[no_mangle] pub extern "C" fn js_build_class_keys_array( class_id: u32, field_count: u32, packed_keys: *const u8, packed_keys_len: u32, + rep: u64, ) -> *mut ArrayHeader { - let shape_id = class_id - .wrapping_mul(10007) - .wrapping_add(field_count.wrapping_mul(100003)) - .wrapping_add(1000000); + let shape_id = super::alloc_plain::class_keys_cache_slot(class_id, field_count); let cached = shape_cache_get(shape_id); if !cached.is_null() { remember_class_keys(class_id, field_count, cached); @@ -492,6 +472,7 @@ pub extern "C" fn js_build_class_keys_array( shape_id, crate::object::canonical_keys::LiveObject::none(), crate::object::ObjectKeys::new(arr, 0), + rep, ); remember_class_keys(class_id, field_count, keys); return keys.arr(); @@ -541,6 +522,7 @@ pub extern "C" fn js_build_class_keys_array( shape_id, crate::object::canonical_keys::LiveObject::none(), unsafe { crate::object::ObjectKeys::owned(arr) }, + rep, ); remember_class_keys(class_id, field_count, keys); // Generated code keeps only the array; `js_object_alloc_class_inline_keys_stamped` @@ -585,10 +567,7 @@ pub extern "C" fn js_object_alloc_class_with_keys( // makes — and an instance allocated first would have to be carried across // all of them (it used to be carried raw across the first two, which can // collect). - let shape_id = class_id - .wrapping_mul(10007) - .wrapping_add(field_count.wrapping_mul(100003)) - .wrapping_add(1000000); + let shape_id = super::alloc_plain::class_keys_cache_slot(class_id, field_count); let (cached, cached_runtime_id) = shape_cache_get_with_id(shape_id); let (keys_arr, runtime_shape_id) = if !cached.is_null() { (cached, cached_runtime_id) @@ -606,9 +585,13 @@ pub extern "C" fn js_object_alloc_class_with_keys( shape_id, crate::object::canonical_keys::LiveObject::none(), unsafe { crate::object::ObjectKeys::owned(arr) }, + super::field_rep::REP_ANY, ); (keys, shape_cache_get_with_id(shape_id).1) }; + // The entry's shape carries the class's birth rep (a module-init entry, + // `js_build_class_keys_array`); every birth from it carries that rep too. + let rep = super::alloc_plain::shape_rep_of(runtime_shape_id); let (ptr, arr) = alloc_instance_keeping_keys(total_size, keys_arr.arr()); let keys_arr = crate::object::ObjectKeys::new(arr, keys_arr.count()); @@ -625,7 +608,10 @@ pub extern "C" fn js_object_alloc_class_with_keys( // for every class that lands here — and a split population is a // permanent PIC miss, not a slow start. See // `shapes::birth_stamp_object_shape`. - crate::object::shapes::birth_stamp_object_shape(ptr, runtime_shape_id, field_count); + crate::object::shapes::birth_stamp_object_shape(ptr, runtime_shape_id, field_count, rep); + if rep != super::field_rep::REP_ANY { + super::field_rep_store::birth_fill_f64_lanes(ptr); + } } remember_class_keys(class_id, field_count, keys_arr); ptr @@ -710,6 +696,7 @@ pub extern "C" fn js_object_alloc_class_dynamic_parent( shape_id, crate::object::canonical_keys::LiveObject::none(), unsafe { crate::object::ObjectKeys::owned(arr) }, + super::field_rep::REP_ANY, ); debug_assert_eq!(merged.count() as usize, merged_len); ( @@ -739,7 +726,8 @@ pub extern "C" fn js_object_alloc_class_dynamic_parent( crate::gc::layout_init_pointer_free(ptr as *mut u8); // The dynamically-parented subclass shape needs the same birth stamp // as every other class instance, or its sites split the same way. - crate::object::shapes::birth_stamp_object_shape(ptr, runtime_shape_id, field_count); + let rep = super::field_rep::REP_ANY; + crate::object::shapes::birth_stamp_object_shape(ptr, runtime_shape_id, field_count, rep); } remember_class_keys(class_id, field_count, merged_arr); ptr @@ -815,6 +803,7 @@ pub extern "C" fn js_object_alloc_with_shape( shape_id, crate::object::canonical_keys::LiveObject::none(), unsafe { crate::object::ObjectKeys::owned(arr) }, + super::field_rep::REP_ANY, ); (keys, shape_cache_get_with_id(shape_id).1) }; @@ -845,7 +834,13 @@ pub extern "C" fn js_object_alloc_with_shape( // pre-stamp window for shape-cached objects. // #8113: `field_count` is the LOGICAL live-slot bound; the extra // physical slots above it stay available for dynamic growth. - crate::object::shapes::birth_stamp_object_shape(obj_ptr, runtime_shape_id, field_count); + let rep = super::field_rep::REP_ANY; + crate::object::shapes::birth_stamp_object_shape( + obj_ptr, + runtime_shape_id, + field_count, + rep, + ); } } diff --git a/crates/perry-runtime/src/object/alloc_plain.rs b/crates/perry-runtime/src/object/alloc_plain.rs index ce1a0b5564..82d9325907 100644 --- a/crates/perry-runtime/src/object/alloc_plain.rs +++ b/crates/perry-runtime/src/object/alloc_plain.rs @@ -53,7 +53,45 @@ pub(crate) fn alloc_plain_record_inline_keys_stamped( keys_array: *mut ArrayHeader, shape_id: u32, ) -> *mut ObjectHeader { - alloc_class_inline_keys_stamped_impl(0, 0, field_count, keys_array, shape_id, true) + let rep = super::field_rep::REP_ANY; + alloc_class_inline_keys_stamped_impl(0, 0, field_count, keys_array, shape_id, rep, true) +} + +/// The shape-cache slot of `class_id`'s keys built with `field_count` keys +/// (`js_build_class_keys_array`, `js_object_alloc_class_with_keys`). +pub(super) fn class_keys_cache_slot(class_id: u32, field_count: u32) -> u32 { + class_id + .wrapping_mul(10007) + .wrapping_add(field_count.wrapping_mul(100003)) + .wrapping_add(1000000) +} + +/// The birth rep of `class_id`'s instances, read off the shape its module +/// init minted beside its canonical keys (`js_build_class_keys_array` mints +/// that shape with the class's birth rep): the shape is the record, so no +/// table carries the rep. `REP_ANY` for a class-less birth, or when the slot +/// names other keys (or nothing). +pub(super) fn class_keys_birth_rep( + class_id: u32, + field_count: u32, + keys: crate::object::ObjectKeys, +) -> u64 { + if class_id == 0 { + return super::field_rep::REP_ANY; + } + let (cached, id) = super::shape_cache_get_with_id(class_keys_cache_slot(class_id, field_count)); + if id == 0 || cached.arr() != keys.arr() || cached.count() != keys.count() { + return super::field_rep::REP_ANY; + } + shape_rep_of(id) +} + +/// The birth rep an id names (`F64` for a lane its lineage has since +/// deprecated: a birth still carries it, `birth_fill_f64_lanes`). +pub(super) fn shape_rep_of(shape_id: u32) -> u64 { + crate::object::shapes::shape_record_by_id(shape_id).map_or(super::field_rep::REP_ANY, |r| { + super::field_rep::identity(r.rep()) + }) } /// The runtime class-instance allocation, optionally born marked. @@ -64,6 +102,8 @@ pub(super) fn alloc_class_instance_with_keys_impl( keys: crate::object::ObjectKeys, premark_plain: bool, ) -> *mut ObjectHeader { + // Read before the allocation: `keys` is current only until then. + let rep = class_keys_birth_rep(class_id, field_count, keys); let (ptr, birth_slots, _, keys) = super::alloc::object_alloc_class_inline_keys_impl( class_id, parent_class_id, @@ -73,27 +113,38 @@ pub(super) fn alloc_class_instance_with_keys_impl( premark_plain, ); unsafe { - let id = crate::object::shapes::shape_id_for_class_keys_ensure( - keys.arr() as *const ArrayHeader, - keys.count(), - class_id, + // The class's birth shape with its birth rep: the same id its + // compiled `new` sites stamp when the live bound agrees. + let id = crate::object::shapes::publish_shape_result( + crate::object::shapes::class_birth_shape_ensure( + keys.arr() as *const ArrayHeader, + keys.count(), + birth_slots, + class_id, + rep, + None, + ), ); - crate::object::shapes::birth_stamp_object_shape(ptr, id, birth_slots); + crate::object::shapes::birth_stamp_object_shape(ptr, id, birth_slots, rep); + if rep != super::field_rep::REP_ANY { + crate::object::field_rep_store::birth_fill_f64_lanes(ptr); + } } ptr } -/// The compiled-class allocation from a module-init ShapeId, optionally born -/// marked. +/// The compiled-class allocation from a module-init ShapeId and the birth rep +/// codegen gave that id, optionally born marked. pub(super) fn alloc_class_inline_keys_stamped_impl( class_id: u32, parent_class_id: u32, field_count: u32, keys_array: *mut ArrayHeader, shape_id: u32, + rep: u64, premark_plain: bool, ) -> *mut ObjectHeader { - let keys = super::alloc::preinstalled_class_keys(keys_array, shape_id); + let keys = preinstalled_class_keys(keys_array, shape_id); let (ptr, birth_slots, used_preinstalled_shape, _) = super::alloc::object_alloc_class_inline_keys_impl( class_id, @@ -105,8 +156,37 @@ pub(super) fn alloc_class_inline_keys_stamped_impl( ); if !used_preinstalled_shape { unsafe { - crate::object::shapes::birth_stamp_object_shape(ptr, shape_id, birth_slots); + crate::object::shapes::birth_stamp_object_shape(ptr, shape_id, birth_slots, rep); } } + // T1: a class birth id's `F64` lanes start as +0.0 (the shape decides, + // whichever id the object ended up carrying). + unsafe { crate::object::field_rep_store::birth_fill_f64_lanes(ptr) }; ptr } + +/// A class keys global's keys, with the count its module-init ShapeId names. +/// A worker agent may not have installed that id yet, and an id that names a +/// different array is not this global's; both fall back to the array itself, +/// which module init built exact. So does an id whose count the array no +/// longer holds: the id's facts diverged from the global beside it, and a +/// count past the array's initialized slots would name keys that are not +/// there. The fallback's count then differs from the id's, so the stamp +/// declines it and publishes an exact descriptor. +#[inline] +fn preinstalled_class_keys( + keys_array: *mut ArrayHeader, + shape_id: u32, +) -> crate::object::ObjectKeys { + // SAFETY: a module keys global is a live keys array (or null). + let owned = unsafe { crate::object::ObjectKeys::owned(keys_array) }; + match crate::object::shapes::shape_descriptor_by_id(shape_id) { + Some(descriptor) + if descriptor.keys == keys_array as u64 + && descriptor.logical_key_count <= owned.count() => + { + descriptor.keys_view() + } + _ => owned, + } +} diff --git a/crates/perry-runtime/src/object/class_birth_rep_tests.rs b/crates/perry-runtime/src/object/class_birth_rep_tests.rs new file mode 100644 index 0000000000..6027dc7acf --- /dev/null +++ b/crates/perry-runtime/src/object/class_birth_rep_tests.rs @@ -0,0 +1,297 @@ +//! Charter step 5, T1: a class's birth shape carries the representation +//! codegen declared for it. One test per guarantee P4's shape-only guard +//! rests on: +//! (a) the birth id is `F64` for exactly the declared lanes, the allocator +//! birth-fills them, and the reverse typed-layout cross-check refuses an +//! intact raw-f64 slot the birth id leaves `Any`; +//! (b) generalizing an instance moves the INSTANCE, never the class's id: +//! the id keeps its identity and the next birth still carries it (an +//! importer's all-`Any` stub never adopts an `F64` id: the rep is part of +//! the static id's content, `static_shape_ids`); +//! (c) a non-Number or non-finite value bound for an `F64` birth lane goes +//! through the checked funnel (generalize / canonicalize), never raw; +//! (d) the runtime takes the rep from the mint's argument and nowhere else: +//! the same keys and class minted with two reps are two identities; +//! (e) one birth shape per class and literal: every allocation path, inline +//! or outlined, stamps the one (keys, proto, rep) id and fills its `F64` +//! lanes. + +use super::field_rep::{slot_rep, REP_ANY, REP_F64}; +use super::shapes::{ + js_object_shape_id_for_class_keys, object_shape_stamp, shape_descriptor_by_id, +}; +use super::ObjectHeader; + +const CID: u32 = 0x5117; +const F64_A_B: u64 = REP_F64 | (REP_F64 << 2); + +fn keys(packed: &[u8], count: u32) -> u64 { + crate::object::js_build_class_keys_array(CID, count, packed.as_ptr(), packed.len() as u32, 0) + as usize as u64 +} + +fn rep_of(id: u32) -> u64 { + shape_descriptor_by_id(id).expect("live shape").rep +} + +unsafe fn slot_bits(obj: *mut ObjectHeader, index: usize) -> u64 { + let fields = (obj as *mut u8).add(std::mem::size_of::()) as *const u64; + *fields.add(index) +} + +unsafe fn birth(keys: u64, count: u32, id: u32) -> *mut ObjectHeader { + crate::object::js_object_alloc_class_inline_keys_stamped( + CID, + 0, + count, + keys as usize as *mut crate::array::ArrayHeader, + id, + super::field_rep::identity(rep_of(id)), + ) +} + +/// (a) + (d): the birth id carries exactly the rep it was minted with, a rep +/// is identity (two reps, two ids), and the stamped allocator fills the +/// `F64` lanes with +0.0 while every `Any` lane keeps `undefined`. +#[test] +fn a_class_birth_id_carries_its_minted_rep_and_births_fill_its_f64_lanes() { + let k = keys(b"a\0b\0c\0", 3); + let typed = js_object_shape_id_for_class_keys(k, 3, CID, F64_A_B); + let untyped = js_object_shape_id_for_class_keys(k, 3, CID, REP_ANY); + assert_ne!(typed, untyped, "the rep is shape identity"); + assert_eq!(rep_of(typed), F64_A_B); + assert_eq!(rep_of(untyped), REP_ANY); + assert_eq!(js_object_shape_id_for_class_keys(k, 3, CID, F64_A_B), typed); + unsafe { + let obj = birth(k, 3, typed); + assert_eq!(object_shape_stamp(obj), typed); + assert_eq!(slot_bits(obj, 0), 0.0f64.to_bits()); + assert_eq!(slot_bits(obj, 1), 0.0f64.to_bits()); + assert_eq!(slot_bits(obj, 2), crate::value::TAG_UNDEFINED); + let plain = birth(k, 3, untyped); + assert_eq!(slot_bits(plain, 0), crate::value::TAG_UNDEFINED); + } +} + +/// (a) reverse direction: a compiled birth id that declares an `F64` lane +/// declares all of its intact layout's raw-f64 slots, so a raw-f64 slot under +/// an `Any` lane of such an id is a codegen disagreement the invariant trips. +#[test] +#[should_panic(expected = "field-rep typed-layout cross-check")] +fn the_reverse_cross_check_fires_on_a_raw_f64_slot_the_birth_id_leaves_any() { + let k = keys(b"x\0y\0p\0", 3); + // Slot 0 F64, slot 1 raw-f64 in the layout but `Any` in the id. + let id = js_object_shape_id_for_class_keys(k, 3, CID, REP_F64); + unsafe { + let obj = birth(k, 3, id); + let raw = [0b011u64]; + let pointers = [0b100u64]; + crate::gc::js_gc_declare_typed_shape_layout( + obj as usize as u64, + 3, + raw.as_ptr(), + 1, + pointers.as_ptr(), + 1, + ); + let fields = (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; + // GC_STORE_AUDIT(INIT): Numbers into the raw slots of a fresh object. + *fields.add(1) = 2.5f64.to_bits(); + super::field_rep_store::assert_f64_lanes_hold_numbers( + obj, + super::shapes::object_shape_record(obj), + 3, + ); + } +} + +/// (b) + (c): a non-Number stored into an `F64` birth lane goes through the +/// checked funnel: the instance moves off the birth id to a shape whose lane +/// is `Any`; the birth id keeps its `F64` identity and the next birth still +/// gets it (the class's id global never needs to move). NaN / Infinity keep +/// the lane and are stored canonical. +#[test] +fn generalizing_an_instance_moves_the_instance_not_the_class_birth_id() { + let k = keys(b"u\0v\0", 2); + let id = js_object_shape_id_for_class_keys(k, 2, CID, F64_A_B); + unsafe { + let obj = birth(k, 2, id); + let key_v = crate::string::js_string_from_bytes(b"v".as_ptr(), 1); + crate::object::js_object_set_field_by_name(obj, key_v, f64::INFINITY); + assert_eq!( + object_shape_stamp(obj), + id, + "Infinity is a Number: lane kept" + ); + assert_eq!(slot_bits(obj, 1), f64::INFINITY.to_bits()); + let key_u = crate::string::js_string_from_bytes(b"u".as_ptr(), 1); + let s = crate::string::js_string_from_bytes(b"not a number".as_ptr(), 12); + let boxed = f64::from_bits(crate::value::js_nanbox_string(s as i64).to_bits()); + crate::object::js_object_set_field_by_name(obj, key_u, boxed); + let moved = object_shape_stamp(obj); + assert_ne!(moved, id, "the instance left the birth id"); + assert_eq!(slot_rep(rep_of(moved), 0), REP_ANY); + assert_eq!(slot_rep(rep_of(moved), 1), REP_F64); + assert_eq!( + super::field_rep::identity(rep_of(id)), + F64_A_B, + "the birth id's identity never changes" + ); + let next = birth(k, 2, id); + assert_eq!( + object_shape_stamp(next), + id, + "the next birth still gets the F64 id" + ); + assert_eq!(slot_bits(next, 0), 0.0f64.to_bits()); + } +} + +/// A loop region's bare store runs no field-representation check, so its +/// word must not admit a store of a value not proven a canonical double +/// (`boxed_mask`) into a non-`Any` lane; a proven double, or an `Any` lane, +/// packs as before. +#[test] +fn a_region_word_refuses_a_boxed_store_into_an_f64_lane() { + use super::shapes::{js_region_loop_pack, REGION_GUARD_WORD_EMPTY}; + let k = keys(b"ra\0rb\0", 2); + let typed = js_object_shape_id_for_class_keys(k, 2, CID, REP_F64); + let untyped = js_object_shape_id_for_class_keys(k, 2, CID, REP_ANY); + let (ra, rb) = unsafe { + let (slots, len) = crate::object::keys_array_dense_slots_resolved( + k as usize as *const crate::array::ArrayHeader, + ); + assert!(len >= 2); + ((*slots).to_bits(), (*slots.add(1)).to_bits()) + }; + let pack = |id: u32, key: u64, stored: u32, boxed: u32| { + js_region_loop_pack(id, 1, key, 0, 0, 0, 0, stored, boxed) + }; + assert_eq!( + pack(typed, ra, 1, 1), + REGION_GUARD_WORD_EMPTY, + "a boxed store into the F64 lane of `ra` is refused" + ); + assert_ne!( + pack(typed, ra, 1, 0), + REGION_GUARD_WORD_EMPTY, + "a proven double packs" + ); + assert_ne!( + pack(typed, rb, 1, 1), + REGION_GUARD_WORD_EMPTY, + "`rb` is an Any lane" + ); + assert_ne!( + pack(untyped, ra, 1, 1), + REGION_GUARD_WORD_EMPTY, + "an all-Any shape packs" + ); +} + +/// Design step 4 x T1: the rep is part of a static id's content, so a class +/// birth with an `F64` lane adopts its static id like an all-`Any` one, and +/// the same keys with the other rep are another content under another id. +#[test] +fn a_class_birth_with_an_f64_lane_adopts_its_static_id() { + use super::static_shapes::js_object_shape_id_for_class_keys_static; + let k = keys(b"sa\0sb\0", 2); + let f64_id = crate::object::shapes::SHAPE_ID_BASE + 0x3a61; + let any_id = crate::object::shapes::SHAPE_ID_BASE + 0x3a62; + let typed = js_object_shape_id_for_class_keys_static(k, 2, 2, CID, f64_id, REP_F64); + assert_eq!(typed, f64_id, "the F64 birth adopts its static id"); + assert_eq!(rep_of(typed), REP_F64); + let untyped = js_object_shape_id_for_class_keys_static(k, 2, 2, CID, any_id, REP_ANY); + assert_eq!(untyped, any_id, "the all-Any content is another id"); + assert_eq!( + js_object_shape_id_for_class_keys_static(k, 2, 2, CID, f64_id, REP_F64), + typed, + "a second registration resolves to the same id" + ); +} + +/// (e) The shape is the truth: a literal (anonymous shape class) or a class +/// born with `F64` lanes has ONE birth shape, whichever allocator runs. The +/// compiled inline `new` stamps the module-init birth id (the stamped +/// allocator here, which stamps the same id); the outlined births — the +/// shape-cache allocator (`js_object_alloc_class_with_keys`) and the runtime +/// construct path from the class memo (`alloc_class_instance_with_keys`, +/// `new` of a class value, `Reflect.construct`) — carry the birth rep and so +/// land on that id too, with every `F64` lane a canonical double from birth +/// and after a Number store (an INT32 immediate included). For the literal, +/// the shape-cache mint beside the keys IS the birth shape (so a seed-less +/// static request of it misses, `static_shape_seeds`); for a named class it +/// is the default-prototype sibling, and the birth still carries the rep. +#[test] +fn every_birth_path_of_a_rep_literal_and_class_stamps_one_shape_and_fills_its_f64_lanes() { + for (cid, literal) in [(0x511A_u32, true), (0x511B_u32, false)] { + if literal { + unsafe { crate::object::js_register_anon_shape_class_id(cid) }; + } + let packed = b"pa\0pb\0pc\0"; + let k = crate::object::js_build_class_keys_array( + cid, + 3, + packed.as_ptr(), + packed.len() as u32, + F64_A_B, + ) as usize as u64; + // Module init's birth id (what the inline `new` bakes in). + let id = js_object_shape_id_for_class_keys(k, 3, cid, F64_A_B); + assert_eq!(rep_of(id), F64_A_B); + let (_, cache_id) = + super::shape_cache_get_with_id(super::alloc_plain::class_keys_cache_slot(cid, 3)); + assert_eq!(rep_of(cache_id), F64_A_B, "the cache mint carries the rep"); + assert_eq!( + cache_id == id, + literal, + "a literal's cache mint is its birth shape; a class's is its sibling" + ); + unsafe { + let inline = crate::object::js_object_alloc_class_inline_keys_stamped( + cid, + 0, + 3, + k as usize as *mut crate::array::ArrayHeader, + id, + F64_A_B, + ); + let cached = crate::object::js_object_alloc_class_with_keys( + cid, + 0, + 3, + packed.as_ptr(), + packed.len() as u32, + ); + let (memo_keys, memo_count) = + crate::object::registered_class_keys_array(cid).expect("class memo"); + let constructed = + crate::object::alloc::alloc_class_instance_with_keys(cid, 0, memo_count, memo_keys); + for (path, obj) in [ + ("inline", inline), + ("shape cache", cached), + ("construct", constructed), + ] { + assert_eq!( + object_shape_stamp(obj), + id, + "{path} (literal={literal}): one birth shape" + ); + assert_eq!(slot_bits(obj, 0), 0.0f64.to_bits(), "{path}: F64 lane"); + assert_eq!(slot_bits(obj, 1), 0.0f64.to_bits(), "{path}: F64 lane"); + // An INT32 immediate through the funnel: lane kept, canonical. + crate::object::store_object_field_slot(obj, 1, crate::value::INT32_TAG | 7); + assert_eq!( + object_shape_stamp(obj), + id, + "{path}: a Number keeps the lane" + ); + assert_eq!( + slot_bits(obj, 1), + 7.0f64.to_bits(), + "{path}: canonical double" + ); + } + } + } +} diff --git a/crates/perry-runtime/src/object/field_get_set/field_ops.rs b/crates/perry-runtime/src/object/field_get_set/field_ops.rs index 8502632e88..42011cc70a 100644 --- a/crates/perry-runtime/src/object/field_get_set/field_ops.rs +++ b/crates/perry-runtime/src/object/field_get_set/field_ops.rs @@ -147,8 +147,6 @@ pub extern "C" fn js_object_set_field(obj: *mut ObjectHeader, field_index: u32, } else { value }; - let fields_ptr = (obj as *mut u8).add(std::mem::size_of::()) as *mut JSValue; - let slot = fields_ptr.add(field_index as usize); // #7164 publication order (same invariant as the two "#7154 // publication order" sites in field_set_by_name/tail.rs): widen // `field_count` FIRST, before the store. `object::gc_field_slot_range` @@ -170,13 +168,8 @@ pub extern "C" fn js_object_set_field(obj: *mut ObjectHeader, field_index: u32, if field_index >= stored_field_count { set_object_live_slot_count(obj, field_index + 1); } - crate::object::proto_validity::note_marked_value_write(obj); - crate::gc::runtime_store_jsvalue_slot( - obj as usize, - slot as usize, - field_index as usize, - value.bits(), - ); + // The funnel: the field-representation store check (charter step 5). + crate::object::store_object_field_slot(obj, field_index as usize, value.bits()); } } diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss/outline_split.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss/outline_split.rs index 59c4872bec..aed1905cbf 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss/outline_split.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss/outline_split.rs @@ -105,6 +105,8 @@ pub extern "C" fn js_object_get_field_ic_fast_miss( ) -> f64 { // Under feedback the fast entry asked nothing, so the probe runs here // (and records its observe and guard pass, as the single helper did). + // Charter step 5: migrate-on-miss (DESIGN §1.5 step 4). + crate::object::field_rep_store::migrate_on_miss_value(obj_bits as u64); let probe_mru = crate::typed_feedback::typed_feedback_active(); get_field_ic_dispatch(obj_bits, key, site_id, cache_slot, probe_mru) } diff --git a/crates/perry-runtime/src/object/field_rep.rs b/crates/perry-runtime/src/object/field_rep.rs index 48d025aa47..853556d254 100644 --- a/crates/perry-runtime/src/object/field_rep.rs +++ b/crates/perry-runtime/src/object/field_rep.rs @@ -49,6 +49,17 @@ pub(crate) fn with_slot_rep(rep: u64, slot: u32, value: u64) -> u64 { (rep & !(0b11 << shift)) | (value << shift) } +/// The lanes of every slot below `slot` (the whole word past it): the part +/// of a predecessor's rep a key-add at `slot` carries. +#[inline] +pub(crate) fn lanes_below(slot: u32) -> u64 { + if slot >= REP_SLOTS { + u64::MAX + } else { + (1u64 << (2 * slot)) - 1 + } +} + /// Does no lane carry the reserved `11`? #[inline] pub(crate) fn is_valid(rep: u64) -> bool { @@ -66,6 +77,21 @@ pub(crate) fn identity(rep: u64) -> u64 { (rep & !deprecated) | (deprecated >> 1) } +/// The slots whose lane is exactly `F64` (`01`), one bit per slot: the +/// lanes a class birth shape declares (T1) and the allocator birth-fills. +#[inline] +pub(crate) fn f64_lane_slots(rep: u64) -> u32 { + let lanes = rep & LANE_LOW & !(rep >> 1); + let mut slots = 0u32; + let mut rest = lanes; + while rest != 0 { + let bit = rest.trailing_zeros(); + slots |= 1 << (bit / 2); + rest &= rest - 1; + } + slots +} + /// Does any lane carry the deprecated `10`? #[inline] pub(crate) fn has_deprecated(rep: u64) -> bool { diff --git a/crates/perry-runtime/src/object/field_rep_store.rs b/crates/perry-runtime/src/object/field_rep_store.rs index b058016b0d..414d8e521f 100644 --- a/crates/perry-runtime/src/object/field_rep_store.rs +++ b/crates/perry-runtime/src/object/field_rep_store.rs @@ -81,7 +81,7 @@ pub(crate) unsafe fn object_store_generalize(obj: *mut ObjectHeader, slot: u32) let Some(record) = shape_record_by_id(id) else { return; }; - record.deprecate_rep_slot(slot); + deprecate_lane(record, slot); let target = normalized_shape(id); debug_assert_eq!( object_slot_rep_of(target, slot), @@ -93,6 +93,74 @@ pub(crate) unsafe fn object_store_generalize(obj: *mut ObjectHeader, slot: u32) } } +/// Deprecate lane `slot` of `record` (a learned fact). The first time, move +/// the prototype-validity word: every emitted key-add memo records it and +/// refuses on a mismatch, so no memo keeps serving the deprecated shape as a +/// key-add target; the runtime key-add then resolves onward to the +/// normalized shape and new objects are born into it (DESIGN §1.5 step 4). +/// Bounded like generalization itself: once per lane of a lineage. +fn deprecate_lane(record: super::shapes::ShapeRecordRef, slot: u32) { + if record.deprecate_rep_slot(slot) { + crate::object::proto_validity::bump_proto_validity(); + crate::proxy::store_census(crate::proxy::C_REP_VALIDITY_BUMP); + } +} + +/// Key-add convergence (DESIGN §1.5 step 4): a lineage slot that has seen +/// both a Number and a non-Number is `Any`. `obj` was just stamped `id` by a +/// key-add at `slot` that stored `value_bits`. The sibling of `id` that +/// differs only in lane `slot` (`F64` for a non-Number store, `Any` for a +/// Number store) is looked up in the identity table (one probe, never a +/// mint); when it exists, the `F64` one of the two is deprecated at `slot`, +/// so Number key-adds resolve onward to the `Any` shape, objects still +/// carrying the `F64` one migrate on their next miss, and `obj` itself is +/// restamped to the normalized shape (it holds a Number or its lane is +/// already `Any`). Returns the id `obj` carries. +unsafe fn converge_key_add(obj: *mut ObjectHeader, id: u32, slot: u32, value_bits: u64) -> u32 { + if slot >= REP_SLOTS { + return id; + } + let Some(d) = shape_descriptor_by_id(id) else { + return id; + }; + if field_rep::has_deprecated(d.rep) { + return id; + } + let number = field_rep::f64_slot_bits(value_bits).is_some(); + let lane = slot_rep(d.rep, slot); + let other = match (number, lane) { + (false, REP_ANY) => field_rep::REP_F64, + (true, field_rep::REP_F64) => REP_ANY, + _ => return id, + }; + let Some(sibling) = super::shapes::shape_descriptor_find_with_rep( + d.keys as usize as *const crate::array::ArrayHeader, + d.logical_key_count, + d.live_inline_slot_count, + d.semantic_generation, + d.object_kind, + d.hole_count, + d.proto_id, + d.summary, + field_rep::with_slot_rep(d.rep, slot, other), + ) else { + return id; + }; + let f64_shape = if number { id } else { sibling }; + crate::proxy::store_census(crate::proxy::C_REP_CONVERGE); + if let Some(record) = shape_record_by_id(f64_shape) { + deprecate_lane(record, slot); + } + if !number { + return id; + } + let target = normalized_shape(id); + if target != id { + stamp_object_shape_id_with_carrier_note(obj, target); + } + target +} + /// Migrate-on-miss (DESIGN §1.5 step 4): a receiver whose shape has a /// deprecated lane is restamped to the normalized shape, so a site converges /// on it instead of going polymorphic. Returns whether it restamped. @@ -111,6 +179,7 @@ pub(crate) unsafe fn migrate_deprecated_receiver(obj: *mut ObjectHeader) -> bool return false; } stamp_object_shape_id_with_carrier_note(obj, target); + crate::proxy::store_census(crate::proxy::C_REP_MIGRATE); true } @@ -124,6 +193,296 @@ pub(crate) unsafe fn migrate_on_miss(addr: usize) { } } +/// [`migrate_on_miss`] for a miss entry that receives its receiver as a +/// NaN-boxed value: only a pointer-tagged value is considered. +#[inline] +pub(crate) fn migrate_on_miss_value(bits: u64) { + if bits & crate::value::TAG_MASK == crate::value::POINTER_TAG { + // SAFETY: `migrate_on_miss` accepts any address and considers only a + // live shaped object. + unsafe { migrate_on_miss((bits & crate::value::POINTER_MASK) as usize) }; + } +} + +/// The rep of shape `id` (`Any` for an unknown id). +#[inline] +pub(crate) fn shape_rep(id: u32) -> u64 { + shape_record_by_id(id).map_or(REP_ANY, |record| record.rep()) +} + +/// T2: the rep of the shape a key-add at `slot` produces from a predecessor +/// carrying `pred_rep`. The predecessor's lanes below `slot` carry (its +/// deprecated lanes normalized to `Any`); the new lane is `F64` iff the value +/// is a JS Number stored INLINE (an overflow slot is outside the store +/// check, so it is always `Any`). A key-only add (`value_bits` = `None`) +/// stores no value yet: its lane is `Any`. +#[inline] +pub(crate) fn key_add_rep(pred_rep: u64, slot: u32, value_bits: Option, inline: bool) -> u64 { + let carried = field_rep::normalized(pred_rep) & field_rep::lanes_below(slot); + if slot >= REP_SLOTS { + return carried; + } + let lane = match value_bits { + Some(bits) if inline && field_rep::f64_slot_bits(bits).is_some() => field_rep::REP_F64, + _ => REP_ANY, + }; + field_rep::with_slot_rep(carried, slot, lane) +} + +/// T2 for a cached key-add edge: may the edge's `target` serve a value of +/// this class at `slot`? The target is the class guard (no bit in the cache +/// key): an `F64` lane admits only a Number, a target with a deprecated lane +/// never serves (the slow path resolves onward to its normalized form), and +/// an `Any` lane admits every value (always a valid claim; a lineage whose +/// edge was learned from a non-Number converges on it). `value_bits` = `None` +/// is a key-only add, which an `F64` lane refuses. +#[inline] +pub(crate) fn cached_key_add_admits(target: u32, slot: u32, value_bits: Option) -> bool { + let rep = shape_rep(target); + if rep == REP_ANY { + return true; + } + if field_rep::has_deprecated(rep) { + return false; + } + slot_rep(rep, slot) == REP_ANY + || value_bits.is_some_and(|bits| field_rep::f64_slot_bits(bits).is_some()) +} + +/// T2 at a slow-path key-add: publish the keys edge `new_keys`, which appends +/// `slot`, with the successor's rep in the LAST publish before the caller's +/// value store, so the all-`Any` twin of the successor is never minted. +/// Returns the id `obj` carries (the id to teach the transition cache). May +/// mint, so it is a collection point: callers re-read their roots after it. +/// +/// * The bound grows (`slot` is outside the live bound): the keys edge is +/// published at the OLD bound (the new slot is outside it, so that +/// intermediate is the same shape as without step 5), then the bound +/// publish carries the rep. The slot holds its allocation-time `undefined` +/// from that stamp to the caller's store; nothing in between collects, and +/// no mint happens after the stamp (mint-then-stamp). +/// * The slot is already inside the live bound: a Number is written into it +/// FIRST (a non-pointer, and the slot is past the key list, so no reader +/// sees it), then the keys edge carries the rep. The `F64` claim holds at +/// every instant, collections inside the mint included. +/// * Overflow / key-only adds carry the predecessor's lanes; the new slot is +/// `Any` (and outside the rep's reach). +pub(crate) unsafe fn publish_key_add_edge( + obj: *mut ObjectHeader, + new_keys: super::ObjectKeys, + pred_rep: u64, + slot: u32, + value_bits: Option, + inline: bool, +) -> u32 { + let rep = key_add_rep(pred_rep, slot, value_bits, inline); + if inline && slot >= super::object_live_slot_count(obj) { + super::set_object_keys(obj, new_keys); + super::mark_object_dynamic_shape_unknown(obj); + super::shapes::publish_object_live_slot_count_rep(obj, slot + 1, Some(rep)); + } else { + if slot_rep(rep, slot) == field_rep::REP_F64 { + if let Some(bits) = value_bits.and_then(field_rep::f64_slot_bits) { + super::slot_store::store_object_field_slot(obj, slot as usize, bits); + } + } + let live = super::object_live_slot_count(obj); + super::set_object_keys_with_live_rep(obj, new_keys, live, rep); + super::mark_object_dynamic_shape_unknown(obj); + } + let id = publish_key_add_rep(obj, pred_rep, slot, value_bits, inline); + match value_bits { + Some(bits) if inline && id != 0 && !crate::object::dictionary::is_dictionary(obj) => { + converge_key_add(obj, id, slot, bits) + } + _ => id, + } +} + +/// The fix-up after [`publish_key_add_edge`]: restamp `obj` to the successor +/// that carries the predecessor's lanes plus the new lane when the publish +/// could not carry it (a stable-tombstone receiver keeps its id across an +/// append; the identity table answered with a record that has since learned +/// a deprecated lane). A no-op when the stamped rep already is the rep. +/// Runs before the value is written (shape word first, §3.3). +unsafe fn publish_key_add_rep( + obj: *mut ObjectHeader, + pred_rep: u64, + slot: u32, + value_bits: Option, + inline: bool, +) -> u32 { + let id = object_shape_stamp(obj); + if id == 0 || crate::object::dictionary::is_dictionary(obj) { + return id; + } + let Some(d) = shape_descriptor_by_id(id) else { + return id; + }; + let rep = key_add_rep(pred_rep, slot, value_bits, inline); + if rep == d.rep { + return id; + } + let target = normalized_shape(publish_shape_result(shape_descriptor_intern_with_rep( + d.keys as usize as *const crate::array::ArrayHeader, + d.logical_key_count, + d.live_inline_slot_count, + d.semantic_generation, + d.object_kind, + d.hole_count, + d.proto_id, + // The record's complete summary: the same facts, another rep. + d.summary, + rep, + // A re-intern of a live record's facts under another rep names no + // static id. + None, + ))); + if target != id { + stamp_object_shape_id_with_carrier_note(obj, target); + } + target +} + +/// Is `slot` of shape `id` an `Any` lane (the store IC words' flag: a +/// non-`Any` lane is published with the flag that makes the emitted hit +/// check the value, DESIGN §3.2)? +#[inline] +pub(crate) fn shape_slot_is_any(id: u32, slot: u32) -> bool { + object_slot_rep_of(id, slot) == REP_ANY +} + +/// Does every trace of an object check the field-representation invariant +/// ([`assert_f64_lanes_hold_numbers`])? Always in a debug build or with the +/// `field-rep-assert` feature; with `gc-instruments`, when +/// `PERRY_FIELD_REPR_VERIFY=1` (DESIGN §3.1 verify mode). A binary built +/// without either feature compiles no check, and the knob is one of +/// `gc::instruments::INSTRUMENT_KNOBS`, so setting it there aborts at startup +/// instead of passing having checked nothing. +#[cfg(any( + debug_assertions, + feature = "field-rep-assert", + feature = "gc-instruments" +))] +#[inline] +pub(crate) fn field_rep_verify_enabled() -> bool { + #[cfg(any(debug_assertions, feature = "field-rep-assert"))] + { + true + } + #[cfg(not(any(debug_assertions, feature = "field-rep-assert")))] + { + use std::sync::OnceLock; + static CACHED: OnceLock = OnceLock::new(); + *crate::once_init::get_or_init(&CACHED, || { + matches!( + std::env::var("PERRY_FIELD_REPR_VERIFY").ok().as_deref(), + Some("1") | Some("on") | Some("true") + ) + }) + } +} + +/// T1 birth fill: every `F64` lane of `obj`'s (birth) shape starts as `+0.0` +/// instead of the allocator's `undefined`, so the invariant holds from the +/// moment the object exists, before its constructor stores (a collection may +/// run in between). Codegen gives a class `F64` lanes only for fields its +/// constructor proof writes before anything can read them, so the `+0.0` is +/// never observed. Codegen's inline allocation emits the same fill itself. +/// +/// # Safety +/// `obj` is a freshly allocated, stamped, unpublished ordinary object. +pub(crate) unsafe fn birth_fill_f64_lanes(obj: *mut ObjectHeader) { + let Some(record) = super::shapes::object_shape_record(obj) else { + return; + }; + // Deprecated lanes too: a birth into a lineage that has generalized a + // lane still carries it (the id is fixed), and the invariant covers it. + let mut lanes = field_rep::f64_lane_slots(field_rep::identity(record.rep())); + if lanes == 0 { + return; + } + let live = record.live_inline_slot_count(); + let fields = (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; + while lanes != 0 { + let slot = lanes.trailing_zeros(); + lanes &= lanes - 1; + if slot < live { + // GC_STORE_AUDIT(INIT): a fresh unpublished object's F64 lane; + // +0.0 is a canonical double and never a pointer. + *fields.add(slot as usize) = 0.0f64.to_bits(); + } + } +} + +/// The field-representation invariant (charter step 5): inside the live +/// bound, every slot under an `F64` (or deprecated) lane of the receiver's +/// shape holds a canonical double. Run at every trace of an object when +/// [`field_rep_verify_enabled`], so a writer that skips the store check trips +/// it at the next collection. +/// +/// The typed-layout cross-check rides along while #8405's per-object typed +/// layouts still exist (P4 deletes them): an intact typed layout must never +/// call a slot the shape gives an `F64` lane a POINTER slot, which is the +/// one disagreement that would let the two tracers see different children. +#[cfg(any( + debug_assertions, + feature = "field-rep-assert", + feature = "gc-instruments" +))] +pub(crate) unsafe fn assert_f64_lanes_hold_numbers( + obj: *const ObjectHeader, + record: Option, + live: usize, +) { + let Some(record) = record else { + return; + }; + let rep = record.rep(); + if rep == REP_ANY { + return; + } + let fields = (obj as *const u8).add(std::mem::size_of::()) as *const u64; + // T1 reverse direction: a compiled birth id that declares any `F64` lane + // declares ALL of its layout's raw-f64 slots below `REP_SLOTS` (the class + // proof covers every `number` field or none), so an intact typed layout + // may not call a slot raw-f64 that this id leaves `Any`. Runtime-minted + // records (a normalized lineage) are exempt: their `Any` lane is a + // generalization, not a declaration. + let declares_f64 = record.is_external_carrier() && field_rep::f64_lane_slots(rep) != 0; + for slot in 0..live.min(REP_SLOTS as usize) { + if slot_rep(rep, slot as u32) == REP_ANY { + if declares_f64 + && matches!( + crate::gc::layout_typed_slot_kinds_for_user(obj as usize, slot), + Some((true, _)) + ) + { + panic!( + "field-rep typed-layout cross-check: slot {slot} of {obj:p} (shape {:#x}, rep {rep:#x}) is raw-f64 in its intact typed layout but an Any lane of its birth shape", + object_shape_stamp(obj) + ); + } + continue; + } + if let Some((_raw_f64, true)) = + crate::gc::layout_typed_slot_kinds_for_user(obj as usize, slot) + { + panic!( + "field-rep typed-layout cross-check: slot {slot} of {obj:p} (shape {:#x}, rep {rep:#x}) is an F64 lane but a pointer slot of its intact typed layout", + object_shape_stamp(obj) + ); + } + let bits = *fields.add(slot); + if field_rep::f64_slot_bits(bits) != Some(bits) { + panic!( + "field-rep invariant: slot {slot} of {obj:p} (shape {:#x}, rep {rep:#x}) holds {bits:#018x}, not a canonical double", + object_shape_stamp(obj) + ); + } + } +} + /// The shape a generalized lineage converges to from `id`: the same facts /// with every deprecated lane `Any`. The record the identity table answers /// with may itself have learned a deprecated lane since, so this follows the @@ -149,8 +508,8 @@ pub(crate) fn normalized_shape(mut id: u32) -> u32 { // The record's complete summary: the same facts, another rep. d.summary, rep, - // A re-intern under another rep names no static id (only - // REP_ANY facts can). + // A re-intern of a live record's facts under another rep names + // no static id. None, )); debug_assert_ne!(next, id, "normalizing {id:#x} found itself"); diff --git a/crates/perry-runtime/src/object/field_rep_store_tests.rs b/crates/perry-runtime/src/object/field_rep_store_tests.rs index db4f5fbc9f..e64b0ccccf 100644 --- a/crates/perry-runtime/src/object/field_rep_store_tests.rs +++ b/crates/perry-runtime/src/object/field_rep_store_tests.rs @@ -1,7 +1,7 @@ //! Charter step 5 (P2a): the runtime store check and the generalization of a -//! shape's `F64` lane (`field_rep_store`). Nothing in the runtime produces an -//! `F64` shape yet, so these tests mint one directly through the one intern -//! that takes a rep (`shape_descriptor_ensure_with_rep`) and stamp it on a +//! shape's `F64` lane (`field_rep_store`), and (P2b) the key-add producer. +//! The store-check tests mint their shapes directly through the one intern +//! that takes a rep (`shape_descriptor_ensure_with_rep`) and stamp them on a //! live object whose lanes hold Numbers. use super::field_rep::{slot_rep, with_slot_rep, REP_ANY, REP_F64, REP_F64_DEPRECATED}; @@ -22,8 +22,12 @@ unsafe fn abc() -> (*mut ObjectHeader, u32) { for (i, name) in ["a", "b", "c"].iter().enumerate() { crate::object::js_object_set_field_by_name(obj, key(name), (i + 1) as f64); } - let id = object_shape_stamp(obj); - assert_ne!(id, 0, "the fixture object is shaped"); + // The key-adds earned `F64` lanes (P2b); restamp to the all-`Any` + // sibling (a valid claim for any object), so each test states its lanes. + let stamped = object_shape_stamp(obj); + assert_ne!(stamped, 0, "the fixture object is shaped"); + let id = with_rep(stamped, REP_ANY); + stamp_object_shape_id_with_carrier_note(obj, id); (obj, id) } @@ -179,3 +183,261 @@ fn normalization_keeps_live_lanes_and_reaches_a_fixed_point() { assert!(object_shape_descriptor(other).is_some()); } } + +// ---- P2b: the key-add producer (T2) ------------------------------------- + +/// A fresh `{}` with one inline allocation, keyed by names no other test +/// uses, so the transition cache holds only this test's edges. +unsafe fn add(obj: *mut ObjectHeader, name: &str, value: f64) -> u32 { + crate::object::js_object_set_field_by_name(obj, key(name), value); + object_shape_stamp(obj) +} + +#[test] +fn a_number_key_add_publishes_an_f64_lane_and_a_non_number_an_any_lane() { + let _lock = crate::gc::global_side_table_test_lock(); + unsafe { + let s = crate::string::js_string_from_bytes(b"txt".as_ptr(), 3); + let text = f64::from_bits(crate::JSValue::string_ptr(s).bits()); + // Twice: the first object takes the slow path, the second the cached edge. + let mut ids = Vec::new(); + for _ in 0..2 { + let obj = crate::object::js_object_alloc(0, 4); + add(obj, "p2b_n_a", 1.5); + let id = add(obj, "p2b_n_b", text); + let rep = rep_of(id); + assert_eq!(slot_rep(rep, 0), REP_F64, "a Number key-add earns F64"); + assert_eq!(slot_rep(rep, 1), REP_ANY, "a string key-add stays Any"); + ids.push(id); + } + assert_eq!( + ids[0], ids[1], + "the cached edge reaches the slow path's shape" + ); + } +} + +#[test] +fn a_cached_f64_edge_refuses_a_non_number() { + let _lock = crate::gc::global_side_table_test_lock(); + unsafe { + let first = crate::object::js_object_alloc(0, 4); + let f64_id = add(first, "p2b_r_a", 7.0); + assert_eq!(slot_rep(rep_of(f64_id), 0), REP_F64); + let s = crate::string::js_string_from_bytes(b"x".as_ptr(), 1); + let second = crate::object::js_object_alloc(0, 4); + let id = add( + second, + "p2b_r_a", + f64::from_bits(crate::JSValue::string_ptr(s).bits()), + ); + assert_ne!(id, f64_id, "a string must not take the F64 edge"); + assert_eq!(slot_rep(rep_of(id), 0), REP_ANY); + } +} + +#[test] +fn an_int32_key_add_is_stored_as_its_double_on_both_paths() { + let _lock = crate::gc::global_side_table_test_lock(); + unsafe { + let boxed = f64::from_bits(crate::value::INT32_TAG | 5); + for _ in 0..2 { + let obj = crate::object::js_object_alloc(0, 4); + add(obj, "p2b_i_a", 1.0); + let id = add(obj, "p2b_i_b", boxed); + assert_eq!(slot_rep(rep_of(id), 1), REP_F64); + assert_eq!( + slot_bits(obj, 1), + 5.0f64.to_bits(), + "canonical double in an F64 lane" + ); + } + } +} + +#[test] +fn a_restamp_and_a_bound_change_keep_the_lanes() { + let _lock = crate::gc::global_side_table_test_lock(); + unsafe { + let obj = crate::object::js_object_alloc(0, 4); + let id = add(obj, "p2b_k_a", 3.0); + assert_eq!(slot_rep(rep_of(id), 0), REP_F64); + let d = object_shape_descriptor(obj).expect("shaped"); + let again = super::shapes::stamp_object_shape( + obj, + d.keys as usize as *const crate::array::ArrayHeader, + d.logical_key_count, + d.live_inline_slot_count, + ); + assert_eq!(again, id, "a same-edge restamp is the same shape"); + let grown = + super::shapes::publish_object_live_slot_count(obj, d.live_inline_slot_count + 1); + assert_eq!( + slot_rep(rep_of(grown), 0), + REP_F64, + "a bound change moves no slot" + ); + } +} + +#[test] +fn a_key_add_after_generalization_converges_on_the_normalized_shape() { + let _lock = crate::gc::global_side_table_test_lock(); + unsafe { + // A second key, so the add takes the append arm: the first-key arm's + // trailing same-edge restamp would normalize on its own. + let first = crate::object::js_object_alloc(0, 4); + add(first, "p2b_g_0", 0.5); + let f64_id = add(first, "p2b_g_a", 1.0); + let s = crate::string::js_string_from_bytes(b"y".as_ptr(), 1); + let general = add( + first, + "p2b_g_a", + f64::from_bits(crate::JSValue::string_ptr(s).bits()), + ); + assert_ne!(general, f64_id, "the store generalized"); + assert_eq!(slot_rep(rep_of(general), 1), REP_ANY); + // A new object adding the same key with a Number: the cached edge's + // target learned a deprecated lane, so it must not serve. + let next = crate::object::js_object_alloc(0, 4); + add(next, "p2b_g_0", 0.5); + assert_eq!( + add(next, "p2b_g_a", 2.0), + general, + "new objects never get S again" + ); + } +} + +/// P2d key-add convergence (DESIGN §1.5 step 4) with BOTH siblings born on +/// the runtime path, where no site memo re-primes onto one of them: a +/// Number key-add makes the `F64` successor, a string key-add of the same +/// key from the same predecessor makes the `Any` one. The second must +/// deprecate the first's lane, so an object still on it converges on the +/// `Any` shape at its next miss and a later Number key-add is born there: +/// one shape for the lineage, not two for good. +#[test] +fn a_non_number_key_add_deprecates_its_f64_sibling() { + let _lock = crate::gc::global_side_table_test_lock(); + unsafe { + let first = crate::object::js_object_alloc(0, 4); + add(first, "p2d_c_0", 0.5); + let f64_id = add(first, "p2d_c_a", 1.0); + assert_eq!(slot_rep(rep_of(f64_id), 1), REP_F64); + let second = crate::object::js_object_alloc(0, 4); + add(second, "p2d_c_0", 0.5); + let any_id = add(second, "p2d_c_a", boxed("s")); + assert_ne!(any_id, f64_id, "the string took the Any sibling"); + assert_eq!(slot_rep(rep_of(any_id), 1), REP_ANY); + assert!( + super::field_rep::has_deprecated(rep_of(f64_id)), + "the string key-add deprecated the F64 sibling's lane" + ); + assert!(migrate_deprecated_receiver(first)); + assert_eq!( + object_shape_stamp(first), + any_id, + "one shape for the lineage" + ); + let third = crate::object::js_object_alloc(0, 4); + add(third, "p2d_c_0", 0.5); + assert_eq!( + add(third, "p2d_c_a", 2.0), + any_id, + "a Number key-add is born into the Any shape" + ); + } +} + +fn boxed(name: &str) -> f64 { + f64::from_bits(crate::value::js_nanbox_string(key(name) as i64).to_bits()) +} + +/// P2c: the invariant check must be able to fail. A non-Number written +/// raw under an `F64` lane trips it. +#[test] +#[should_panic(expected = "field-rep invariant")] +fn the_invariant_check_fires_on_a_non_number_under_an_f64_lane() { + unsafe { + let (obj, id) = abc(); + let f64_a = with_rep(id, with_slot_rep(REP_ANY, 0, REP_F64)); + stamp_object_shape_id_with_carrier_note(obj, f64_a); + let fields = (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; + super::field_rep_store::assert_f64_lanes_hold_numbers( + obj, + super::shapes::object_shape_record(obj), + 3, + ); + // GC_STORE_AUDIT(INIT): the deliberate unchecked store this test + // exists to catch; nothing collects before the check below. + *fields = boxed("not a number").to_bits(); + super::field_rep_store::assert_f64_lanes_hold_numbers( + obj, + super::shapes::object_shape_record(obj), + 3, + ); + } +} + +/// Delete's raw moves (the hole write, the shift that writes `b`'s string +/// into slot 0, the squeeze) carry no store check. They need none: every +/// delete of a receiver whose lanes are live republishes its shape with all +/// lanes `Any` before it moves a slot. A shared key list is forked or +/// compacted, and both publish through `set_object_keys` / +/// `publish_object_shape_from` (`REP_ANY`); an owned list only exists after +/// such a fork, and its in-place re-adds keep the fork's `Any` record. +#[test] +fn delete_publishes_any_lanes_before_its_raw_moves() { + unsafe { + let obj = crate::object::js_object_alloc(0, 4); + crate::object::js_object_set_field_by_name(obj, key("a"), 1.5); + let s = boxed("s"); + crate::object::js_object_set_field_by_name(obj, key("b"), s); + crate::object::js_object_set_field_by_name(obj, key("c"), 2.5); + let before = super::field_rep_store::shape_rep(object_shape_stamp(obj)); + assert_eq!( + slot_rep(before, 0), + REP_F64, + "the fixture has an F64 lane at a" + ); + assert_eq!(slot_rep(before, 2), REP_F64, "and at c"); + assert_eq!(crate::object::js_object_delete_field(obj, key("a")), 1); + assert_eq!( + super::field_rep_store::shape_rep(object_shape_stamp(obj)), + REP_ANY, + "the delete successor carries no lane" + ); + super::field_rep_store::assert_f64_lanes_hold_numbers( + obj, + super::shapes::object_shape_record(obj), + 3, + ); + let b = crate::object::js_object_get_field_by_name_f64(obj, key("b")); + assert_eq!(b.to_bits(), s.to_bits(), "b moved down intact"); + } +} + +/// T2 grants a class instance's inline key-add the `F64` lane like any +/// other receiver's. The class-keyed writers stay sound because they compare +/// the ShapeId against the class's birth shape, whose lanes are `Any`: an +/// instance carrying a lane is on a different shape and misses them. +#[test] +fn a_class_instance_key_add_earns_the_lane_too() { + unsafe { + let plain = crate::object::js_object_alloc(0, 4); + crate::object::js_object_set_field_by_name(plain, key("n"), 1.5); + let plain_rep = super::field_rep_store::shape_rep(object_shape_stamp(plain)); + assert_eq!( + slot_rep(plain_rep, 0), + REP_F64, + "a plain object earns the lane" + ); + let inst = crate::object::js_object_alloc(0, 4); + (*inst).class_id = 0x00C0_FFEE; + assert!(!crate::object::is_anon_shape_class_id((*inst).class_id)); + crate::object::js_object_set_field_by_name(inst, key("n"), 1.5); + let inst_rep = super::field_rep_store::shape_rep(object_shape_stamp(inst)); + assert_ne!(object_shape_stamp(inst), 0, "the instance is shaped"); + assert_eq!(slot_rep(inst_rep, 0), REP_F64); + } +} diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs index 48332b0578..7ca44332c6 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -214,7 +214,11 @@ pub extern "C" fn js_object_set_field_by_name( let prev_shape_id = super::shapes::object_shape_stamp(o); if prev_shape_id != 0 { if let Some((next_keys, slot_idx, target_shape_id)) = - transition_cache_lookup(prev_shape_id, key) + transition_cache_lookup_for_value( + prev_shape_id, + key, + Some(value.to_bits()), + ) { // Same store semantics as the in-body fast // path: strip a raw-null POINTER_TAG value, @@ -243,17 +247,12 @@ pub extern "C" fn js_object_set_field_by_name( crate::object::INLINE_SLOT_FLOOR as u32, ) as usize; if (slot_idx as usize) < alloc_limit { - let fields_ptr = (o as *mut u8) - .add(std::mem::size_of::()) - as *mut JSValue; - let slot = fields_ptr.add(slot_idx as usize); if slot_idx >= live_slots { set_object_live_slot_count(o, slot_idx + 1); } - crate::object::proto_validity::note_marked_value_write(o); - crate::gc::runtime_store_jsvalue_slot( - o as usize, - slot as usize, + // The funnel (charter step 5 store check). + crate::object::store_object_field_slot( + o, slot_idx as usize, vbits, ); diff --git a/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs b/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs index b5aec0c602..ba53bc3284 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs @@ -684,7 +684,7 @@ fn object_set_field_by_name_transition_fast_impl_value( let prev_shape_id = super::shapes::object_shape_stamp(obj); let Some((next_keys, slot_idx, target_shape_id)) = - transition_cache_lookup(prev_shape_id, interned_key) + transition_cache_lookup_for_value(prev_shape_id, interned_key, Some(value.to_bits())) else { return None; }; diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index 07fecb493b..8a2851508c 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -569,7 +569,11 @@ pub(crate) fn set_field_by_name_object_tail( super::prop_plan::receiver_proto_bits(obj), ); } - let lane_probe = transition_cache_lookup(prev_shape_id, interned_key); + let lane_probe = transition_cache_lookup_for_value( + prev_shape_id, + interned_key, + Some(value.to_bits()), + ); if let Some((next_keys, slot_idx, target_shape_id)) = lane_probe { // Defensive: strip a raw-null POINTER_TAG value the same // way the slow overflow path below does, so a bogus @@ -599,21 +603,14 @@ pub(crate) fn set_field_by_name_object_tail( // check) by the prelude above, and `vbits` has had // the null-POINTER-TAG replacement applied. No // point re-doing it in `js_object_set_field`. - let fields_ptr = - (obj as *mut u8).add(std::mem::size_of::()) as *mut JSValue; - let slot = fields_ptr.add(slot_idx as usize); // Publish the expanded traced range and its exact // descriptor before the pointer-bearing slot value. if slot_idx >= live_slots { set_object_live_slot_count(obj, slot_idx + 1); } - crate::object::proto_validity::note_marked_value_write(obj); - crate::gc::runtime_store_jsvalue_slot( - obj as usize, - slot as usize, - slot_idx as usize, - vbits, - ); + // The funnel: the store check keeps an `F64` lane's + // double canonical (charter step 5). + crate::object::store_object_field_slot(obj, slot_idx as usize, vbits); } else { // Cached slot is past the object's inline capacity — // store in the overflow map (same as the slow path's @@ -663,8 +660,6 @@ pub(crate) fn set_field_by_name_object_tail( } }; refresh_roots_after_alloc!(); - set_object_keys(obj, new_keys); - super::mark_object_dynamic_shape_unknown(obj); // Reallocate fields to hold at least one value // Note: We assume the object has enough field slots pre-allocated @@ -675,9 +670,17 @@ pub(crate) fn set_field_by_name_object_tail( // slot is undefined-initialized at allocation, so the widened range // can only expose non-pointer sentinels — then publish the value. // Bump field_count so Object.keys()/values()/entries() see the new property. - if crate::object::object_live_slot_count(obj) == 0 { - set_object_live_slot_count(obj, 1); - } + // Charter step 5 (T2): the keys edge, the bound, and the successor's + // rep, published before the value (`publish_key_add_edge`). + super::field_rep_store::publish_key_add_edge( + obj, + new_keys, + super::field_rep_store::shape_rep(prev_shape_id), + 0, + Some(value.to_bits()), + true, + ); + refresh_roots_after_alloc!(); js_object_set_field(obj, 0, JSValue::from_bits(value.to_bits())); refresh_roots_after_alloc!(); mirror_class_object_static_write(obj, key, value); @@ -879,8 +882,17 @@ pub(crate) fn set_field_by_name_object_tail( }; refresh_roots_after_alloc!(); if new_index >= alloc_limit { - set_object_keys(obj, new_keys); - super::mark_object_dynamic_shape_unknown(obj); + // Charter step 5 (T2): carry the predecessor's lanes; an overflow + // slot is `Any`. + super::field_rep_store::publish_key_add_edge( + obj, + new_keys, + super::field_rep_store::shape_rep(prev_shape_id), + new_index as u32, + None, + false, + ); + refresh_roots_after_alloc!(); // #7538: derive the stored bits from the REFRESHED `value` — // see the twin below the linear scan. let vbits = overflow_store_bits(value, obj, new_index); @@ -915,17 +927,23 @@ pub(crate) fn set_field_by_name_object_tail( ); return; } - set_object_keys(obj, new_keys); - super::mark_object_dynamic_shape_unknown(obj); // #7154 publication order: `gc_field_slot_range` bounds the // collector's view of the payload by `field_count`, so a slot at an // index the count does not yet cover is invisible to BOTH tracing // and evacuation rewriting. Widen the count FIRST — every physical // slot is undefined-initialized at allocation, so the widened range // can only expose non-pointer sentinels — then publish the value. - if new_index as u32 >= crate::object::object_live_slot_count(obj) { - set_object_live_slot_count(obj, new_index as u32 + 1); - } + // Charter step 5 (T2): the keys edge, the bound, and the successor's + // rep, published before the value (`publish_key_add_edge`). + super::field_rep_store::publish_key_add_edge( + obj, + new_keys, + super::field_rep_store::shape_rep(prev_shape_id), + new_index as u32, + Some(value.to_bits()), + true, + ); + refresh_roots_after_alloc!(); js_object_set_field(obj, new_index as u32, JSValue::from_bits(value.to_bits())); refresh_roots_after_alloc!(); mirror_class_object_static_write(obj, key, value); @@ -1074,8 +1092,17 @@ pub(crate) fn set_field_by_name_object_tail( if new_index >= alloc_limit { // No inline room — store in the overflow HashMap so the value is not lost. // Also add the key to keys_array so Object.keys() sees it. - set_object_keys(obj, new_keys); - super::mark_object_dynamic_shape_unknown(obj); + // Charter step 5 (T2): carry the predecessor's lanes; an overflow + // slot is `Any`. + super::field_rep_store::publish_key_add_edge( + obj, + new_keys, + super::field_rep_store::shape_rep(prev_shape_id), + new_index as u32, + None, + false, + ); + refresh_roots_after_alloc!(); // #7538: the bits stored into overflow must come from the // REFRESHED `value`. This was snapshotted ABOVE the // `js_array_push` that grows the keys array — an allocation, so a @@ -1127,8 +1154,6 @@ pub(crate) fn set_field_by_name_object_tail( return; } // Publish the canonical successor computed above. - set_object_keys(obj, new_keys); - super::mark_object_dynamic_shape_unknown(obj); // Set the field at the new index and update logical field_count // #7154 publication order: `gc_field_slot_range` bounds the @@ -1138,9 +1163,17 @@ pub(crate) fn set_field_by_name_object_tail( // slot is undefined-initialized at allocation, so the widened range // can only expose non-pointer sentinels — then publish the value. // Bump field_count to reflect the newly added property - if new_index as u32 >= crate::object::object_live_slot_count(obj) { - set_object_live_slot_count(obj, new_index as u32 + 1); - } + // Charter step 5 (T2): the keys edge, the bound, and the successor's + // rep, published before the value (`publish_key_add_edge`). + super::field_rep_store::publish_key_add_edge( + obj, + new_keys, + super::field_rep_store::shape_rep(prev_shape_id), + new_index as u32, + Some(value.to_bits()), + true, + ); + refresh_roots_after_alloc!(); js_object_set_field(obj, new_index as u32, JSValue::from_bits(value.to_bits())); refresh_roots_after_alloc!(); mirror_class_object_static_write(obj, key, value); diff --git a/crates/perry-runtime/src/object/gc_slots.rs b/crates/perry-runtime/src/object/gc_slots.rs index 03e46d248f..28bdf16077 100644 --- a/crates/perry-runtime/src/object/gc_slots.rs +++ b/crates/perry-runtime/src/object/gc_slots.rs @@ -60,6 +60,14 @@ pub(crate) unsafe fn gc_field_slot_range( if field_count > 1_000_000 { return None; } + #[cfg(any( + debug_assertions, + feature = "field-rep-assert", + feature = "gc-instruments" + ))] + if super::field_rep_store::field_rep_verify_enabled() { + super::field_rep_store::assert_f64_lanes_hold_numbers(obj, record, field_count); + } let fields = (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; Some(crate::gc::HeapSlotRange::new(fields, field_count)) } diff --git a/crates/perry-runtime/src/object/json_construction.rs b/crates/perry-runtime/src/object/json_construction.rs index ba3dbfbd9d..1db21054bd 100644 --- a/crates/perry-runtime/src/object/json_construction.rs +++ b/crates/perry-runtime/src/object/json_construction.rs @@ -43,7 +43,7 @@ unsafe fn finish_inline_json_object( // matching descriptor only for the stale-id case. let id = shapes::shape_id_for_keys_ensure(keys, count as u32); set_object_keys_with_live(object, keys_view, count as u32); - shapes::birth_stamp_object_shape(object, id, count as u32); + shapes::birth_stamp_object_shape(object, id, count as u32, super::field_rep::REP_ANY); } shapes::store_kind::check_store_facts(object); @@ -222,7 +222,8 @@ pub(crate) unsafe fn object_from_json_fields_preinstalled( if !shapes::try_birth_stamp_preinstalled_shape(obj, shape_id, keys_view, count as u32) { let id = shapes::shape_id_for_keys_ensure(keys, count as u32); set_object_keys_with_live(obj, keys_view, count as u32); - shapes::birth_stamp_object_shape(obj, id, count as u32); + let rep = super::field_rep::REP_ANY; + shapes::birth_stamp_object_shape(obj, id, count as u32, rep); } crate::gc::layout_init_pointer_free(raw); obj diff --git a/crates/perry-runtime/src/object/literal_constructor.rs b/crates/perry-runtime/src/object/literal_constructor.rs index ddfca92fea..400c10eb0c 100644 --- a/crates/perry-runtime/src/object/literal_constructor.rs +++ b/crates/perry-runtime/src/object/literal_constructor.rs @@ -85,9 +85,13 @@ mod tests { crate::value::js_nanbox_string(head_key as i64), crate::value::js_nanbox_pointer(descriptor as i64), ); - let keys = - crate::object::js_build_class_keys_array(1017302, 3, b"head\0text\0n\0".as_ptr(), 12) - as u64; + let keys = crate::object::js_build_class_keys_array( + 1017302, + 3, + b"head\0text\0n\0".as_ptr(), + 12, + 0, + ) as u64; let text = b"later value must survive the first setter"; let string = crate::js_string_from_bytes(text.as_ptr(), text.len() as u32); // The caller's plain buffer is deliberately NOT rooted. The helper diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 6fdab6441d..bcae72f7a5 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -171,6 +171,8 @@ pub(crate) use side_table_roots::{ test_seed_transition_cache_entry, test_transition_cache_occupancy, }; #[cfg(test)] +mod class_birth_rep_tests; +#[cfg(test)] mod field_rep_store_tests; pub(crate) mod iterator_prototypes; pub(crate) mod map_set_subclass; @@ -797,11 +799,16 @@ fn shape_cache_get_with_id(shape_id: u32) -> (ObjectKeys, u32) { /// which `descriptor_trap_collection_preserves_for_in_target_and_keys` caught /// because its trap collects once per key — fourteen collections through one /// enumeration, where the `ownKeys` sibling collects once and saw nothing. +/// +/// `rep` is the birth rep of the shape bound beside the keys (charter step 5): +/// a class's module-init entry carries the class's, every other entry +/// `REP_ANY`. #[must_use] fn shape_cache_insert( shape_id: u32, live: canonical_keys::LiveObject, keys: ObjectKeys, + rep: u64, ) -> (canonical_keys::LiveObject, ObjectKeys) { // #10868 step 2.5 stage 1b: the cache holds the CANONICAL array for this // static shape's key list, so two compile-time shapes that spell the same @@ -850,10 +857,20 @@ fn shape_cache_insert( // #6804: bind the runtime ShapeId once at insert (one probe per shape // BIRTH), so every later allocation of this shape reads it from the // cache entry it already touches. + // The plain (prototype-default) shape of these keys with `rep`: for an + // anonymous literal class it IS the literal's birth shape, for a named + // class its all-default-prototype sibling. let runtime_shape_id = if keys_array.is_null() { 0 } else { - shapes::shape_id_for_keys_ensure(keys_array, keys.count()) + shapes::publish_shape_result(shapes::class_birth_shape_ensure( + keys_array, + keys.count(), + keys.count(), + 0, + rep, + None, + )) }; let st = crate::state::state(); let slot = (shape_id as usize) & (SHAPE_INLINE_CACHE_SIZE - 1); @@ -1252,6 +1269,25 @@ fn transition_cache_lookup( } } +/// [`transition_cache_lookup`] for a key-add of a value of known class +/// (`None` = a key-only add): a hit whose target's field representation does +/// not admit the value is a miss (charter step 5, T2; the target is the +/// class guard, so the cache key carries no class bit). +#[inline(always)] +fn transition_cache_lookup_for_value( + prev_shape_id: u32, + interned_key: *const crate::StringHeader, + value_bits: Option, +) -> Option<(ObjectKeys, u32, u32)> { + let hit = transition_cache_lookup(prev_shape_id, interned_key)?; + if field_rep_store::cached_key_add_admits(hit.2, hit.1, value_bits) { + return Some(hit); + } + #[cfg(feature = "shape-mint-diag")] + shape_mint_census::note_transition_rep_refused(); + None +} + const TRANSITION_CACHE_EAGER_SHARE_MAX_SLOT: u32 = 64; #[inline(always)] @@ -1566,9 +1602,14 @@ pub(crate) fn test_shape_cache_insert( ) -> *mut ArrayHeader { // A test hands in a freshly built, exclusively owned list. let keys = unsafe { ObjectKeys::owned(keys_array) }; - shape_cache_insert(shape_id, canonical_keys::LiveObject::none(), keys) - .1 - .arr() + shape_cache_insert( + shape_id, + canonical_keys::LiveObject::none(), + keys, + field_rep::REP_ANY, + ) + .1 + .arr() } #[cfg(test)] @@ -1813,6 +1854,18 @@ unsafe fn set_object_keys_with_live( obj: *mut ObjectHeader, keys: ObjectKeys, live_inline_slot_count: u32, +) { + set_object_keys_with_live_rep(obj, keys, live_inline_slot_count, field_rep::REP_ANY); +} + +/// `set_object_keys_with_live` publishing the successor with field +/// representation `rep` (charter step 5, T2: `field_rep_store::publish_key_add_edge`). +#[cfg_attr(feature = "shape-mint-diag", track_caller)] +unsafe fn set_object_keys_with_live_rep( + obj: *mut ObjectHeader, + keys: ObjectKeys, + live_inline_slot_count: u32, + rep: u64, ) { let keys_array = keys.arr(); // #6759 C3c: a stamped shape id (carried in the `parent_class_id` word) @@ -1873,7 +1926,7 @@ unsafe fn set_object_keys_with_live( // (`shapes::stamp_object_shape_id_with_carrier_note`), which // `publish_object_shape_from` and every other post-birth publish now // route through — this call site no longer needs to remember the note. - shapes::publish_object_shape_from(obj, predecessor, keys, live_inline_slot_count); + shapes::publish_object_shape_from_rep(obj, predecessor, keys, live_inline_slot_count, rep); // #10868 step 2.5: this is where a receiver whose key list is unique to // it stops interning (`dictionary::should_latch_to_dictionary`). The run // is nonzero only when the append that produced `keys` created the list. diff --git a/crates/perry-runtime/src/object/object_ops/keys_array.rs b/crates/perry-runtime/src/object/object_ops/keys_array.rs index ecdf83ae09..1218798e08 100644 --- a/crates/perry-runtime/src/object/object_ops/keys_array.rs +++ b/crates/perry-runtime/src/object/object_ops/keys_array.rs @@ -131,7 +131,11 @@ unsafe fn ensure_key_in_keys_array_inner( }; if let Some(handle) = interned.as_ref() { let probe = handle.with_const_ptr::(|interned_key| { - super::super::transition_cache_lookup(prev_shape_id, interned_key) + super::super::transition_cache_lookup_for_value( + prev_shape_id, + interned_key, + None, + ) }); if let Some((next_keys, slot_idx, target_shape_id)) = probe { let live = crate::object::object_live_slot_count(obj); @@ -284,7 +288,7 @@ unsafe fn ensure_key_in_keys_array_inner( } if let (Some(handle), true) = (interned_handle.as_ref(), prev_shape_id != 0) { let probe = handle.with_const_ptr::(|interned| { - super::super::transition_cache_lookup(prev_shape_id, interned) + super::super::transition_cache_lookup_for_value(prev_shape_id, interned, None) }); if let Some((next_keys, slot_idx, target_shape_id)) = probe { let live = crate::object::object_live_slot_count(obj); diff --git a/crates/perry-runtime/src/object/shape_mint_census.rs b/crates/perry-runtime/src/object/shape_mint_census.rs index 741ef3ac60..14578c5a41 100644 --- a/crates/perry-runtime/src/object/shape_mint_census.rs +++ b/crates/perry-runtime/src/object/shape_mint_census.rs @@ -213,6 +213,15 @@ static TC_MISS_PLACES: AtomicU64 = AtomicU64::new(0); static TC_MISS_UNSHARED: AtomicU64 = AtomicU64::new(0); static TC_MISS_TARGET_LEN: AtomicU64 = AtomicU64::new(0); static TC_MISS_UNSTABLE: AtomicU64 = AtomicU64::new(0); +/// Hits the value class refused (charter step 5, T2): counted in `TC_HITS` +/// too, since the lookup matched before the class guard ran. +static TC_REP_REFUSED: AtomicU64 = AtomicU64::new(0); + +pub(crate) fn note_transition_rep_refused() { + if armed() { + TC_REP_REFUSED.fetch_add(1, Ordering::Relaxed); + } +} static TC_INSERTS: AtomicU64 = AtomicU64::new(0); static TC_EVICTIONS: AtomicU64 = AtomicU64::new(0); @@ -584,7 +593,7 @@ pub(crate) fn dump() { ); } out.push_str(&format!( - " transition cache ({} entries, direct-mapped):\n lookups {} hits {} ({:.1}%)\n miss_empty {} miss_COLLIDE {} miss_places_key {} miss_unshared {} miss_target_len {} miss_unstable {}\n inserts {} EVICTIONS {} ({:.1}%)\n", + " transition cache ({} entries, direct-mapped):\n lookups {} hits {} ({:.1}%)\n miss_empty {} miss_COLLIDE {} miss_places_key {} miss_unshared {} miss_target_len {} miss_unstable {}\n rep_refused {} (of the hits: the value class did not fit the target, T2)\n inserts {} EVICTIONS {} ({:.1}%)\n", 16384, tc_total, tc_h, @@ -595,6 +604,7 @@ pub(crate) fn dump() { tc_s, tc_t, tc_u, + TC_REP_REFUSED.load(Ordering::Relaxed), TC_INSERTS.load(Ordering::Relaxed), TC_EVICTIONS.load(Ordering::Relaxed), 100.0 * TC_EVICTIONS.load(Ordering::Relaxed) as f64 diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index 53f6978407..aac35ab199 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -237,6 +237,15 @@ impl ShapeRecordRef { self.0.as_ptr() as *mut u64 } + /// Was this record minted or installed for a compiled image (a class + /// birth id, a literal's module-init id)? Codegen owns what such an id + /// declares. + #[inline] + pub(crate) fn is_external_carrier(self) -> bool { + // SAFETY: a live slab record (type docs). + unsafe { (*self.0.as_ptr()).has(RECORD_FLAG_EXTERNAL_CARRIER) } + } + /// The record's field-representation word (`field_rep`), deprecated /// lanes included. #[inline] @@ -251,14 +260,17 @@ impl ShapeRecordRef { /// at `slot`. The word is written atomically because readers of a /// published record never take the table borrow. #[inline] - pub(crate) fn deprecate_rep_slot(self, slot: u32) { + /// Returns whether this call deprecated the lane (it was `F64`). + pub(crate) fn deprecate_rep_slot(self, slot: u32) -> bool { use super::field_rep::{slot_rep, with_slot_rep, REP_F64, REP_F64_DEPRECATED}; let word = self.rep_word(); let rep = word.load(std::sync::atomic::Ordering::Relaxed); - if slot_rep(rep, slot) == REP_F64 { - let next = with_slot_rep(rep, slot, REP_F64_DEPRECATED); - word.store(next, std::sync::atomic::Ordering::Release); + if slot_rep(rep, slot) != REP_F64 { + return false; } + let next = with_slot_rep(rep, slot, REP_F64_DEPRECATED); + word.store(next, std::sync::atomic::Ordering::Release); + true } #[inline] @@ -1261,8 +1273,10 @@ pub(crate) fn shape_descriptor_ensure_with_holes( /// /// `requested` is the static id of these facts, exactly as for /// [`shape_descriptor_ensure_with_holes`]. A static id names a birth -/// content, which carries no field representation, so it is adopted only -/// for `rep == REP_ANY`; any other request of it is refused (and aborts). +/// content, whose field representation is the birth rep codegen declared +/// (charter step 5, T1: `Any` or `F64` lanes, part of the content), so it is +/// adopted for a rep with no deprecated lane; a request of it with a +/// deprecated lane is refused (and aborts): no birth is born deprecated. #[allow(clippy::too_many_arguments)] #[cfg_attr(feature = "shape-mint-diag", track_caller)] pub(crate) fn shape_descriptor_ensure_with_rep( @@ -1307,6 +1321,60 @@ pub(crate) fn shape_descriptor_ensure_with_rep( ) } +/// The id of the indexed record with exactly these facts, if the identity +/// table holds one. Probe only: never mints (the lookup half of +/// [`shape_descriptor_intern_with_rep`]). +#[allow(clippy::too_many_arguments)] +pub(crate) fn shape_descriptor_find_with_rep( + keys: *const ArrayHeader, + logical_key_count: u32, + live_inline_slot_count: u32, + semantic_generation: u64, + object_kind: ShapeObjectKind, + hole_count: u32, + proto_id: u64, + summary: u8, + rep: u64, +) -> Option { + if !super::field_rep::is_valid(rep) { + return None; + } + let keys_id = keys as usize as u64; + let facts = shapes_store::facts_key_proto( + keys_id, + logical_key_count, + live_inline_slot_count, + semantic_generation, + object_kind, + hole_count, + proto_id, + summary, + rep, + ); + let table = &crate::state::state().shapes; + let inner = table.inner.borrow(); + let ids = inner.by_facts.get(&facts)?; + let slab = table.slab(); + ids.as_slice().iter().copied().find(|&id| { + slab.record_ptr(id).is_some_and(|record| { + // SAFETY: live slab record, read immediately. + let record = unsafe { *record }; + record.has(RECORD_FLAG_FACTS_INDEXED) + && record.facts_match_proto( + keys_id, + logical_key_count, + live_inline_slot_count, + semantic_generation, + object_kind, + hole_count, + proto_id, + summary, + rep, + ) + }) + }) +} + /// The twin of an existing shape that differs only in `object_kind` (charter /// step 3: the store facts are kinds). Every other fact, the attribute summary /// included, is copied from `source`'s record, whose summary was derived from @@ -1434,13 +1502,13 @@ pub(crate) fn shape_descriptor_intern_with_rep( } } // A static id is adopted only for facts it can name (ordinary band, - // generation 0, no field representation) and only if this agent has no - // record under it yet. + // generation 0, a birth rep: no deprecated lane) and only if this agent + // has no record under it yet. let adopted = requested.filter(|&id| { is_static_shape_id(id) && !object_kind.is_exotic() && semantic_generation == 0 - && rep == super::field_rep::REP_ANY + && !super::field_rep::has_deprecated(rep) && table.slab().record_ptr(id).is_none() }); if let (Some(requested), None) = (requested, adopted) { @@ -1873,15 +1941,25 @@ pub extern "C" fn js_object_shape_id_for_keys(keys: u64, key_count: u32) -> u32 } /// [`js_object_shape_id_for_keys`] for a class's birth shape: codegen passes -/// the class id, because the shape names the prototype that class implies. +/// the class id, because the shape names the prototype that class implies, +/// and the birth `rep` (charter step 5, T1): `F64` for exactly the slots the +/// class's typed layout declares raw-f64 at allocation. The rep is codegen's +/// decision, made once from the class source; the runtime never re-derives it. #[no_mangle] pub extern "C" fn js_object_shape_id_for_class_keys( keys: u64, key_count: u32, class_id: u32, + rep: u64, ) -> u32 { - let id = - shape_id_for_class_keys_ensure(keys as usize as *const ArrayHeader, key_count, class_id); + let id = publish_shape_result(class_birth_shape_ensure( + keys as usize as *const ArrayHeader, + key_count, + key_count, + class_id, + rep, + None, + )); // SAFETY: `id` was resolved from this agent's live slab record above. unsafe { note_external_shape_carrier(shape_descriptor_by_id(id)) }; id @@ -1898,19 +1976,53 @@ pub extern "C" fn js_object_shape_id_for_class_keys_live( key_count: u32, live: u32, class_id: u32, + rep: u64, ) -> u32 { - let id = publish_shape_result(shape_descriptor_ensure_with_generation( + let id = publish_shape_result(class_birth_shape_ensure( keys as usize as *const ArrayHeader, key_count, + live, + class_id, + rep, + None, + )); + // SAFETY: `id` was resolved from this agent's live slab record above. + unsafe { note_external_shape_carrier(shape_descriptor_by_id(id)) }; + id +} + +/// A class's birth shape with its codegen-declared rep. An `F64` lane past +/// the key count (slack) or on a reserved lane is invalid facts. `requested` +/// is the driver's static id for these facts (design step 4), whose content +/// includes the rep. +pub(crate) fn class_birth_shape_ensure( + keys: *const ArrayHeader, + key_count: u32, + live: u32, + class_id: u32, + rep: u64, + requested: Option, +) -> Result { + let key_lanes = if key_count >= super::field_rep::REP_SLOTS { + u64::MAX + } else { + super::field_rep::lanes_below(key_count) + }; + if rep & !key_lanes != 0 { + return Err(ShapeDescriptorError::InvalidFacts); + } + shape_descriptor_ensure_with_rep( + keys, + key_count, live.max(key_count), 0, ShapeObjectKind::Ordinary, + 0, class_proto_id(class_id), 0, - )); - // SAFETY: `id` was resolved from this agent's live slab record above. - unsafe { note_external_shape_carrier(shape_descriptor_by_id(id)) }; - id + rep, + requested, + ) } /// #10123: the inline slot a PLAIN ordinary shape assigns to `key`, or `-1`. @@ -2164,7 +2276,13 @@ static KEEP_JS_REGION_GUARD_PRIME: unsafe extern "C" fn( /// compare, on its miss side, selects the region's spill copy. A spill key is /// served to READS only: a key in `stored_mask` must be inline (a spill store /// owes the buffer's own GC bookkeeping, which the bare store does not do). +/// +/// A key in `boxed_mask` is one a bare store may write a value the compiler +/// did not prove a canonical double (charter step 5): the bare store runs no +/// field-representation check, so its slot must be an `Any` lane of the +/// shape. A proven canonical double is a valid value of every lane. #[no_mangle] +#[allow(clippy::too_many_arguments)] pub extern "C" fn js_region_loop_pack( shape_id: u32, n: u32, @@ -2174,8 +2292,9 @@ pub extern "C" fn js_region_loop_pack( k3: u64, k4: u64, stored_mask: u32, + boxed_mask: u32, ) -> u64 { - region_loop_pack(shape_id, n, [k0, k1, k2, k3, k4], stored_mask) + region_loop_pack(shape_id, n, [k0, k1, k2, k3, k4], stored_mask, boxed_mask) .unwrap_or(REGION_GUARD_WORD_EMPTY) } @@ -2198,6 +2317,8 @@ enum RegionRefusal { SpillUnservable, /// An inline slot past the word's 6-bit field (>= 32). Range, + /// A key a bare store may write a non-double into is not an `Any` lane. + F64Stored, } fn region_loop_pack( @@ -2205,6 +2326,7 @@ fn region_loop_pack( n: u32, keys: [u64; 5], stored_mask: u32, + boxed_mask: u32, ) -> Result { use RegionRefusal::*; if !is_site_matchable_shape_id(shape_id) || n == 0 || n > REGION_GUARD_MAX_KEYS { @@ -2251,6 +2373,13 @@ fn region_loop_pack( }; let mut word = u64::from(id); for (i, &(spilled, n_at)) in at.iter().enumerate().take(n as usize) { + if !spilled + && boxed_mask & (1 << i) != 0 + && (n_at as u32) < super::field_rep::REP_SLOTS + && super::field_rep::slot_rep(descriptor.rep, n_at as u32) != super::field_rep::REP_ANY + { + return Err(F64Stored); + } let field = match spilled { false if n_at < 32 => n_at, true if n_at < 31 => 32 + n_at, @@ -2288,8 +2417,9 @@ pub unsafe extern "C" fn js_region_loop_prime( k4: u64, last: u32, stored_mask: u32, + boxed_mask: u32, ) -> u64 { - let verdict = region_loop_pack(shape_id, n, [k0, k1, k2, k3, k4], stored_mask); + let verdict = region_loop_pack(shape_id, n, [k0, k1, k2, k3, k4], stored_mask, boxed_mask); region_loop_prime_census(verdict); let packed = verdict.unwrap_or(REGION_GUARD_WORD_EMPTY); if word.is_null() { @@ -2315,9 +2445,9 @@ pub unsafe extern "C" fn js_region_loop_prime( fn region_loop_prime_census(verdict: Result) { use crate::hot_diag::{ recv_route_note_runtime, RT_ROUTE_RLOOP_PRIME_OK, RT_ROUTE_RLOOP_REFUSE_ABSENT, - RT_ROUTE_RLOOP_REFUSE_BAND, RT_ROUTE_RLOOP_REFUSE_KIND, RT_ROUTE_RLOOP_REFUSE_RANGE, - RT_ROUTE_RLOOP_REFUSE_SPILL_STORED, RT_ROUTE_RLOOP_REFUSE_SPILL_UNSERVABLE, - RT_ROUTE_RLOOP_REFUSE_SUMMARY, + RT_ROUTE_RLOOP_REFUSE_BAND, RT_ROUTE_RLOOP_REFUSE_F64_STORED, RT_ROUTE_RLOOP_REFUSE_KIND, + RT_ROUTE_RLOOP_REFUSE_RANGE, RT_ROUTE_RLOOP_REFUSE_SPILL_STORED, + RT_ROUTE_RLOOP_REFUSE_SPILL_UNSERVABLE, RT_ROUTE_RLOOP_REFUSE_SUMMARY, }; let route = match verdict { Ok(_) => RT_ROUTE_RLOOP_PRIME_OK, @@ -2328,6 +2458,7 @@ fn region_loop_prime_census(verdict: Result) { Err(RegionRefusal::SpillStored) => RT_ROUTE_RLOOP_REFUSE_SPILL_STORED, Err(RegionRefusal::SpillUnservable) => RT_ROUTE_RLOOP_REFUSE_SPILL_UNSERVABLE, Err(RegionRefusal::Range) => RT_ROUTE_RLOOP_REFUSE_RANGE, + Err(RegionRefusal::F64Stored) => RT_ROUTE_RLOOP_REFUSE_F64_STORED, }; recv_route_note_runtime(route); } @@ -2347,6 +2478,7 @@ static KEEP_JS_REGION_LOOP_PRIME: unsafe extern "C" fn( u64, u32, u32, + u32, ) -> u64 = js_region_loop_prime; /// Mint a fresh (counter) ShapeId for a codegen-registered typed layout and @@ -2355,7 +2487,12 @@ static KEEP_JS_REGION_LOOP_PRIME: unsafe extern "C" fn( /// keys alone: two objects with identical property names but different raw /// slot representations must never share a pre-baked GC descriptor. The /// fallback of [`install_static_typed_shape_id`]. -pub(crate) fn mint_typed_shape_id(keys: *const ArrayHeader, key_count: u32, proto_id: u64) -> u32 { +pub(crate) fn mint_typed_shape_id( + keys: *const ArrayHeader, + key_count: u32, + proto_id: u64, + rep: u64, +) -> u32 { let id = alloc_shape_id().unwrap_or_else(|_| shape_id_exhausted_abort()); if !shapes_slot_list::install_external_shape_id( id, @@ -2366,6 +2503,7 @@ pub(crate) fn mint_typed_shape_id(keys: *const ArrayHeader, key_count: u32, prot // A codegen-registered typed layout is a class allocation's: F-A // admitted (charter step 3). ShapeObjectKind::Ordinary, + rep, ) { invalid_shape_facts_abort(); } @@ -2384,6 +2522,7 @@ pub(crate) fn install_static_typed_shape_id( keys: *const ArrayHeader, key_count: u32, proto_id: u64, + rep: u64, ) -> bool { is_static_shape_id(id) && shapes_slot_list::install_external_shape_id( @@ -2395,6 +2534,7 @@ pub(crate) fn install_static_typed_shape_id( // A codegen-registered typed layout is a class allocation's: F-A // admitted (charter step 3). ShapeObjectKind::Ordinary, + rep, ) } @@ -2615,6 +2755,14 @@ pub(crate) unsafe fn stamp_object_shape( debug_assert_object_shape_parity(obj); return id; }; + // Charter step 5: a restamp of the SAME keys edge moves no slot, so the + // lineage's field representation carries (normalized). Any other edge + // publishes all-`Any`, which is always a valid claim. + let rep = if lineage.keys == keys as u64 && lineage.logical_key_count == key_count { + super::field_rep::normalized(lineage.rep) + } else { + super::field_rep::REP_ANY + }; // A same-facts republish (the read side's `lookup_ways`) keeps a // proof-carrying receiver on its proof shape (charter step 3): it changes // nothing the proof depends on, so retiring the proof here would make @@ -2628,7 +2776,7 @@ pub(crate) unsafe fn stamp_object_shape( } else { store_kind::mint_kind(lineage.object_kind, obj) }; - let id = publish_shape_result(shape_descriptor_ensure_with_holes( + let id = publish_shape_result(shape_descriptor_ensure_with_rep( keys, key_count, lineage.live_inline_slot_count, @@ -2639,6 +2787,7 @@ pub(crate) unsafe fn stamp_object_shape( lineage.hole_count, lineage.proto_id, receiver_extra_summary(obj), + rep, None, )); if id != (*obj).parent_class_id { @@ -2671,11 +2820,20 @@ pub(crate) unsafe fn stamp_object_shape( /// No `shape_word_is_writable` check beyond the null test: the callers have just /// written `class_id` into a header they allocated, so the receiver is a genuine /// `ObjectHeader` and never the `RegExpHeader` alias. +/// +/// `rep` is the newborn's birth rep (charter step 5): its shape is its +/// structural facts WITH that rep, whichever allocator runs. The supplied id is +/// taken only when it names that rep too, a worker's install of it carries it, +/// and the exact fallback is minted with it. So a class or literal born with +/// `F64` lanes gets the one (keys, proto, rep) id on every path, never an +/// all-`Any` twin of it. The caller fills the `F64` lanes +/// (`field_rep_store::birth_fill_f64_lanes`) once the slots are initialized. #[inline] pub(crate) unsafe fn birth_stamp_object_shape( obj: *mut crate::object::ObjectHeader, runtime_shape_id: u32, live_inline_slot_count: u32, + rep: u64, ) { if obj.is_null() || !shape_word_is_writable(obj) { return; @@ -2687,7 +2845,10 @@ pub(crate) unsafe fn birth_stamp_object_shape( let keys = current.keys as usize as *mut ArrayHeader; let key_count = current.logical_key_count; let supplied_id_is_local = - descriptor_matches_object(runtime_shape_id, obj, live_inline_slot_count) + (descriptor_matches_object(runtime_shape_id, obj, live_inline_slot_count) + && shape_descriptor_field_by_id(runtime_shape_id, |d| { + super::field_rep::identity(d.rep) == rep + }) == Some(true)) || shapes_slot_list::install_external_shape_id( runtime_shape_id, keys, @@ -2695,10 +2856,29 @@ pub(crate) unsafe fn birth_stamp_object_shape( live_inline_slot_count, object_proto_id(obj), store_kind::receiver_ordinary_kind(obj), + rep, ); if supplied_id_is_local { stamp_object_shape_id_with_carrier_note(obj, runtime_shape_id); debug_assert_object_shape_parity(obj); + } else if rep != super::field_rep::REP_ANY { + // The exact descriptor below is all-`Any`; the newborn's shape is those + // facts with its birth rep. Minted while that descriptor is stamped + // (the `publish_object_live_slot_count` discipline). + let id = publish_shape_result(shape_descriptor_ensure_with_rep( + keys, + key_count, + live_inline_slot_count, + current.semantic_generation, + current.object_kind, + current.hole_count, + current.proto_id, + receiver_extra_summary(obj), + rep, + None, + )); + stamp_object_shape_id_with_carrier_note(obj, id); + debug_assert_object_shape_parity(obj); } else { // `current` was just published from the newborn's explicit keys edge // and allocation bound, so it is already the exact descriptor. The @@ -2794,6 +2974,17 @@ pub(crate) unsafe fn birth_publish_object_shape( pub(crate) unsafe fn publish_object_live_slot_count( obj: *mut crate::object::ObjectHeader, live_inline_slot_count: u32, +) -> u32 { + publish_object_live_slot_count_rep(obj, live_inline_slot_count, None) +} + +/// [`publish_object_live_slot_count`] whose successor carries `rep` (charter +/// step 5, T2: the key-add that grows the bound publishes its value's lane +/// here). `None` carries the predecessor's lanes below the new bound. +pub(crate) unsafe fn publish_object_live_slot_count_rep( + obj: *mut crate::object::ObjectHeader, + live_inline_slot_count: u32, + rep: Option, ) -> u32 { if obj.is_null() || !shape_word_is_writable(obj) { return 0; @@ -2805,7 +2996,21 @@ pub(crate) unsafe fn publish_object_live_slot_count( return object_shape_stamp(obj); } } - synchronize_object_shape_descriptor_from(obj, predecessor, live_inline_slot_count) + match predecessor { + // Charter step 5: a bound change moves no slot, so the predecessor's + // lanes below the new bound stay valid claims. + Some(current) => publish_object_shape_from_rep( + obj, + Some(current), + current.keys_view(), + live_inline_slot_count, + rep.unwrap_or_else(|| { + super::field_rep::normalized(current.rep) + & super::field_rep::lanes_below(live_inline_slot_count) + }), + ), + None => synchronize_object_shape_descriptor_from(obj, None, live_inline_slot_count), + } } /// Install the exact descriptor for the object's current authoritative keys @@ -2863,6 +3068,27 @@ pub(crate) unsafe fn publish_object_shape_from( predecessor: Option, keys_view: crate::object::ObjectKeys, live_inline_slot_count: u32, +) -> u32 { + publish_object_shape_from_rep( + obj, + predecessor, + keys_view, + live_inline_slot_count, + super::field_rep::REP_ANY, + ) +} + +/// [`publish_object_shape_from`] with the successor's field representation +/// (charter step 5). A caller passes a rep only when it knows no slot moved +/// relative to the predecessor it carries lanes from; `REP_ANY` is always a +/// valid claim. +#[cfg_attr(feature = "shape-mint-diag", track_caller)] +pub(crate) unsafe fn publish_object_shape_from_rep( + obj: *mut crate::object::ObjectHeader, + predecessor: Option, + keys_view: crate::object::ObjectKeys, + live_inline_slot_count: u32, + rep: u64, ) -> u32 { if obj.is_null() || !shape_word_is_writable(obj) { return 0; @@ -2960,7 +3186,7 @@ pub(crate) unsafe fn publish_object_shape_from( Some(descriptor) => descriptor.proto_id, None => object_proto_id(obj), }; - let id = publish_shape_result(shape_descriptor_ensure_with_holes( + let id = publish_shape_result(shape_descriptor_ensure_with_rep( keys, key_count, live_inline_slot_count, @@ -2969,6 +3195,7 @@ pub(crate) unsafe fn publish_object_shape_from( hole_count, proto_id, receiver_extra_summary(obj), + rep, None, )); stamp_object_shape_id_with_carrier_note(obj, id); diff --git a/crates/perry-runtime/src/object/shapes_slot_list.rs b/crates/perry-runtime/src/object/shapes_slot_list.rs index e65f192fe8..636d80bd67 100644 --- a/crates/perry-runtime/src/object/shapes_slot_list.rs +++ b/crates/perry-runtime/src/object/shapes_slot_list.rs @@ -1129,8 +1129,12 @@ pub(super) fn install_external_shape_id( live_inline_slot_count: u32, proto_id: u64, object_kind: super::ShapeObjectKind, + rep: u64, ) -> bool { - if !super::is_shape_id(id) || (keys.is_null() && logical_key_count != 0) { + if !super::is_shape_id(id) + || (keys.is_null() && logical_key_count != 0) + || !crate::object::field_rep::is_valid(rep) + { return false; } // SAFETY: a live keys array or null; derived exactly as every mint does. @@ -1146,7 +1150,8 @@ pub(super) fn install_external_shape_id( 0, ) .with_proto_id(proto_id) - .with_summary(summary); + .with_summary(summary) + .with_rep(rep); record.set(super::shapes_store::RECORD_FLAG_EXTERNAL_CARRIER, true); let table = &crate::state::state().shapes; let mut inner = table.inner.borrow_mut(); @@ -1161,7 +1166,7 @@ pub(super) fn install_external_shape_id( 0, proto_id, summary, - crate::object::field_rep::REP_ANY, + rep, ); if matches { // SAFETY: same record and agent discipline as above. diff --git a/crates/perry-runtime/src/object/shapes_store_kind_tests.rs b/crates/perry-runtime/src/object/shapes_store_kind_tests.rs index 71e1f20591..c7fcea381f 100644 --- a/crates/perry-runtime/src/object/shapes_store_kind_tests.rs +++ b/crates/perry-runtime/src/object/shapes_store_kind_tests.rs @@ -265,7 +265,7 @@ fn an_explicit_id_of_the_wrong_kind_is_declined() { Some(ShapeObjectKind::Ordinary) ); let unmarked = - crate::object::js_object_alloc_class_inline_keys_stamped(0, 0, 1, keys, ordinary); + crate::object::js_object_alloc_class_inline_keys_stamped(0, 0, 1, keys, ordinary, 0); assert_ne!( object_shape_id(unmarked), ordinary, diff --git a/crates/perry-runtime/src/object/shapes_test_support.rs b/crates/perry-runtime/src/object/shapes_test_support.rs index 8b7f116c24..8f2532d667 100644 --- a/crates/perry-runtime/src/object/shapes_test_support.rs +++ b/crates/perry-runtime/src/object/shapes_test_support.rs @@ -253,6 +253,7 @@ pub(crate) fn test_install_external_shape_id( live_inline_slot_count, crate::object::shapes::PROTO_ID_DEFAULT, crate::object::shapes::ShapeObjectKind::Ordinary, + crate::object::field_rep::REP_ANY, ) } diff --git a/crates/perry-runtime/src/object/shapes_tests.rs b/crates/perry-runtime/src/object/shapes_tests.rs index aed11794ba..917d6e0ee4 100644 --- a/crates/perry-runtime/src/object/shapes_tests.rs +++ b/crates/perry-runtime/src/object/shapes_tests.rs @@ -84,16 +84,21 @@ mod c3c_tests { let _lock = crate::gc::global_side_table_test_lock(); const CID: u32 = 0x0C3C_7902; let packed = b"birth_a\0birth_b"; - let keys = - crate::object::js_build_class_keys_array(CID, 2, packed.as_ptr(), packed.len() as u32); - let shape_id = js_object_shape_id_for_class_keys(keys as usize as u64, 2, CID); + let keys = crate::object::js_build_class_keys_array( + CID, + 2, + packed.as_ptr(), + packed.len() as u32, + 0, + ); + let shape_id = js_object_shape_id_for_class_keys(keys as usize as u64, 2, CID, 0); assert!( is_shape_id(shape_id), "module init must mint a real ShapeId" ); let obj = - crate::object::js_object_alloc_class_inline_keys_stamped(CID, 0, 2, keys, shape_id); + crate::object::js_object_alloc_class_inline_keys_stamped(CID, 0, 2, keys, shape_id, 0); let birth_word = unsafe { (*obj).parent_class_id }; assert_eq!( birth_word, shape_id, @@ -115,9 +120,14 @@ mod c3c_tests { let _lock = crate::gc::global_side_table_test_lock(); const CID: u32 = 0x0C3C_7903; let packed = b"direct_a\0direct_b"; - let keys = - crate::object::js_build_class_keys_array(CID, 2, packed.as_ptr(), packed.len() as u32); - let shape_id = js_object_shape_id_for_class_keys(keys as usize as u64, 2, CID); + let keys = crate::object::js_build_class_keys_array( + CID, + 2, + packed.as_ptr(), + packed.len() as u32, + 0, + ); + let shape_id = js_object_shape_id_for_class_keys(keys as usize as u64, 2, CID, 0); let payload = std::mem::size_of::() + crate::object::INLINE_SLOT_FLOOR * std::mem::size_of::(); let obj = crate::arena::arena_alloc_gc(payload, 8, crate::gc::GC_TYPE_OBJECT) @@ -157,12 +167,17 @@ mod c3c_tests { let _lock = crate::gc::global_side_table_test_lock(); const CID: u32 = 0x0C3C_7904; let packed = b"wide_a\0wide_b"; - let keys = - crate::object::js_build_class_keys_array(CID, 2, packed.as_ptr(), packed.len() as u32); + let keys = crate::object::js_build_class_keys_array( + CID, + 2, + packed.as_ptr(), + packed.len() as u32, + 0, + ); let narrow_id = js_object_shape_id_for_keys(keys as usize as u64, 2); let obj = - crate::object::js_object_alloc_class_inline_keys_stamped(CID, 0, 3, keys, narrow_id); + crate::object::js_object_alloc_class_inline_keys_stamped(CID, 0, 3, keys, narrow_id, 0); let actual_id = unsafe { (*obj).parent_class_id }; assert_ne!( actual_id, narrow_id, @@ -185,8 +200,13 @@ mod c3c_tests { let _lock = crate::gc::global_side_table_test_lock(); const CID: u32 = 0x0C3C_7926; let packed = b"count_mismatch"; - let keys = - crate::object::js_build_class_keys_array(CID, 1, packed.as_ptr(), packed.len() as u32); + let keys = crate::object::js_build_class_keys_array( + CID, + 1, + packed.as_ptr(), + packed.len() as u32, + 0, + ); let stale_id = js_object_shape_id_for_keys(keys as usize as u64, 1); unsafe { @@ -196,7 +216,7 @@ mod c3c_tests { (*keys).length = 0; } let obj = - crate::object::js_object_alloc_class_inline_keys_stamped(CID, 0, 1, keys, stale_id); + crate::object::js_object_alloc_class_inline_keys_stamped(CID, 0, 1, keys, stale_id, 0); let actual_id = unsafe { (*obj).parent_class_id }; assert_ne!( actual_id, stale_id, @@ -554,6 +574,7 @@ mod descriptor_tests_8067 { 1, PROTO_ID_DEFAULT, ShapeObjectKind::Ordinary, + crate::object::field_rep::REP_ANY, )); assert_eq!( @@ -670,6 +691,7 @@ mod descriptor_tests_8067 { 2, PROTO_ID_DEFAULT, ShapeObjectKind::Ordinary, + crate::object::field_rep::REP_ANY, )); assert_eq!( shape_descriptor_by_id(module_id).unwrap().keys, @@ -1322,6 +1344,7 @@ mod region_guard_pack_tests { count, packed.as_ptr(), packed.len() as u32, + 0, ); js_object_shape_id_for_keys(keys as usize as u64, count) } diff --git a/crates/perry-runtime/src/object/static_shapes.rs b/crates/perry-runtime/src/object/static_shapes.rs index a3f50e75ef..1bb2156564 100644 --- a/crates/perry-runtime/src/object/static_shapes.rs +++ b/crates/perry-runtime/src/object/static_shapes.rs @@ -25,7 +25,7 @@ //! replayed into another agent is minted there BY FACTS on first sight; the //! tests in `static_shapes_tests.rs` pin both orders. -use super::shapes::{self, ShapeObjectKind}; +use super::shapes; use crate::array::ArrayHeader; /// Seed (or find) the birth shape of a class's instances under the static id @@ -33,7 +33,8 @@ use crate::array::ArrayHeader; /// class born wide, else `key_count`). `requested == 0` means the driver /// assigned no static id. Returns the id instances are stamped with: /// `requested` whenever this is the first mint of these facts in this agent, -/// which class registration guarantees. +/// which class registration guarantees. `rep` is the class's birth rep +/// (charter step 5, T1), part of the facts the static id names. #[no_mangle] pub extern "C" fn js_object_shape_id_for_class_keys_static( keys: u64, @@ -41,16 +42,14 @@ pub extern "C" fn js_object_shape_id_for_class_keys_static( live: u32, class_id: u32, requested: u32, + rep: u64, ) -> u32 { - let id = shapes::publish_shape_result(shapes::shape_descriptor_ensure_with_holes( + let id = shapes::publish_shape_result(shapes::class_birth_shape_ensure( keys as usize as *const ArrayHeader, key_count, - live.max(key_count), - 0, - ShapeObjectKind::Ordinary, - 0, - shapes::class_proto_id(class_id), - 0, + live, + class_id, + rep, Some(requested).filter(|&id| id != 0), )); // SAFETY: `id` was resolved from this agent's live slab record above. @@ -62,8 +61,16 @@ pub extern "C" fn js_object_shape_id_for_class_keys_static( /// Seed a literal (plain, `proto_id = 0`) shape under the static id /// `requested`, from `count` NUL-separated key names in `packed`, with the /// birth live bound `live` (`>= count` for a literal born wide, else -/// `count`). Returns the id a birth of this key list now resolves to in this -/// agent. +/// `count`) and the birth rep `rep` (charter step 5, T1: the literal's `F64` +/// lanes, part of the facts the static id names). Returns the id a birth of +/// this key list now resolves to in this agent. +/// +/// The seed IS the literal's own birth mint run early: the same +/// [`shapes::class_birth_shape_ensure`] its module init runs through +/// [`js_object_shape_id_for_class_keys_static`] (an anonymous literal class +/// has no vtable class, so class id 0 names the same plain prototype), with +/// the same rep. A seed and a lazy mint of equal (keys, live, rep) are +/// therefore one lookup by facts and one ShapeId. #[no_mangle] pub extern "C" fn js_shape_seed_plain( requested: u32, @@ -71,6 +78,7 @@ pub extern "C" fn js_shape_seed_plain( packed_len: u32, count: u32, live: u32, + rep: u64, ) -> u32 { if count == 0 || packed.is_null() || packed_len == 0 { return 0; @@ -82,15 +90,12 @@ pub extern "C" fn js_shape_seed_plain( return 0; } let keys = unsafe { canonical_keys_for_names(&names) }; - let id = shapes::publish_shape_result(shapes::shape_descriptor_ensure_with_holes( + let id = shapes::publish_shape_result(shapes::class_birth_shape_ensure( keys.arr(), keys.count(), - live.max(keys.count()), - 0, - ShapeObjectKind::Ordinary, - 0, - shapes::PROTO_ID_DEFAULT, + live, 0, + rep, Some(requested), )); // SAFETY: as above. @@ -173,11 +178,12 @@ static KEEP_JS_OBJECT_SHAPE_ID_FOR_CLASS_KEYS_STATIC: extern "C" fn( u32, u32, u32, + u64, ) -> u32 = js_object_shape_id_for_class_keys_static; #[cfg(feature = "keepalive-anchors")] #[used(compiler)] -static KEEP_JS_SHAPE_SEED_PLAIN: extern "C" fn(u32, *const u8, u32, u32, u32) -> u32 = +static KEEP_JS_SHAPE_SEED_PLAIN: extern "C" fn(u32, *const u8, u32, u32, u32, u64) -> u32 = js_shape_seed_plain; #[cfg(feature = "keepalive-anchors")] diff --git a/crates/perry-runtime/src/object/static_shapes_tests.rs b/crates/perry-runtime/src/object/static_shapes_tests.rs index a21065eab3..15709b208b 100644 --- a/crates/perry-runtime/src/object/static_shapes_tests.rs +++ b/crates/perry-runtime/src/object/static_shapes_tests.rs @@ -5,6 +5,10 @@ use super::*; use crate::object::shapes::{is_shape_id, SHAPE_ID_BASE, STATIC_SHAPE_ID_END}; fn seed(requested: u32, names: &[&str]) -> u32 { + seed_with_rep(requested, names, 0) +} + +fn seed_with_rep(requested: u32, names: &[&str], rep: u64) -> u32 { let packed: Vec = names .iter() .flat_map(|n| n.bytes().chain(std::iter::once(0))) @@ -15,6 +19,7 @@ fn seed(requested: u32, names: &[&str]) -> u32 { packed.len() as u32, names.len() as u32, names.len() as u32, + rep, ) } @@ -46,6 +51,93 @@ fn a_seed_mints_the_requested_id_and_every_later_mint_of_those_facts_resolves_to ); } +/// Charter step 5 x step 4: a literal born with `F64` lanes is seeded with +/// its birth rep, so the seeded id is the shape every lazy mint of the same +/// (keys, live, rep) reaches: the literal's own class-keys mint (module init +/// of its anonymous class) and a chain of Number key-adds. A Number stored +/// into an `F64` lane of the seeded shape keeps the shape (the store check's +/// fast outcome: no generalization, the canonical double in the slot). +/// Sabotage: a seed that drops its rep mints the all-`Any` facts under the +/// static id, and the seeded record no longer carries the birth rep (nor +/// would the lazy mint below reach it). +#[test] +fn a_rep_literal_seed_is_the_shape_its_lazy_mints_reach_and_keeps_its_f64_lanes() { + use crate::object::field_rep::{slot_rep, with_slot_rep, REP_F64}; + use crate::object::shapes::object_shape_stamp; + let _lock = crate::gc::global_side_table_test_lock(); + const ANON_CLASS_ID: u32 = 0x0075_5eed; + let rep = with_slot_rep(with_slot_rep(0, 0, REP_F64), 1, REP_F64); + let requested = SHAPE_ID_BASE + 0x5678; + let id = seed_with_rep(requested, &["lt5s_a", "lt5s_b"], rep); + assert_eq!(id, requested, "the seed must mint the requested static id"); + let record = shapes::shape_descriptor_by_id(id).expect("seeded record"); + assert_eq!(record.rep, rep, "the seeded shape must carry the birth rep"); + assert_eq!(record.proto_id, shapes::PROTO_ID_DEFAULT); + + // The literal's module-init mint WITHOUT a static id (a module whose + // guards embed none): the lazy mint of the same facts is the seeded id. + unsafe { crate::object::js_register_anon_shape_class_id(ANON_CLASS_ID) }; + let packed = b"lt5s_a\0lt5s_b\0"; + let keys = crate::object::js_build_class_keys_array( + ANON_CLASS_ID, + 2, + packed.as_ptr(), + packed.len() as u32, + 0, + ) as u64; + assert_eq!( + shapes::js_object_shape_id_for_class_keys(keys, 2, ANON_CLASS_ID, rep), + requested, + "the literal's lazy mint must resolve to the seeded id" + ); + // ...and its static request (module init with the id) hits. + assert_eq!( + js_object_shape_id_for_class_keys_static(keys, 2, 2, ANON_CLASS_ID, requested, rep), + requested + ); + // The all-`Any` sibling is other facts: never the static id. + let any = shapes::js_object_shape_id_for_class_keys(keys, 2, ANON_CLASS_ID, 0); + assert_ne!( + any, requested, + "the rep is identity: Any lanes are another shape" + ); + + unsafe { + // Number key-adds on a plain `{}` earn F64 lanes and reach the seed. + let obj = crate::object::js_object_alloc_with_parent(0, 0, 2); + crate::object::shapes::store_kind::premark_plain_ordinary(obj); + for (name, v) in [("lt5s_a", 1.5f64), ("lt5s_b", 2.5)] { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::object::js_object_set_field_by_name(obj, key, v); + } + assert_eq!( + object_shape_stamp(obj), + requested, + "a Number key-add chain of these keys must reach the seeded id" + ); + // An F64-lane store of a Number stays on the shape. + crate::object::store_object_field_slot(obj, 1, crate::value::INT32_TAG | 7); + assert_eq!( + object_shape_stamp(obj), + requested, + "a Number store never transitions" + ); + let fields = + (obj as *mut u8).add(std::mem::size_of::()) as *const u64; + assert_eq!( + *fields.add(1), + 7.0f64.to_bits(), + "the lane holds the canonical double" + ); + let record = shapes::shape_descriptor_by_id(requested).expect("seeded record"); + assert_eq!( + slot_rep(record.rep, 1), + REP_F64, + "the lane is still F64 (not deprecated)" + ); + } +} + /// Run `name` in a child test process with `SABOTAGE_ENV` set and require it /// to abort with the mint's refusal message. fn child_aborts_with_refusal(name: &str) { @@ -130,15 +222,15 @@ fn a_class_seed_takes_the_class_prototype_identity() { const SEEDED_CLASS_ID: u32 = 0x0074_1c11; let packed = b"lt4k_a\0lt4k_b\0"; let keys = - crate::object::js_build_class_keys_array(SEEDED_CLASS_ID, 2, packed.as_ptr(), 14) as u64; + crate::object::js_build_class_keys_array(SEEDED_CLASS_ID, 2, packed.as_ptr(), 14, 0) as u64; let requested = SHAPE_ID_BASE + 0x3456; - let id = js_object_shape_id_for_class_keys_static(keys, 2, 2, SEEDED_CLASS_ID, requested); + let id = js_object_shape_id_for_class_keys_static(keys, 2, 2, SEEDED_CLASS_ID, requested, 0); let record = shapes::shape_descriptor_by_id(id).expect("seeded record"); assert_eq!(record.proto_id, shapes::class_proto_id(SEEDED_CLASS_ID)); // Registration is idempotent: a second registration (another module // importing the class) resolves to the same id. assert_eq!( - js_object_shape_id_for_class_keys_static(keys, 2, 2, SEEDED_CLASS_ID, requested), + js_object_shape_id_for_class_keys_static(keys, 2, 2, SEEDED_CLASS_ID, requested, 0), id ); assert!(is_carrier(id)); @@ -229,8 +321,9 @@ fn a_class_registered_before_the_pools_answers_the_megamorphic_confirm() { 2, packed.as_ptr(), packed.len() as u32, + 0, ) as u64; - let id = shapes::js_object_shape_id_for_class_keys(keys, 2, LATE_POOL_CLASS_ID); + let id = shapes::js_object_shape_id_for_class_keys(keys, 2, LATE_POOL_CLASS_ID, 0); // Module B's pool runs afterwards and mints its key literals. let (x, b) = (pool_atom("ltca_x"), pool_atom("ltca_only_in_b")); assert!( diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 938c93787c..a86c1a3233 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -38,6 +38,7 @@ pub(crate) use put_value::{ }; pub use put_value::{js_put_value_set_packed_miss, PackedSetSite, PACKED_SET_EMPTY}; pub(crate) use put_value::{packed_set_cache_resolve, PackedSetWaysSlot, PACKED_SET_CHAIN_WORD}; +pub(crate) use put_value::{store_census, C_REP_CONVERGE, C_REP_MIGRATE, C_REP_VALIDITY_BUMP}; pub use put_value::{write_pic_way_entry, WritePicCache, WritePicCacheSlot, WRITE_PIC_WORDS}; mod json; mod metadata; @@ -3057,6 +3058,7 @@ mod tests { 4, packed.as_ptr(), packed.len() as u32, + 0, ); let first = crate::object::js_object_alloc_class_inline_keys(0x6809_01, 0, 4, keys); let second = crate::object::js_object_alloc_class_inline_keys(0x6809_01, 0, 4, keys); @@ -3190,6 +3192,7 @@ mod tests { 4, other_packed.as_ptr(), other_packed.len() as u32, + 0, ); assert_ne!( keys, other_keys, @@ -3287,6 +3290,7 @@ mod tests { 5, wide_packed.as_ptr(), wide_packed.len() as u32, + 0, ); let narrow = crate::object::js_object_alloc_class_inline_keys(0x6812_03, 0, 4, wide_keys); let narrow_values = [boxed_object(narrow)]; diff --git a/crates/perry-runtime/src/proxy/put_value.rs b/crates/perry-runtime/src/proxy/put_value.rs index d5d1768df9..116b00dcbb 100644 --- a/crates/perry-runtime/src/proxy/put_value.rs +++ b/crates/perry-runtime/src/proxy/put_value.rs @@ -415,6 +415,9 @@ mod packed_add; mod packed_set; pub(crate) use packed_add::note_packed_add_carriers; pub use packed_add::PackedSetSite; +pub(crate) use packed_add::{ + census as store_census, C_REP_CONVERGE, C_REP_MIGRATE, C_REP_VALIDITY_BUMP, +}; pub use packed_set::{js_put_value_set_packed_miss, PACKED_SET_EMPTY}; pub(crate) use packed_set::{packed_set_cache_resolve, PackedSetWaysSlot, PACKED_SET_CHAIN_WORD}; @@ -468,6 +471,8 @@ pub extern "C" fn js_put_value_set_ic_miss( cache_slot: *mut WritePicCacheSlot, way: i32, ) -> f64 { + // Charter step 5: migrate-on-miss (DESIGN §1.5 step 4). + crate::object::field_rep_store::migrate_on_miss_value(target.to_bits()); // Inherited-access lane: a key-adding store on a class instance whose // chain this site has already proved clear takes the transition append // (`object::chain_store`). Before any scope: the try allocates nothing on @@ -1074,6 +1079,8 @@ pub extern "C" fn js_put_value_set_dyn_ic_miss( value: f64, strict: i32, ) -> f64 { + // Charter step 5: migrate-on-miss (DESIGN §1.5 step 4). + crate::object::field_rep_store::migrate_on_miss_value(target.to_bits()); // The slot is read once here; a null cache means the site has never // primed, which the stub probe below treats as "no site token" exactly as // it treated an all-zero global (#9708). diff --git a/crates/perry-runtime/src/proxy/put_value/packed_add.rs b/crates/perry-runtime/src/proxy/put_value/packed_add.rs index 92312ae34e..2d68621144 100644 --- a/crates/perry-runtime/src/proxy/put_value/packed_add.rs +++ b/crates/perry-runtime/src/proxy/put_value/packed_add.rs @@ -173,7 +173,13 @@ pub const ADD_WAYS_WORD: usize = 3; pub const ADD_WAY_WORDS: usize = 2; /// Low bits of the guard word that hold the slot. pub const ADD_SLOT_BITS: u32 = 16; -const ADD_SLOT_MASK: u64 = (1 << ADD_SLOT_BITS) - 1; +/// Charter step 5 (P2c): the top bit of the guard's slot field marks a memo +/// whose successor's lane at the slot is not `Any`; the emitted hit then +/// refuses a value whose exponent is all ones (every non-double, and the +/// doubles the funnel canonicalizes) before it stamps anything. **Must equal +/// perry-codegen `expr/put_value_store_ic.rs::ADD_F64_SLOT`.** +pub const ADD_F64_SLOT: u64 = 1 << (ADD_SLOT_BITS - 1); +const ADD_SLOT_MASK: u64 = ADD_F64_SLOT - 1; const SPILL_FLIP: u32 = crate::object::field_get_set::PACKED_SPILL_FLIP; @@ -284,6 +290,13 @@ pub(crate) const C_PRIME_INTERCEPTED: usize = 21; pub(crate) const C_FULL_KEYADD_CLASS: usize = 22; pub(crate) const C_FULL_KEYADD_SPILL: usize = 23; pub(crate) const C_PRIME_UNVERIFIED: usize = 24; +/// Charter step 5: a lane's first deprecation, which moves the +/// prototype-validity word (`field_rep_store::deprecate_lane`). +pub(crate) const C_REP_VALIDITY_BUMP: usize = 25; +/// A key-add found its sibling differing only in the new lane. +pub(crate) const C_REP_CONVERGE: usize = 26; +/// A receiver on a shape with a deprecated lane moved to the normalized shape. +pub(crate) const C_REP_MIGRATE: usize = 27; #[cfg_attr(test, allow(dead_code))] const CENSUS_NAMES: [&str; 48] = [ @@ -312,9 +325,9 @@ const CENSUS_NAMES: [&str; 48] = [ "rt.full.key_add.class_instance", "rt.full.key_add.spill", "rt.prime.unverified", - "rt.25", - "rt.26", - "rt.27", + "rt.rep.validity_bump", + "rt.rep.converge", + "rt.rep.migrate", "rt.28", "rt.29", "rt.30", @@ -456,6 +469,14 @@ pub(crate) unsafe fn packed_add_try( let post = (shapes >> 32) as u32; let post_d = crate::object::shapes::shape_descriptor_by_id(post)?; let slot = (guard & ADD_SLOT_MASK) as usize; + // Charter step 5 (T2): the post-shape is the class guard of the memo. + if !crate::object::field_rep_store::cached_key_add_admits( + post, + slot as u32, + Some(value.to_bits()), + ) { + return None; + } // The audited funnel: layout-unknown marking, the stamp, the prototype // validity bump for a marked receiver, the old-generation carrier note. if !crate::object::shapes::install_cached_object_shape_version( @@ -625,7 +646,7 @@ pub(crate) unsafe fn packed_add_prime( && pre_d.semantic_generation == post_d.semantic_generation && pre_d.object_kind == post_d.object_kind && post_d.live_inline_slot_count == expected_live - && n < (1 << ADD_SLOT_BITS) - 1 + && u64::from(n) < ADD_SLOT_MASK && crate::object::object_is_regular(obj) && eligible_key(key) && crate::object::keys_find_slot_by_key_ptr( @@ -710,7 +731,12 @@ pub(crate) unsafe fn packed_add_prime( } let pre_word = if inline { pre } else { pre ^ SPILL_FLIP }; let shapes = u64::from(pre_word) | (u64::from(post) << 32); - let guard = (generation << ADD_SLOT_BITS) | u64::from(n); + let f64_slot = if inline && !crate::object::field_rep_store::shape_slot_is_any(post, n) { + ADD_F64_SLOT + } else { + 0 + }; + let guard = (generation << ADD_SLOT_BITS) | f64_slot | u64::from(n); let same_pre = |word: u64| word != PACKED_SET_EMPTY && unflip(word as u32) == pre; // A way that holds this pre-shape (a stale guard) is superseded. if let Some(ways) = site_ways(site_ptr) { diff --git a/crates/perry-runtime/src/proxy/put_value/packed_add_tests.rs b/crates/perry-runtime/src/proxy/put_value/packed_add_tests.rs index b5d3e79e8a..1d95c5c1f3 100644 --- a/crates/perry-runtime/src/proxy/put_value/packed_add_tests.rs +++ b/crates/perry-runtime/src/proxy/put_value/packed_add_tests.rs @@ -294,3 +294,30 @@ fn a_far_memo_moves_into_the_inline_ways() { assert_eq!(at(h + 1).0, next_home); assert_eq!(at(h + 3).0, hot); } + +/// Charter step 5 (P2c): a memo whose successor has an `F64` lane at the +/// slot carries the flag the emitted hit refuses non-doubles with; a memo +/// learned from a non-Number does not. +#[test] +fn a_number_key_add_memo_carries_the_store_check_flag() { + let key = interned(b"p2c_added_number"); + let first = parsed(b"{\"p2c_q\":1}"); + let site = leaked_site(); + miss(site, first, key, 5.5); + assert!( + !crate::object::field_rep_store::shape_slot_is_any(stamp(first), 1), + "the successor has an F64 lane" + ); + let guard = site.add_guard.load(Ordering::Relaxed); + assert_ne!(guard & ADD_F64_SLOT, 0); + assert_eq!(guard & ADD_SLOT_MASK, 1); + assert_eq!(guard >> ADD_SLOT_BITS, add_generation()); + + let other = interned(b"p2c_added_string"); + let second = parsed(b"{\"p2c_q\":1}"); + let text = crate::string::js_string_from_bytes(b"s".as_ptr(), 1); + let boxed = f64::from_bits(crate::value::js_nanbox_string(text as i64).to_bits()); + let site2 = leaked_site(); + miss(site2, second, other, boxed); + assert_eq!(site2.add_guard.load(Ordering::Relaxed) & ADD_F64_SLOT, 0); +} diff --git a/crates/perry-runtime/src/proxy/put_value/packed_set.rs b/crates/perry-runtime/src/proxy/put_value/packed_set.rs index d58b36e49d..3682cf4c8d 100644 --- a/crates/perry-runtime/src/proxy/put_value/packed_set.rs +++ b/crates/perry-runtime/src/proxy/put_value/packed_set.rs @@ -72,6 +72,15 @@ use super::*; /// as either), so the compare refuses an unprimed site by itself. pub const PACKED_SET_EMPTY: u64 = 0xFFFF_FFFF; +/// Charter step 5 (P2c, DESIGN §3.2): the top bit of a store word whose slot +/// is not an `Any` lane of its ShapeId. The emitted hit then stores inline +/// only a value whose exponent is not all ones (a finite double, already +/// canonical); anything else takes the miss, whose store is the checked +/// funnel. A lane never becomes `Any` -> `F64` under one id, so the flag is a +/// function of the id like the rest of the word. **Must equal perry-codegen +/// `expr/put_value_store_ic.rs` (the word's sign bit).** +pub const PACKED_SET_F64_SLOT: u64 = 1 << 63; + /// Ways in a site's cache. The first [`PACKED_SET_INLINE_WAYS`] are compared by /// the emitted code (**must equal `PACKED_SET_INLINE_WAYS` in /// `perry-codegen/src/expr/put_value_store_ic.rs`**); the rest by this entry. @@ -325,7 +334,7 @@ unsafe fn packed_ways_store_impl( for (way, word) in ways.iter().enumerate() { let word = word.load(Ordering::Relaxed); let stamp = word as u32; - let index = (word >> 32) as u32; + let index = ((word & !PACKED_SET_F64_SLOT) >> 32) as u32; if stamp == sid && way >= first_way { // Charter step 3: the matched id is an `Ordinary` shape (the only // kind `prime_packed_set` publishes), which proves the receiver @@ -461,7 +470,12 @@ unsafe fn prime_packed_set( } else { (stamp ^ SPILL_FLIP, idx) }; - let entry = (u64::from(index) << 32) | u64::from(key32); + let f64_slot = if inline && !crate::object::field_rep_store::shape_slot_is_any(stamp, idx) { + PACKED_SET_F64_SLOT + } else { + 0 + }; + let entry = (u64::from(index) << 32) | u64::from(key32) | f64_slot; // The way cache: fill the first empty way, never evict. if !cache_slot.is_null() { diff --git a/crates/perry-runtime/src/proxy/put_value/packed_set_tests.rs b/crates/perry-runtime/src/proxy/put_value/packed_set_tests.rs index c8f19e9ce3..e4c08dc8d5 100644 --- a/crates/perry-runtime/src/proxy/put_value/packed_set_tests.rs +++ b/crates/perry-runtime/src/proxy/put_value/packed_set_tests.rs @@ -572,7 +572,11 @@ fn an_object_create_receiver_publishes_its_shape_and_inline_slot() { stamp(target), "an Object.create receiver must publish its ShapeId to the site word" ); - assert_eq!(word >> 32, 1, "high half: `b` is the second own slot"); + assert_eq!( + (word & !PACKED_SET_F64_SLOT) >> 32, + 1, + "high half: `b` is the second own slot" + ); // Its shape is store-admitted (charter step 3: `Ordinary`), so the // published word is actually served inline. assert!( @@ -583,3 +587,27 @@ fn an_object_create_receiver_publishes_its_shape_and_inline_slot() { let got = crate::object::js_object_get_prototype_of(target); assert_eq!(got.to_bits(), proto.to_bits()); } + +/// Charter step 5 (P2c): a store word for a non-`Any` lane carries the flag +/// that makes the emitted hit check the value; an `Any` lane's does not. +#[test] +fn an_f64_lane_publishes_the_store_check_flag() { + let key_x = interned(b"p2c_f64_x"); + let target = parsed(br#"{"p2c_a":1}"#); + crate::object::js_object_set_field_by_name(object_of(target), key_x as *mut _, 2.5); + assert!( + !crate::object::field_rep_store::shape_slot_is_any(stamp(target), 1), + "the key-add of a Number earned an F64 lane" + ); + let (_, word) = store_fresh(target, key_x, 3.5); + assert_eq!(word as u32, stamp(target)); + assert_ne!(word & PACKED_SET_F64_SLOT, 0); + assert_eq!((word & !PACKED_SET_F64_SLOT) >> 32, 1); + let (_, any_word) = store_fresh(target, interned(b"p2c_a"), 4.0); + assert_eq!( + any_word & PACKED_SET_F64_SLOT, + 0, + "a JSON birth lane is Any" + ); + assert_eq!(any_word >> 32, 0); +} diff --git a/crates/perry-runtime/src/thread_static_shape_tests.rs b/crates/perry-runtime/src/thread_static_shape_tests.rs index 1157803c4e..577766fddb 100644 --- a/crates/perry-runtime/src/thread_static_shape_tests.rs +++ b/crates/perry-runtime/src/thread_static_shape_tests.rs @@ -23,6 +23,7 @@ fn seed_here() -> u32 { packed.len() as u32, 2, 2, + 0, ) } diff --git a/crates/perry-runtime/src/typed_feedback/guards.rs b/crates/perry-runtime/src/typed_feedback/guards.rs index c5cf7b7627..d85288f31c 100644 --- a/crates/perry-runtime/src/typed_feedback/guards.rs +++ b/crates/perry-runtime/src/typed_feedback/guards.rs @@ -350,8 +350,17 @@ fn class_field_fast_contract( let obj = object_addr as *const ObjectHeader; let descriptor = crate::object::shapes::object_shape_descriptor(obj); let shape_id = crate::object::shapes::object_shape_stamp(obj); + // The ShapeId compare is the whole proof, the lane included: the + // expected id is the class's birth shape, whose rep is part of its + // identity, so a receiver that carries it carries its lanes. A lane + // that is not `Any` there sends the store through the checked + // funnel (charter step 5). let shape_ok = (*obj).class_id == expected_class_id && shape_id == expected_shape_id + && crate::object::field_rep_store::shape_slot_is_any( + expected_shape_id, + expected_field_index, + ) && descriptor.is_some_and(|facts| { facts.object_kind.is_ordinary_layout() && expected_field_index < facts.live_inline_slot_count @@ -1123,6 +1132,8 @@ pub extern "C" fn js_class_field_get_ic_fast_miss( require_raw_f64: i32, cache_slot: *mut crate::object::PicCacheSlot, ) -> f64 { + // Charter step 5: migrate-on-miss (DESIGN §1.5 step 4). + crate::object::field_rep_store::migrate_on_miss_value(receiver.to_bits()); if typed_feedback_enabled() { return js_class_field_get_ic( site_id, @@ -1185,13 +1196,9 @@ pub extern "C" fn js_class_field_set_ic_fast( return CLASS_FIELD_SET_FAST_STORE_SLOW; } // `js_object_set_field`'s store for an in-bound index and a value that - // is not a null POINTER (both established above). - crate::gc::runtime_store_jsvalue_slot( - object_addr, - slot as usize, - expected_field_index as usize, - vbits, - ); + // is not a null POINTER (both established above), through the checked + // funnel (charter step 5). + crate::object::store_object_field_slot(obj, expected_field_index as usize, vbits); } CLASS_FIELD_SET_FAST_DONE } @@ -1211,6 +1218,8 @@ pub extern "C" fn js_class_field_set_ic_fast_miss( value: f64, require_raw_f64: i32, ) { + // Charter step 5: migrate-on-miss (DESIGN §1.5 step 4). + crate::object::field_rep_store::migrate_on_miss_value(receiver.to_bits()); match status { CLASS_FIELD_SET_FAST_GUARD_FAILED => { let key_raw = key as u64 & crate::value::POINTER_MASK; diff --git a/crates/perry/src/commands/compile/object_cache.rs b/crates/perry/src/commands/compile/object_cache.rs index ee085e81b6..aea40857e0 100644 --- a/crates/perry/src/commands/compile/object_cache.rs +++ b/crates/perry/src/commands/compile/object_cache.rs @@ -329,6 +329,8 @@ fn compute_object_cache_key_with_env( // embeds as immediates. A cached object is reused exactly when every id // it embeds is unchanged. h.field("static_shape_ids", &format!("{:?}", opts.static_shape_ids)); + // The seed sidecar's line format: an entry of another format is a miss. + h.field("static_seed_format", perry_codegen::STATIC_SEED_FORMAT); h.field( "program_class_shape_ids", &format!("{:?}", opts.program_class_shape_ids), diff --git a/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs b/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs index 952f68095b..0a8f4c6b2d 100644 --- a/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs +++ b/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs @@ -898,6 +898,7 @@ fn static_seeds_round_trip_and_an_entry_without_them_misses() { live: 2, proto: perry_codegen::BirthProto::Literal, typed: None, + rep: 0b0101, }, ); cache.store_static_seeds(key, &[line.as_str()]); @@ -905,8 +906,8 @@ fn static_seeds_round_trip_and_an_entry_without_them_misses() { assert_eq!(seeds, vec![line.clone()]); let (id, shape) = perry_codegen::decode_static_seed(&seeds[0]).expect("decodes"); assert_eq!( - (id, shape.keys.as_slice(), shape.key_count), - (0x1000_0042, &b"u\0v\0"[..], 2) + (id, shape.keys.as_slice(), shape.key_count, shape.rep), + (0x1000_0042, &b"u\0v\0"[..], 2, 0b0101) ); } diff --git a/crates/perry/src/commands/compile/optimized_libs/freshness.rs b/crates/perry/src/commands/compile/optimized_libs/freshness.rs index 1b9ea5f13b..4f0a6c144c 100644 --- a/crates/perry/src/commands/compile/optimized_libs/freshness.rs +++ b/crates/perry/src/commands/compile/optimized_libs/freshness.rs @@ -127,6 +127,7 @@ pub(crate) const GC_INSTRUMENT_KNOBS: &[&str] = &[ "PERRY_GC_VERIFY_MARK", "PERRY_GC_VERIFY_CLASSIFIER", "PERRY_STACK_SYMBOLS", + "PERRY_FIELD_REPR_VERIFY", ]; fn all_instruments_requested() -> bool { diff --git a/scripts/shape_descriptor_census.py b/scripts/shape_descriptor_census.py index a4cecd0307..5288d3952b 100644 --- a/scripts/shape_descriptor_census.py +++ b/scripts/shape_descriptor_census.py @@ -526,7 +526,9 @@ def assert_authority_surfaces(sources: dict[str, str]) -> None: ensure_append, "by-id descriptor before reverse accelerator", ) - sync = function_body(shapes, "publish_object_shape_from") + # The structural publish body (charter step 5: `publish_object_shape_from` + # delegates to it with an all-Any rep). + sync = function_body(shapes, "publish_object_shape_from_rep") # #9317 routed every post-birth ShapeId publication through # `stamp_object_shape_id_with_carrier_note`, which performs the header # write and then arms `old_carrier` for a promoted receiver. The header @@ -545,14 +547,19 @@ def assert_authority_surfaces(sources: dict[str, str]) -> None: # test-only. for name in ( "publish_object_shape_from", + "publish_object_shape_from_rep", "publish_object_live_slot_count", + "publish_object_live_slot_count_rep", "birth_publish_object_shape", "stamp_object_shape", "birth_stamp_object_shape", ): if "clear_object_shape_stamp" in function_body(shapes, name): raise CensusError(f"{name} clears the shape stamp: the live-slot bound has no mirror") - if "clear_object_shape_stamp" in function_body(object_mod, "set_object_keys_with_live"): + if any( + "clear_object_shape_stamp" in function_body(object_mod, name) + for name in ("set_object_keys_with_live", "set_object_keys_with_live_rep") + ): raise CensusError( "set_object_keys_with_live clears the shape stamp: " "the live-slot bound has no mirror" @@ -625,7 +632,8 @@ def assert_authority_surfaces(sources: dict[str, str]) -> None: assert_before( cache_arm, "set_object_live_slot_count", - "runtime_store_jsvalue_slot", + # The checked store funnel (charter step 5 P2b; was the raw slot store). + "store_object_field_slot", "transition-cache count before value", ) @@ -1179,13 +1187,14 @@ def run_sabotage_selftests(sources: dict[str, str], baseline: dict[str, object]) inverted_publication = dict(sources) path = "crates/perry-runtime/src/object/shapes.rs" publication_body = function_body( - inverted_publication[path], "publish_object_shape_from" + inverted_publication[path], "publish_object_shape_from_rep" ) inverted_body = swap_once( publication_body, # #9029 tombstones: the lineage publish carries hole_count, so the - # mint call in publish_object_shape_from is the _with_holes form. - "shape_descriptor_ensure_with_holes(", + # mint call in the structural publish is the form taking a rep + # (charter step 5). + "shape_descriptor_ensure_with_rep(", "stamp_object_shape_id_with_carrier_note", ) inverted_publication[path] = inverted_publication[path].replace( @@ -1218,7 +1227,7 @@ def run_sabotage_selftests(sources: dict[str, str], baseline: dict[str, object]) ) early_retirement = dict(sources) - publish_body = function_body(early_retirement[path], "publish_object_shape_from") + publish_body = function_body(early_retirement[path], "publish_object_shape_from_rep") early_body = swap_once( publish_body, "stamp_object_shape_id_with_carrier_note", @@ -1353,7 +1362,7 @@ def run_sabotage_selftests(sources: dict[str, str], baseline: dict[str, object]) # #8113: a re-introduced clear-then-remint window. cleared_publication = dict(sources) path = "crates/perry-runtime/src/object/shapes.rs" - cleared_body = function_body(cleared_publication[path], "publish_object_live_slot_count") + cleared_body = function_body(cleared_publication[path], "publish_object_live_slot_count_rep") cleared_publication[path] = cleared_publication[path].replace( cleared_body, cleared_body.replace( diff --git a/test-files/test_gap_class_birth_f64.ts b/test-files/test_gap_class_birth_f64.ts new file mode 100644 index 0000000000..802b0f1b2e --- /dev/null +++ b/test-files/test_gap_class_birth_f64.ts @@ -0,0 +1,101 @@ +// Charter step 5, T1: a class whose constructor writes every `number` field +// from a parameter before anything can read it is born with F64 lanes for +// those fields, filled with +0.0 until the constructor stores. Nothing may +// observe that fill, and every writer that meets a non-Number or a +// non-finite value must take the checked path. Each section ends with a full +// collection: a runtime built with `field-rep-assert` checks every F64 lane +// at every trace. +declare function gc(): void; +function collect(): void { + if (typeof gc === "function") gc(); +} + +class Vec2 { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + setX(v: number): void { + this.x = v; + } + len2(): number { + return this.x * this.x + this.y * this.y; + } +} + +// 1. Numbers, then non-Numbers through the constructor prologue. +const odd: any[] = ["s", undefined, null, { n: 1 }, NaN, Infinity, -0, 7]; +const made: Vec2[] = []; +for (let i = 0; i < 300; i++) made.push(new Vec2(i + 0.5, i * 2)); +for (const v of odd) made.push(new Vec2(v as any, 1)); +collect(); +console.log("ctor", made.length, made[299].len2(), odd.map((v, i) => String(made[300 + i].x)).join(",")); + +// 2. The class-field store guard: primed with Numbers, then fed the rest. +const w = made.slice(0, 20); +for (let i = 0; i < 400; i++) w[i % 20].setX(i * 0.25); +for (let i = 0; i < odd.length; i++) w[i].setX(odd[i] as any); +collect(); +console.log("guard", w.map((o) => String(o.x)).join(",")); + +// 3. A missing argument: the field is `undefined`, never the +0.0 fill. +const bare = new (Vec2 as any)(); +collect(); +console.log("bare", String(bare.x), String(bare.y), typeof bare.x); + +// 4. A field read before its store: not F64 at birth; the read sees undefined. +class Peek { + a: number; + seen: string; + constructor(a: number) { + this.seen = String((this as any).a); + this.a = a; + } +} +const peeks: Peek[] = []; +for (let i = 0; i < 50; i++) peeks.push(new Peek(i)); +collect(); +console.log("peek", peeks[0].seen, peeks[49].a); + +// 5. Subclass ordering: the base constructor calls an override that reads a +// subclass field before the subclass constructor stores it. +class Base { + shown: string; + constructor() { + this.shown = this.show(); + } + show(): string { + return "base"; + } +} +class Derived extends Base { + y: number; + constructor(y: number) { + super(); + this.y = y; + } + show(): string { + return String(this.y); + } +} +const ds: Derived[] = []; +for (let i = 0; i < 50; i++) ds.push(new Derived(i + 1)); +collect(); +console.log("derived", ds[0].shown, ds[49].y); + +// 6. A subclass of a declared base: the base prologue runs through super(). +class Point3 extends Vec2 { + z: number; + constructor(x: number, y: number, z: number) { + super(x, y); + this.z = z; + } +} +const ps: Point3[] = []; +for (let i = 0; i < 100; i++) ps.push(new Point3(i, i + 1, i + 2)); +ps[3].setX("str" as any); +ps[4].z = NaN; +collect(); +console.log("sub", ps[99].len2(), ps[99].z, String(ps[3].x), String(ps[4].z)); diff --git a/test-files/test_gap_field_rep_converge.ts b/test-files/test_gap_field_rep_converge.ts new file mode 100644 index 0000000000..649af8f74d --- /dev/null +++ b/test-files/test_gap_field_rep_converge.ts @@ -0,0 +1,34 @@ +// Charter step 5 (DESIGN §5.2): a lineage slot that sees a non-Number in 1 of +// 1000 key-adds converges on one shape. The key-add of `v` is learned from a +// Number, then a string arrives; after it, Number key-adds must be born into +// the `Any` shape (the F64 sibling is deprecated), so the objects all share +// one shape and a later store site stays monomorphic. Run with +// PERRY_STORE_CENSUS=1 (compile + run): the key-add and store misses stay +// near the number of generalizations, not near the number of objects. +declare function gc(): void; +function fresh(): any { + return {}; +} +function addV(o: any, v: any): void { + o.v = v; +} +function setV(o: any, v: any): void { + o.v = v; +} +const objs: any[] = []; +for (let i = 0; i < 20000; i++) { + const o = fresh(); + addV(o, i % 1000 === 7 ? "s" + i : i + 0.5); + objs.push(o); +} +for (let r = 0; r < 5; r++) { + for (let i = 0; i < objs.length; i++) setV(objs[i], i % 1000 === 7 ? "t" + i : i + r); +} +if (typeof gc === "function") gc(); +let strings = 0; +let sum = 0; +for (const o of objs) { + if (typeof o.v === "string") strings++; + else sum += o.v; +} +console.log(strings, sum); diff --git a/test-files/test_gap_field_rep_store_check.ts b/test-files/test_gap_field_rep_store_check.ts new file mode 100644 index 0000000000..69783a138c --- /dev/null +++ b/test-files/test_gap_field_rep_store_check.ts @@ -0,0 +1,218 @@ +// Charter step 5: every writer of an F64 slot runs the store check. +// A key-add of a Number gives the new shape an F64 lane. Each section below +// owns its keys and its store sites, so no earlier section has deprecated the +// lane it relies on, and each ends with a full collection, which traces every +// live object: a runtime built with `field-rep-assert` checks each F64 lane at +// every trace, so a non-Number stored raw under an F64 lane aborts the run. +// 1. the emitted store IC hit on an F64 lane is served a non-Number; +// 2. the emitted key-add memo, learned from a Number, is served a non-Number; +// 3. the inline arm is served NaN / Infinity / -0 (the funnel canonicalizes); +// 4. delete moves a string down into the slot of an F64 lane; +// 5. class-instance fields (class-field guard, constructor prologue) are +// learned from Numbers, then served non-Numbers; +// 6. a class instance grows past its declared fields by a Number key-add, +// then its declared field is stored through the class-field guard. +declare function gc(): void; +function collect(): void { + if (typeof gc === "function") gc(); +} +function fresh(): any { + return {}; +} + +// 1. Existing-key store IC hit. +function addIa(o: any, v: any): void { + o.ia = v; +} +function setIa(o: any, v: any): void { + o.ia = v; +} +function icHit(): string { + const keep: any[] = []; + for (let i = 0; i < 200; i++) { + const o = fresh(); + addIa(o, i + 0.5); + keep.push(o); + } + // ONE loop, so the site (even an inlined copy) is primed on the F64 shape + // by the Numbers and then hit with the first non-Number. + for (let i = 0; i < 400; i++) { + const v = i < 200 ? i * 2 + 0.25 : i % 2 === 0 ? "s" + i : { n: i }; + setIa(keep[i % 200], v); + } + collect(); + let s = 0; + let n = 0; + for (const o of keep) { + if (typeof o.ia === "string") s++; + else n += o.ia.n; + } + return s + " " + n; +} + +// 2. Key-add memo hit. +function addKb(o: any, v: any): void { + o.kb = v; +} +function keyAdd(): string { + const added: any[] = []; + // ONE loop: the memo is learned from Numbers, then served non-Numbers. + for (let i = 0; i < 400; i++) { + const o = fresh(); + addKb(o, i < 200 ? i + 1.5 : i % 2 === 0 ? "t" + i : [i]); + added.push(o); + } + collect(); + let s = 0; + let a = 0; + let sum = 0; + for (const o of added) { + if (typeof o.kb === "string") s++; + else if (Array.isArray(o.kb)) a += o.kb[0]; + else sum += o.kb; + } + return s + " " + a + " " + sum; +} + +// 3. Doubles the inline arm must not store as is. +function addSc(o: any, v: any): void { + o.sc = v; +} +function setSc(o: any, v: any): void { + o.sc = v; +} +function specials(): string { + const special: any[] = []; + for (let i = 0; i < 300; i++) { + const o = fresh(); + addSc(o, 1.5); + special.push(o); + } + for (let i = 0; i < 600; i++) { + const v = i < 300 ? 2.5 : i % 3 === 0 ? NaN : i % 3 === 1 ? Infinity : -0; + setSc(special[i % 300], v); + } + collect(); + let nan = 0; + let inf = 0; + let negz = 0; + for (const o of special) { + if (Number.isNaN(o.sc)) nan++; + else if (o.sc === Infinity) inf++; + else if (Object.is(o.sc, -0)) negz++; + } + return nan + " " + inf + " " + negz; +} + +// 4. Delete shifts a string down into the slot of an F64 lane. +function addDa(o: any, v: any): void { + o.da = v; +} +function addDs(o: any, v: any): void { + o.ds = v; +} +function addDz(o: any, v: any): void { + o.dz = v; +} +function deletes(): string { + const del: any[] = []; + for (let i = 0; i < 300; i++) { + const o = fresh(); + addDa(o, i + 0.5); + addDs(o, "str" + i); + addDz(o, i + 1.5); + delete o.da; + del.push(o); + } + collect(); + let ds = 0; + let dz = 0; + for (const o of del) { + if (typeof o.ds === "string" && o.ds.startsWith("str")) ds++; + dz += o.dz; + if ("da" in o) ds = -1; + } + return ds + " " + dz + " " + Object.keys(del[7]).join(","); +} + +// 5. Class instances: constructor-prologue stores and class-field stores. +class Pt { + cx: any; + cy: any; + constructor(x: any, y: any) { + this.cx = x; + this.cy = y; + } +} +function setCx(p: Pt, v: any): void { + p.cx = v; +} +function classes(): string { + const pts: Pt[] = []; + for (let i = 0; i < 400; i++) { + const x = i < 200 ? i + 0.5 : i % 2 === 0 ? "d" + i : [i]; + pts.push(new Pt(x, i + 0.25)); + } + for (let i = 0; i < 400; i++) { + const v = i < 200 ? i + 0.75 : i % 2 === 0 ? "c" + i : { n: i }; + setCx(pts[i % 200], v); + } + collect(); + let s = 0; + let n = 0; + let sum = 0; + for (const p of pts) { + if (typeof p.cx === "string") s++; + else if (Array.isArray(p.cx)) n += p.cx[0]; + else if (typeof p.cx === "object") n += p.cx.n; + else sum += p.cx; + sum += p.cy; + } + return s + " " + n + " " + sum; +} + +// 6. A class instance's key-add past its declared fields, then class-keyed +// stores of its declared field on the grown instance. +class Bag { + bx: any; + constructor(x: any) { + this.bx = x; + } +} +function addExtra(o: any, v: any): void { + o.extra = v; +} +function setBx(b: Bag, v: any): void { + b.bx = v; +} +function grown(): string { + const bags: Bag[] = []; + for (let i = 0; i < 400; i++) { + const b = new Bag(i + 0.5); + addExtra(b, i < 200 ? i + 0.25 : "e" + i); + bags.push(b); + } + for (let i = 0; i < 800; i++) { + const v = i < 400 ? i + 0.75 : i % 2 === 0 ? "b" + i : { n: i }; + setBx(bags[i % 400], v); + } + collect(); + let s = 0; + let n = 0; + let sum = 0; + for (const b of bags) { + const e: any = (b as any).extra; + if (typeof e === "string") s++; + else sum += e; + if (typeof b.bx === "string") s++; + else n += b.bx.n; + } + return s + " " + n + " " + sum; +} + +console.log(icHit()); +console.log(keyAdd()); +console.log(specials()); +console.log(deletes()); +console.log(classes()); +console.log(grown()); diff --git a/test-files/test_gap_region_store_f64_lane.ts b/test-files/test_gap_region_store_f64_lane.ts new file mode 100644 index 0000000000..0b9252fd33 --- /dev/null +++ b/test-files/test_gap_region_store_f64_lane.ts @@ -0,0 +1,63 @@ +// Charter step 5: a loop region's bare store runs no field-representation +// check. A store of a value not proven a Number into a slot that is an F64 +// lane of the receiver's shape must not happen bare (the region refuses the +// word); the stored value must read back exactly, and later reads of every +// field must agree with node. + +class Pt { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +function storeAny(p: Pt, v: any, n: number): number { + let h = 0; + for (let i = 0; i < n; i++) { + (p as any).x = v; + h = h + p.y; + } + return h; +} + +function storeNum(p: Pt, n: number): number { + let h = 0; + for (let i = 0; i < n; i++) { + p.x = i * 0.5; + h = h + p.y; + } + return h; +} + +function storeLit(o: any, v: any, n: number): number { + let h = 0; + for (let i = 0; i < n; i++) { + o.a = v; + h = h + o.b; + } + return h; +} + +const pts: Pt[] = []; +for (let i = 0; i < 64; i++) pts.push(new Pt(i + 0.25, i * 2)); +console.log(storeNum(pts[3], 100), pts[3].x, pts[3].y); +const vals: any[] = ["s", { k: 1 }, null, undefined, 7, 2.5, NaN, -0, Infinity, [1, 2]]; +for (const v of vals) { + const p = new Pt(1.5, 3); + console.log(storeAny(p, v, 50), String(p.x), typeof p.x, p.y); +} +for (let r = 0; r < 3; r++) { + const junk: any[] = []; + for (let j = 0; j < 20000; j++) junk.push({ j, s: "x" + j }); + for (const p of pts) storeAny(p, r === 1 ? "t" + p.y : p.y + 0.5, 3); + console.log(r, junk.length, pts.map((p) => String(p.x)).slice(0, 5).join(",")); +} +for (const v of vals) { + const o: any = { a: 1.25, b: 4 }; + console.log(storeLit(o, v, 50), String(o.a), o.b); +} +const q: any = { a: 0.5, b: 1 }; +q.c = 2.5; +console.log(storeLit(q, "str", 10), q.a, q.c); diff --git a/test-files/test_gap_typed_recv_clone_alias.ts b/test-files/test_gap_typed_recv_clone_alias.ts new file mode 100644 index 0000000000..b9aa7b2d1d --- /dev/null +++ b/test-files/test_gap_typed_recv_clone_alias.ts @@ -0,0 +1,61 @@ +// A method call on a receiver whose CLASS codegen proved may take the class's +// typed-receiver clone, which reads fields as raw doubles. An alias the +// object escaped to can store a non-Number into a field (moving the object +// off its birth shape), so the clone must run only while the object's +// (class id, ShapeId) pair is still the class's own. + +class Holder { + a: number = 1; + b: number = 2; + left(): number { + return this.a + this.b; + } +} + +class H2 { + a: number; + b: number; + constructor(a: number, b: number) { + this.a = a; + this.b = b; + } + left(): number { + return this.a + this.b; + } + sum3(): number { + return this.left() + this.a; + } +} + +function f(x: any) { + x.a = "u"; +} +function g(x: any) { + x.b = { valueOf() { return 40; } }; +} + +const h4 = new Holder(); +f(h4); +const y: any = h4; +console.log(typeof y.a, y.a, y.b, h4.a, h4.left()); + +const k = new H2(1, 2); +f(k); +console.log(k.left(), k.sum3()); + +const m = new H2(3, 4); +g(m); +console.log(m.left(), m.sum3()); + +let acc = 0; +const ks: H2[] = []; +for (let i = 0; i < 2000; i++) { + const p = new H2(i, 0.5); + if (i % 500 === 7) f(p); + ks.push(p); +} +for (const p of ks) { + const v: any = p.left(); + acc += typeof v === "number" ? v : v.length; +} +console.log(acc, String(ks[7].left()), ks[8].left());