Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,18 @@ jobs:
echo "::error::two of the six recorders had never fired before it existed."
exit 1
fi
# #7034 §3 (the array-element escape) is a SEPARATE analysis
# (collectors/ptr_shape_elements.rs) behind the same knob, and NO
# real corpus workload promotes an element local -- so if it stopped
# issuing facts entirely, every assertion above would still pass and
# this job would stay green. Its own fixture is what makes that
# visible.
if ! printf '%s' "$out" | grep -q "fixture_ptr_shape_elements: ptr-shape-consumed promoted 0"; then
echo "::error::The array-element fixture kept its promotions with"
echo "::error::Ptr<Shape> disabled. Either the element analysis is not"
echo "::error::behind the knob, or the fixture stopped exercising it."
exit 1
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
echo "Census correctly went red with PERRY_PTR_SHAPE_LOCALS=0."

- name: Upload census reports
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"

# Keep the compiler caches out of ~/.cache/zig and out of the source tree. The path is stable so
# rebuilds stay warm; point PERRY_ZIG_CACHE_DIR elsewhere for a cold build.
tmp_root="${TMPDIR:-/tmp}"
zig_cache="${PERRY_ZIG_CACHE_DIR:-${tmp_root%/}/perry-zig-cache}"
export ZIG_GLOBAL_CACHE_DIR="$zig_cache/global"
export ZIG_LOCAL_CACHE_DIR="$zig_cache/json_pipeline"

mkdir -p zig-out/bin
zig build-exe src/main.zig \
-O ReleaseFast \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
# script against the host target, which has the same version mismatch.
set -euo pipefail
cd "$(dirname "$0")"

# Keep the compiler caches out of ~/.cache/zig and out of the source tree. The path is stable so
# rebuilds stay warm; point PERRY_ZIG_CACHE_DIR elsewhere for a cold build.
tmp_root="${TMPDIR:-/tmp}"
zig_cache="${PERRY_ZIG_CACHE_DIR:-${tmp_root%/}/perry-zig-cache}"
export ZIG_GLOBAL_CACHE_DIR="$zig_cache/global"
export ZIG_LOCAL_CACHE_DIR="$zig_cache/image_conv"

mkdir -p zig-out/bin
zig build-exe src/main.zig \
-O ReleaseFast \
Expand Down
32 changes: 31 additions & 1 deletion benchmarks/repsel_census/baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,36 @@
"ptr_shape_method": 1
}
},
{
"name": "fixture_ptr_shape_elements",
"role": "liveness",
"source": "benchmarks/repsel_census/fixtures/fixture_ptr_shape_elements.ts",
"floors": {
"ptr-shape": 3,
"ptr-shape-consumed": 3,
"ptr-numarray": 0,
"canonical-i32": 1,
"canonical-u32": 0,
"canonical-str": 0,
"int-valued-ta": 0,
"spec-abi-entry": 1,
"spec-abi-taptr-slot": 0
},
"candidates": {
"ptr-shape": 3,
"ptr-numarray": 0,
"canonical-slot": 2,
"int-valued-ta": 0,
"spec-abi": 1
},
"unconsumed_mechanisms": {},
"consumption_sites": {
"ptr_shape_get_number": 3,
"ptr_shape_method": 1,
"class_field_get.shape_proven_load": 1,
"ptr_shape_set": 2
}
},
{
"name": "fixture_ptr_numarray",
"role": "liveness",
Expand Down Expand Up @@ -678,5 +708,5 @@
"consumption_sites": {}
}
],
"generated_at": "2026-07-31T09:26:30.260742Z"
"generated_at": "2026-07-31T21:18:40.428173Z"
}
80 changes: 80 additions & 0 deletions benchmarks/repsel_census/fixtures/fixture_ptr_shape_elements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Liveness fixture for the `Ptr<Shape>` ARRAY-ELEMENT escape (#7034 §3).
//
// The other two `ptr_shape` fixtures prove that a *contained* local promotes
// and that each consumption site fires. Neither of them touches an array, so
// both stay green if `collectors/ptr_shape_elements.rs` stops issuing facts
// entirely — and the 18 real corpus workloads promote zero element locals
// today, so the corpus cannot see it either. Without this file the element
// rule has no gate at all: it would be exactly CLAUDE.md failure mode 4, a
// green job whose subject never ran.
//
// What this program has to get right, all at once:
//
// 1. `rows` must satisfy every element-array conjunct: one `const rows = []`
// binding, only `push` writes of one class, only `.length` and in-bounds
// `rows[i]` reads, and no other use at all. It is deliberately NOT
// returned — `return rows` is admitted by the rule, but returning it
// would let the deforestation pass (`perry-transform/src/deforest`)
// rewrite the local array into a `__deforest_out` PARAMETER, which this
// analysis cannot see. That is a real coverage hole (it is why
// `batch.ts` is unchanged by #7034 §3) and it must not silently make
// this fixture vacuous.
// 2. The producer local `row` must escape ONLY through the push, so its
// promotion is attributable to the element exemption and to nothing
// else. Its field store before the push is what keeps it out of scalar
// replacement (#7115) — without it the object is deleted outright and
// no access site is reached.
// 3. Both read forms must appear: the explicit `const s = rows[i]` inside a
// `i < rows.length` loop, and the `for (const r of rows)` iterator form,
// which desugars to the same shape. If the desugar ever changes, this
// fixture's count drops and the gate goes red — which is the point.
// 4. Every read must be a declared field of `Row`, and no member of the
// group may escape: one `r.extra = 1` anywhere voids the WHOLE group by
// design, and would take this fixture to zero.
//
// Do not "tidy" this file. In particular do not add `return rows`, do not
// hoist the `new Row(...)` into the `push` call (that removes the producer
// local this fixture is here to promote), and do not merge the two read
// loops.

class Row {
id: number;
weight: number;
score: number;
constructor(id: number, weight: number) {
this.id = id;
this.weight = weight;
this.score = 0;
}
rescore(f: number): number {
return this.weight * f + this.id;
}
}

function build(n: number): number {
const rows: Row[] = [];
for (let i = 0; i < n; i++) {
// Producer local: its only escape is the push (note 2).
const row = new Row(i, i * 0.5);
row.score = row.weight + 1;
rows.push(row);
}

let total = 0;

// Read form A: explicit indexed binding under an `i < rows.length` loop.
for (let i = 0; i < rows.length; i++) {
const s = rows[i];
s.score = s.score + s.weight;
total = total + s.score + s.id;
}

// Read form B: `for…of`, which desugars to the same bounded `rows[__idx]`.
for (const r of rows) {
total = total + r.rescore(2) + r.weight;
}

return total;
}

console.log("ptr_shape_elements:" + build(6));
Loading
Loading