From f64f12cd0d6ef86a798513bfe07f8d069900c887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 15 Aug 2026 00:53:20 +0200 Subject: [PATCH 1/3] ci(object): make the ObjectHeader shape-descriptor census a real gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #8086 built the exact-callsite census #8067 asked for — the instrument that keeps `object_type`, `field_count` and `keys_array` retired as ShapeId takes over their facts — and then wired it into nothing. `grep -rn shape_descriptor_census` over every workflow, script and doc returns only the script naming its own baseline, so it has never been able to fail a build. Add it to `lint` alongside the other fifteen audits. Wiring it up exposed that one of its two arms was vacuous. The emitted-guard check rejected `add(..., "0"|"12"|"16")`, but all four functions in its list — `emit_class_field_loop_preheader_check`, `emit_proven_shape_recheck`, `emit_class_field_inline_precheck` and `emit_element_shape_field_load` — build their header address with `blk.gep(I8, &p, &[(I64, "N")])`. The pattern therefore matched a syntax those functions never emit. Planting `gep(I8, &elem_ptr, &[(I64, "16")])` in `emit_element_shape_field_load` — a read of the `keys_array` offset #8047 removes — left the census green. Match the gep form too. Sabotage-verified, each reverted after: * new `(*obj).field_count` read in `object/spill.rs` -> exit 1 * keys-pointer token in `proxy/put_value.rs::dyn_ic_try_store` -> exit 1 * offset 16 in `emit_element_shape_field_load` -> exit 1 (was 0) * offset 12 in the three `class_field_inline_guard` emitters -> exit 1 each Clean tree green before and after every one. No behaviour change; the census reports the same summary it did on 12f758a22. Refs #8047, #8067, #8086. --- .github/workflows/test.yml | 18 ++++++++++++++++++ scripts/shape_descriptor_census.py | 12 +++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3983d9b84a..be1cfb4131 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -221,6 +221,24 @@ jobs: if: ${{ !cancelled() }} run: python3 scripts/class_id_collisions.py + # #8047/#8067. `ObjectHeader.object_type`, `field_count` and `keys_array` + # are being retired in favour of the authoritative ShapeId descriptor. + # #8086 migrated the guards AND built this exact-callsite census to keep + # them retired -- then wired it into nothing, so it has never been able to + # fail a build. A gate that no job invokes is documentation. + # + # The census is exact rather than a grep count: it strips comments and + # string literals, classifies each site declaration-vs-access, and diffs + # the full multiset against a reviewed baseline, so it can tell an + # ObjectHeader read from another struct's identically-named field. It + # also asserts the authority surfaces directly (no guard may compare a + # keys pointer, no emitter may bake header offsets 0/12/16) and carries + # its own lexer self-test plus sabotage self-tests, which run + # unconditionally on every invocation. + - name: Object-header shape-descriptor census + if: ${{ !cancelled() }} + run: python3 scripts/shape_descriptor_census.py + # #7645. The copying minor skips its eligibility preflight — the walk that # proves nothing reachable is pinned — whenever the young-pin latch is # clear. That is sound only while EVERY creation of GC_FLAG_PINNED goes diff --git a/scripts/shape_descriptor_census.py b/scripts/shape_descriptor_census.py index 94d2e7d06b..b6de7b523e 100644 --- a/scripts/shape_descriptor_census.py +++ b/scripts/shape_descriptor_census.py @@ -463,7 +463,17 @@ def assert_authority_surfaces(sources: dict[str, str]) -> None: ): for name in names: body = function_body(source, name) - if re.search(r"expected_keys|add\s*\([^\n]*\"(?:0|12|16)\"", body): + # Match BOTH emission forms. These four guards build their header + # address with `blk.gep(I8, &p, &[(I64, "N")])`, not `add(..)`, so + # an `add`-only pattern was vacuous for every function in this + # list -- planting `gep(I8, &elem_ptr, &[(I64, "16")])` in + # `emit_element_shape_field_load` left the census green. + if re.search( + r"expected_keys" + r"|add\s*\([^\n]*\"(?:0|12|16)\"" + r"|gep\s*\([^\n]*\(\s*I64\s*,\s*\"(?:0|12|16)\"\s*\)", + body, + ): raise CensusError(f"{name} emits a removed ObjectHeader fact") generic_body = function_body(raw_generic_pic, "lower_generic_property_get") From 3d6e83158faa9af4aab0da123fa399deb32383d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 15 Aug 2026 00:53:58 +0200 Subject: [PATCH 2/3] docs(changelog): add fragment for #8110 --- changelog.d/8110-shape-census-gate.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 changelog.d/8110-shape-census-gate.md diff --git a/changelog.d/8110-shape-census-gate.md b/changelog.d/8110-shape-census-gate.md new file mode 100644 index 0000000000..700e0fbc3d --- /dev/null +++ b/changelog.d/8110-shape-census-gate.md @@ -0,0 +1,12 @@ +### Fixed + +- Wire the `ObjectHeader` shape-descriptor census into `lint`. #8086 built the + exact-callsite census #8067 required — the instrument that keeps + `object_type`, `field_count` and `keys_array` retired while `ShapeId` takes + over their facts — but referenced it from no workflow, so it had never been + able to fail a build. Wiring it up exposed that its emitted-guard arm was + vacuous: it rejected `add(..., "0"|"12"|"16")`, while all four functions it + names build their header address with `blk.gep(I8, &p, &[(I64, "N")])`, so a + planted read of the removed `keys_array` offset in + `emit_element_shape_field_load` left the census green. The check now matches + the gep form, and each of the four emitters is sabotage-verified red. From 9bcf2ceb2b0c2fde9a4c2e7c2b863cb72982a08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 16 Aug 2026 07:46:31 +0200 Subject: [PATCH 3/3] fix(ci): make the shape census pass on current main before wiring it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wiring the census into `lint` exposed two things it could not survive. The authority-surface check hardcoded `descriptors: HashMap`. #8157 changed that field to `crate::fast_hash::PtrHashMap` (SipHash on a bare u32 was 25% of self time in `shapes`), so the check failed on a rename it should not care about. The fact it asserts is that a by-id table EXISTS — the pattern now accepts an optional path qualifier and either hasher, and a real sabotage (swapping the field to `Vec`) still trips it. The exact-callsite baseline is refreshed for six sites added since it was written: five in `param_type_guard.rs` from #8165's class-typed parameter guards, one in `process/node_module/source_map.rs`. They are raw reads of the three header words #8047/#8113 are trying to retire — recorded rather than hidden, so the migration list stays honest. --- scripts/shape_descriptor_census.py | 5 ++++- scripts/shape_descriptor_census_baseline.json | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/scripts/shape_descriptor_census.py b/scripts/shape_descriptor_census.py index b6de7b523e..ebb69b1ff3 100644 --- a/scripts/shape_descriptor_census.py +++ b/scripts/shape_descriptor_census.py @@ -279,7 +279,10 @@ def assert_authority_surfaces(sources: dict[str, str]) -> None: raw_write_pics = sources["crates/perry-codegen/src/expr/proxy_reflect.rs"] for pattern, label in ( - (r"descriptors\s*:\s*HashMap\s*<\s*u32\s*,\s*ShapeDescriptor", "by-id descriptor table"), + # `PtrHashMap` since #8157 (SipHash on a bare u32 was 25% of self time in + # `shapes`). The fact this asserts is that a by-id table EXISTS, not which + # hasher backs it, so accept either spelling. + (r"descriptors\s*:\s*(?:[\w:]+::)?(?:Ptr)?HashMap\s*<\s*u32\s*,\s*ShapeDescriptor", "by-id descriptor table"), (r"logical_key_count\s*:\s*u32", "exact logical-key fact"), (r"live_inline_slot_count\s*:\s*u32", "exact live-slot fact"), (r"semantic_generation\s*:\s*u64", "semantic transition fact"), diff --git a/scripts/shape_descriptor_census_baseline.json b/scripts/shape_descriptor_census_baseline.json index 0ed4761541..a3f7e73134 100644 --- a/scripts/shape_descriptor_census_baseline.json +++ b/scripts/shape_descriptor_census_baseline.json @@ -10,8 +10,8 @@ "crates/perry-codegen/src/expr/property_set.rs|crate::target_layout::object_header_size_bytes(": 3, "crates/perry-codegen/src/expr/property_set.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple)": 1, "crates/perry-codegen/src/expr/property_set.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 2, - "crates/perry-codegen/src/expr/proxy_reflect.rs|(crate::target_layout::object_header_size_bytes(ctx.target_triple) / 8).to_string();": 1, "crates/perry-codegen/src/expr/proxy_reflect.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, + "crates/perry-codegen/src/expr/proxy_reflect.rs|let header_bytes = crate::target_layout::object_header_size_bytes(ctx.target_triple);": 1, "crates/perry-codegen/src/lower_call/new.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, "crates/perry-codegen/src/lower_call/new_alloc.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple);": 1, "crates/perry-codegen/src/lower_call/scalar_method.rs|let header_skip = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, @@ -274,9 +274,15 @@ "crates/perry-runtime/src/object/spill.rs|field_count|access|std::cmp::max((*obj).field_count, crate::object::INLINE_SLOT_FLOOR as u32);": 1, "crates/perry-runtime/src/object/spill.rs|field_count|declaration|pub(crate) fn reserve_object_spill(obj_ptr: usize, field_count: u32) {": 1, "crates/perry-runtime/src/os.rs|field_count|declaration|let field_count: u32 = 14;": 1, + "crates/perry-runtime/src/param_type_guard.rs|field_count|access|for _ in 0..field_count {": 1, + "crates/perry-runtime/src/param_type_guard.rs|field_count|access|let inline_fields = ((*object).field_count as usize).max(crate::object::INLINE_SLOT_FLOOR);": 1, + "crates/perry-runtime/src/param_type_guard.rs|field_count|access||| (*object).field_count as usize > MAX_CONTAINER_LEN": 1, + "crates/perry-runtime/src/param_type_guard.rs|keys_array|access|let keys = (*object).keys_array;": 1, + "crates/perry-runtime/src/param_type_guard.rs|object_type|access|if (*object).object_type != crate::error::OBJECT_TYPE_REGULAR": 1, "crates/perry-runtime/src/perf_hooks.rs|keys_array|access|let keys_ptr = (*obj).keys_array as usize;": 1, "crates/perry-runtime/src/perf_hooks.rs|keys_array|access|recorded != 0 && (*obj).keys_array as usize == recorded": 1, "crates/perry-runtime/src/pointer_event.rs|field_count|declaration|let field_count: u32 = 4;": 1, + "crates/perry-runtime/src/process/node_module/source_map.rs|keys_array|access|(*obj).keys_array = std::ptr::null_mut();": 1, "crates/perry-runtime/src/promise/then_probe.rs|keys_array|access|let keys = (*obj).keys_array;": 2, "crates/perry-runtime/src/proxy.rs|object_type|access|&& (*(addr as *const crate::ObjectHeader)).object_type": 1, "crates/perry-runtime/src/proxy/put_value.rs|field_count|access|object_array_numeric_write_slots(array, &keys[..field_count as usize], receiver_count)": 1, @@ -290,9 +296,9 @@ "crates/perry-runtime/src/typed_feedback/tests.rs|field_count|access|(*obj).field_count = 0;": 1, "crates/perry-runtime/src/typed_feedback/tests.rs|field_count|access|(*obj).field_count = original_field_count;": 1, "crates/perry-runtime/src/typed_feedback/tests.rs|field_count|access|let original_field_count = unsafe { (*obj).field_count };": 1, + "crates/perry-runtime/src/typed_feedback/tests.rs|keys_array|access|&(*obj).keys_array as *const _ as usize,": 1, "crates/perry-runtime/src/typed_feedback/tests.rs|keys_array|access|(*obj).keys_array = original_keys;": 1, "crates/perry-runtime/src/typed_feedback/tests.rs|keys_array|access|(*obj).keys_array = std::ptr::null_mut();": 1, - "crates/perry-runtime/src/typed_feedback/tests.rs|keys_array|access|&(*obj).keys_array as *const _ as usize,": 1, "crates/perry-runtime/src/typed_feedback/tests.rs|keys_array|access|assert_ne!(unsafe { (*obj).keys_array }, original_keys);": 1, "crates/perry-runtime/src/typed_feedback/tests.rs|keys_array|access|let keys = unsafe { (*obj).keys_array };": 1, "crates/perry-runtime/src/url/search_params.rs|keys_array|access|let keys_arr = (*obj).keys_array;": 2, @@ -328,11 +334,11 @@ }, "summary": { "codegen_object_header_size_sites": 32, - "raw_member_files": 99, + "raw_member_files": 101, "raw_member_sites": { - "field_count": 159, - "keys_array": 181, - "object_type": 31 + "field_count": 162, + "keys_array": 183, + "object_type": 32 } } }