Skip to content

chore: merge train 227 (v0.5.1606) - #10763

Merged
proggeramlug merged 8 commits into
mainfrom
train227r
Sep 19, 2026
Merged

proggeramlug merged 8 commits into
mainfrom
train227r

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Merge train 227 — two PRs validated together as one tree, released as v0.5.1606.

Trains land as their own PR, so the source PRs are closed, not merged, and their close-keywords never fire.

Contents

PR Change
#10619 perf(runtime): give the shadow-stack root state the hot-TLS fast path, cfg-split to Darwin aarch64 (−6.3% try entry)
#10691 refactor(stdlib): remove the dotenv native binding, compile the real package from source

#10619 — held, then cleared on evidence

This PR originally caused a Linux-only debug-profile SIGSEGV in cargo-test (#10709). It was held until re-scoped: the hot-TLS swap is now cfg-split to Darwin aarch64, and the exact CI job that was red is green.

The mechanism is still unknown and #10709 tracks that. What justifies landing anyway:

  • The split is correct on its own merits. tls_hot.rs states the published-cache shortcut is Darwin-specific, and that elsewhere "resolving a thread-local is already a fixed offset and the extra cache indirection has no demonstrated benefit". Off-Darwin the swap cost a TLS access plus a slot indirection where a raw thread_local! is one fixed-offset access — it was a pessimization on those targets even without the crash.
  • The crashing configuration no longer compiles, which is what CLAUDE.md's GC-knob kill-policy asks for: the losing mode should stop compiling rather than linger as an untested configuration.
  • The fault is confined, not hidden. 164 files already use perry_thread_local!; a general defect in that path would have CI red across the board. It isn't. The fault is specific to SHADOW's usage, so gating narrows the reachable surface rather than concealing a shared bug.

The win was re-measured honestly and the title corrected from −8%: 162.2 → 152.0 instructions per try entry (−6.3%), with a bare-loop control reading ~1.0 in both arms, against a base built from the PR's own parent.

Counts re-derived on the assembled tree

workspace_architecture.py --check on this tree: 77 members / externalize=28 / keep=44, re-derived here rather than taken from the PR. For the record, because it caused a real error upstream: main is 78/29/44 — the 77/28/44 figure is this train's assembled value, and confusing the two sent a downstream rebase off by one crate before its own script caught it.

Also green on the assembled tree: ledger 376 rows / 326 providers, unrooted_local_shape at 578, zero perry-ext-dotenv entries in the regenerated lock.

Validation

Assembled on 91a566c8af; source heads asserted unchanged since assembly. Both PRs proven fully represented by patch-id and by subject+author — zero dropped.

All nine cheap gates, cargo check --workspace --all-targets under -D warnings, the release build of all five pinned artifacts, every unit suite, and a 7-area gap sweep (try, catch, error, throw, require, cjs, module) with zero unexplained regressions and every area asserted to have run a non-zero number of tests. Sweep run with PERRY_RUN_TIMEOUT=30 rather than the harness default of 10s, so a loaded host cannot turn a pass into a timeout the harness would classify as a failure. lint completed its full 6-of-6 compile tier with nothing outside the known-red public-baseline step.

Follow-up this train creates

shipped_unproven_bindings_are_partial in well_known.rs iterates ["dotenv", "nanoid"]. With dotenv gone it reads ["nanoid"], and the pending nanoid removal would take it to [] — a test that iterates nothing, asserts nothing and reports green forever. That PR deletes the test rather than emptying it; its subject population (uuid, dotenv, nanoid) is now empty, and shipped_subset_bindings_are_partial still covers undici, node-forge, lru-cache and qs.

Ralph Küpper and others added 8 commits September 19, 2026 23:06
…path

Every non-throwing `try` entry paid a real out-of-line `_tlv_get_addr`
call, unconditionally, that nothing else in `try_push_with_kind` did.

Profiling `try { t += v; } catch { t = 0; }` (differenced inside each
binary: `(try80-try16)/64` vs `(loop80-loop16)/64` control, per
CLAUDE.md's verification method, N=20000, median of 7) confirmed the
178-instruction/entry baseline and pinned the cost to one leaf via
`sample`+`PERRY_KEEP_SYMBOLS=1` at 20M iterations:
`try_push_with_kind`+176 -> `_tlv_get_addr`, ~6% of instructions
retired per entry.

Root cause: `CatchSavepoint::capture()`'s shadow field is captured
unconditionally on every entry (`shadow_stack_savepoint`), and once any
function anywhere in the process has pushed a shadow frame — true
almost immediately for any real program, since a `catch (e)` binding
itself needs one — the `SHADOW_FRAMES` latch from #7469's wave-1 fix is
already set, so the latch stops making this read rare in practice.
Unlike `EXCEPTION_STATE`, `CALL_METHOD_DEPTH`, and the named
`runtime_handle_stack`/`temp_roots` fields, `SHADOW` was still a raw
`thread_local!`, so this one read never got the `tls_hot` fast path at
all.

Fix: move `SHADOW` (crates/perry-runtime/src/gc/roots/shadow_stack.rs)
from `thread_local!` to `crate::perry_thread_local!` — a pure
storage-mechanism swap (same type, same `.with()`/`.try_with()` call
sites, same const-init/drop-free semantics: `ShadowStackState` still
has no `Drop`, so no destructor is registered) that changes nothing
about liveness, throw-time restore, or the fixed-address contract
`js_shadow_frame_enter` depends on for its whole-activation pointer
cache. Confirmed via `--trace llvm`/`sample` that the leaf disappears
entirely post-change.

Measured (mybase = own build at this commit's parent, 9df5075,
codegen-units=16; both PERRY_NO_AUTO_OPTIMIZE=1):
  before: try 179.0-179.4 instr/entry, control -0.001..-0.2
  after:  try 164.3-165.7 instr/entry, control -0.14..+0.44
  -> ~14-15 instructions/entry (~8%), controls stay noise-floor both arms.

`try_push_with_kind`'s remaining `CatchSavepoint::capture()` work and
`js_try_end`'s own (already tls_hot-fast) EXCEPTION_STATE resolution
are UNTOUCHED — js_try_end disassembles to ~28 near-minimal
instructions with nothing left to cut without threading a pointer
through codegen from push to end, which was evaluated and deferred as
materially riskier (touches try_stmt.rs's early-exit/closure/
generator/async call sites) for a smaller remaining win.

scripts/thread_local_cold_allowlist.json: only the
`shadow_stack.rs: 2 -> 1` line this change causes. `_hot_declarations`
is left untouched — verify() never reads it (only `files` is
enforced) — because a plain `--update` on unmodified main already
produces 466, not the committed 460: six pre-existing, unrelated
`perry_thread_local!` additions had drifted the count before this
change touched anything. Fixing that drift is not this PR's job.

Correctness: the shadow stack is the GC's precise root set, not pure
exception bookkeeping, so this was checked past "it compiles":
- Added test-files/test_gap_try_entry_shadow_hot_tls.ts (byte-identical
  to node v26.5.1 --experimental-strip-types --no-warnings, checked
  against both this change and the pristine parent arm): same-level
  catch, a throw 4 frames deep, a throw crossing Array.prototype.map's
  runtime trampoline, finally on both the normal and throwing paths,
  nested try with an inner rethrow, a catch that itself throws, 25
  levels of try/finally nesting with finally-order and try_depth
  restored checked afterward, and a non-throwing control loop. Every
  caught value is read back after GC-pressure allocation.
- Ran that fixture, plus the pre-existing
  test_gap_gc_catch_param_rooting.ts, test_gap_try_savepoint_subsystems.ts,
  test_gap_try_finally_no_catch_rethrow.ts and test_gap_try_setjmp_volatile.ts,
  under PERRY_GC_SCHEDULE_SEED (5 seeds) + PERRY_GC_SCHEDULE_RATE=1 +
  PERRY_GC_SCHEDULE_ALLOC_KB=0 + PERRY_GC_PROTECT_FROMSPACE=1 +
  PERRY_GC_DIAG=1 (per CLAUDE.md's rooting-bug instruments) — a
  collect-at-every-safepoint, evacuating, quarantine-and-mprotect
  schedule. All 5 seeds matched node byte-for-byte; the diagnostic
  output confirms the instrument was live, not vacuous
  (forced_collections=2566, copying_minors=2566, moved_objects=37000,
  fromspace retired_set up to #4 with bytes_protected growing) — no
  stale-shadow-stack SIGSEGV, no output drift.
- `RUST_TEST_THREADS=1 cargo test --release -p perry-runtime --lib`:
  4010 passed. The 2 failures
  (gc::tests::copy_slot_decode::sabotaged_remembering_arm_is_refused_by_the_coverage_cross_check,
  gc::tests::heap_generation::a_free_or_move_outside_every_scope_is_caught_in_debug_builds)
  are pre-existing: reverted this change with `git checkout --` and
  reran just those two on the pristine parent commit — identical
  failures, same messages, confirming they assert debug_assert!-gated
  behavior compiled out under --release, unrelated to this change.
- `cargo fmt --all -- --check`, `scripts/check_file_size.sh`,
  `scripts/check_test_registration.py`,
  `RUSTFLAGS="-D warnings" cargo check -p perry-runtime --all-targets`:
  all clean.
#10619 routed SHADOW through perry_thread_local! unconditionally for an
~8% try-entry win, measured on Darwin. CI's cargo-test job (Linux, debug
profile) SIGSEGV'd on it (run 35374727647, job 105594641738); the crash
was never pinned to a specific test or line (stdout block-buffering ate
the FAILED line), and it did not reproduce on macOS with the Darwin
path, on macOS with the Darwin path forced off, or on a Linux x86_64
VM matching the CI runner's triple through 1968+ of ~4013 tests.

tls_hot.rs's own docs say the hot-cache shortcut only pays for itself
on Darwin aarch64; everywhere else it is strictly more work than the
raw thread_local! it replaced (an extra HOT resolution plus a slot
indirection), win or no SIGSEGV. So SHADOW's declaration is now
cfg-split: perry_thread_local! only under
all(target_vendor = "apple", target_arch = "aarch64", target_pointer_width = "64"),
a plain thread_local! (the pre-#10619 form) everywhere else. This keeps
the measured win where it was measured, removes the pessimization
elsewhere, and removes CI's only exposure to the unconfirmed SIGSEGV
mechanism. The open investigation is tracked in #10709.

Re-verified: debug and release perry-runtime lib tests, GC-stress with
live copying minors and from-space quarantine, the gap fixture
byte-identical to node 26.5.1, and a fresh differential probe showing
the try-entry win survives on Darwin (162.2 -> 152.0 instructions/
entry, ~6.3%, bare-loop control ~1 instruction in both arms).
Pushed the cfg-gated fix and read CI's own cargo-test job on the real
failing runner: green (run 35433215970) where the unconditional
perry_thread_local! swap was red (run 35374727647), same commit
otherwise. That settles causation directly, superseding the local
repro attempts (macOS both arms, qemu Linux) which all came back
clean and were inconclusive on their own -- including a qemu-VM A/B
whose two SIGKILLs were momentarily misread as a reproduced crash
before being traced to an operator pkill -f self-match, not a fault.

Also resolves why cargo-test stayed green on #10644/#10647/#10650/
#10651 against the same base: none of them touch shadow_stack.rs, and
this PR was never merged to main, so their runs never contained the
change at all.

The internal mechanism inside tls_hot.rs's resolution path is still
not understood; #10709 tracks that open half. This commit only updates
the code comment and changelog to say plainly what is now confirmed
versus what remains unknown.
dotenv.parse(Buffer) -- the idiomatic dotenv.parse(fs.readFileSync(...)) --
returned 0 keys, silently. dotenv.config() reported no error but populated
neither result.parsed nor process.env: a silent total no-op. Real npm
dotenv matches Node exactly.

Per #10678 (duplicate extern "C" exports across perry-ext-*/perry-stdlib
pairs), this binding existed twice: crates/perry-ext-dotenv/ (the
governance-tracked crate) and crates/perry-stdlib/src/dotenv.rs (a second,
independent implementation behind the default-on bundled-dotenv feature,
exporting the same js_dotenv_config/js_dotenv_config_path/js_dotenv_parse
symbols).

Removed both, the 2-entry NativeModSig dispatch block + its dedicated
regression test in native_table/utils_crypto.rs, the js_dotenv_* FFI
declarations, the well_known_bindings.toml entry, the "dotenv"/"dotenv/config"
NATIVE_MODULES entries + manifest rows, the bundled-dotenv stdlib feature,
3 Android stubs, and "dotenv" from PERRY_NATIVE_EXTENSION_PACKAGES (that
array makes the module walker skip a node_modules/dotenv/ tree entirely;
with the binding gone, dotenv's real source needs to reach the walker like
any other npm package).

Six tests edited (not deleted) to keep testing their real subject rather
than a removed registry entry: binding_faithfulness.rs's
lookup_preserves_registered_subpaths_before_falling_back kept its
mysql2/promise half, dropped the dotenv/config half; well_known.rs dropped
"dotenv" from shipped_unproven_bindings_are_partial's array (kept
nanoid/uuid, separate PRs), deleted dotenv_is_registered, and retargeted
node_prefix_stripped_on_lookup from "dotenv" to "bcrypt" (generic
node:-prefix-stripping logic, not dotenv-specific); deleted
dotenv_parse_is_registered (api-manifest) and
dotenv_parse_dispatches_to_native_impl_as_an_object (utils_crypto.rs) --
both were regression guards for the exact rows removed above; trimmed the
two dotenv/config-only side-effect-only-module allowlists in
unimplemented_api_check.rs and manifest_consistency.rs to &[].

Also fixed tests/release/packages/next-app-route/provider/stdlib/Cargo.toml
(a standalone workspace with its own Cargo.lock, not a member of the main
workspace, so cargo check --workspace never touches it) which referenced
the now-deleted bundled-dotenv feature.

Regenerated docs/api/perry.d.ts, docs/src/api/reference.md, and
docs/src/native-libraries/governance.md. Updated workspace-architecture.json
(workspace_members 83->82, externalize 33->32).
@proggeramlug
proggeramlug merged commit 7fe8009 into main Sep 19, 2026
24 of 26 checks passed
@proggeramlug
proggeramlug deleted the train227r branch September 19, 2026 22:02
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 3937bf7e-aa71-40f6-88ed-29a5439ecf42

📥 Commits

Reviewing files that changed from the base of the PR and between 91a566c and df21f0d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/10619-shadow-stack-hot-tls.md
  • changelog.d/10691-dotenv-native-binding-removal.md
  • crates/perry-api-manifest/src/entries.rs
  • 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
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/utilities.rs
  • crates/perry-codegen/tests/manifest_consistency.rs
  • crates/perry-ext-dotenv/Cargo.toml
  • crates/perry-ext-dotenv/src/lib.rs
  • crates/perry-hir/tests/unimplemented_api_check.rs
  • crates/perry-runtime/src/gc/roots/shadow_stack.rs
  • crates/perry-stdlib/Cargo.toml
  • crates/perry-stdlib/src/dotenv.rs
  • crates/perry-stdlib/src/lib.rs
  • crates/perry-ui-android/src/stdlib_stubs.rs
  • crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs
  • crates/perry/src/commands/compile/resolve.rs
  • crates/perry/src/commands/compile/well_known.rs
  • crates/perry/src/commands/stdlib_features.rs
  • crates/perry/well_known_bindings.toml
  • docs/api/perry.d.ts
  • docs/src/api/reference.md
  • docs/src/native-libraries/governance.md
  • test-files/test_gap_try_entry_shadow_hot_tls.ts
  • tests/release/packages/next-app-route/provider/stdlib/Cargo.toml
  • workspace-architecture.json
 _______________________________________________________________________________________________________________________________________
< Test your estimates. Mathematical analysis of algorithms doesn't tell you everything. Try timing your code in its target environment. >
 ---------------------------------------------------------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

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