Skip to content

fix(ci): unbreak the thread-local policy gate, and stop counting #[cfg(test)] declarations - #7824

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7814-thread-local-policy
Aug 11, 2026
Merged

fix(ci): unbreak the thread-local policy gate, and stop counting #[cfg(test)] declarations#7824
proggeramlug merged 2 commits into
mainfrom
fix/7814-thread-local-policy

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #7814.

scripts/check_thread_locals.py is red on main (exit 1). Because it is
deliberately not yet in branch protection's required contexts, nothing surfaces
it — and that red state is exactly what blocks tls-budget's documented
promotion step, so the gate stays advisory indefinitely. It is the shape
CLAUDE.md calls gate theatre: it runs, it reports failure, and it cannot block
anything.

Two of the eight raw blocks are real — and they are not the ones reported

The ticket names array/indexing.rs, map.rs, set.rs and
registry_latch_probes.rs as violations "in hot paths". All four are
#[cfg(test)].
They do not exist in a shipping build and cannot cost a single
_tlv_get_addr. (registry_latch_probes.rs is not even compiled outside tests —
lib.rs:143 declares it #[cfg(test)] mod.)

The genuinely live declarations are in gc/schedule.rs, which the ticket does
not mention:

declaration gated? on what path
SAFEPOINT_COUNTER no bumped on every safepoint
SCHEDULE_NEXT_CANDIDATE_BYTES no read on every poll
SCHEDULE_OVERRIDE #[cfg(test)]
SCHEDULE_STRIDE_OVERRIDE #[cfg(test)]

The two ungated ones are converted to crate::perry_thread_local!.

#[cfg(test)] declarations are out of scope by construction

Recording a test-only declaration as "cold" records the wrong fact — it is not
cold, it is absent — and spends the allowlist's credibility on entries nobody
can ever act on. The scan now skips three statically-visible gated shapes:

  • #[cfg(test)] directly above the block,
  • an inline #[cfg(test)] mod … { … } enclosing it,
  • the block's whole file being declared #[cfg(test)] mod <stem>;, closed
    transitively so gc/tests/mod.rs carries its subtree.

Unlike hot-vs-cold, this is a static fact the scan can actually see.

The new rule is the kind that fails quietly, so it is sabotage-tested

An over-broad exclusion makes the gate pass by seeing less, which looks
identical to a fix. --self-test therefore grew two cases per shape: gated is
not counted, and removing the gate makes the same declaration fail again. It
now proves the checker can fail in six directions rather than four, and the
whole-file rule is checked in both directions too (#[cfg(test)] mod probes;
excluded, plain mod probes; back in scope).

The regeneration is provably one-way

entries 101 -> 91   raw blocks 157 -> 129
ADDED: none
GREW:  none

Asserted mechanically against the pre-image, because --update absorbing a real
violation is the failure mode the ticket warns about. Three dropped entries were
already stale: gc/zeal.rs (left by #7741's removal of PERRY_GC_ZEAL),
arena/quarantine.rs and gc/oldgen_defrag.rs.

_hot_declarations moves 157 -> 160, and only two of those three are mine: the
recorded value was already one behind the tree, since nothing compares it
except the capacity ceiling (160 vs 768).

Validation

  • scripts/check_thread_locals.pythread-local policy OK: 160 hot declarations, 129 raw blocks in 91 recorded cold files, capacity 768, exit 0.
  • scripts/check_thread_locals.py --self-test → passes in all six directions.
  • cargo build -p perry-runtime clean; cargo test -p perry-runtime -- --test-threads=1 — see the comment below.

Left for the maintainer, and the point of the exercise

Promote tls-budget to a required context after its first green run on main.
The workflow's own header specifies this step, and CLAUDE.md's corollary is that
leaving it undone is how gc-root-dominance sat red for weeks.

Summary by CodeRabbit

  • Bug Fixes

    • Improved thread-local policy validation so test-only declarations no longer trigger production checks.
    • Updated garbage-collection scheduling state to comply with the supported thread-local policy without changing behavior.
    • Removed obsolete policy exceptions and refreshed the approved declaration list.
  • Tests

    • Expanded self-tests to verify both accepted and rejected declaration patterns across additional scenarios.
    • Added coverage for test modules and files included indirectly by test-only code.

…declarations, stop counting `#[cfg(test)]` ones

`scripts/check_thread_locals.py` has been red on `main` (exit 1). Because it is
deliberately not yet a required context, nothing surfaced it — and that red
state is exactly what blocks `tls-budget`'s documented promotion step, so the
gate stays advisory indefinitely. This is the shape CLAUDE.md calls gate
theatre: it runs, it reports failure, and it cannot block anything.

TWO OF THE EIGHT RAW BLOCKS ARE REAL, AND THEY ARE NOT THE ONES REPORTED.

#7814 names `array/indexing.rs`, `map.rs`, `set.rs` and `registry_latch_probes.rs`
as "hot path" violations. All four are `#[cfg(test)]`: they do not exist in a
shipping build and cannot cost a single `_tlv_get_addr`. The genuinely live
declarations are in `gc/schedule.rs`, which the ticket does not mention —
`SAFEPOINT_COUNTER` (bumped on every safepoint) and
`SCHEDULE_NEXT_CANDIDATE_BYTES` (read on every poll). Those two are converted
to `crate::perry_thread_local!`.

`#[cfg(test)]` DECLARATIONS ARE OUT OF SCOPE BY CONSTRUCTION.

Recording a test-only declaration as "cold" records the wrong fact — it is not
cold, it is absent — and spends the allowlist's credibility on entries nobody
can ever act on. The scan now skips three statically-visible gated shapes: the
attribute directly above a block, an inline `#[cfg(test)] mod … { … }`, and a
file whose whole module is declared `#[cfg(test)] mod <stem>;` (closed
transitively, so `gc/tests/mod.rs` carries its subtree).

That is the direction that can go wrong quietly — an over-broad exclusion makes
the gate pass by seeing less — so `--self-test` grew two cases per shape:
gated is not counted, AND removing the gate makes the same declaration fail
again. It now proves the checker can fail in six directions, not four.

The allowlist regeneration is provably one-way: 101 -> 91 entries, 157 -> 129
blocks, with nothing added and no entry's count increased. Three of the dropped
entries were already stale (`gc/zeal.rs` from #7741's removal of
`PERRY_GC_ZEAL`, plus `arena/quarantine.rs` and `gc/oldgen_defrag.rs`).

`_hot_declarations` also moves 157 -> 160, and only two of those three are
mine: the recorded value was already one behind the tree, since nothing
compares it except the capacity ceiling.

Still a maintainer action, and the point of the exercise: promote `tls-budget`
to a required context after its first green run on `main`.

Refs #7814
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The thread-local policy checker now excludes test-only declarations across module-tree forms. GC scheduling declarations use crate::perry_thread_local!. The cold allowlist and changelog reflect the updated shipping-build scan.

Changes

Thread-local policy gate

Layer / File(s) Summary
Shipping-build checker scope
scripts/check_thread_locals.py
The scanner resolves test-gated module files, skips direct and inline test declarations, and expands self-tests to six failure directions.
GC scheduling policy conversion
crates/perry-runtime/src/gc/schedule.rs
SAFEPOINT_COUNTER and SCHEDULE_NEXT_CANDIDATE_BYTES now use crate::perry_thread_local!.
Allowlist and changelog update
scripts/thread_local_cold_allowlist.json, changelog.d/7824-thread-local-policy-gate.md
The allowlist updates declaration counts and removes obsolete entries. The changelog documents the changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PolicyCheck as check_thread_locals.py
  participant ModuleTree as Rust module tree
  participant Sources as Rust source files
  participant Allowlist as thread_local_cold_allowlist.json
  PolicyCheck->>ModuleTree: Resolve cfg(test) module descendants
  ModuleTree-->>PolicyCheck: Return test-only files
  PolicyCheck->>Sources: Count shipping raw thread_local! blocks
  Sources-->>PolicyCheck: Return production declarations
  PolicyCheck->>Allowlist: Compare declaration counts and entries
  Allowlist-->>PolicyCheck: Return policy result
Loading

Possibly related PRs

  • PerryTS/perry#7758: Introduced the related thread-local policy checker that this PR extends.
  • PerryTS/perry#7317: Added the GC scheduling code whose thread-local declarations this PR updates.
  • PerryTS/perry#7226: Modified related thread-local handling in crates/perry-runtime.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the policy-gate fix and exclusion of test-only declarations.
Description check ✅ Passed The description provides detailed scope, rationale, related issue, validation results, and remaining maintainer action.
Linked Issues check ✅ Passed The PR addresses [#7814] by removing the stale entry, fixing live declarations, and restoring a passing policy check.
Out of Scope Changes check ✅ Passed The runtime, scanner, allowlist, self-tests, and changelog changes all support the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7814-thread-local-policy

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

cargo test -p perry-runtime -- --test-threads=1: 2051 passed, 0 failed, 4 ignored (exit 0). cargo build -p perry-runtime clean.

$ python3 scripts/check_thread_locals.py --self-test
self-test: the checker can fail in all six directions
$ python3 scripts/check_thread_locals.py; echo "exit=$?"
thread-local policy OK: 160 hot declarations, 129 raw blocks in 91 recorded cold files, capacity 768
exit=0

@proggeramlug
proggeramlug marked this pull request as ready for review August 11, 2026 01:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@scripts/check_thread_locals.py`:
- Around line 129-142: The brace_span scanner must ignore braces inside Rust
comments and literals when finding block boundaries. Replace its
character-by-character matching with Rust-aware lexical scanning while
preserving the returned opening and closing offsets, and add a --self-test case
covering braces in a string or comment so declarations outside the intended
block are classified correctly.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ddcfcafd-a5da-43d5-934e-0ce6aaa48287

📥 Commits

Reviewing files that changed from the base of the PR and between 1804991 and 00f41da.

📒 Files selected for processing (4)
  • changelog.d/7824-thread-local-policy-gate.md
  • crates/perry-runtime/src/gc/schedule.rs
  • scripts/check_thread_locals.py
  • scripts/thread_local_cold_allowlist.json

Comment on lines +129 to +142
def brace_span(src: str, start: int) -> tuple[int, int]:
"""`(open, close)` offsets of the brace-matched block opening at/after `start`."""
i = src.index("{", start)
depth = 0
j = i
while j < len(src):
if src[j] == "{":
depth += 1
elif src[j] == "}":
depth -= 1
if depth == 0:
break
j += 1
return i, j

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Parse Rust block boundaries without counting braces in literals or comments.

brace_span treats every { and } as syntax. A } in a string before a test-only thread_local! ends the inline module span early. The scanner then counts a declaration that cannot ship. A { can also extend the span and hide a shipping declaration.

Use a Rust-aware lexical scan that ignores comments and literals before matching braces. Add a --self-test case with braces inside a string or comment.

Also applies to: 191-201, 363-395

🤖 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/check_thread_locals.py` around lines 129 - 142, The brace_span
scanner must ignore braces inside Rust comments and literals when finding block
boundaries. Replace its character-by-character matching with Rust-aware lexical
scanning while preserving the returned opening and closing offsets, and add a
--self-test case covering braces in a string or comment so declarations outside
the intended block are classified correctly.

@proggeramlug
proggeramlug merged commit 9f0fc52 into main Aug 11, 2026
1 of 18 checks passed
@proggeramlug
proggeramlug deleted the fix/7814-thread-local-policy branch August 11, 2026 05:26
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.

check_thread_locals.py is red on main (4 raw thread_local! in hot paths + a stale gc/zeal.rs entry), blocking tls-budget's promotion to a required gate

1 participant