diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b2873ea80..8a8a7f7985 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1266,6 +1266,12 @@ jobs: echo "::error::The consumption counter is not tracking the compiler." exit 1 fi + if ! printf '%s' "$out" | grep -q "CONSUMPTION SITE NEVER EXERCISED"; then + echo "::error::No consumption site went dark with Ptr disabled." + echo "::error::The per-site coverage gate is not tracking the compiler:" + echo "::error::two of the six recorders had never fired before it existed." + exit 1 + fi echo "Census correctly went red with PERRY_PTR_SHAPE_LOCALS=0." - name: Upload census reports diff --git a/benchmarks/repsel_census/README.md b/benchmarks/repsel_census/README.md index a6daffdc6a..e30795e6d6 100644 --- a/benchmarks/repsel_census/README.md +++ b/benchmarks/repsel_census/README.md @@ -65,9 +65,18 @@ of them is recorded at the site where the proof is dropped: reproducible without the report at all: compile the workload twice, once with `PERRY_PTR_SHAPE_LOCALS=0`, and compare the objects. +`--no-link` does **not** honour `-o`: the object goes to a per-run temp +directory and the path is printed. So capture the printed path in each arm and +compare those — comparing the `-o` arguments compares two files that were never +created. + ```bash -perry compile -o /tmp/x --no-link --no-cache # prints the .o path -PERRY_PTR_SHAPE_LOCALS=0 perry compile -o /tmp/x --no-link --no-cache +obj() { # echo the object path this compile actually wrote + "$@" --no-link --no-cache 2>&1 | sed -n 's/^Wrote object file: //p' +} +a=$(obj perry compile -o /tmp/ignored) +b=$(PERRY_PTR_SHAPE_LOCALS=0 obj perry compile -o /tmp/ignored) +cmp "$a" "$b" && echo "IDENTICAL — the promotion emitted nothing" ``` Byte-identical objects mean the promotions the report counted as wins changed @@ -76,6 +85,18 @@ nothing. `07_object_create` and `12_binary_trees` are byte-identical today. sites** — which is why the census reports its consumption as 0 and the object A/B alone would have been misleading. +### Per-site coverage + +The consumed count is per **value**, so one working recorder is enough to mark a +value consumed and the other five could rot unnoticed. `CONSUMPTION_SITES` (in +the script) registers all six, and a site that records nothing corpus-wide fails +the run. + +This is not hypothetical. When coverage was first measured, four fired and two +had **never fired on any workload here** — +`class_field_get_number.shape_proven_load` and `ptr_shape_update`. Both were +reachable; nothing reached them. `fixture_ptr_shape_sites.ts` exists to. + Only `ptr-shape` has consumption instrumentation. The other seven census keys report *no consumption data* rather than a zero (`CONSUMPTION_INSTRUMENTED` in the script), because "uninstrumented" and "never diff --git a/benchmarks/repsel_census/baseline.json b/benchmarks/repsel_census/baseline.json index 2831bcde7a..f9d3fb3609 100644 --- a/benchmarks/repsel_census/baseline.json +++ b/benchmarks/repsel_census/baseline.json @@ -23,7 +23,43 @@ "int-valued-ta": 0, "spec-abi": 1 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": { + "ptr_shape_get_number": 2, + "ptr_shape_set": 1, + "ptr_shape_method": 1 + } + }, + { + "name": "fixture_ptr_shape_sites", + "role": "liveness", + "source": "benchmarks/repsel_census/fixtures/fixture_ptr_shape_sites.ts", + "floors": { + "ptr-shape": 1, + "ptr-shape-consumed": 1, + "ptr-numarray": 0, + "canonical-i32": 0, + "canonical-u32": 0, + "canonical-str": 0, + "int-valued-ta": 0, + "spec-abi-entry": 1, + "spec-abi-taptr-slot": 0 + }, + "candidates": { + "ptr-shape": 1, + "ptr-numarray": 0, + "canonical-slot": 1, + "int-valued-ta": 0, + "spec-abi": 1 + }, + "unconsumed_mechanisms": {}, + "consumption_sites": { + "ptr_shape_get_number": 1, + "class_field_get_number.shape_proven_load": 1, + "ptr_shape_set": 1, + "ptr_shape_update": 1, + "ptr_shape_method": 1 + } }, { "name": "fixture_ptr_numarray", @@ -47,7 +83,8 @@ "int-valued-ta": 0, "spec-abi": 2 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "fixture_canonical_slots", @@ -71,7 +108,8 @@ "int-valued-ta": 0, "spec-abi": 3 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "fixture_int_valued_ta", @@ -95,7 +133,8 @@ "int-valued-ta": 1, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "fixture_spec_abi_taptr", @@ -119,7 +158,8 @@ "int-valued-ta": 0, "spec-abi": 1 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "batch", @@ -145,6 +185,11 @@ }, "unconsumed_mechanisms": { "module_init_context": 1 + }, + "consumption_sites": { + "ptr_shape_get_number": 1, + "ptr_shape_set": 2, + "class_field_get.shape_proven_load": 2 } }, { @@ -169,7 +214,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_02_loop_overhead", @@ -193,7 +239,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_03_array_write", @@ -217,7 +264,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_04_array_read", @@ -241,7 +289,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_05_fibonacci", @@ -265,7 +314,8 @@ "int-valued-ta": 0, "spec-abi": 1 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_06_math_intensive", @@ -289,7 +339,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_07_object_create", @@ -315,7 +366,8 @@ }, "unconsumed_mechanisms": { "scalar_replaced": 1 - } + }, + "consumption_sites": {} }, { "name": "suite_08_string_concat", @@ -339,7 +391,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_09_method_calls", @@ -365,6 +418,11 @@ }, "unconsumed_mechanisms": { "module_init_context": 1 + }, + "consumption_sites": { + "class_field_get.shape_proven_load": 1, + "ptr_shape_get_number": 1, + "ptr_shape_set": 1 } }, { @@ -389,7 +447,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_11_prime_sieve", @@ -413,7 +472,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_12_binary_trees", @@ -439,7 +499,8 @@ }, "unconsumed_mechanisms": { "scalar_replaced": 1 - } + }, + "consumption_sites": {} }, { "name": "suite_13_factorial", @@ -463,7 +524,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_14_closure", @@ -487,7 +549,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_15_mandelbrot", @@ -511,7 +574,8 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_16_matrix_multiply", @@ -535,7 +599,8 @@ "int-valued-ta": 0, "spec-abi": 1 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} }, { "name": "suite_17_loop_data_dependent", @@ -559,8 +624,9 @@ "int-valued-ta": 0, "spec-abi": 0 }, - "unconsumed_mechanisms": {} + "unconsumed_mechanisms": {}, + "consumption_sites": {} } ], - "generated_at": "2026-07-31T04:32:15.277779Z" + "generated_at": "2026-07-31T05:15:37.361861Z" } diff --git a/benchmarks/repsel_census/fixtures/fixture_ptr_shape_sites.ts b/benchmarks/repsel_census/fixtures/fixture_ptr_shape_sites.ts new file mode 100644 index 0000000000..42298b5ab1 --- /dev/null +++ b/benchmarks/repsel_census/fixtures/fixture_ptr_shape_sites.ts @@ -0,0 +1,71 @@ +// Liveness fixture for the `Ptr` CONSUMPTION SITES (#7106 follow-up). +// +// `fixture_ptr_shape.ts` proves the census can observe a promotion. This file +// proves something narrower and, it turned out, more urgent: that each codegen +// lowering which CONSUMES a `Ptr` proof is exercised by the corpus at +// all. +// +// When per-site coverage was first measured, four of the six recorders fired +// and two had never fired on any workload in the census: +// +// - `class_field_get_number.shape_proven_load` (expr/property_get/helpers.rs) +// -- the bare fixed-offset load for a field that is NUMERIC-proven, as +// opposed to merely shape-proven. Needs a class whose every reachable +// store to the field is proven number-producing. +// - `ptr_shape_update` (expr/instance_misc1.rs) -- `o.f++` lowered to +// load/fadd/store at a fixed offset, with no by-name runtime call. +// +// Both are reachable. Nothing exercised them, so a break in either would have +// been invisible: the census counts promoted VALUES, and every value in the +// corpus was already consumed at some other site. +// +// What this program has to get right, all at once: +// +// 1. `c` must satisfy every `collectors/ptr_shape.rs` rule (see the sibling +// fixture's header for the list) -- so: bound by one `Let` from a `new`, +// never reassigned, captured, passed, returned or aliased. +// 2. Both fields must be NUMERIC-proven, which is what selects the +// `class_field_get_number` load over the plain shape-proven one. Every +// store is an integer literal or a proven-number expression. +// 3. `c` must survive SCALAR REPLACEMENT (#7115) -- otherwise the object is +// deleted, no access site is reached, and the promotion is counted but +// consumed nowhere. This is the delicate part. `c.v = c.v + 1` is what +// does it: a plain in-loop field store defeats scalar replacement, and +// dropping it takes this fixture to zero consumed sites. `c.v++` ALONE +// does not -- an earlier draft with only the update was scalar-replaced +// and exercised nothing. +// 4. `mix()` must be too complex for `simple_scalar_method_summary`, so the +// method call does not re-enable scalar replacement, and it supplies the +// `ptr_shape_method` site. +// +// Do not "tidy" this file. In particular do not fold `c.v = c.v + 1` into the +// `c.w++` update, and do not simplify `mix()`. + +class Ctr { + v: number; + w: number; + constructor() { + this.v = 0; + this.w = 1; + } + mix(): number { + return this.v * this.v + this.w * this.w; + } +} + +function counted(n: number): number { + let total = 0; + for (let i = 0; i < n; i++) { + const c = new Ctr(); + // Plain field store: defeats scalar replacement (see note 3). + c.v = c.v + 1; + // Field update: the `ptr_shape_update` site. + c.w++; + // Numeric-proven reads: the `class_field_get_number` site. + // Method call on a proven receiver: the `ptr_shape_method` site. + total = total + c.v * c.w + c.mix(); + } + return total; +} + +console.log("ptr_shape_sites:" + counted(4)); diff --git a/changelog.d/7119-repsel-per-site-consumption.md b/changelog.d/7119-repsel-per-site-consumption.md new file mode 100644 index 0000000000..facc4f9652 --- /dev/null +++ b/changelog.d/7119-repsel-per-site-consumption.md @@ -0,0 +1,93 @@ +Follow-up to #7117, which made the census count *consumed* promotions rather +than selected ones. Two gaps in that instrument, one found by re-running the +sabotage matrix and one raised in review. + +## Two consumption recorders had never fired + +`Ptr` consumption is recorded at six codegen sites. The consumed count +is per **value**, so one working recorder marks a value consumed and the other +five can rot silently — the limitation #7117 shipped as known-and-ungated. + +Measured, it was worse than "ungated": **two of the six had never fired on any +workload in the corpus.** + +| site | file | fired before | +|---|---|---| +| `ptr_shape_get_number` | `expr/property_get/helpers.rs` | yes | +| `ptr_shape_set` | `expr/property_set.rs` | yes | +| `class_field_get.shape_proven_load` | `expr/property_get.rs` | yes | +| `ptr_shape_method` | `lower_call/property_get/dynamic_dispatch.rs` | yes | +| `class_field_get_number.shape_proven_load` | `expr/property_get/helpers.rs` | **no** | +| `ptr_shape_update` | `expr/instance_misc1.rs` | **no** | + +Both are reachable; nothing reached them. `fixture_ptr_shape_sites.ts` does, +and it took a specific shape to get there — an in-loop **plain field store** is +what defeats scalar replacement (#7115). With only the `++` update the object is +deleted and nothing is consumed at all, which is the same trap #7117 documented +one level up. + +- `site` is now a first-class `Entry` field and is what `Consumed` entries dedup + on, so one value consumed at three sites stays three facts. +- `CONSUMPTION_SITES` registry in the script, not the baseline. Every registered + site must fire somewhere in the corpus; a consumption recorded at an + unregistered site (or with no site) raises rather than being absorbed. +- Corpus: **7 selected, 3 consumed**, all six sites exercised. + +## Review of #7117 + +**Accepted.** + +- The census self-test's "missing analyses" fixture still declared + `schema_version: 1` after `SUPPORTED_REPORT_SCHEMA` moved to 2, so it raised + on schema drift and the branch it names went unexercised. It now tracks the + constant. +- The README's IR-comparison recipe wrote both arms to the same path — and + `--no-link` does not honour `-o` at all, so neither file existed. The + documented procedure could not have reproduced anything. Replaced with one + that captures the printed object path, verified end-to-end. +- `render_text` printed the consumption line only when a count was nonzero, so + an analysis whose promotions were **all** wasted (`selected > 0`, + `consumed == 0`, no mechanism — what a `PERRY_CANONICAL_I32_LOCALS=0` build + produces) read identically to an uninstrumented one. Instrumentation is now a + property of `Analysis` (`records_consumption()`), serialized, and the census + cross-checks it against `CONSUMPTION_INSTRUMENTED` so the two tables cannot + drift. +- `check_unconsumed_is_explained` summed the gap across analyses; a mechanism + recorded for one representation could excuse a silent gap in another. Now per + analysis. +- The NOT-INSTRUMENTED list keyed off a `-consumed` name suffix rather than the + table; `seen_consumed`'s annotation described a 2-tuple while a 3-tuple is + inserted. + +**Rejected, with evidence.** "Unconsumed entries are not deduplicated per value, +so the mechanism totals count access sites." They are already per value: an +unconsumed entry carries no `site`, so `Entry::dedup_key` collapses them in the +compiler. On `batch`, `totals` has two access sites and reports one +`module_init_context`. Documented rather than duplicated. + +## A previously-passing sabotage arm had gone green + +Re-running the matrix after the per-site change (rather than assuming it still +held) caught this: once `site` entered `dedup_key`, stripping `outcome` from the +key no longer turned anything red, and the unit test asserting that property +built a consumed entry with **no site** — a shape the recorders never emit, so +it passed vacuously. + +`outcome` is still correct to keep, but for a different pair (`Unconsumed` vs +`Denied`) and **it cannot currently be observed to matter**: a denied value was +never selected while an unconsumed one always was, so no binding emits both. The +docs and tests now say which element defends which pair, and label `outcome` as +defensive with a hand-constructed test as its only exercise rather than implying +gate coverage that does not exist. + +## Verification + +- Byte-neutral: **24/24** workloads identical with the report off vs on, and + **24/24** between the pre-change and post-change compilers with the report off. +- Nine sabotage arms red, control green: dropping `site` from `dedup_key`, + removing all six consumption recorders, removing **either single** uncovered + recorder (new — this is the gap that is now closed), removing either mechanism + recorder, counting per access site, and folding proven-`this` consumption into + the local column. +- No floor lowered against `origin/main`; `batch` `ptr-shape` still 2. +- CI's sabotage step additionally asserts a consumption site goes dark. diff --git a/crates/perry-codegen/src/opt_report/mod.rs b/crates/perry-codegen/src/opt_report/mod.rs index c20c8c2da1..f050dfc8b8 100644 --- a/crates/perry-codegen/src/opt_report/mod.rs +++ b/crates/perry-codegen/src/opt_report/mod.rs @@ -246,6 +246,33 @@ impl Analysis { } } + /// Whether codegen records CONSUMPTION for this analysis — i.e. whether a + /// `consumed: 0` on its row means "nothing was applied" or merely "nobody + /// measured". + /// + /// The distinction has to be a property of the analysis, not something a + /// reader infers from the numbers. Without it the text report cannot print + /// a consumption line for an analysis whose promotions were ALL wasted + /// (`selected > 0, consumed == 0`) — the worst case this outcome exists to + /// expose — because a printed zero would be indistinguishable from the + /// false zero it would have to print for an uninstrumented analysis. + /// + /// The census keeps the same table (`CONSUMPTION_INSTRUMENTED`) and + /// cross-checks it against this flag, so the two cannot drift. + pub fn records_consumption(self) -> bool { + match self { + Analysis::PtrShape => true, + // Canonical i32/u32 MOVE the storage, so consumption is structural + // rather than a separate event. Canonical `Str` does not move + // storage and genuinely can be selected-and-unconsumed, but its + // consumers are the string-op lowerings and are not instrumented. + Analysis::PtrNumArray + | Analysis::CanonicalSlot + | Analysis::IntValuedTa + | Analysis::SpecAbi => false, + } + } + /// The file whose rules produced the denial, cited in the report so the /// rule numbers are checkable against source. pub fn rule_source(self) -> &'static str { @@ -360,6 +387,15 @@ pub struct Entry { /// TypeErrors), so this is populated for allocation sites and `None` for /// ordinary locals — HIR drops positions at lowering. pub byte_offset: Option, + /// For [`Outcome::Consumed`]: which codegen lowering applied the proof + /// (`ptr_shape_set`, `ptr_shape_update`, …). `None` for every other + /// outcome. + /// + /// A first-class field rather than prose inside `detail`, because the + /// census gates on it: each recorder must fire somewhere in the corpus, or + /// it is a site nobody has ever watched work. Two of the six were in + /// exactly that state when this field was added. + pub site: Option, } impl Entry { @@ -380,12 +416,28 @@ impl Entry { /// (a boxed entry plus a typed clone), which would otherwise double-count /// every denial in it. /// - /// `outcome` is part of the key, and must stay part of it: a `Consumed` - /// entry and the `Selected` entry for the same value agree on every other - /// field (both carry `rule: None`), so without it the consumption record - /// for a value collapses into its selection record and the consumed tally - /// is structurally pinned at zero — a dead counter of exactly the kind - /// this outcome exists to expose. + /// Two elements below are load-bearing, for different pairs: + /// + /// - **`site`** separates a `Consumed` entry from the `Selected` entry for + /// the same value (only consumed entries carry one), and separates the + /// several sites that consumed one value from each other. It is what the + /// per-site coverage gate counts. + /// - **`outcome`** separates an `Unconsumed` entry from a `Denied` one. + /// Both carry `rule: Some(..)` and neither carries a `site`, so with a + /// shared rule name every other element matches — and "the proof was made + /// and thrown away" versus "the proof was never made" are opposite claims + /// about the same binding. + /// + /// **`outcome` is defensive, and currently cannot be observed to matter.** + /// Say so rather than implying coverage that does not exist: today the two + /// rule vocabularies are disjoint, and more fundamentally the collision is + /// unreachable, because a DENIED value was never selected while an + /// UNCONSUMED one always was — no binding can produce both. Removing + /// `outcome` from this key therefore turns no gate red, and the only thing + /// exercising it is the unit test that constructs the collision by hand. + /// It is kept because a dedup key should carry the full discriminant: the + /// cost is one tuple element, and the failure mode if a future rule name + /// collides is silent data loss in the report that feeds the census. fn dedup_key( &self, ) -> ( @@ -406,20 +458,16 @@ impl Entry { self.analysis, self.outcome, self.rule.clone(), - // `detail` participates for CONSUMED entries only, where it names - // the lowering that applied the proof. One value consumed at three - // access sites is genuinely three facts worth reporting, and - // collapsing them here would leave the census's own per-value - // reduction untestable end-to-end — an unexercised branch standing - // between the compiler and the number that gets published. + // `site` participates for CONSUMED entries only. One value consumed + // at three access sites is genuinely three facts worth reporting: + // the census gates on per-site coverage, and collapsing them here + // would both hide that and leave the census's own per-value + // reduction untestable end-to-end. // // Every other outcome keeps the pre-existing identity, so denial // and selection tallies (and therefore the baseline's `candidates` // column) are unchanged. - match self.outcome { - Outcome::Consumed => self.detail.clone(), - _ => None, - }, + self.site.clone(), ) } } @@ -635,6 +683,7 @@ pub(crate) fn deny(d: Denial<'_>) { invoked_per_element: per_element, detail: d.detail, byte_offset: d.byte_offset, + site: None, }); } @@ -664,6 +713,7 @@ pub(crate) fn deny_named(function: &str, region: RegionKind, d: Denial<'_>) { invoked_per_element: None, detail: d.detail, byte_offset: d.byte_offset, + site: None, }); } @@ -721,6 +771,7 @@ pub(crate) fn select( invoked_per_element: per_element, detail, byte_offset: None, + site: None, }); } @@ -756,6 +807,7 @@ pub(crate) fn select_explicit( invoked_per_element: None, detail: None, byte_offset: None, + site: None, }); } @@ -824,6 +876,7 @@ pub(crate) fn consume( invoked_per_element: None, detail: Some(format!("consumed at {site}")), byte_offset: None, + site: Some(site.to_string()), }); } @@ -871,6 +924,7 @@ pub(crate) fn unconsumed(u: Unconsumed<'_>) { invoked_per_element: None, detail: u.detail, byte_offset: None, + site: None, }); } @@ -910,6 +964,7 @@ mod tests { invoked_per_element: per_element.map(str::to_string), detail: None, byte_offset: None, + site: None, } } @@ -956,12 +1011,11 @@ mod tests { /// The trap this outcome exists to avoid, at the data-structure level. /// - /// A `Consumed` entry and the `Selected` entry for the same value agree on - /// module, function, name, position, analysis AND rule (both `None`). If - /// `outcome` is not part of the identity, `take_entries` drops every - /// consumption record as a duplicate of its own selection and the consumed - /// tally is pinned at zero — a dead counter that looks like an honest - /// "nothing was consumed". + /// Built the way the RECORDERS build them — a consumed entry always carries + /// a `site` — so the test cannot pass on a shape the compiler never emits. + /// An earlier version omitted the site and was therefore vacuous: it + /// asserted a property that `outcome` alone provided, while the real + /// separation in a live build comes from `site`. #[test] fn dedup_key_separates_a_consumption_from_its_own_selection() { let mut selected = entry("f", 0, None); @@ -969,6 +1023,7 @@ mod tests { selected.rule = None; let mut consumed = selected.clone(); consumed.outcome = Outcome::Consumed; + consumed.site = Some("ptr_shape_set".into()); assert_ne!( selected.dedup_key(), consumed.dedup_key(), @@ -985,16 +1040,40 @@ mod tests { assert_eq!(kept, 2, "de-duplication swallowed the consumption record"); } - /// An `Unconsumed` record must likewise not collapse into the `Denied` - /// record for the same value: they carry opposite meanings (the proof was - /// made and dropped vs the proof was never made) and a workload can - /// legitimately produce both for one binding under different analyses. + /// One value consumed at several sites must stay several entries: the + /// per-site coverage gate counts them, and collapsing them would hide a + /// recorder that stopped firing. + #[test] + fn dedup_key_separates_two_sites_that_consumed_one_value() { + let mut a = entry("f", 0, None); + a.outcome = Outcome::Consumed; + a.rule = None; + a.site = Some("ptr_shape_set".into()); + let mut b = a.clone(); + b.site = Some("ptr_shape_update".into()); + assert_ne!(a.dedup_key(), b.dedup_key()); + } + + /// The pair `outcome` defends — constructed by hand, because the recorders + /// cannot currently produce it. + /// + /// A denied value was never selected and an unconsumed one always was, so + /// no binding emits both, and the sabotage arm that strips `outcome` from + /// the key leaves the census green. This test is the ONLY thing exercising + /// that element; it is deliberately not presented as gate coverage. See + /// `dedup_key`'s note. #[test] fn dedup_key_separates_an_unconsumed_record_from_a_denial() { let denied = entry("f", 0, None); + assert_eq!(denied.outcome, Outcome::Denied); + assert!(denied.rule.is_some() && denied.site.is_none()); let mut unconsumed = denied.clone(); unconsumed.outcome = Outcome::Unconsumed; - assert_ne!(denied.dedup_key(), unconsumed.dedup_key()); + assert_ne!( + denied.dedup_key(), + unconsumed.dedup_key(), + "with a shared rule name these differ ONLY by outcome" + ); } #[test] diff --git a/crates/perry-codegen/src/opt_report/render.rs b/crates/perry-codegen/src/opt_report/render.rs index 9b76a5e031..2347217a10 100644 --- a/crates/perry-codegen/src/opt_report/render.rs +++ b/crates/perry-codegen/src/opt_report/render.rs @@ -109,7 +109,13 @@ pub fn render_text(entries: &[Entry]) -> String { t.denied, t.candidates(), ); - if t.consumed > 0 || t.unconsumed > 0 { + // Printed whenever the analysis is INSTRUMENTED, not whenever the + // numbers happen to be nonzero. An analysis whose promotions were all + // wasted has `consumed == 0` and possibly no mechanism record either; + // staying silent there would render the worst case identically to an + // uninstrumented analysis, which is the confusion this column exists to + // remove. + if analysis.records_consumption() { let _ = writeln!( out, " {:<16} {:>4} of those selections were CONSUMED by codegen{}", @@ -309,6 +315,9 @@ struct JsonAnalysis<'a> { denied: usize, consumed: usize, unconsumed: usize, + /// Whether `consumed` means "nothing applied" or "nobody measured". The + /// census cross-checks its own table against this, so the two cannot drift. + records_consumption: bool, } #[derive(Debug, serde::Serialize)] @@ -356,6 +365,7 @@ pub fn render_json(entries: &[Entry]) -> String { denied: t.denied, consumed: t.consumed, unconsumed: t.unconsumed, + records_consumption: a.records_consumption(), } }) .collect(), @@ -389,6 +399,7 @@ mod tests { invoked_per_element: None, detail: None, byte_offset: None, + site: None, } } @@ -411,6 +422,7 @@ mod tests { invoked_per_element: None, detail: None, byte_offset: None, + site: None, } } @@ -531,6 +543,9 @@ mod tests { fn with_outcome(analysis: Analysis, name: &str, outcome: Outcome, rule: Option<&str>) -> Entry { let mut e = selected(analysis, name, "Ptr"); e.outcome = outcome; + if outcome == Outcome::Consumed { + e.site = Some("ptr_shape_set".to_string()); + } e.rule = rule.map(str::to_string); e.reason = rule.map(|_| "the context gate dropped it".to_string()); e.issue = rule.map(|_| "#7109".to_string()); @@ -568,6 +583,56 @@ mod tests { ); } + /// The worst case: every promotion of an instrumented analysis was wasted, + /// and no mechanism was recorded either (the state a + /// `PERRY_CANONICAL_I32_LOCALS=0` build produces, where the context gate is + /// closed but the env-knob arm deliberately records no denial). The summary + /// must still say `0 consumed`; staying silent renders it identically to an + /// analysis nobody instrumented. + #[test] + fn a_fully_wasted_analysis_still_reports_zero_consumed() { + let entries = vec![ + selected(Analysis::PtrShape, "acc", "Ptr"), + selected(Analysis::PtrShape, "totals", "Ptr"), + ]; + let text = render_text(&entries); + assert!( + text.contains("0 of those selections were CONSUMED by codegen"), + "a fully wasted instrumented analysis must still print its zero; got:\n{text}" + ); + } + + /// ...and an UNinstrumented analysis must stay silent, because a printed + /// zero there would be a false claim that codegen applied nothing. + #[test] + fn an_uninstrumented_analysis_prints_no_consumption_line() { + let entries = vec![selected(Analysis::CanonicalSlot, "i", "I32")]; + let text = render_text(&entries); + assert!( + !text.contains("CONSUMED by codegen"), + "canonical-slot records no consumption; a zero there would be a lie; got:\n{text}" + ); + } + + /// The compiler-side flag and the census's `CONSUMPTION_INSTRUMENTED` table + /// are two spellings of one fact. Serializing the flag is what lets the + /// census detect drift instead of silently trusting its own copy. + #[test] + fn json_exposes_which_analyses_record_consumption() { + let json: serde_json::Value = serde_json::from_str(&render_json(&[selected( + Analysis::PtrShape, + "a", + "Ptr", + )])) + .unwrap(); + for row in json["summary"]["by_analysis"].as_array().unwrap() { + let flag = row["records_consumption"] + .as_bool() + .unwrap_or_else(|| panic!("missing records_consumption on {row:?}")); + assert_eq!(flag, row["analysis"] == "ptr-shape"); + } + } + /// A build where every promotion is applied must NOT grow the section — /// otherwise it is noise and stops being read. #[test] diff --git a/scripts/compiler_output_harness/repsel_census.py b/scripts/compiler_output_harness/repsel_census.py index 80217800d9..a628578056 100644 --- a/scripts/compiler_output_harness/repsel_census.py +++ b/scripts/compiler_output_harness/repsel_census.py @@ -132,6 +132,31 @@ "ptr-shape": "ptr-shape-consumed", } +#: Every codegen lowering that consumes a `Ptr` proof, and where it +#: lives. **Held in code, never in the baseline** -- same reason as +#: [`LIVENESS_FLOORS`]. +#: +#: The census counts promoted VALUES, so one recorder is enough to mark a value +#: consumed. That makes per-site rot invisible: five of these six could stop +#: firing and every count would be unchanged. When coverage was first measured, +#: two had **never fired on any workload in the corpus** -- +#: `class_field_get_number.shape_proven_load` and `ptr_shape_update` -- so a +#: break in either would have gone unnoticed indefinitely. Both are reachable; +#: `fixture_ptr_shape_sites.ts` was written to reach them. +#: +#: [`check_consumption_site_coverage`] fails a run where any site recorded +#: nothing corpus-wide, and `census_from_report` rejects a site this table does +#: not name -- a new consumption lowering must be registered here, not silently +#: absorbed into an existing count. +CONSUMPTION_SITES: dict[str, str] = { + "class_field_get_number.shape_proven_load": "expr/property_get/helpers.rs", + "ptr_shape_get_number": "expr/property_get/helpers.rs", + "class_field_get.shape_proven_load": "expr/property_get.rs", + "ptr_shape_set": "expr/property_set.rs", + "ptr_shape_update": "expr/instance_misc1.rs", + "ptr_shape_method": "lower_call/property_get/dynamic_dispatch.rs", +} + #: `SlotRep` debug spelling -> census key, for the `canonical-slot` analysis. CANONICAL_REPS = { "I32": "canonical-i32", @@ -163,6 +188,10 @@ # fixture's `p` is a function-body local with an in-loop field store, so it # is consumed; verified against emitted IR, not against this counter. "fixture_ptr_shape": {"ptr-shape": 1, "ptr-shape-consumed": 1}, + # Written for the two consumption sites nothing else in the corpus reached. + # Its value is the SITE coverage it provides (checked separately); the + # count floor here just keeps it honest as a promotion too. + "fixture_ptr_shape_sites": {"ptr-shape": 1, "ptr-shape-consumed": 1}, "fixture_ptr_numarray": {"ptr-numarray": 1}, "fixture_canonical_slots": { "canonical-i32": 1, @@ -232,6 +261,27 @@ def census_from_report(report: dict[str, Any]) -> dict[str, Any]: if not isinstance(entries, list): raise HarnessError("--opt-report JSON has no entries list") + # Two spellings of one fact: CONSUMPTION_INSTRUMENTED here, and + # `Analysis::records_consumption()` in the compiler. A duplicated predicate + # that can drift is worth more than a duplicated predicate that cannot, only + # if something checks it — so check it. + drift = [ + row["analysis"] + for row in rows + if isinstance(row, dict) + and "records_consumption" in row + and bool(row["records_consumption"]) + != (row.get("analysis") in CONSUMPTION_INSTRUMENTED) + ] + if drift: + raise HarnessError( + f"CONSUMPTION_INSTRUMENTED disagrees with the compiler's " + f"Analysis::records_consumption() for {sorted(drift)}. One of the two " + "tables was updated and the other was not; whichever is stale, the " + "census is now reporting consumption data it does not have (or hiding " + "data it does)." + ) + missing_consumption = [ a for a in CONSUMPTION_INSTRUMENTED if "consumed" not in by_analysis.get(a, {}) ] @@ -279,8 +329,14 @@ def census_from_report(report: dict[str, Any]) -> dict[str, Any]: # population. It is reported separately instead (`consumed_receiver`). consumed_receiver = 0 unconsumed_mechanisms: dict[str, int] = {} - seen_consumed: set[tuple[str, Any]] = set() + # Keyed by analysis as well as by rule. `unconsumed_mechanisms` alone is + # rule-keyed, and with a second instrumented analysis a gap in analysis A + # would be excused by a mechanism recorded for analysis B. + unconsumed_by_analysis: dict[str, int] = {} + consumption_sites: dict[str, int] = {} + seen_consumed: set[tuple[str, Any, Any]] = set() unknown_consumed: set[str] = set() + unknown_sites: set[str] = set() for entry in entries: analysis = entry.get("analysis") outcome = entry.get("outcome") @@ -288,6 +344,11 @@ def census_from_report(report: dict[str, Any]) -> dict[str, Any]: if analysis not in CONSUMPTION_INSTRUMENTED: unknown_consumed.add(str(analysis)) continue + site = entry.get("site") + if site is None or site not in CONSUMPTION_SITES: + unknown_sites.add(str(site)) + else: + consumption_sites[site] = consumption_sites.get(site, 0) + 1 if entry.get("position") != "local": consumed_receiver += 1 continue @@ -298,8 +359,26 @@ def census_from_report(report: dict[str, Any]) -> dict[str, Any]: seen_consumed.add(key) counts[CONSUMPTION_INSTRUMENTED[analysis]] += 1 elif outcome == "unconsumed": + # Counted per VALUE, not per access site, so these totals are + # directly comparable with the selected/consumed columns beside + # them. `report_ptr_shape_context_drop` fires at every access site + # of a dropped local, but an unconsumed entry carries no `site`, so + # `Entry::dedup_key` in the compiler already collapses them: on + # `batch`, `totals` has two access sites and reports ONE + # `module_init_context`. Verified, not assumed. rule = str(entry.get("rule") or "") unconsumed_mechanisms[rule] = unconsumed_mechanisms.get(rule, 0) + 1 + unconsumed_by_analysis[str(analysis)] = ( + unconsumed_by_analysis.get(str(analysis), 0) + 1 + ) + if unknown_sites: + raise HarnessError( + f"--opt-report recorded consumption at unregistered site(s) " + f"{sorted(unknown_sites)}. Add them to CONSUMPTION_SITES: a lowering " + "that consumes a representation without being registered cannot be " + "checked for liveness, and folding it into an existing count is how " + "a recorder stops firing without anyone noticing." + ) if unknown_consumed: raise HarnessError( f"--opt-report recorded consumption for analysis/analyses " @@ -325,7 +404,9 @@ def census_from_report(report: dict[str, Any]) -> dict[str, Any]: "counts": counts, "candidates": candidates, "unconsumed_mechanisms": unconsumed_mechanisms, + "unconsumed_by_analysis": unconsumed_by_analysis, "consumed_receiver": consumed_receiver, + "consumption_sites": consumption_sites, } @@ -554,6 +635,34 @@ def check_consumption_invariant(observed: dict[str, dict[str, Any]]) -> list[str return failures +def check_consumption_site_coverage(observed: dict[str, dict[str, Any]]) -> list[str]: + """Every registered consumption lowering must fire somewhere in the corpus. + + The consumed COUNT is per value, so one working recorder marks a value + consumed and the other five could rot silently. This is the site-level + analogue of [`check_instrument_liveness`]: a recorder that never fires is + either dead or unexercised, and both are things the census should say out + loud rather than average away. + + Not hypothetical. When per-site coverage was first measured, four of six + recorders fired and two -- `class_field_get_number.shape_proven_load` and + `ptr_shape_update` -- had never fired on any workload here. + """ + totals = {site: 0 for site in CONSUMPTION_SITES} + for entry in observed.values(): + for site, n in entry.get("consumption_sites", {}).items(): + if site in totals: + totals[site] += int(n) + return [ + f"consumption site {site!r} ({CONSUMPTION_SITES[site]}) recorded nothing " + "across the entire corpus. Either the lowering no longer fires, or its " + "recorder was removed, or no workload reaches it — and the promotion " + "counts look identical in all three cases." + for site in CONSUMPTION_SITES + if totals[site] == 0 + ] + + def check_unconsumed_is_explained(observed: dict[str, dict[str, Any]]) -> list[str]: """A workload with wasted promotions must be able to NAME a mechanism. @@ -574,21 +683,24 @@ def check_unconsumed_is_explained(observed: dict[str, dict[str, Any]]) -> list[s failures: list[str] = [] for name, entry in sorted(observed.items()): counts = entry["counts"] - wasted = sum( - int(counts.get(a, 0)) - int(counts.get(k, 0)) - for a, k in CONSUMPTION_INSTRUMENTED.items() - ) - if wasted <= 0: - continue - if sum(int(v) for v in entry.get("unconsumed_mechanisms", {}).values()) > 0: - continue - failures.append( - f"{name}: {wasted} selected promotion(s) were not consumed, and not one " - "of them names a mechanism. A wasted promotion with no rule attached is " - "the state this census was built to end: it reads exactly like an honest " - "zero. Either a mechanism recorder was removed, or a new way to drop a " - "proof exists and needs one." - ) + by_analysis = entry.get("unconsumed_by_analysis", {}) + # Per analysis, never summed. Aggregating would let a mechanism recorded + # for one representation excuse a silent gap in another, and would let a + # negative gap cancel a positive one, the moment a second analysis is + # instrumented. + for analysis, consumed_key in CONSUMPTION_INSTRUMENTED.items(): + wasted = int(counts.get(analysis, 0)) - int(counts.get(consumed_key, 0)) + if wasted <= 0: + continue + if int(by_analysis.get(analysis, 0)) > 0: + continue + failures.append( + f"{name}: {wasted} selected {analysis} promotion(s) were not consumed, " + "and not one of them names a mechanism. A wasted promotion with no rule " + "attached is the state this census was built to end: it reads exactly " + "like an honest zero. Either a mechanism recorder was removed, or a new " + "way to drop a proof exists and needs one." + ) return failures @@ -692,8 +804,19 @@ def render_consumption_report( f" (plus {receiver} consumption(s) of a proven `this` receiver, which is " "never counted as a selection at all — see CONSUMPTION_INSTRUMENTED)" ) - uninstrumented = [k for k in CENSUS_KEYS if k not in CONSUMPTION_INSTRUMENTED - and not k.endswith("-consumed")] + sites: dict[str, int] = {site: 0 for site in CONSUMPTION_SITES} + for entry in observed.values(): + for site, n in entry.get("consumption_sites", {}).items(): + if site in sites: + sites[site] += int(n) + lines.append(" consumption sites exercised by the corpus:") + for site, n in sorted(sites.items(), key=lambda kv: (-kv[1], kv[0])): + lines.append(f" {site:<42} {n}" + (" <- NEVER FIRES" if n == 0 else "")) + # Derived from the table, not from a "-consumed" name suffix: a future + # consumed key spelled differently would otherwise be reported as NOT + # INSTRUMENTED, which is the opposite of the truth. + instrumented = set(CONSUMPTION_INSTRUMENTED) | set(CONSUMPTION_INSTRUMENTED.values()) + uninstrumented = [k for k in CENSUS_KEYS if k not in instrumented] lines.append( " NOT INSTRUMENTED (no consumption data, reported as absent not as zero): " + ", ".join(uninstrumented) @@ -799,6 +922,7 @@ def census(args: argparse.Namespace) -> int: # consistency assertion about the counter, not a corpus-wide claim. invariant = check_consumption_invariant(observed) unexplained = check_unconsumed_is_explained(observed) + site_gaps = check_consumption_site_coverage(observed) if not partial else [] if partial: print( @@ -819,6 +943,7 @@ def census(args: argparse.Namespace) -> int: ("UNREACHED BY EVERY ANALYSIS", unreached), ("CONSUMPTION COUNTER IS INCOHERENT", invariant), ("WASTED PROMOTION WITH NO NAMED MECHANISM", unexplained), + ("CONSUMPTION SITE NEVER EXERCISED", site_gaps), ): if not problems: continue @@ -872,6 +997,7 @@ def _update( workload["candidates"] = observed[name]["candidates"] # Context, never gated: which mechanism ate each wasted promotion. workload["unconsumed_mechanisms"] = observed[name].get("unconsumed_mechanisms", {}) + workload["consumption_sites"] = observed[name].get("consumption_sites", {}) baseline["generated_at"] = utc_now() path.write_text(json.dumps(baseline, indent=2) + "\n", encoding="utf-8") print(f"Wrote {path.relative_to(REPO_ROOT)} ({len(workloads)} workload(s)).") @@ -943,7 +1069,12 @@ def self_test(_args: argparse.Namespace) -> int: ({"schema_version": 99}, "schema drift"), ( { - "schema_version": 1, + # MUST track SUPPORTED_REPORT_SCHEMA. At schema 1 this fixture + # raised on schema drift before it ever reached the + # missing-analyses branch, so the case passed for the wrong + # reason and the branch it names went unexercised (caught in + # review of #7117). + "schema_version": SUPPORTED_REPORT_SCHEMA, "summary": {"by_analysis": [{"analysis": "ptr-shape", "selected": 0, "denied": 0}]}, "entries": [], }, @@ -991,11 +1122,14 @@ def self_test(_args: argparse.Namespace) -> int: {"analysis": "ptr-shape", "outcome": "selected", "rep": "Ptr"}, # `acc`: consumed at three access sites, but it is ONE value. {"analysis": "ptr-shape", "outcome": "consumed", "rep": "Ptr", - "position": "local", "local_id": 9, "function": "totalsRow"}, + "position": "local", "local_id": 9, "function": "totalsRow", + "site": "ptr_shape_set"}, {"analysis": "ptr-shape", "outcome": "consumed", "rep": "Ptr", - "position": "local", "local_id": 9, "function": "totalsRow"}, + "position": "local", "local_id": 9, "function": "totalsRow", + "site": "ptr_shape_get_number"}, {"analysis": "ptr-shape", "outcome": "consumed", "rep": "Ptr", - "position": "local", "local_id": 9, "function": "totalsRow"}, + "position": "local", "local_id": 9, "function": "totalsRow", + "site": "ptr_shape_update"}, # `totals`: proven, counted as a win, dropped by the context gate. {"analysis": "ptr-shape", "outcome": "unconsumed", "rep": "Ptr", "position": "local", "local_id": 4, "function": "module_init", @@ -1003,7 +1137,8 @@ def self_test(_args: argparse.Namespace) -> int: # A proven `this`, which was never selected: must NOT inflate the # consumed column, or the invariant below stops holding. {"analysis": "ptr-shape", "outcome": "consumed", "rep": "Ptr", - "position": "param", "local_id": None, "function": "C.m"}, + "position": "param", "local_id": None, "function": "C.m", + "site": "ptr_shape_method"}, ], } result = census_from_report(wasted) @@ -1037,7 +1172,8 @@ def self_test(_args: argparse.Namespace) -> int: rogue = json.loads(json.dumps(wasted)) rogue["entries"].append( {"analysis": "canonical-slot", "outcome": "consumed", "rep": "I32", - "position": "local", "local_id": 1, "function": "f"} + "position": "local", "local_id": 1, "function": "f", + "site": "ptr_shape_set"} ) try: census_from_report(rogue) @@ -1069,12 +1205,52 @@ def self_test(_args: argparse.Namespace) -> int: "w": { "counts": {"ptr-shape": 2, "ptr-shape-consumed": 1}, "unconsumed_mechanisms": {"module_init_context": 1}, + "unconsumed_by_analysis": {"ptr-shape": 1}, } } ) assert not check_unconsumed_is_explained( {"w": {"counts": {"ptr-shape": 1, "ptr-shape-consumed": 1}, "unconsumed_mechanisms": {}}} ), "nothing wasted means nothing to explain" + # The explanation must be keyed to the analysis that has the gap. A + # mechanism belonging to some OTHER analysis must not excuse it. + assert check_unconsumed_is_explained( + { + "w": { + "counts": {"ptr-shape": 2, "ptr-shape-consumed": 1}, + "unconsumed_mechanisms": {"module_init_context": 1}, + "unconsumed_by_analysis": {"ptr-numarray": 1}, + } + } + ), "a mechanism from a different analysis must not excuse the gap" + + # Per-site liveness: a recorder that never fires must be red, even though + # every promotion count is unchanged. + assert not check_consumption_site_coverage( + {"w": {"consumption_sites": {s: 1 for s in CONSUMPTION_SITES}}} + ) + for missing in CONSUMPTION_SITES: + gaps = check_consumption_site_coverage( + {"w": {"consumption_sites": {s: 1 for s in CONSUMPTION_SITES if s != missing}}} + ) + assert any(missing in g for g in gaps), (missing, gaps) + + # A consumption recorded at an unregistered site (or with no site at all) + # must raise rather than be absorbed into an existing count. + for bad_site in ("brand_new_lowering", None): + rogue = json.loads(json.dumps(wasted)) + for e in rogue["entries"]: + if e["outcome"] == "consumed": + if bad_site is None: + e.pop("site", None) + else: + e["site"] = bad_site + try: + census_from_report(rogue) + except HarnessError: + pass + else: # pragma: no cover + raise AssertionError(f"unregistered consumption site {bad_site!r} accepted") print("repsel census self-test OK") return 0 diff --git a/tests/test_repsel_census.py b/tests/test_repsel_census.py index b8dc0f2d2a..a1b7ca7069 100644 --- a/tests/test_repsel_census.py +++ b/tests/test_repsel_census.py @@ -73,7 +73,13 @@ def win(analysis: str, rep: str) -> dict: return {"analysis": analysis, "outcome": "selected", "rep": rep} -def consumed_entry(analysis: str, local_id, function: str = "f", position: str = "local") -> dict: +def consumed_entry( + analysis: str, + local_id, + function: str = "f", + position: str = "local", + site: str = "ptr_shape_set", +) -> dict: return { "analysis": analysis, "outcome": "consumed", @@ -81,6 +87,7 @@ def consumed_entry(analysis: str, local_id, function: str = "f", position: str = "position": position, "local_id": local_id, "function": function, + "site": site, } @@ -421,11 +428,54 @@ def test_wasted_promotions_must_name_a_mechanism(self): "w": { "counts": {"ptr-shape": 2, "ptr-shape-consumed": 1}, "unconsumed_mechanisms": {"scalar_replaced": 1}, + "unconsumed_by_analysis": {"ptr-shape": 1}, + } + } + ) + ) + + def test_a_mechanism_from_another_analysis_does_not_excuse_the_gap(self): + """The explanation must belong to the analysis that has the gap. + + `unconsumed_mechanisms` is keyed by rule name only. Summing it across + analyses would let a mechanism recorded for `ptr-numarray` excuse a + silent `ptr-shape` gap the moment a second analysis is instrumented. + """ + self.assertTrue( + CENSUS.check_unconsumed_is_explained( + { + "w": { + "counts": {"ptr-shape": 2, "ptr-shape-consumed": 1}, + "unconsumed_mechanisms": {"some_other_rule": 3}, + "unconsumed_by_analysis": {"ptr-numarray": 3}, } } ) ) + def test_the_instrumentation_tables_must_not_drift(self): + """`CONSUMPTION_INSTRUMENTED` here vs `records_consumption()` in Rust. + + Two spellings of one fact. A duplicated predicate is tolerable only if + something checks it -- the census refuses a report whose compiler-side + flag disagrees with its own table. + """ + payload = report(selected={"ptr-shape": 1}, entries=[win("ptr-shape", "Ptr")]) + for row in payload["summary"]["by_analysis"]: + row["records_consumption"] = row["analysis"] in CENSUS.CONSUMPTION_INSTRUMENTED + CENSUS.census_from_report(payload) # agrees: fine + + for row in payload["summary"]["by_analysis"]: + if row["analysis"] == "ptr-shape": + row["records_consumption"] = False + with self.assertRaises(HarnessError): + CENSUS.census_from_report(payload) + + for row in payload["summary"]["by_analysis"]: + row["records_consumption"] = True + with self.assertRaises(HarnessError): + CENSUS.census_from_report(payload) + def test_the_instrumentation_table_lives_in_code_not_the_baseline(self): """Same rule as LIVENESS_FLOORS and ZERO_CANDIDATE_ALLOWLIST. @@ -470,6 +520,93 @@ def test_every_instrumented_analysis_has_a_nonzero_consumed_liveness_floor(self) "counter nobody has watched go red.", ) + def test_every_consumption_site_must_fire_somewhere_in_the_corpus(self): + """The per-site liveness gate. + + The consumed count is per VALUE, so one working recorder is enough to + mark a value consumed and the other five can rot silently. This is what + makes that visible -- and it is not hypothetical: when coverage was + first measured, `class_field_get_number.shape_proven_load` and + `ptr_shape_update` had never fired on any workload in the corpus. + """ + every = { + "w": { + "consumption_sites": {s: 1 for s in CENSUS.CONSUMPTION_SITES}, + } + } + self.assertFalse(CENSUS.check_consumption_site_coverage(every)) + for missing in CENSUS.CONSUMPTION_SITES: + partial = { + "w": { + "consumption_sites": { + s: 1 for s in CENSUS.CONSUMPTION_SITES if s != missing + } + } + } + failures = CENSUS.check_consumption_site_coverage(partial) + self.assertTrue( + any(missing in f for f in failures), + f"a dead {missing!r} recorder must be visible", + ) + + def test_an_unregistered_consumption_site_is_loud(self): + """A new consumption lowering must be registered, not absorbed. + + Folding an unknown site into an existing count is how a recorder stops + firing without anyone noticing. + """ + payload = report( + selected={"ptr-shape": 1}, + consumed={"ptr-shape": 1}, + entries=[ + win("ptr-shape", "Ptr"), + consumed_entry("ptr-shape", 1, site="brand_new_lowering"), + ], + ) + with self.assertRaises(HarnessError): + CENSUS.census_from_report(payload) + + def test_a_consumption_entry_with_no_site_is_loud(self): + payload = report( + selected={"ptr-shape": 1}, + consumed={"ptr-shape": 1}, + entries=[win("ptr-shape", "Ptr")] + + [{k: v for k, v in consumed_entry("ptr-shape", 1).items() if k != "site"}], + ) + with self.assertRaises(HarnessError): + CENSUS.census_from_report(payload) + + def test_the_site_registry_lives_in_code_not_the_baseline(self): + source = ( + CENSUS.REPO_ROOT / "scripts/compiler_output_harness/repsel_census.py" + ).read_text(encoding="utf-8") + self.assertIn("CONSUMPTION_SITES: dict[str, str] = {", source) + baseline = json.loads( + (CENSUS.REPO_ROOT / "benchmarks/repsel_census/baseline.json").read_text() + ) + self.assertNotIn("consumption_sites_registry", baseline) + + def test_the_shipped_baseline_exercises_every_consumption_site(self): + """Pins the coverage the site fixture was added to provide. + + `--update` records per-workload site counts as context. If a site drops + out of the shipped baseline, the corpus stopped reaching it and the + gate above would have gone red on a real run -- this catches it without + needing a compiler. + """ + baseline = json.loads( + (CENSUS.REPO_ROOT / "benchmarks/repsel_census/baseline.json").read_text() + ) + seen: set[str] = set() + for w in baseline["workloads"]: + seen.update(w.get("consumption_sites", {})) + for site in CENSUS.CONSUMPTION_SITES: + self.assertIn( + site, + seen, + f"no workload in the shipped baseline exercises {site!r}", + ) + def test_the_shipped_baseline_shows_a_consumed_gap(self): """The finding itself, pinned.