Skip to content

fix(dotenv): register dotenv.parse so it stops compiling to a swallowed runtime throw - #7199

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
jdalton:fix/dotenv-parse-manifest-wiring
Aug 1, 2026
Merged

fix(dotenv): register dotenv.parse so it stops compiling to a swallowed runtime throw#7199
proggeramlug merged 1 commit into
PerryTS:mainfrom
jdalton:fix/dotenv-parse-manifest-wiring

Conversation

@jdalton

@jdalton jdalton commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Problem

dotenv.parse(...) compiles to a deferred throw-on-reach runtime error, even though the native implementation has always been there and linked into every binary.

js_dotenv_parse is defined and exported by both crates/perry-stdlib/src/dotenv.rs and the bundled well-known binding crates/perry-ext-dotenv/src/lib.rs, and it is declared to codegen in runtime_decls/stdlib_ffi/utilities.rs. What was missing was the last two links in the chain: the API manifest only ever registered dotenv.config, and NATIVE_MODULE_TABLE only ever had a config dispatch row. So the #463 unimplemented-API gate fired on every dotenv.parse call site and — under the default (defer) policy — compiled it to a value that throws only if reached.

Before:

$ perry repro.ts -o repro && ./repro
notice: 1 ahead-of-time-unsupported site handled at runtime:
  - unimplemented API   repro.ts:3   → deferred runtime error (throws only if reached)
Error: dotenv.parse is not implemented in Perry (ahead-of-time) (repro.ts:3)

Why this is a data-loss bug, not a missing feature

The usual shape for loading a config file is a try/catch around the parse, because a malformed .env is not supposed to be fatal. Socket Firewall's readConfigFile() is exactly that:

function readConfigFile(p: string) {
  try {
    return dotenv.parse(fs.readFileSync(p, 'utf8'))
  } catch {
    return {}
  }
}

The deferred throw lands inside that catch, so it is swallowed. The program does not crash, does not warn, and does not print a deferred-site notice at runtime — it simply runs as though the user had no .env file at all. Every setting in it is silently dropped. A user debugging "why is my config being ignored" has nothing to go on.

Fix

Two rows, no new runtime code.

  • crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs — add the dotenv.parse dispatch row: args: &[NA_STR], ret: NR_OBJ_FROM_JSON_STR.
  • crates/perry-api-manifest/src/entries/part_1.rs — register dotenv.parse(src: string): any.
  • docs/api/perry.d.ts, docs/src/api/reference.md — regenerated from the manifest.

The return kind is the load-bearing detail. js_dotenv_parse hands back a JSON string, not an object. NR_OBJ_FROM_JSON_STR is the existing return kind (used by jsonwebtoken.decode) that pipes the pointer through js_json_parse, so what TypeScript actually receives is a real object. With a plain NR_STR the call would have compiled and "worked" while returning the string {"FOO":"bar"}, and parsed.FOO would have read undefined — a quieter version of the same bug. The test asserts the return kind for that reason.

Verification

End-to-end against a perry-dev build:

import dotenv from 'dotenv';
const parsed = dotenv.parse('FOO=bar\n# comment\nBAZ="qux"\nNUM=42\n');
console.log(typeof parsed);                            // object
console.log(parsed.FOO, parsed.BAZ, parsed.NUM);       // bar qux 42
console.log(Object.keys(parsed).sort().join(','));     // BAZ,FOO,NUM

function readConfigFile(text: string) {
  try { return dotenv.parse(text) as Record<string, string> } catch { return {} }
}
console.log(readConfigFile('API_TOKEN=secret\n').API_TOKEN);  // secret

import { parse } from 'dotenv';
console.log(parse('A=1\n').A);                         // 1
console.log(typeof dotenv.parse(''), Object.keys(dotenv.parse('')).length);  // object 0

Compiles with zero deferred-site notices and exits 0 with the output above. Both the default-import (dotenv.parse) and named-import (import { parse }) forms route through the new row. Empty input returns {}, not null.

Tests

Both are #[cfg(test)] unit tests in src/, so they run in the per-PR cargo-test gate (--lib --bins) rather than only in the nightly integration tier.

  • perry-api-manifestdotenv_parse_is_registered: asserts the symbol exists, is a static module method, and takes one String parameter.
  • perry-codegendotenv_parse_dispatches_to_native_impl_as_an_object: asserts the dispatch row points at js_dotenv_parse, takes NA_STR, and returns NR_OBJ_FROM_JSON_STR.

cargo test --lib -p perry-api-manifest (35 passed) and cargo test --lib -p perry-codegen (515 passed) are green, as is cargo test -p perry-codegen --test manifest_consistency (the #512 drift gate that keeps the two tables in sync).

Note on the version bump

This is one of three independent PRs I am opening off the same main. Each bumps [workspace.package] version to 0.5.1278 per the CLAUDE.md flow, so whichever lands second and third will need the version line rebased.

Summary by CodeRabbit

  • New Features
    • Added dotenv.parse(src) to parse dotenv-formatted text into an object.
    • Supports both default and named imports.
    • Handles empty input and deferred notices correctly.
  • Documentation
    • Updated API documentation and reference listings to include dotenv.parse.
  • Chores
    • Updated the project version to 0.5.1278.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 57faa730-3086-4cb2-9441-4f0560a64d90

📥 Commits

Reviewing files that changed from the base of the PR and between eeb8ce4 and 23d1dd3.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/7199-dotenv-parse-manifest-wiring.md
  • crates/perry-api-manifest/src/entries/part_1.rs
  • crates/perry-api-manifest/src/lib.rs
  • crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs
  • docs/api/perry.d.ts
  • docs/src/api/reference.md

📝 Walkthrough

Walkthrough

dotenv.parse(src) is registered in the API manifest, routed through native dispatch, and converted from a JSON string to an object. Tests validate the manifest and dispatch metadata. Generated API references, changelog data, and version metadata are updated.

Changes

dotenv.parse API

Layer / File(s) Summary
Manifest contract and registration
crates/perry-api-manifest/src/entries/part_1.rs, crates/perry-api-manifest/src/lib.rs
Adds dotenv.parse(src: string): any to the manifest and validates its registration, parameter count, and parameter type.
Native dispatch and decoding
crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs
Routes dotenv.parse to js_dotenv_parse, passes the string argument, and converts the JSON-string result to an object. Tests validate the dispatch metadata and return type.
API publication and version metadata
docs/api/perry.d.ts, docs/src/api/reference.md, changelog.d/7199-dotenv-parse-manifest-wiring.md, Cargo.toml, CLAUDE.md
Updates generated API declarations, API counts, changelog content, and the workspace and documented versions to 0.5.1278.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Manifest as API manifest
  participant Dispatch as Native dispatch table
  participant Parser as js_dotenv_parse
  participant Runtime as Object runtime
  Caller->>Manifest: resolve dotenv.parse(src)
  Manifest-->>Dispatch: provide native method contract
  Dispatch->>Parser: pass src string
  Parser-->>Dispatch: return JSON string
  Dispatch->>Runtime: decode JSON string as object
  Runtime-->>Caller: return parsed object
Loading

Suggested labels: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed and relevant but omits the template headings, related issue, checklist, and required test-plan checklist. Use the repository template headings and checklist, state the related issue or n/a, and address the prohibited version and CLAUDE.md changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: registering dotenv.parse to prevent swallowed runtime errors.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@jdalton
jdalton force-pushed the fix/dotenv-parse-manifest-wiring branch from 08d5bea to 23d1dd3 Compare August 1, 2026 16:30
@proggeramlug
proggeramlug merged commit 97e843f into PerryTS:main Aug 1, 2026
5 of 7 checks passed
jdalton added a commit to jdalton/perry that referenced this pull request Aug 1, 2026
… allowlist fingerprint; restore the workspace version

Follow-ups to the review threads on PerryTS#7196 and PerryTS#7212, both of which merged
before the findings were worked through.

Cargo.toml — PerryTS#7196 set `[workspace.package].version` back to 0.5.1277,
undoing the 0.5.1278 bump PerryTS#7199 had landed four commits earlier. Nothing
since has touched the line, so main is currently shipping a version number
it already released. Restored.

docs/src/internals/gc-rooting-invariant.md — the checker description read
"reports any root store that does not dominate a preceding collection
point", which is vacuous: a store can never dominate anything that precedes
it, so the sentence is true of every store in the program. What the checker
actually reports is the collection point — `window_hits(origin, bind)`
collects the calls that can run between the instruction producing a GC value
and the `js_shadow_slot_bind` that publishes it. Reworded to name the
collection point as the reported object and to keep the true relation (the
root store must dominate it), which is the rule stated at the top of the same
page. Also noted that the gate command shown does not pass
`--stale-registers`, so cases 3 and 4 only surface when it is run by hand.

CLAUDE.md — the shape summary claimed all three failures present as "a
rooted slot holding a dangling pointer", contradicting its own two preceding
clauses: PerryTS#7184's bind is a silent no-op so nothing is bound, and the
`alloca_entry` shape is never a slot at all. Split per shape. It also said
the class "only bites under PERRY_GC_MOVING_LOOP_POLLS=1"; the checker's
own POLL_CAPABLE_RUNTIME set treats js_object_set_field_by_name,
js_object_get_property and js_call_function as moving-capable with no poll
involved, and PerryTS#7211's allowlist entry is exactly such a window. Polls widen
in-loop coverage; they are not a precondition.

docs/src/internals/rfc-rooting-by-construction.md — `Plain(String)` cannot
be `Copy`; the migration section costed it as if it were. It is `Clone`,
which still imposes nothing on the caller. Added the emitter/frame branding
gap to "What it cannot catch": PhantomData<&'e Emitter> records a lifetime,
not an instance, and `Rooted` carries a bare SlotIdx, so the design as
written catches ordering mistakes but not provenance ones.

scripts/gc_root_dominance_allowlist.json — the fingerprint format line said
"<first collector>". It is `sorted(set(callees))[0]`, the alphabetically
first, not the first in program order. A hand-derived entry that guesses
program order matches nothing, and an entry that matches nothing fails the
build — so the misleading line pointed straight at a red gate.

scripts/gc_root_dominance_check.py, three surgical changes:
  * the self-test failure text described sinking the root store; `_mutate`
    splices _SEED_CALL above the store and moves nothing.
  * `--stale-registers` returned before the `--unrooted-allocas` block, so
    passing both ran one pass and silently skipped the other. Now an
    argparse error, matching the --max-stale and --fatal-sinks guards
    directly above it.
  * the stale-allowlist-entry report returned 2 before the uncovered-
    violation report could run. Fix one violation and introduce another in
    the same PR and the log showed only the bookkeeping problem. Both
    reports now print; the exit code is unchanged (2 when an entry is
    stale, 1 when only uncovered violations remain).

Verified: `--self-test` OK. Against the parent, a corpus with one stale
entry and two uncovered violations printed only the stale entry and hid
both violations; it now prints all three and still exits 2.

Refs PerryTS#7196, PerryTS#7212, PerryTS#7199, PerryTS#7211.
jdalton added a commit to jdalton/perry that referenced this pull request Aug 1, 2026
… allowlist fingerprint; restore the workspace version

Follow-ups to the review threads on PerryTS#7196 and PerryTS#7212, both of which merged
before the findings were worked through.

Cargo.toml — PerryTS#7196 set `[workspace.package].version` back to 0.5.1277,
undoing the 0.5.1278 bump PerryTS#7199 had landed four commits earlier. Nothing
since has touched the line, so main is currently shipping a version number
it already released. Restored.

docs/src/internals/gc-rooting-invariant.md — the checker description read
"reports any root store that does not dominate a preceding collection
point", which is vacuous: a store can never dominate anything that precedes
it, so the sentence is true of every store in the program. What the checker
actually reports is the collection point — `window_hits(origin, bind)`
collects the calls that can run between the instruction producing a GC value
and the `js_shadow_slot_bind` that publishes it. Reworded to name the
collection point as the reported object and to keep the true relation (the
root store must dominate it), which is the rule stated at the top of the same
page. Also noted that the gate command shown does not pass
`--stale-registers`, so cases 3 and 4 only surface when it is run by hand.

CLAUDE.md — the shape summary claimed all three failures present as "a
rooted slot holding a dangling pointer", contradicting its own two preceding
clauses: PerryTS#7184's bind is a silent no-op so nothing is bound, and the
`alloca_entry` shape is never a slot at all. Split per shape. It also said
the class "only bites under PERRY_GC_MOVING_LOOP_POLLS=1"; the checker's
own POLL_CAPABLE_RUNTIME set treats js_object_set_field_by_name,
js_object_get_property and js_call_function as moving-capable with no poll
involved, and PerryTS#7211's allowlist entry is exactly such a window. Polls widen
in-loop coverage; they are not a precondition.

docs/src/internals/rfc-rooting-by-construction.md — `Plain(String)` cannot
be `Copy`; the migration section costed it as if it were. It is `Clone`,
which still imposes nothing on the caller. Added the emitter/frame branding
gap to "What it cannot catch": PhantomData<&'e Emitter> records a lifetime,
not an instance, and `Rooted` carries a bare SlotIdx, so the design as
written catches ordering mistakes but not provenance ones.

scripts/gc_root_dominance_allowlist.json — the fingerprint format line said
"<first collector>". It is `sorted(set(callees))[0]`, the alphabetically
first, not the first in program order. A hand-derived entry that guesses
program order matches nothing, and an entry that matches nothing fails the
build — so the misleading line pointed straight at a red gate.

scripts/gc_root_dominance_check.py, three surgical changes:
  * the self-test failure text described sinking the root store; `_mutate`
    splices _SEED_CALL above the store and moves nothing.
  * `--stale-registers` returned before the `--unrooted-allocas` block, so
    passing both ran one pass and silently skipped the other. Now an
    argparse error, matching the --max-stale and --fatal-sinks guards
    directly above it.
  * the stale-allowlist-entry report returned 2 before the uncovered-
    violation report could run. Fix one violation and introduce another in
    the same PR and the log showed only the bookkeeping problem. Both
    reports now print; the exit code is unchanged (2 when an entry is
    stale, 1 when only uncovered violations remain).

Verified: `--self-test` OK. Against the parent, a corpus with one stale
entry and two uncovered violations printed only the stale entry and hid
both violations; it now prints all three and still exits 2.

Refs PerryTS#7196, PerryTS#7212, PerryTS#7199, PerryTS#7211.
jdalton added a commit to jdalton/perry that referenced this pull request Aug 2, 2026
… allowlist fingerprint; restore the workspace version

Follow-ups to the review threads on PerryTS#7196 and PerryTS#7212, both of which merged
before the findings were worked through.

Cargo.toml — PerryTS#7196 set `[workspace.package].version` back to 0.5.1277,
undoing the 0.5.1278 bump PerryTS#7199 had landed four commits earlier. Nothing
since has touched the line, so main is currently shipping a version number
it already released. Restored.

docs/src/internals/gc-rooting-invariant.md — the checker description read
"reports any root store that does not dominate a preceding collection
point", which is vacuous: a store can never dominate anything that precedes
it, so the sentence is true of every store in the program. What the checker
actually reports is the collection point — `window_hits(origin, bind)`
collects the calls that can run between the instruction producing a GC value
and the `js_shadow_slot_bind` that publishes it. Reworded to name the
collection point as the reported object and to keep the true relation (the
root store must dominate it), which is the rule stated at the top of the same
page. Also noted that the gate command shown does not pass
`--stale-registers`, so cases 3 and 4 only surface when it is run by hand.

CLAUDE.md — the shape summary claimed all three failures present as "a
rooted slot holding a dangling pointer", contradicting its own two preceding
clauses: PerryTS#7184's bind is a silent no-op so nothing is bound, and the
`alloca_entry` shape is never a slot at all. Split per shape. It also said
the class "only bites under PERRY_GC_MOVING_LOOP_POLLS=1"; the checker's
own POLL_CAPABLE_RUNTIME set treats js_object_set_field_by_name,
js_object_get_property and js_call_function as moving-capable with no poll
involved, and PerryTS#7211's allowlist entry is exactly such a window. Polls widen
in-loop coverage; they are not a precondition.

docs/src/internals/rfc-rooting-by-construction.md — `Plain(String)` cannot
be `Copy`; the migration section costed it as if it were. It is `Clone`,
which still imposes nothing on the caller. Added the emitter/frame branding
gap to "What it cannot catch": PhantomData<&'e Emitter> records a lifetime,
not an instance, and `Rooted` carries a bare SlotIdx, so the design as
written catches ordering mistakes but not provenance ones.

scripts/gc_root_dominance_allowlist.json — the fingerprint format line said
"<first collector>". It is `sorted(set(callees))[0]`, the alphabetically
first, not the first in program order. A hand-derived entry that guesses
program order matches nothing, and an entry that matches nothing fails the
build — so the misleading line pointed straight at a red gate.

scripts/gc_root_dominance_check.py, three surgical changes:
  * the self-test failure text described sinking the root store; `_mutate`
    splices _SEED_CALL above the store and moves nothing.
  * `--stale-registers` returned before the `--unrooted-allocas` block, so
    passing both ran one pass and silently skipped the other. Now an
    argparse error, matching the --max-stale and --fatal-sinks guards
    directly above it.
  * the stale-allowlist-entry report returned 2 before the uncovered-
    violation report could run. Fix one violation and introduce another in
    the same PR and the log showed only the bookkeeping problem. Both
    reports now print; the exit code is unchanged (2 when an entry is
    stale, 1 when only uncovered violations remain).

Verified: `--self-test` OK. Against the parent, a corpus with one stale
entry and two uncovered violations printed only the stale entry and hid
both violations; it now prints all three and still exits 2.

Refs PerryTS#7196, PerryTS#7212, PerryTS#7199, PerryTS#7211.
jdalton added a commit to jdalton/perry that referenced this pull request Aug 2, 2026
… allowlist fingerprint; restore the workspace version

Follow-ups to the review threads on PerryTS#7196 and PerryTS#7212, both of which merged
before the findings were worked through.

Cargo.toml — PerryTS#7196 set `[workspace.package].version` back to 0.5.1277,
undoing the 0.5.1278 bump PerryTS#7199 had landed four commits earlier. Nothing
since has touched the line, so main is currently shipping a version number
it already released. Restored.

docs/src/internals/gc-rooting-invariant.md — the checker description read
"reports any root store that does not dominate a preceding collection
point", which is vacuous: a store can never dominate anything that precedes
it, so the sentence is true of every store in the program. What the checker
actually reports is the collection point — `window_hits(origin, bind)`
collects the calls that can run between the instruction producing a GC value
and the `js_shadow_slot_bind` that publishes it. Reworded to name the
collection point as the reported object and to keep the true relation (the
root store must dominate it), which is the rule stated at the top of the same
page. Also noted that the gate command shown does not pass
`--stale-registers`, so cases 3 and 4 only surface when it is run by hand.

CLAUDE.md — the shape summary claimed all three failures present as "a
rooted slot holding a dangling pointer", contradicting its own two preceding
clauses: PerryTS#7184's bind is a silent no-op so nothing is bound, and the
`alloca_entry` shape is never a slot at all. Split per shape. It also said
the class "only bites under PERRY_GC_MOVING_LOOP_POLLS=1"; the checker's
own POLL_CAPABLE_RUNTIME set treats js_object_set_field_by_name,
js_object_get_property and js_call_function as moving-capable with no poll
involved, and PerryTS#7211's allowlist entry is exactly such a window. Polls widen
in-loop coverage; they are not a precondition.

docs/src/internals/rfc-rooting-by-construction.md — `Plain(String)` cannot
be `Copy`; the migration section costed it as if it were. It is `Clone`,
which still imposes nothing on the caller. Added the emitter/frame branding
gap to "What it cannot catch": PhantomData<&'e Emitter> records a lifetime,
not an instance, and `Rooted` carries a bare SlotIdx, so the design as
written catches ordering mistakes but not provenance ones.

scripts/gc_root_dominance_allowlist.json — the fingerprint format line said
"<first collector>". It is `sorted(set(callees))[0]`, the alphabetically
first, not the first in program order. A hand-derived entry that guesses
program order matches nothing, and an entry that matches nothing fails the
build — so the misleading line pointed straight at a red gate.

scripts/gc_root_dominance_check.py, three surgical changes:
  * the self-test failure text described sinking the root store; `_mutate`
    splices _SEED_CALL above the store and moves nothing.
  * `--stale-registers` returned before the `--unrooted-allocas` block, so
    passing both ran one pass and silently skipped the other. Now an
    argparse error, matching the --max-stale and --fatal-sinks guards
    directly above it.
  * the stale-allowlist-entry report returned 2 before the uncovered-
    violation report could run. Fix one violation and introduce another in
    the same PR and the log showed only the bookkeeping problem. Both
    reports now print; the exit code is unchanged (2 when an entry is
    stale, 1 when only uncovered violations remain).

Verified: `--self-test` OK. Against the parent, a corpus with one stale
entry and two uncovered violations printed only the stale entry and hid
both violations; it now prints all three and still exits 2.

Refs PerryTS#7196, PerryTS#7212, PerryTS#7199, PerryTS#7211.
proggeramlug pushed a commit that referenced this pull request Aug 2, 2026
… allowlist fingerprint; restore the workspace version (#7224)

Follow-ups to the review threads on #7196 and #7212, both of which merged
before the findings were worked through.

Cargo.toml — #7196 set `[workspace.package].version` back to 0.5.1277,
undoing the 0.5.1278 bump #7199 had landed four commits earlier. Nothing
since has touched the line, so main is currently shipping a version number
it already released. Restored.

docs/src/internals/gc-rooting-invariant.md — the checker description read
"reports any root store that does not dominate a preceding collection
point", which is vacuous: a store can never dominate anything that precedes
it, so the sentence is true of every store in the program. What the checker
actually reports is the collection point — `window_hits(origin, bind)`
collects the calls that can run between the instruction producing a GC value
and the `js_shadow_slot_bind` that publishes it. Reworded to name the
collection point as the reported object and to keep the true relation (the
root store must dominate it), which is the rule stated at the top of the same
page. Also noted that the gate command shown does not pass
`--stale-registers`, so cases 3 and 4 only surface when it is run by hand.

CLAUDE.md — the shape summary claimed all three failures present as "a
rooted slot holding a dangling pointer", contradicting its own two preceding
clauses: #7184's bind is a silent no-op so nothing is bound, and the
`alloca_entry` shape is never a slot at all. Split per shape. It also said
the class "only bites under PERRY_GC_MOVING_LOOP_POLLS=1"; the checker's
own POLL_CAPABLE_RUNTIME set treats js_object_set_field_by_name,
js_object_get_property and js_call_function as moving-capable with no poll
involved, and #7211's allowlist entry is exactly such a window. Polls widen
in-loop coverage; they are not a precondition.

docs/src/internals/rfc-rooting-by-construction.md — `Plain(String)` cannot
be `Copy`; the migration section costed it as if it were. It is `Clone`,
which still imposes nothing on the caller. Added the emitter/frame branding
gap to "What it cannot catch": PhantomData<&'e Emitter> records a lifetime,
not an instance, and `Rooted` carries a bare SlotIdx, so the design as
written catches ordering mistakes but not provenance ones.

scripts/gc_root_dominance_allowlist.json — the fingerprint format line said
"<first collector>". It is `sorted(set(callees))[0]`, the alphabetically
first, not the first in program order. A hand-derived entry that guesses
program order matches nothing, and an entry that matches nothing fails the
build — so the misleading line pointed straight at a red gate.

scripts/gc_root_dominance_check.py, three surgical changes:
  * the self-test failure text described sinking the root store; `_mutate`
    splices _SEED_CALL above the store and moves nothing.
  * `--stale-registers` returned before the `--unrooted-allocas` block, so
    passing both ran one pass and silently skipped the other. Now an
    argparse error, matching the --max-stale and --fatal-sinks guards
    directly above it.
  * the stale-allowlist-entry report returned 2 before the uncovered-
    violation report could run. Fix one violation and introduce another in
    the same PR and the log showed only the bookkeeping problem. Both
    reports now print; the exit code is unchanged (2 when an entry is
    stale, 1 when only uncovered violations remain).

Verified: `--self-test` OK. Against the parent, a corpus with one stale
entry and two uncovered violations printed only the stale entry and hid
both violations; it now prints all three and still exits 2.

Refs #7196, #7212, #7199, #7211.
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.

2 participants