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/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. diff --git a/scripts/shape_descriptor_census.py b/scripts/shape_descriptor_census.py index 94d2e7d06b..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"), @@ -463,7 +466,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") 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 } } }