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
16 changes: 12 additions & 4 deletions benchmarks/compiler_output/workloads.toml
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,10 @@ allowed_missed_reason_kinds = [
[workloads.h1_native_rep_equivalence.runtime_budgets]
allocations_traced = 0
gc_collections_traced = 0
# Includes root barrier setup now counted by the static analyzer.
# Heap write barriers only. The inline shadow-stack root-shading barriers
# added by #7088 (one per rooted Buffer local: src, dst, same) are counted
# under root_shading_barriers_static, not here; they are gated behind
# incremental marking and never fire in this workload (write_barriers_traced=0).
write_barriers_static = 1
write_barriers_traced = 0
boxed_number_allocations_static = 0
Expand Down Expand Up @@ -445,7 +448,10 @@ no_conversions = true

[[workloads.h1_native_rep_equivalence.named_regions.selectors]]
function_contains = "main"
label_any = ["for.body.2"]
# for.body.2 -> for.body.14 after #7088: the inline shadow-slot stores add
# ss.* blocks ahead of the module-init loops, shifting the deterministic
# per-function block counter by 12 (2/6/10 -> 14/18/22).
label_any = ["for.body.14"]
counter_min = { load_i8 = 1, store_i8 = 1 }

[[workloads.h1_native_rep_equivalence.named_regions]]
Expand All @@ -457,7 +463,8 @@ no_conversions = true

[[workloads.h1_native_rep_equivalence.named_regions.selectors]]
function_contains = "main"
label_any = ["for.body.6"]
# for.body.6 -> for.body.18 after #7088 (see direct_bounded).
label_any = ["for.body.18"]
counter_min = { load_i8 = 1, store_i8 = 1 }

[[workloads.h1_native_rep_equivalence.named_regions]]
Expand All @@ -469,7 +476,8 @@ no_conversions = true

[[workloads.h1_native_rep_equivalence.named_regions.selectors]]
function_contains = "main"
label_any = ["for.body.10"]
# for.body.10 -> for.body.22 after #7088 (see direct_bounded).
label_any = ["for.body.22"]
counter_min = { load_i8 = 1, store_i8 = 1 }

[[workloads.h1_native_rep_equivalence.named_regions]]
Expand Down
32 changes: 32 additions & 0 deletions changelog.d/7136-native-region-proof-inline-root-barrier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
**ci(compiler-output-regression):** the `native-region-proof` gate accounts for
#7088's inline shadow-slot root barrier, restoring green on `main`.

#7088 moved the per-store shadow-stack root store — and its
Comment on lines +2 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid Markdown heading parsing for issue references.

The leading #7088 tokens on Line 2 and Line 4 trigger markdownlint MD018. Write Issue 7088 or escape the hash so these lines remain prose.

Proposed Markdown fix
-#7088's inline shadow-slot root barrier, restoring green on `main`.
+Issue 7088's inline shadow-slot root barrier, restoring green on `main`.
 
-#7088 moved the per-store shadow-stack root store — and its
+Issue 7088 moved the per-store shadow-stack root store — and its
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#7088's inline shadow-slot root barrier, restoring green on `main`.
#7088 moved the per-store shadow-stack root store — and its
Issue 7088's inline shadow-slot root barrier, restoring green on `main`.
Issue 7088 moved the per-store shadow-stack root store — and its
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 2-2: No space after hash on atx style heading

(MD018, no-missing-space-atx)


[warning] 4-4: No space after hash on atx style heading

(MD018, no-missing-space-atx)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/7136-native-region-proof-inline-root-barrier.md` around lines 2 -
4, Update the prose in the changelog entry to avoid leading hash-prefixed issue
references being parsed as Markdown headings: replace the visible “#7088”
references with “Issue 7088” while preserving the existing wording.

Source: Linters/SAST tools

incremental-mark root-shading barrier — from a `js_shadow_slot_bind` /
`js_shadow_slot_set` runtime call to inline IR. That barrier was always
emitted; it just lived *inside* the runtime function, invisible to the
harness's static call counter. Inlining made the `js_write_barrier_root_nanbox`
call site visible, so `write_barriers_static` jumped (e.g. h1_native_rep_equivalence
0→3, one per rooted Buffer local) and every affected `native-region-proof`
workload tripped its heap-barrier budget. The same inline lowering inserts
`ss.*` blocks ahead of the module-init loops, shifting the deterministic
per-function block counter by 12 and blanking the `direct_bounded` /
`local_cast` / `helper_index` region labels (`for.body.2/6/10` → `14/18/22`).

Neither is a real regression: the root-shading barrier is gated behind
`PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT`, never fires in these workloads
(`write_barriers_traced` stays 0), and #7088 proves it observationally
identical to the call it replaced. The barriers sit in guarded `ss.barrier`
blocks at root-bind sites, never inside the native loops, which still carry
raw `load i8`/`store i8` with alias metadata and no runtime calls.

- `structural_counters` now scores `write_barriers_static` on the
optimizer-controlled *heap* barriers (`js_write_barrier`,
`js_write_barrier_slot`) only. The shadow-stack root-shading barriers
(`js_write_barrier_root_nanbox`, `js_write_barrier_root_heap_word`) are
reported under a new `root_shading_barriers_static` field — still visible,
no longer inflating the heap-barrier budget. Real regressions stay caught:
heap barriers are still counted, and a root barrier that actually *fires*
is caught by the `write_barriers_traced` budget.
- `h1_native_rep_equivalence`'s region selectors follow the renumbered loop
bodies (`for.body.14/18/22`).
19 changes: 17 additions & 2 deletions scripts/compiler_output_harness/analyzers.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,23 @@ def structural_counters(ir_before: str, ir_after: str, assembly: str) -> dict[st
"ptrtoint": ir_after.count(" ptrtoint "),
"runtime_calls": runtime_calls,
"boxed_number_allocations": after_calls.get("js_boxed_number_new", 0),
# Heap write barriers — the perf-relevant, optimizer-controlled
# barriers this gate exists to catch. Counted statically because a
# native region that stores a GC pointer into a heap object needs
# one; the proof budgets bound how many.
"write_barriers": after_calls.get("js_write_barrier", 0)
+ after_calls.get("js_write_barrier_slot", 0)
+ after_calls.get("js_write_barrier_root_nanbox", 0)
+ after_calls.get("js_write_barrier_slot", 0),
# GC shadow-stack root-shading barriers (#7088). Before #7088 these
# lived inside the `js_shadow_slot_bind` / `js_shadow_slot_set`
# runtime calls and were invisible to this static IR counter; #7088
# emits the shadow-slot store — and its barrier — inline, so the
# call site is now visible here. Each is emitted once per rooted
# pointer-capable local (a structural constant, not an optimizer
# choice) and is gated behind PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT,
# so it never fires unless incremental marking is live — meaning it
# is caught, if it ever regresses at runtime, by write_barriers_traced.
# Tracked separately so it does not inflate the heap-barrier budget.
"root_shading_barriers": after_calls.get("js_write_barrier_root_nanbox", 0)
Comment on lines +224 to +240

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 6 \
  'write_barrier_calls|js_write_barrier_root_(nanbox|heap_word)|PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT' .

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runtime implementations containing root barrier calls =="
rg -n -C 8 \
  'js_write_barrier_root_(nanbox|heap_word)|runtime_write_barrier|write_barrier_calls|trace' \
  crates/perry-runtime crates/perry-core scripts || true

echo
echo "== analyzers relevant functions =="
sed -n '420,510p' scripts/compiler_output_harness/analyzers.py
sed -n '140,255p' scripts/compiler_output_harness/analyzers.py

echo
echo "== test regression relevant section =="
sed -n '1500,1590p' tests/test_compiler_output_regression.py

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files defining root barrier implementations =="
rg -n -C 12 \
  'fn js_write_barrier_root_(nanbox|heap_word)|pub.*js_write_barrier_root_(nanbox|heap_word)|js_write_barrier_root_(nanbox|heap_word)\(' \
  crates/perry-runtime/src 2>/dev/null || true

echo
echo "== runtime_write_barrier tracing helpers =="
rg -n -C 10 \
  'runtime_write_barrier(_root_(nanbox|heap_word))?|record_write_barrier|gc_cycle_trace|write_barrier_calls|take_write_barrier_trace' \
  crates/perry-runtime/src/gc crates/perry-runtime/src 2>/dev/null \
  --glob '!**/target/**' \
  --glob '!**/tests/**' || true

echo
echo "== analyzer summary and structural_counter gate =="
sed -n '200,245p' scripts/compiler_output_harness/analyzers.py
sed -n '430,510p' scripts/compiler_output_harness/analyzers.py

echo
echo "== tests using root-barrier runtime counters =="
rg -n -C 8 \
  'write_barriers_traced|write_barrier_calls|js_write_barrier_root_(nanbox|heap_word)|root_shading_barriers_static' \
  tests scripts crates/perry-runtime/src/gc/tests 2>/dev/null || true

Repository: PerryTS/perry

Length of output: 50370


Add runtime coverage for root-shading barriers.

runtime_write_barrier_root_nanbox() and runtime_write_barrier_root_heap_word() return without emitting write_barrier.calls when the cycle is inactive. Since these helpers are removed from write_barriers_static, fallbacks like structural_counters can allow removed root barriers unless write_barriers_traced has a focused test for both root helpers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/compiler_output_harness/analyzers.py` around lines 224 - 240, Add
focused runtime coverage for both runtime_write_barrier_root_nanbox() and
runtime_write_barrier_root_heap_word() in write_barriers_traced, verifying each
emits write_barrier.calls when incremental marking is active and does not emit
it when inactive. Ensure the tests cover the fallback path so
structural_counters cannot permit removed root-shading barriers.

+ after_calls.get("js_write_barrier_root_heap_word", 0),
"buffer_slow_path_calls": sum(
count
Expand Down Expand Up @@ -445,6 +459,7 @@ def runtime_counter_summary(
"allocations_traced": traced_allocations,
"gc_collections_traced": gc_collections,
"write_barriers_static": int(after.get("write_barriers", 0) or 0),
"root_shading_barriers_static": int(after.get("root_shading_barriers", 0) or 0),
"write_barriers_traced": traced_write_barriers,
"boxed_number_allocations_static": int(
after.get("boxed_number_allocations", 0) or 0
Expand Down
18 changes: 11 additions & 7 deletions tests/test_compiler_output_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,21 @@
H1_MIN_IR = """
define i32 @main() {
entry:
br label %for.body.2
for.body.2:
br label %for.body.14
for.body.14:
%i = load i32, ptr %slot
store i32 %i, ptr %slot
%ok = icmp slt i32 %i, %n
%p0 = getelementptr i8, ptr %src, i32 %i
%b = load i8, ptr %p0
store i8 %b, ptr %p0
br label %for.body.6
for.body.6:
br label %for.body.18
for.body.18:
%p1 = getelementptr i8, ptr %src, i32 %i
%b1 = load i8, ptr %p1
store i8 %b1, ptr %p1
br label %for.body.10
for.body.10:
br label %for.body.22
for.body.22:
%p2 = getelementptr i8, ptr %src, i32 %i
%b2 = load i8, ptr %p2
store i8 %b2, ptr %p2
Expand Down Expand Up @@ -1556,7 +1556,11 @@ def test_runtime_counter_summary_combines_static_and_trace_counts(self):
self.assertEqual(summary["gc_collections_traced"], 2)
self.assertEqual(summary["allocations_traced"], 4)
self.assertEqual(summary["write_barriers_traced"], 3)
self.assertEqual(summary["write_barriers_static"], 4)
# write_barriers_static counts only heap barriers (js_write_barrier +
# js_write_barrier_slot); the inline shadow-stack root-shading barriers
# (#7088) are tracked separately so they do not inflate the budget.
self.assertEqual(summary["write_barriers_static"], 2)
self.assertEqual(summary["root_shading_barriers_static"], 2)
self.assertEqual(summary["boxed_number_allocations_static"], 1)
self.assertEqual(summary["buffer_slow_path_accesses_static"], 2)
self.assertEqual(summary["array_slow_path_accesses_static"], 2)
Expand Down
Loading