Skip to content

batch: land #8892, #8893, #8894 - #8898

Merged
proggeramlug merged 7 commits into
mainfrom
merge/batch-8892-8894
Aug 27, 2026
Merged

batch: land #8892, #8893, #8894#8898
proggeramlug merged 7 commits into
mainfrom
merge/batch-8892-8894

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Batch landing of three reviewed PRs, validated once as a single merged tree. No fixes needed — all three gate-clean as submitted.

PR
#8892 fix(hir): late-bind new X() to a class declared later in the module
#8893 fix(runtime): make the class registries per image
#8894 perf(codegen): bound TailCallElim's alloca walk

#8891 and #8896 are not in this batch:

Validation (merged tree)

  • all 30 lint-job gates pass
  • perry-runtime 2755, perry-codegen 1328, perry-stdlib 124, perry-hir 350 — all 0 failed
  • df checked before and after; no result produced under ENOSPC

Summary by CodeRabbit

  • Bug Fixes

    • Fixed dynamic constructor calls so constructors declared later or nested resolve correctly.
    • Improved unresolved constructor errors to include the missing identifier and follow standard runtime behavior.
    • Fixed class and method dispatch when multiple application images are loaded.
    • Worker threads now consistently use the correct class definitions from their spawning context.
  • Performance

    • Added safeguards to prevent excessive tail-call optimization work during compilation, while preserving optimized builds where appropriate.

Ralph Küpper and others added 7 commits August 27, 2026 21:55
…erenceError

Coop's Next.js App Route fixture died at module init on 0.5.1519 with
the nameless `ReferenceError: identifier is not defined`. The identifier
is `SentinelNode` in next/dist/server/lib/lru-cache.js: the CJS wrap
hoists `LRUCache` out of the module IIFE but never sees `SentinelNode`
(its doc comment closes on the `class` line, and the textual hoister
anchors on `class ` at column 0), so the hoisted constructor's
`new SentinelNode()` is lowered before the `__perry_cjs_factory` body
registers the class. The unresolved-`new` guard from #8643 (905017b,
inside the 1516..1519 window) turned that lowering-time miss into an
unconditional nameless throw; before it, the by-name `Expr::New` bound
at codegen through the module class table, which is why 0.5.1516 loaded.

- `pre_scan_class_decl_names` records every class DECLARATION name in
  the module at any depth; the guard keeps the late-bound by-name
  construction for those.
- Any other unresolved constructor is read off `globalThis` when the
  `new` executes (`js_global_get_or_throw_unresolved`, shared with the
  bare-identifier arm via `unresolved_global_get_expr`), so a
  runtime-created global constructs and a true miss throws
  `ReferenceError: <name> is not defined` -- with the identifier, as
  #8730 and #8882 asked. The compile log names it too, with the same
  "unknown identifier" warning the bare-identifier arm prints.

Regression tests: a hoisted class constructing a sibling declared inside
a later closure keeps `New { class_name }` (fails without the new guard
clause, verified); a `typeof`-guarded `new IntersectionObserver()`
lowers to the named runtime lookup; the #8739 positive control now
expects the named form.

Fixes #8882. Refs #8730.

Claude-Session: https://claude.ai/code/session_01UZJbhb2FTuakurTHPAKQgd
…host several apps

Every class-id-keyed table module init writes — vtables, static methods and
accessors, constructors and flags, the parent map and its dense mirror, names,
lengths, registered ids, bind lengths, the extends-Error / DataView /
typed-array marks, the hasInstance / toStringTag hooks, generic-origin and
fetch-parent maps, anon-shape ids — was a process-global static keyed by a
compile-time class id. Class ids come from a small sequential counter in
codegen, so N dlopen'd copies of one application register the SAME ids with
DIFFERENT func_ptrs (each image's own code addresses) into one HashMap, and
insert is last-writer-wins: after the last image's init every class of every
earlier image dispatched into the last image's code, and only the
last-initialised application worked (#8546). No write order over a shared
table works, so the 21 tables move into one ClassImageTables per image.

A thread resolves its image through a perry_thread_local! handle, falling back
to the process-wide primary image. js_gc_init — codegen's first runtime call in
both `main` and `perry_module_init`, on the thread that runs that image's
module init — enters an image: the first thread to enter owns the primary,
every later one gets a fresh image. perry/thread workers and worker_threads
Workers adopt their spawner's image before running anything, because they never
run module init. A thread that neither entered nor adopted (a pump firing JS
for the primary heap, a reactor thread, a libtest thread) uses the primary,
i.e. the process-global table it saw before, so single-image programs are
unchanged. Each former `static RwLock<..>` is a `static ImageTable<RwLock<..>>`
whose read()/write() return the same guard types, so the call sites are
untouched. Latches and VTABLE_GEN stay process-global on purpose.

Tests: two application threads registering the same class id with different
method addresses each dispatch to their own (sabotage-verified: with the enter
made a no-op the last writer wins and the test fails on the func_ptr); a
spawned worker shares its spawner's image while a second application sees
neither; a thread without an image reads the primary.

Claude-Session: https://claude.ai/code/session_01UZJbhb2FTuakurTHPAKQgd
…nctions

`TailCallElimPass::markTails` walks the transitive SSA uses of every alloca;
only loads/stores and nocapture call arguments stop it. On a
statepoint-rewritten function an alloca handed to any runtime call reaches
the statepoint token, its gc.relocates and, through their gc-live bundles,
every later statepoint, so each walk covers the whole function and the pass
costs allocas x uses. Coop's Next.js route (jsonwebtoken's bundled entry:
400 allocas, 643k post-RS4GC instructions, 3.4k statepoints, 477k
relocates; ~1.6M visited uses per alloca) held one LLVM worker for ~100
CPU-minutes in that walk on a unit whose remaining `-Os` passes take ~16 s.

Before the optimization pipeline runs, estimate the walk as
`allocas x instructions` per function and stamp
`"disable-tail-calls"="true"` on any function over the budget (default
2^26; `PERRY_LL_TRE_MAX_ALLOCA_WALK=<n>` raises/lowers it, `0`/`off`
disables). That attribute is TRE's own early-out, so the function keeps
every other pass at the requested level (#8421); it gives up exactly
tail-recursion-to-loop and sibling-call codegen, and it is not `optnone`
(#8583). The trip is logged with the function's name and factors, and the
knob is a build/object cache input.

Fixes #8883

Claude-Session: https://claude.ai/code/session_01UZJbhb2FTuakurTHPAKQgd
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 216b3349-bca0-4959-a8d9-fecb5ea31549

📥 Commits

Reviewing files that changed from the base of the PR and between a581b4c and e55c66b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (30)
  • changelog.d/8882-late-bound-class-new.md
  • changelog.d/8893-class-registry-per-image.md
  • crates/perry-codegen/src/inprocess.rs
  • crates/perry-hir/Cargo.toml
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/expr_new.rs
  • crates/perry-hir/src/lower/lower_expr.rs
  • crates/perry-hir/src/lower/lower_expr/arm_ident.rs
  • crates/perry-hir/src/lower/lower_expr/helpers.rs
  • crates/perry-hir/src/lower/lower_module_fn.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower/mod.rs
  • crates/perry-hir/src/lower/pre_scan.rs
  • crates/perry-hir/src/lower/pre_scan/class_decl_names.rs
  • crates/perry-hir/src/lower/tests.rs
  • crates/perry-hir/tests/aliased_native_new_resolution.rs
  • crates/perry-runtime/src/gc/mod.rs
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry-runtime/src/object/class_image.rs
  • crates/perry-runtime/src/object/class_meta_registry.rs
  • crates/perry-runtime/src/object/class_registry/class_meta.rs
  • crates/perry-runtime/src/object/class_registry/gc_roots.rs
  • crates/perry-runtime/src/object/class_registry/state.rs
  • crates/perry-runtime/src/object/data_view_registry.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/thread.rs
  • crates/perry-stdlib/src/worker_threads.rs
  • crates/perry/src/commands/compile/build_cache.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry/src/commands/compile/object_cache/object_cache_tests.rs

📝 Walkthrough

Walkthrough

The PR adds runtime resolution for unresolved constructors, per-image class registries with worker propagation, and a configurable TailCallElim walk budget with cache-key integration and regression tests.

Changes

Late-bound constructor resolution

Layer / File(s) Summary
Nested class-name pre-scan
crates/perry-hir/Cargo.toml, crates/perry-hir/src/lower/...
The lowering context records class declarations at any nesting depth. A module pre-scan populates the set before expression lowering.
Runtime constructor fallback
crates/perry-hir/src/lower/expr_new.rs, crates/perry-hir/src/lower/lower_expr/..., crates/perry-hir/src/lower/tests.rs, crates/perry-hir/tests/aliased_native_new_resolution.rs, changelog.d/8882-late-bound-class-new.md
Known class names retain late-bound New lowering. Other unresolved names use js_global_get_or_throw_unresolved with the identifier and source offset. Tests verify named runtime lookup and nested-class resolution.

Per-image class registries

Layer / File(s) Summary
Image selection and lifecycle
crates/perry-runtime/src/object/class_image.rs, crates/perry-runtime/src/object/mod.rs, crates/perry-runtime/src/gc/mod.rs, changelog.d/8893-class-registry-per-image.md
The runtime adds per-image registry storage, thread-local image selection, primary-image fallback, image handles, and initialization tests.
Registry table migration
crates/perry-runtime/src/object/class_constructors.rs, crates/perry-runtime/src/object/class_meta_registry.rs, crates/perry-runtime/src/object/class_registry/*, crates/perry-runtime/src/object/data_view_registry.rs
Class constructors, metadata, dispatch, parent, built-in-extension, and class-ID registries now select storage from the current image.
Worker image adoption
crates/perry-runtime/src/thread.rs, crates/perry-stdlib/src/worker_threads.rs
Worker paths capture the spawner image and adopt it before executing worker code.

TailCallElim walk budget

Layer / File(s) Summary
Budgeted TailCallElim pipeline
crates/perry-codegen/src/inprocess.rs
The optimizer estimates function walk cost, applies configurable budget modes above -O0, stamps over-budget functions with disable-tail-calls, and records diagnostics and statistics.
Code-generation cache invalidation
crates/perry/src/commands/compile/build_cache.rs, crates/perry/src/commands/compile/object_cache.rs, crates/perry/src/commands/compile/object_cache/object_cache_tests.rs
Build and object cache keys now include PERRY_LL_TRE_MAX_ALLOCA_WALK. Tests verify key changes when the variable changes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ModuleLowering
  participant ClassNamePreScan
  participant LowerNew
  participant RuntimeGlobalLookup

  ModuleLowering->>ClassNamePreScan: scan module class declarations
  ClassNamePreScan-->>ModuleLowering: return class_decl_names_any_depth
  ModuleLowering->>LowerNew: lower new Identifier()
  LowerNew->>RuntimeGlobalLookup: resolve unresolved identifier at runtime
  RuntimeGlobalLookup-->>LowerNew: constructor or named ReferenceError
Loading
sequenceDiagram
  participant js_gc_init
  participant ClassImage
  participant ImageTable
  participant Worker

  js_gc_init->>ClassImage: enter current thread image
  ClassImage-->>ImageTable: select image-local registry
  Worker->>ClassImage: adopt spawner image
  Worker->>ImageTable: access shared class tables
Loading

Suggested reviewers: thehypnoo, jdalton

✨ 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 merge/batch-8892-8894

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.

1 participant