fix(dotenv): register dotenv.parse so it stops compiling to a swallowed runtime throw - #7199
Merged
proggeramlug merged 1 commit intoAug 1, 2026
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (8)
📝 WalkthroughWalkthrough
Changesdotenv.parse API
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
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…ferring to a runtime throw
jdalton
force-pushed
the
fix/dotenv-parse-manifest-wiring
branch
from
August 1, 2026 16:30
08d5bea to
23d1dd3
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_parseis defined and exported by bothcrates/perry-stdlib/src/dotenv.rsand the bundled well-known bindingcrates/perry-ext-dotenv/src/lib.rs, and it is declared to codegen inruntime_decls/stdlib_ffi/utilities.rs. What was missing was the last two links in the chain: the API manifest only ever registereddotenv.config, andNATIVE_MODULE_TABLEonly ever had aconfigdispatch row. So the #463 unimplemented-API gate fired on everydotenv.parsecall site and — under the default (defer) policy — compiled it to a value that throws only if reached.Before:
Why this is a data-loss bug, not a missing feature
The usual shape for loading a config file is a
try/catcharound the parse, because a malformed.envis not supposed to be fatal. Socket Firewall'sreadConfigFile()is exactly that: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.envfile 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 thedotenv.parsedispatch row:args: &[NA_STR],ret: NR_OBJ_FROM_JSON_STR.crates/perry-api-manifest/src/entries/part_1.rs— registerdotenv.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_parsehands back a JSON string, not an object.NR_OBJ_FROM_JSON_STRis the existing return kind (used byjsonwebtoken.decode) that pipes the pointer throughjs_json_parse, so what TypeScript actually receives is a real object. With a plainNR_STRthe call would have compiled and "worked" while returning the string{"FOO":"bar"}, andparsed.FOOwould have readundefined— a quieter version of the same bug. The test asserts the return kind for that reason.Verification
End-to-end against a
perry-devbuild: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{}, notnull.Tests
Both are
#[cfg(test)]unit tests insrc/, so they run in the per-PRcargo-testgate (--lib --bins) rather than only in the nightly integration tier.perry-api-manifest—dotenv_parse_is_registered: asserts the symbol exists, is a static module method, and takes oneStringparameter.perry-codegen—dotenv_parse_dispatches_to_native_impl_as_an_object: asserts the dispatch row points atjs_dotenv_parse, takesNA_STR, and returnsNR_OBJ_FROM_JSON_STR.cargo test --lib -p perry-api-manifest(35 passed) andcargo test --lib -p perry-codegen(515 passed) are green, as iscargo 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] versionto0.5.1278per the CLAUDE.md flow, so whichever lands second and third will need the version line rebased.Summary by CodeRabbit
dotenv.parse(src)to parse dotenv-formatted text into an object.dotenv.parse.