Skip to content

ci(object): make the ObjectHeader shape-descriptor census a real gate - #8110

Draft
proggeramlug wants to merge 2 commits into
mainfrom
gc/8047-census-gate
Draft

ci(object): make the ObjectHeader shape-descriptor census a real gate#8110
proggeramlug wants to merge 2 commits into
mainfrom
gc/8047-census-gate

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

What

Wires scripts/shape_descriptor_census.py into the lint job, and fixes a
vacuous arm inside it that the wiring exposed.

Why

#8086 shipped the exact-callsite census that #8067 required — the instrument
that keeps ObjectHeader::object_type, field_count and keys_array retired
while ShapeId takes over their facts, and the thing #8047 says must exist
before anyone touches the layout. It is a good instrument: it strips comments
and string literals, classifies every site declaration vs access, diffs the
full multiset against a reviewed baseline (so it can tell an ObjectHeader read
from another struct's identically-named field), and carries its own lexer
self-test plus sabotage self-tests.

It is invoked by nothing:

$ grep -rn "shape_descriptor_census" --include=*.yml --include=*.sh \
      --include=*.py --include=*.toml --include=*.md . \
  | grep -v '^./scripts/shape_descriptor_census'
scripts/shape_descriptor_census.py:15:BASELINE_PATH = ROOT / "scripts" / ...

The only hit is the script naming its own baseline. lint runs fifteen other
python audits; this is not one of them.

The vacuous arm

Wiring it up and sabotage-testing it found that the emitted-guard check could
not fail for any function it names. It matched

re.search(r"expected_keys|add\s*\([^\n]*\"(?:0|12|16)\"", body)

but all four functions in that list build their header address with
blk.gep(I8, &p, &[(I64, "N")]), not add(..):

function actual emission
emit_class_field_loop_preheader_check blk.gep(I8, &obj_ptr, &[(I64, "4")])
emit_proven_shape_recheck blk.gep(I8, &obj_ptr, &[(I64, "8")])
emit_class_field_inline_precheck blk.gep(I8, &obj_ptr, &[(I64, "4")])
emit_element_shape_field_load blk.gep(I8, &elem_ptr, &[(I64, "8")])

So the pattern targeted a syntax none of them uses. Planting a read of the
keys_array offset that #8047 removes —
gep(I8, &elem_ptr, &[(I64, "16")]) in emit_element_shape_field_load — left
the census green. This patch matches the gep form as well.

The two write-PIC checks below it are unaffected: lower_generic_property_get
and the proxy_reflect emitters genuinely do use ctx.block().add(I64, .., "N"),
and their own regexes already spell that.

Verification

Sabotage-verified, every change reverted afterwards, clean tree green before and
after each:

sabotage before after
new (*obj).field_count read in object/spill.rs exit 1 exit 1
keys-pointer token in proxy/put_value.rs::dyn_ic_try_store exit 1 exit 1
offset 16 in emit_element_shape_field_load exit 0 exit 1
offset 12 in emit_class_field_loop_preheader_check exit 0 exit 1
offset 12 in emit_proven_shape_recheck exit 0 exit 1
offset 12 in emit_class_field_inline_precheck exit 0 exit 1

All sixteen lint steps, enumerated from .github/workflows/test.yml rather
than from memory, pass on this branch: cargo fmt --all -- --check,
check_file_size.sh, workspace_architecture.py (--self-test and --check),
local_binding_type_audit.py, gc_store_site_inventory.py,
addr_class_inventory.py, class_id_collisions.py, gc_pin_sites.py,
gc_runtime_root_holders.py, check_gc_env_knobs.py,
check_llvm_corpus_currency.py, check_gc_doc_claims.py, raw_handle_debt.py,
check_test_registration.py, and the newly wired census.

No behaviour change: the census reports the same summary on this branch as on
12f758a22.

Note this is a new gate, so per CLAUDE.md it has never been green — run it
once on main before promoting it to a required context.

Not in this PR

The #8047 layout change itself. A full re-audit on 12f758a22 found the
prerequisite still incomplete; findings are posted on #8047.

Refs #8047, #8067, #8086.

Ralph Küpper added 2 commits August 15, 2026 00:53
#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 12f758a.

Refs #8047, #8067, #8086.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c1c886d-25bd-47f5-a7e0-bebe07df260e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Verified and endorsed — and this is my miss to own.

I audited and merged #8086. My checks were: all four lint gates on a landing-equivalent tree, perry-runtime (2351/0), the full perry-codegen suite diffed test-for-test against main's 11-failure baseline, and specifically whether the raw-handle ratchet had been softened to accommodate the new with_{mut,const}_ptr combinator (it had not). All of that was true and none of it was the right question for the census.

Confirmed independently on main at 12f758a22:

$ grep -rn "shape_descriptor_census" .github/workflows/
(no output)

$ git grep -l "shape_descriptor_census" origin/main
scripts/shape_descriptor_census.py

The script is referenced by exactly one file — itself. #8067's acceptance asked for a census; #8086 built it and wired it into nothing, so it has never gated anything and never could.

And the arm that would have mattered was vacuous. Your finding that it matched add(..., "0"|"12"|"16") while all four named functions emit blk.gep(I8, &p, &[(I64, "N")]) means that even once wired, planting a keys_array-offset read in emit_element_shape_field_load left it green — which you demonstrated rather than argued. That is the same failure this repo has hit repeatedly, and it is worth noting it survived because nothing ran it: an unwired gate cannot fail, so its vacuity was undetectable by any amount of it "passing".

The generalisable lesson I am taking from this, since it is a gap in how I audit: when a PR adds a gate, checker or census, verifying its tests pass is not enough — verify it is wired into a workflow, and verify it can go red. "Does this new gate run?" and "can it fail?" are two separate questions and I asked neither.

Six sabotages with the clean tree re-verified green after each, and all 16 lint steps enumerated from the workflow rather than remembered, is the right standard. No objection from me — this should land.

Two things from your report worth carrying into #8112/#8113 rather than losing here: perry-ffi's object_header_matches_runtime has never executed (runtime-link enabled nowhere in CI, and cargo-test is per-package), and perry-ffi is published to crates.io, so an old mirror against a new runtime reads class_id from a deleted slot with no compile error. A size/padding divergence being invisible is precisely the failure mode a header shrink has.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant