Skip to content

fix: Next.js standalone bring-up — walls 36–44 (dynamic-parent classes, forward-capture, super.method, cjs export-hint, self-new in capturing closure)#5125

Merged
proggeramlug merged 1 commit into
mainfrom
feat/nextjs-walls-38plus
Jun 14, 2026

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Next.js 16.2.9 standalone bring-up — walls 36–43

Continues the compile-as-package / Next.js work. With these fixes the Next.js standalone server compiles (auto-optimize, ~124 MB), boots, listens, routes, and runs request handling through to the OpenTelemetry tracer (previously it died inside the NextNodeServer constructor). Each fix has an isolated repro.

Wall Fix
36 Pre-register sibling class names in function-expression bodies (forward refs in cjs IIFE).
37 Don't hold the class-vtable read lock across a method body (lazy require() took the write lock → deadlock).
38 class X extends _mod.default (interop ESM default-export base) ran no base ctor. cjs_wrap hoist guard now scans the extends head for IIFE-local refs; .default member-extends routes through extends_expr; super/new use the decl-time-stashed parent (CLASS_DYNAMIC_PARENT_VALUE) and invoke a ClassRef parent via run_class_constructor_on_this_flat.
39 Dead 0 && (module.exports = { X: null }) Babel export-hint was extracted as a real named export, overriding the real _export(exports, { X: () => X }) getter with undefined. Shape-2 scan now requires a statement boundary (skips expression context like 0 && (…)).
40 Dynamic require(unresolvable) now throws a MODULE_NOT_FOUND-coded error (Node parity) so try { require(p) } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw } degrades gracefully (Next swallows the absent instrumentation hook).
41 Closures forward-capturing a later-declared function-scope let/const resolved to a globalThis read → ReferenceError (router-server initialize() request handler reads relativeProjectDir declared ~400 lines below → HTTP 500). New shared pre_register_forward_captured_lets boxes only bindings referenced by an earlier closure; preallocs the box at entry (kept out of hoisted_id_set so non-hoistable const = arrow decls aren't reordered).
42 super.method() on a dynamic-parent class returned a bogus 0.0 (static extends_name walk missed). NextNodeServer.makeRequestHandler's super.getRequestHandler() → number → "value is not a function". New js_super_method_call_dynamic resolves from the registered parent (never the child → no override recursion), drops the registry lock before the call (wall-37 safe).
43 Wall-41 via destructuring + cjs-IIFE bodies: tracer's _export(exports, { SpanKind: () => SpanKind }) getter forward-captures the later const { …, SpanKind } = api → "SpanKind is not defined". The forward-capture helper now handles array/object destructuring leaves + { key } shorthand and is shared by lower_fn_expr (the const _cjs = (function(){…})() wrapper).

Validation

  • perry-hir, perry-codegen, perry-runtime, perry test suites pass.
  • The TypedArray typed_array_indexof_includes "failure" under a full run passes in isolation (known test-pollution flake).
  • The 2 issue_4903_listen_callback_deferred tests fail on a pre-existing auto-optimize linker mismatch (perry_ffi::error::system_error_value, code untouched by this PR) — environmental to the worktree, unrelated to these changes.

Known next blocker (not in this PR)

new SelfClass(thisAlias.field) inside a closure that captured const t = this mis-binds the new instance's ctor this → tracer BaseContext.setValue throws. Tracked as wall 44.

Summary by CodeRabbit

  • Bug Fixes
    • Reduced false-positive module.exports = { ... } export detection when it appears in expression contexts.
    • Fixed super() and super.method(...) for classes with dynamically resolved parents, including inheritance interop like module.ClassName.default.
    • Improved forward-capture/boxing for let/const (including destructuring) referenced by earlier closures in the same block, plus forward class-name resolution.
    • Updated the require() shim fallback to throw a MODULE_NOT_FOUND error with a clearer message.
    • Prevented incorrect new inlining in a specific constructor/local capture collision scenario.
  • Chores
    • Updated version/release metadata to 0.5.1168 and refreshed standalone bring-up notes.

@coderabbitai

coderabbitai Bot commented Jun 14, 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: d3422bae-2817-4ea7-9ddf-a3acf20b2c8d

📥 Commits

Reviewing files that changed from the base of the PR and between 46e6bdf and 9ccba92.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • CHANGELOG.md
  • CLAUDE.md
  • Cargo.toml
  • crates/perry-codegen/src/expr/super_method.rs
  • crates/perry-codegen/src/expr/this_super_call.rs
  • crates/perry-codegen/src/lower_call/new.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/destructuring/pattern_binding.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-hir/src/lower_decl/mod.rs
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/native_call_method.rs
  • crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs
  • crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs
  • crates/perry/src/commands/compile/cjs_wrap/wrap.rs
✅ Files skipped from review due to trivial changes (1)
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (16)
  • CLAUDE.md
  • crates/perry/src/commands/compile/cjs_wrap/wrap.rs
  • CHANGELOG.md
  • crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs
  • crates/perry-hir/src/lower_decl/mod.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-hir/src/destructuring/pattern_binding.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-runtime/src/object/native_call_method.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs
  • crates/perry-codegen/src/expr/super_method.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry-codegen/src/lower_call/new.rs

📝 Walkthrough

Walkthrough

Version 0.5.1168 "Next.js standalone bring-up" bundles six grouped bug fixes: CJS module.exports export-hint detection is restricted to statement boundaries; require() shim now throws MODULE_NOT_FOUND instead of a generic error; class hoisting guards the extends head for IIFE-local references; Phase 1.6 adds forward-capture preallocation for let/const closures referenced by earlier closures; class X extends _mod.default is routed through a stashed dynamic parent value for super() resolution; super.method() gains a dynamic runtime dispatch path with deadlock-safe registry-unlock-before-invoke; and new ClassName(...) inside a closure detects and avoids LocalId collisions with closure captures by redirecting to the standalone constructor symbol.

Changes

CJS/HIR/Codegen Multi-Fix: Forward Capture, Dynamic super, Constructor Collision

Layer / File(s) Summary
CJS wrap: export boundary, require error, class hoist guard
crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs, crates/perry/src/commands/compile/cjs_wrap/wrap.rs, crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs
module.exports scan validates statement-boundary positioning to skip expression-context hints; require() shim throws MODULE_NOT_FOUND instead of generic error; class hoisting checks the extends head for IIFE-local references.
Phase 1.6: forward-capture preallocation for let/const
crates/perry-hir/src/lower_decl/block.rs, crates/perry-hir/src/lower_decl/mod.rs, crates/perry-hir/src/destructuring/pattern_binding.rs, crates/perry-hir/src/lower/expr_function.rs
New pre_register_forward_captured_lets scans function bodies for let/const bindings referenced by earlier closures, pre-defines boxed locals with span-keyed reuse maps, and merges them into PreallocateBoxes emission; pattern binding sites reuse pre-registered locals.
Dynamic parent stashing for super() resolution
crates/perry-runtime/src/object/class_registry.rs, crates/perry-hir/src/lower_decl/class_decl.rs, crates/perry-codegen/src/expr/this_super_call.rs, crates/perry-codegen/src/runtime_decls/strings.rs
CLASS_DYNAMIC_PARENT_VALUE stores the evaluated extends value at module-init; class ... extends _mod.default is routed through extends_expr; super() codegen prefers js_get_dynamic_parent_value over re-evaluating extends_expr at the call site.
Dynamic super.method() dispatch and deadlock-safe method lookup
crates/perry-runtime/src/object/class_constructors.rs, crates/perry-runtime/src/object/native_call_method.rs, crates/perry-runtime/src/object/global_this.rs, crates/perry-codegen/src/expr/super_method.rs, crates/perry-codegen/src/runtime_decls/strings.rs
js_super_method_call_dynamic resolves and invokes named parent methods; run_class_constructor_on_this_flat runs constructors on existing this with flat arg buffers; js_fetch_or_value_super handles INT32-tagged ClassRef parents; js_native_call_method records ResolvedMethod and invokes after dropping registry read lock to prevent deadlocks; super_method.rs codegen falls back to dynamic path on static-resolution failure.
Constructor LocalId collision detection in closures
crates/perry-codegen/src/lower_call/new.rs
New local_constructor_symbol_exists and collect_decl_local_ids helpers detect when inlining a constructor would alias its locals with closure-captured variables; when collision is detected, lower_new redirects to standalone constructor symbol instead of inlining.
Version bump and changelog
Cargo.toml, CLAUDE.md, CHANGELOG.md
Workspace version bumped to 0.5.1168; changelog entries added for all fix groups.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(135, 206, 235, 0.5)
    Note over ModuleInit,CLASS_DYNAMIC_PARENT_VALUE: Module initialization: stash dynamic parent value
    ModuleInit->>js_register_class_parent_dynamic: class_id, parent_value (evaluated extends expr)
    js_register_class_parent_dynamic->>CLASS_DYNAMIC_PARENT_VALUE: stash raw NaN-boxed bits
  end

  rect rgba(144, 238, 144, 0.5)
    Note over SuperCallCodegen,js_fetch_or_value_super: super() at constructor call site: retrieve stashed value
    SuperCallCodegen->>js_get_dynamic_parent_value: class_id
    js_get_dynamic_parent_value->>CLASS_DYNAMIC_PARENT_VALUE: read stashed bits
    CLASS_DYNAMIC_PARENT_VALUE-->>js_get_dynamic_parent_value: parent_value f64
    SuperCallCodegen->>js_fetch_or_value_super: parent_val, this_box, args
    js_fetch_or_value_super->>run_class_constructor_on_this_flat: parent_cid, this_raw, args (INT32_TAG path)
    run_class_constructor_on_this_flat-->>js_fetch_or_value_super: success → undefined
  end

  rect rgba(255, 200, 100, 0.5)
    Note over Codegen,call_vtable_method: super.method() dynamic dispatch: deadlock-safe resolution and invoke
    Codegen->>js_super_method_call_dynamic: child_class_id, method_name, this, args
    js_super_method_call_dynamic->>CLASS_VTABLE_REGISTRY: resolve parent chain under read lock
    CLASS_VTABLE_REGISTRY-->>js_super_method_call_dynamic: vtable entry found
    js_super_method_call_dynamic->>call_vtable_method: invoke (lock dropped)
    call_vtable_method-->>Codegen: result f64
  end

  rect rgba(255, 150, 150, 0.5)
    Note over lower_new,call_local_constructor_symbol: new ClassName collision detection
    lower_new->>lower_new: check ctor_alias_collision: closure_captures non-empty && ctor_symbol_exists
    alt collision detected
      lower_new->>call_local_constructor_symbol: redirect (avoid LocalId aliasing)
    else no collision
      lower_new->>lower_new: inline constructor body
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • PerryTS/perry#5050: Modifies super()-call lowering in crates/perry-codegen/src/expr/this_super_call.rs through different Expr::SuperCall arms, touching the same codepath where the dynamic-parent value retrieval is now integrated.

Poem

🐇 Hop hop, the closures box,
No deadlock lurks behind the locks!
extends .default finds its way,
MODULE_NOT_FOUND saves the day.
Phase 1.6 pre-registers with care,
Collision-detection stops mid-air!
The rabbit leaps through forward-declared air,
Version 0.5.1168 blooms so rare! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description violates repository guidelines by including CHANGELOG.md entries, CLAUDE.md version updates, and Cargo.toml workspace version bumps, which contributors are explicitly instructed not to modify. Remove CHANGELOG.md, CLAUDE.md, and Cargo.toml version changes from this PR and reapply them after merge. Ensure all submitted commits respect the maintainer-only version-bump policy stated in the template and CONTRIBUTING.md.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main changes: fixing Next.js standalone compilation with nine interconnected walls addressing dynamic-parent classes, forward-capture, super.method dispatch, CJS export hints, and self-new in capturing closures.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/nextjs-walls-38plus

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry-runtime/src/object/native_call_method.rs (1)

3648-3763: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mirror the unlock-before-invoke fix in the raw-pointer class path.

This resolves the deadlock only for the jsval.is_pointer() branch. The raw-pointer fallback later in this same function still returns call_vtable_method(...) while holding CLASS_VTABLE_REGISTRY.read(), so untagged class instances can still hang on the same lazy-require() → class-registration path this change is trying to eliminate. Please apply the same resolve-then-drop-guard pattern there too.

🤖 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 `@crates/perry-runtime/src/object/native_call_method.rs` around lines 3648 -
3763, The deadlock safety fix shown in the diff applies only to the
jsval.is_pointer() branch by resolving the method under the
CLASS_VTABLE_REGISTRY read lock and releasing it before calling the method body.
The raw-pointer fallback path later in the same function still returns
call_vtable_method(...) while holding the read lock, which can deadlock on the
same lazy-require() and class-registration path. Apply the same
resolve-then-drop-guard pattern to the raw-pointer fallback: wrap the registry
lookup logic in a separate scope that captures the necessary method information
(similar to the ResolvedMethod enum) and releases the lock before invoking
call_vtable_method or any other method-execution function.
🤖 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 `@crates/perry-hir/src/lower_decl/class_decl.rs`:
- Around line 293-313: In the `class_decl.rs` file at lines 293-313 (anchor) and
1295-1304 (sibling), when handling the `parent_name == "default"` case, the code
is incorrectly setting `extends_name` to `Some(parent_name)` in the tuple
returned from the match expression. This causes inherited-field and accessor
lookups to fail later because they key off `extends_name`, and using the literal
"default" string doesn't resolve to the actual parent class. Change the second
element of both the Ok and Err tuple returns from `Some(parent_name)` to `None`,
so that `extends_name` remains unset and the dynamic relationship is carried
only through `extends_expr`, allowing the real parent edge to be wired at
registration time without breaking metadata resolution.

In `@crates/perry-runtime/src/object/class_constructors.rs`:
- Around line 261-270: The super.method() lookup in this function only uses
lookup_class_method_in_chain, which fails to find methods registered at runtime
via js_register_function_prototype_method, CLASS_PROTOTYPE_METHODS, or synthetic
prototype-object parents created by js_set_function_prototype. Instead of
directly calling lookup_class_method_in_chain, route the parent method lookup
through the full parent lookup tower that checks both class methods and
prototype-registered methods, similar to how regular this.method() dispatch
works. This ensures that super.m() can find methods defined on parent prototypes
(like Base.prototype.m in a subclass of function Base(){}).

In `@crates/perry-runtime/src/object/class_registry.rs`:
- Around line 272-284: The static map CLASS_DYNAMIC_PARENT_VALUE stores raw
NaN-boxed parent bits but is not integrated into the garbage collection
side-table machinery, leaving it unscanned and unrewritten during moving GC
operations which can result in stale pointers. Locate the existing GC scan,
snapshot, and reset functions that handle other class side tables in this file,
and add corresponding logic to visit and rewrite the raw u64 bits stored in
CLASS_DYNAMIC_PARENT_VALUE during these GC operations, ensuring that any
pointer-tagged parent values are properly updated to their relocated addresses.

In `@crates/perry-runtime/src/object/global_this.rs`:
- Around line 480-502: When run_class_constructor_on_this_flat() returns false
in the INT32_TAG block, the code currently falls through with callee still being
the INT32-tagged ClassRef. This ClassRef then gets passed to later code (the
usable logic and js_native_call_value), which would incorrectly treat it as
callable and result in undefined being returned without running the base
constructor. Fix this by ensuring that when run_class_constructor_on_this_flat()
returns false, execution does not fall through to js_native_call_value with the
same ClassRef value—either by returning early in the error case, or by
preventing the ClassRef from being treated as callable in the subsequent logic.

In `@crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs`:
- Around line 405-414: The `line_start_ok` check in the extract_exports.rs file
is too permissive because it only validates that the current line starts with
whitespace, ignoring what precedes the previous newline. To tighten this
boundary check, after confirming the current line is indentation-only, you must
also search backward to find the previous newline and verify that the last
non-whitespace character before that previous newline is a valid statement
boundary (such as semicolon, closing brace, or opening brace). This additional
validation prevents multiline expressions like `0 && (\n  module.exports = { X:
null }\n)` from incorrectly passing the check, since the opening parenthesis on
the previous line indicates an ongoing expression context.

---

Outside diff comments:
In `@crates/perry-runtime/src/object/native_call_method.rs`:
- Around line 3648-3763: The deadlock safety fix shown in the diff applies only
to the jsval.is_pointer() branch by resolving the method under the
CLASS_VTABLE_REGISTRY read lock and releasing it before calling the method body.
The raw-pointer fallback path later in the same function still returns
call_vtable_method(...) while holding the read lock, which can deadlock on the
same lazy-require() and class-registration path. Apply the same
resolve-then-drop-guard pattern to the raw-pointer fallback: wrap the registry
lookup logic in a separate scope that captures the necessary method information
(similar to the ResolvedMethod enum) and releases the lock before invoking
call_vtable_method or any other method-execution function.
🪄 Autofix (Beta)

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: 3a7ec8eb-77cb-4096-b3e1-4b1cac485321

📥 Commits

Reviewing files that changed from the base of the PR and between 737633e and 48405c4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • CHANGELOG.md
  • CLAUDE.md
  • Cargo.toml
  • crates/perry-codegen/src/expr/super_method.rs
  • crates/perry-codegen/src/expr/this_super_call.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/destructuring/pattern_binding.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-hir/src/lower_decl/mod.rs
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/native_call_method.rs
  • crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs
  • crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs
  • crates/perry/src/commands/compile/cjs_wrap/wrap.rs

Comment on lines +293 to +313
} else if parent_name == "default" {
// `class X extends _mod.default` — the interop ESM
// default-export-class pattern (Next.js `NextNodeServer
// extends base-server`'s default `Server`). The trailing
// property `default` never resolves through `lookup_class`,
// and a `.default` export is always a real user/registered
// class — never a native-module member like `http.Agent`
// (which inherits via a *named* property and is handled by
// the colliding-name / parentless branches). Route through
// the dynamic `extends_expr` path so `super(opts)`
// re-evaluates the alias at construction time and runs the
// base constructor, and the decl-time
// `RegisterClassParentDynamic` wires the real parent edge
// (inherited methods / `instanceof`). The companion hoist
// guard in `extract_top_level_class_decls` keeps this class
// inside the IIFE so the require alias is assigned before the
// registration runs.
match lower_expr(ctx, super_class) {
Ok(expr) => (None, Some(parent_name), None, Some(Box::new(expr))),
Err(_) => (None, Some(parent_name), None, None),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't preserve "default" as extends_name here.

Later in this file, inherited-field and accessor lookups key off extends_name. Setting it to the literal export slot "default" means class X extends _mod.default still misses the real parent's metadata, so ctor field scanning can allocate duplicate own fields and inherited accessors still won't be recognized even though runtime parent registration succeeds. Leave extends_name unset unless you can resolve the underlying class name, and let extends_expr carry the dynamic relationship.

Also applies to: 1295-1304

🤖 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 `@crates/perry-hir/src/lower_decl/class_decl.rs` around lines 293 - 313, In the
`class_decl.rs` file at lines 293-313 (anchor) and 1295-1304 (sibling), when
handling the `parent_name == "default"` case, the code is incorrectly setting
`extends_name` to `Some(parent_name)` in the tuple returned from the match
expression. This causes inherited-field and accessor lookups to fail later
because they key off `extends_name`, and using the literal "default" string
doesn't resolve to the actual parent class. Change the second element of both
the Ok and Err tuple returns from `Some(parent_name)` to `None`, so that
`extends_name` remains unset and the dynamic relationship is carried only
through `extends_expr`, allowing the real parent edge to be wired at
registration time without breaking metadata resolution.

Comment thread crates/perry-runtime/src/object/class_constructors.rs Outdated
Comment thread crates/perry-runtime/src/object/class_registry.rs
Comment thread crates/perry-runtime/src/object/global_this.rs
Comment thread crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs Outdated
@proggeramlug proggeramlug changed the title fix: Next.js standalone bring-up — walls 36–43 (dynamic-parent classes, forward-capture, super.method, cjs export-hint) fix: Next.js standalone bring-up — walls 36–44 (dynamic-parent classes, forward-capture, super.method, cjs export-hint, self-new in capturing closure) Jun 14, 2026
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Update: wall 44 added.

| 44 | new SelfClass(...) inside a closure that captured a const t = this alias mis-bound the new instance's ctor this — the inlined constructor's const t = this reused a LocalId that is a capture in the lifted closure, so its field writes (t._cc = …) landed on the captured outer object and the new instance got no fields. Exactly OpenTelemetry's BaseContext.setValue → the Next.js tracer threw TypeError: Cannot read properties of undefined (reading 'set') on every request. lower_new's recursion-guard now also redirects to the standalone <class>_constructor symbol (which takes this as an explicit param) when the enclosing closure captures a local the ctor body declares. |

With wall 44, the server runs request handling all the way through the OTEL tracer into the render pipeline. Next blocker (wall 45, not in this PR): a render error's catch handler reads _iserror.getProperError where the module-level _iserror const resolves to undefined in that method scope (and the underlying render error it's trying to report is itself a further wall).

proggeramlug pushed a commit that referenced this pull request Jun 14, 2026
…168)

Re-applies PR #5125's wall 36-44 code cleanly onto current main (which now
has #5124's js_node_system_error_value link fix, unblocking http
auto-optimize). Consolidated commit; per-wall detail in CHANGELOG.
@proggeramlug proggeramlug force-pushed the feat/nextjs-walls-38plus branch from 4c231e4 to 46e6bdf Compare June 14, 2026 15:06
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main (now includes #5124's js_node_system_error_value fix). The 9 wall-36–44 commits are re-applied as one squashed commit on current main (they applied cleanly — no code conflicts with main's 15 new commits) so this PR merges without the version/changelog conflicts the stale base would have caused. Builds green; the http auto-optimize path compiles and runs again.

Wall 45 status (not in this PR): fully diagnosed as a 3-layer bug — (1) class_field_global_index returns a wrong fast-path slot for __perry_cap_* capture fields at scale, (2) dynamic-parent (extends _mod.default) subclasses are under-allocated so inherited captures overflow into the address-keyed overflow side-table, and (3) the parent ctor's by-name capture writes append to the keys-array (allocating), letting GC move this mid-construction. A partial fix (by-name capture access + dynamic-parent instance sizing) cleared layers 1–2 but layer 3 (GC-during-construction) remains, so it was reverted to keep this PR clean. Tracked for a focused follow-up.

@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.

♻️ Duplicate comments (1)
crates/perry-runtime/src/object/class_registry.rs (1)

272-284: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wire CLASS_DYNAMIC_PARENT_VALUE into the existing root lifecycle.

This table stores raw NaN-boxed parent values, but the GC root paths later in this file never visit or rewrite it, and Line 4373 also skips the usual runtime_write_barrier_root_nanbox(...). After a moving or incremental GC, js_get_dynamic_parent_value() can hand super() stale parent bits or miss a freshly-registered parent entirely. Please treat it like the other class side tables: add entries in ClassSideTableRootSlot / class_side_table_root_snapshot, visit it from both scan paths, clear it in test_clear_class_side_table_roots, and route inserts through a helper that emits the root write barrier.

Also applies to: 4362-4378, 4500-4519

🤖 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 `@crates/perry-runtime/src/object/class_registry.rs` around lines 272 - 284,
The CLASS_DYNAMIC_PARENT_VALUE static table stores raw NaN-boxed parent values
but is not integrated into the GC root lifecycle, causing stale or missed parent
values after GC cycles. Integrate it like other class side tables by: adding an
entry to ClassSideTableRootSlot enum, adding snapshot and visit logic in
class_side_table_root_snapshot for both GC scan paths, adding a clear statement
in test_clear_class_side_table_roots, and creating a helper function that routes
inserts through runtime_write_barrier_root_nanbox to emit the proper root write
barrier when registering new parent values.
🤖 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.

Duplicate comments:
In `@crates/perry-runtime/src/object/class_registry.rs`:
- Around line 272-284: The CLASS_DYNAMIC_PARENT_VALUE static table stores raw
NaN-boxed parent values but is not integrated into the GC root lifecycle,
causing stale or missed parent values after GC cycles. Integrate it like other
class side tables by: adding an entry to ClassSideTableRootSlot enum, adding
snapshot and visit logic in class_side_table_root_snapshot for both GC scan
paths, adding a clear statement in test_clear_class_side_table_roots, and
creating a helper function that routes inserts through
runtime_write_barrier_root_nanbox to emit the proper root write barrier when
registering new parent values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2df103c3-038c-46ad-a152-8740b9df5889

📥 Commits

Reviewing files that changed from the base of the PR and between 4c231e4 and 46e6bdf.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • CHANGELOG.md
  • CLAUDE.md
  • Cargo.toml
  • crates/perry-codegen/src/expr/super_method.rs
  • crates/perry-codegen/src/expr/this_super_call.rs
  • crates/perry-codegen/src/lower_call/new.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/destructuring/pattern_binding.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-hir/src/lower_decl/mod.rs
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/native_call_method.rs
  • crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs
  • crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs
  • crates/perry/src/commands/compile/cjs_wrap/wrap.rs
✅ Files skipped from review due to trivial changes (3)
  • Cargo.toml
  • CHANGELOG.md
  • CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (14)
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-codegen/src/expr/this_super_call.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs
  • crates/perry/src/commands/compile/cjs_wrap/wrap.rs
  • crates/perry-hir/src/destructuring/pattern_binding.rs
  • crates/perry-hir/src/lower_decl/mod.rs
  • crates/perry-codegen/src/expr/super_method.rs
  • crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs
  • crates/perry-runtime/src/object/native_call_method.rs
  • crates/perry-codegen/src/lower_call/new.rs
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower_decl/block.rs

…168)

Re-applies PR #5125's wall 36-44 code cleanly onto current main (which now
has #5124's js_node_system_error_value link fix, unblocking http
auto-optimize). Consolidated commit; per-wall detail in CHANGELOG.
@proggeramlug proggeramlug force-pushed the feat/nextjs-walls-38plus branch from 46e6bdf to 9ccba92 Compare June 14, 2026 15:47
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Addressed the rebase + lint failure + CodeRabbit feedback in the latest force-push (9ccba92):

Rebase & lint

  • Rebased onto latest main (clean, no conflicts).
  • Fixed the failing lint job — cargo fmt pass over the touched files (cargo fmt --check now clean). File-size, gc-store-site, gc-ffi-root, and addr-class gates all pass.

CodeRabbit fixes (4/5 applied)

  1. extract_exports.rs — the dead 0 && (module.exports = {...}) hint detector now classifies the boundary by the last non-whitespace token across newlines (;/}/{/SOF ⇒ statement start; (/&/|/,/=/?/:/operators ⇒ expression continuation), so the multiline Babel hint form is caught without regressing the union/object-literal export cases.
  2. class_constructors.rs js_super_method_call_dynamic — added a fallback to lookup_prototype_method (walks the parent chain, drops its read lock before returning so the wall-37 deadlock can't recur) when the class vtable lookup misses, covering function-style parents whose methods live in CLASS_PROTOTYPE_METHODS / a synthetic prototype object.
  3. class_registry.rsCLASS_DYNAMIC_PARENT_VALUE is now scanned in scan_class_side_table_roots_mut (visit_nanbox_u64_slot over its values) so a moving GC roots+forwards the stashed dynamic-parent value bits.
  4. global_this.rs js_fetch_or_value_super — the INT32 (ClassRef) branch now returns undefined unconditionally after running the parent ctor on this, instead of falling through to js_native_call_value with a non-callable NaN-tagged ClassRef.

Skipped with reason
5. ⏭️ class_decl.rs — keeping extends_name = Some("default") for class X extends _mod.default. Verified against current code: setting it to None regresses the dynamic-parent super-constructor path, because this_super_call.rs early-returns 0.0 when extends_name is None before it can reach the extends_expr dynamic dispatch — i.e. the base ctor would stop running (wall 38). The harm cited (duplicate own fields / unrecognized inherited accessors) does not materialize: the capture-union (lookup_class_captures("default")) and inherited-field-dedup (lookup_class_field_names("default")) lookups simply no-op on the unresolved "default" key — they don't duplicate, they skip dedup, and the subclass only declares its own captures. The genuine dynamic-parent field-inheritance gap is tracked separately as wall 45.

Verified wall-38/42 repro still green after the changes: super.getRequestHandler() resolves to a function and the inherited handler returns HR:1,2,3 (base ctor ran + super.method dispatched).

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