Skip to content

fix(runtime,codegen,hir): Next.js standalone walls 31-35#5112

Merged
proggeramlug merged 3 commits into
mainfrom
feat/nextjs-walls-31plus
Jun 14, 2026
Merged

fix(runtime,codegen,hir): Next.js standalone walls 31-35#5112
proggeramlug merged 3 commits into
mainfrom
feat/nextjs-walls-31plus

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Continues the Next.js standalone-compile validation effort (follow-up to #5050, which landed lazy-require + walls 24-29). Five more walls, each with a minimal repro; verified the standalone next-server boots further with each.

Walls

  • 31 — Error.prepareStackTrace / .stack typing. type_analysis no longer types every .stack PropertyGet as String: Error.prepareStackTrace can make .stack an array of CallSites (depd / source-map-support) and a plain object may carry any value — typing it String corrupted the array on store. Adds a CallSite shim + honors a user-set Error.prepareStackTrace (error.rs, global_this.rs: ERROR_CONSTRUCTOR_PTR + default-thunk address).
  • 32 — util.inherits(X, require('stream')). require('stream') is a callable bound-native with no .prototype; synthesize one so util.inherits can chain (util_inherits.rs).
  • 33 — dynamic new Function(body) (depd). depd builds its deprecation wrapper via string-concat new Function(...). No JS interpreter: recognize depd's fixed template and return a working native fn (js_function_ctor_from_strings + depd_wrapfunction_outer_thunk); unknown templates still get the placeholder.
  • 34 — console[method](...). The computed form collapsed the console receiver to the GlobalGet(0) sentinel (numeric 0 at runtime), so console[m](...) threw (number).m is not a function (Next's prefixedLog). Route console[dynamicKey] to js_console_method_by_value, the same bound closure a static console.log read produces.
  • 35 — default params dropped in CJS modules (high-value). nested_fn_decl recorded param.default but never emitted the if (p === undefined) p = <default> prologue. Since cjs_wrap wraps every module body in an IIFE, every top-level function f(a, opts = {}) becomes block-nested and silently dropped its default — broke recursiveReadDir(dir) → the whole setupFsCheck. Mirrors fn_decl.rs.

Validation

  • cargo build --release clean; perry-hir tests pass; targeted perry-runtime tests pass (the GC/stream stress tests are the known parallel-interaction flakes — green in isolation).
  • Next.js standalone server compiles + boots through ✓ Ready in 0ms and setupFsCheck with these applied (next blocker tracked separately as wall 36).

Summary by CodeRabbit

  • New Features
    • Added support for new Function(...) using runtime-provided argument strings (limited dynamic body handling).
    • Improved dynamic console method access via console[key] to resolve and call the correct native method.
    • Enabled structured stack traces with Error.prepareStackTrace, including lazy .stack generation.
    • Applied default parameter values when compiling nested functions.
  • Bug Fixes
    • Refined Error property typing so only message/name are treated as strings (not stack).
    • Enhanced constructor .prototype synthesis for missing or nonstandard cases (e.g., util.inherits).

…ative Stream proto, depd new Function, console[m], default params in CJS)

- type_analysis: `.stack` is no longer typed String unconditionally — it can
  be an array (Error.prepareStackTrace) and a plain object may carry any value;
  typing it String corrupted array stores (wall 31).
- error.rs + global_this: CallSite shim + honor a user-set
  Error.prepareStackTrace; ERROR_CONSTRUCTOR_PTR + default thunk address (wall 31).
- util_inherits: synthesize a .prototype for callable bound-native constructors
  (require('stream')) so util.inherits(X, Stream) chains (wall 32).
- new Function with a runtime-built body: recognize depd's wrapper template and
  return a working native fn (no JS interpreter); js_function_ctor_from_strings
  + depd_wrapfunction_outer_thunk (wall 33).
- console[dynamicKey]: route to js_console_method_by_value (same bound closure as
  console.<key>) — the computed form collapsed the receiver to GlobalGet(0)=0,
  so console[m](...) threw '(number).m is not a function' (wall 34).
- nested_fn_decl: emit the default-param prologue for block-nested function decls
  — every top-level fn in a CJS module becomes block-nested under cjs_wrap, so
  'function f(a, opts={})' dropped its default (wall 35).
@coderabbitai

coderabbitai Bot commented Jun 14, 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: 829e3ad0-6b80-4e58-a698-1ba199ede825

📥 Commits

Reviewing files that changed from the base of the PR and between 04ea3f4 and d54c20c.

📒 Files selected for processing (7)
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-codegen/src/type_analysis.rs
  • crates/perry-runtime/src/builtins/console.rs
  • crates/perry-runtime/src/builtins/mod.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/util_inherits.rs

📝 Walkthrough

Walkthrough

This PR adds four runtime compatibility features: new Function(...) lowering via a js_function_ctor_from_strings runtime helper with depd wrapfunction template detection; computed console[dynamicKey] access via a new js_console_method_by_value FFI; a V8-style Error.prepareStackTrace/CallSite shim replacing the eager .stack string with a lazy getter; and default-parameter application in nested function declarations. Supporting changes fix type analysis to exclude Error#stack from string classification and harden ensure_function_prototype for constructors lacking a .prototype.

Changes

Runtime JS Compatibility Features

Layer / File(s) Summary
new Function(...) constructor — codegen and runtime
crates/perry-codegen/src/lower_call/new.rs, crates/perry-codegen/src/runtime_decls/strings.rs, crates/perry-runtime/src/object/global_this.rs
lower_new detects class_name == "Function" and emits a call to js_function_ctor_from_strings; the runtime entry point matches the depd wrapfunction body template to return a live native closure (arity 5), otherwise falls back to a placeholder object.
console[dynamicKey] computed access — HIR, codegen decl, runtime
crates/perry-hir/src/lower/expr_member.rs, crates/perry-codegen/src/runtime_decls/mod.rs, crates/perry-runtime/src/builtins/console.rs, crates/perry-runtime/src/builtins/mod.rs
HIR rewrites console[expr] to call js_console_method_by_value instead of a generic IndexGet; the runtime FFI extracts the NaN-boxed key string and returns the matching bound native console closure; codegen registers the new FFI declaration.
V8-style Error.prepareStackTrace / lazy .stack getter
crates/perry-runtime/src/error.rs, crates/perry-runtime/src/object/global_this.rs, crates/perry-runtime/src/object/mod.rs
Removes js_node_system_error_value; adds a CallSite shim with placeholder method implementations and a structured stack array builder; js_error_capture_stack_trace now installs a lazy .stack accessor that consults the current Error.prepareStackTrace on read rather than eagerly computing the string. ERROR_CONSTRUCTOR_PTR and default_prepare_stack_trace_func_ptr are added and re-exported.
Default parameters in nested function declarations
crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs
Synthesized default-parameter-application statements are prepended to the nested function body ahead of destructuring statements.
Type-analysis .stack exclusion and ensure_function_prototype robustness
crates/perry-codegen/src/type_analysis.rs, crates/perry-runtime/src/util_inherits.rs
refine_type_from_init and is_string_expr no longer classify Error#stack as String. ensure_function_prototype synthesizes and caches a plain prototype object for both closure-backed and object-backed constructors that lack one.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PerryTS/perry#5078: Directly related — this PR removes the js_node_system_error_value FFI export from error.rs that the referenced PR adds for constructing Node-style coded Errors.

Poem

🐇 Hop hop, a function springs to life from strings,
console[key] now leaps on dynamic wings.
The .stack lies lazy, waking when you peek,
Defaults fill in gaps for params you didn't seek.
With prototypes patched and .stack no longer String,
This bunny's done a lot with one small PR ring! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(runtime,codegen,hir): Next.js standalone walls 31-35' directly matches the PR's primary objective of fixing walls 31-35 in Next.js standalone compilation, using standard commit conventions and clearly identifying the scope.
Description check ✅ Passed The PR description comprehensively covers all required template sections: a clear summary, detailed wall-by-wall changes explaining each fix, related issue/walls referenced, and a complete validation section with test commands.
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-31plus

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: 6

🤖 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-codegen/src/type_analysis.rs`:
- Around line 399-401: In the `refine_type_from_init` function at the "message"
or "name" property check, you are returning `HirType::String` immediately
without first checking if the object has a declared field type for that
property. Reorder the logic to first check if the object parameter has a
declared field definition for this property name, and only fall back to
returning `HirType::String` if no declared field type exists. This ensures that
user-defined classes with their own `name` or `message` fields preserve their
actual declared types instead of being incorrectly overridden to `String`.

In `@crates/perry-runtime/src/builtins/console.rs`:
- Around line 170-174: The js_console_method_by_value function currently returns
undefined for non-string keys without attempting coercion. Apply JavaScript key
coercion to the input key before performing the console method lookup, similar
to how computed member access should work. Replace the direct
jsvalue_string_content check with a function that coerces the key value to a
string first (following standard JS member access coercion rules), so that
numeric and other coercible keys are properly resolved instead of immediately
returning TAG_UNDEFINED.

In `@crates/perry-runtime/src/error.rs`:
- Around line 1089-1093: The PropertyAttrs being passed to set_property_attrs
for the "stack" property descriptor currently uses true as the first argument
(writable flag), but accessor descriptors in ECMAScript should not include a
writable attribute since that applies only to data descriptors. Change the
PropertyAttrs::new call to use false as the first argument instead of true, so
it reads PropertyAttrs::new(false, false, true), to accurately reflect that this
is an accessor descriptor with only configurable and enumerable attributes.

In `@crates/perry-runtime/src/object/global_this.rs`:
- Around line 1259-1287: The depd wrapper detection in
js_function_ctor_from_strings is too loose—it only checks three body substrings,
which could match unrelated Function bodies. Before returning the native thunk
closure, add stricter validation: check that args_len equals 6 (the depd wrapper
expects 5 parameters plus the body) and validate that the parameter names match
the expected depd parameter names ("fn", "log", "deprecate", "message", "site").
Only take the fast path (returning the depd_wrapfunction_outer_thunk closure)
when both the body substring patterns AND the argument count and parameter names
match.
- Around line 5010-5027: The `ERROR_CONSTRUCTOR_PTR` static atomic stores a
process-global raw pointer to the Error constructor, but the file already treats
globalThis as thread-local to handle arena pointer validity across threads. This
creates a thread-safety hazard where one thread's stored constructor pointer can
be read by another thread, potentially causing access to invalid or freed
memory. Replace the process-global `ERROR_CONSTRUCTOR_PTR` with a thread-local
storage mechanism, or refactor the code to resolve the Error constructor from
the current thread's realm instead of storing it in a process-global atomic.
This change will affect how `install_error_static_methods` stores the
constructor and how `default_prepare_stack_trace_func_ptr()` and
`error::error_prepare_stack_trace_override` retrieve it.

In `@crates/perry-runtime/src/util_inherits.rs`:
- Around line 86-109: The prototype synthesis code (starting with
js_object_alloc and extending through the closure_set_dynamic_prop and
js_object_set_field_by_name calls) currently applies to any value type, but
should only apply to callable values. Add a check at the beginning of this
section to verify that the value is callable (checking if it's a bound-native or
function-like value); if the value is not callable, return current directly
instead of proceeding with the prototype synthesis. This preserves the existing
validation failure behavior for non-callable objects while allowing the
synthesis to proceed only for actual callable values like native functions.
🪄 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: af283824-fee2-4538-a37e-0f8765708e05

📥 Commits

Reviewing files that changed from the base of the PR and between 5b61919 and 04ea3f4.

📒 Files selected for processing (12)
  • crates/perry-codegen/src/lower_call/new.rs
  • crates/perry-codegen/src/runtime_decls/mod.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-codegen/src/type_analysis.rs
  • crates/perry-hir/src/lower/expr_member.rs
  • crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs
  • crates/perry-runtime/src/builtins/console.rs
  • crates/perry-runtime/src/builtins/mod.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/util_inherits.rs

Comment thread crates/perry-codegen/src/type_analysis.rs Outdated
Comment thread crates/perry-runtime/src/builtins/console.rs
Comment on lines +1089 to +1093
crate::object::set_property_attrs(
target_ptr as usize,
"stack".to_string(),
crate::object::PropertyAttrs::new(true, false, true),
);

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how PropertyAttrs is used elsewhere for accessor properties
rg -n "set_property_attrs.*accessor" --type rust
rg -n "PropertyAttrs::new.*getter" --type rust -B2 -A2
# Check PropertyAttrs definition to understand if writable is ignored for accessors
ast-grep --pattern 'struct PropertyAttrs { $$$ }'

Repository: PerryTS/perry

Length of output: 10942


🏁 Script executed:

# Find PropertyAttrs struct definition
find . -name "*.rs" -type f | xargs grep -l "struct PropertyAttrs" | head -5

Repository: PerryTS/perry

Length of output: 98


🏁 Script executed:

# Check PropertyAttrs struct and set_property_attrs implementation
rg -B2 -A10 "^[[:space:]]*(pub )?struct PropertyAttrs" --type rust

Repository: PerryTS/perry

Length of output: 39


🏁 Script executed:

# Find and examine set_property_attrs function
rg -B2 -A20 "fn set_property_attrs" --type rust | head -100

Repository: PerryTS/perry

Length of output: 1875


🏁 Script executed:

# Read the specific lines from error.rs to confirm context
sed -n '1080,1100p' crates/perry-runtime/src/error.rs

Repository: PerryTS/perry

Length of output: 783


🏁 Script executed:

# Find PropertyAttrs struct definition and implementation
rg -B5 -A15 "struct PropertyAttrs" crates/perry-runtime/src/object/mod.rs

Repository: PerryTS/perry

Length of output: 788


🏁 Script executed:

# Check how PropertyAttrs is used when reading descriptors
rg -B3 -A3 "PropertyAttrs\|get_property_attrs" crates/perry-runtime/src/object/mod.rs | head -80

Repository: PerryTS/perry

Length of output: 39


🏁 Script executed:

# Search for how Object.getOwnPropertyDescriptor is implemented
rg -B5 -A20 "getOwnPropertyDescriptor\|get_own_property_descriptor" --type rust

Repository: PerryTS/perry

Length of output: 39


🏁 Script executed:

# Check how PropertyAttrs is interpreted when there's also an AccessorDescriptor
rg -B10 -A10 "get_accessor_descriptor\|AccessorDescriptor" crates/perry-runtime/src/object/mod.rs | head -100

Repository: PerryTS/perry

Length of output: 39


🏁 Script executed:

# Check where PropertyAttrs is used when returning property descriptors
rg -B5 -A15 "Object\.getOwnPropertyDescriptor\|get_property_attrs\|PropertyAttrs" crates/perry-runtime/src/object/mod.rs | grep -A15 -B5 "get.*property\|descriptor"

Repository: PerryTS/perry

Length of output: 39


🏁 Script executed:

# Look for how builtin functions construct Object.getOwnPropertyDescriptor responses
rg -B5 -A20 '"getOwnPropertyDescriptor"' --type rust

Repository: PerryTS/perry

Length of output: 31445


🏁 Script executed:

# Find js_object_get_own_property_descriptor implementation to see how it combines accessor + attrs
rg -B5 -A30 "pub.*fn js_object_get_own_property_descriptor" --type rust

Repository: PerryTS/perry

Length of output: 7395


🏁 Script executed:

# Check if there's branching based on whether an accessor exists
rg -B3 -A15 "get_accessor_descriptor.*obj.*key" crates/perry-runtime/src/object/mod.rs

Repository: PerryTS/perry

Length of output: 636


🏁 Script executed:

# Look for how PropertyAttrs is read and used when constructing descriptor results
rg -B2 -A10 "get_property_attrs\|PROPERTY_DESCRIPTORS" crates/perry-runtime/src/object/mod.rs | head -60

Repository: PerryTS/perry

Length of output: 39


🏁 Script executed:

# Get the full implementation of js_object_get_own_property_descriptor
sed -n '1,200p' crates/perry-runtime/src/object/descriptors.rs | tail -150

Repository: PerryTS/perry

Length of output: 6938


🏁 Script executed:

# Search more of the descriptors.rs file to find how PropertyAttrs is applied
sed -n '1,300p' crates/perry-runtime/src/object/descriptors.rs | grep -A50 "js_object_get_own_property_descriptor"

Repository: PerryTS/perry

Length of output: 2805


🏁 Script executed:

# Get lines 100-300 of descriptors.rs to see the full descriptor logic
sed -n '100,300p' crates/perry-runtime/src/object/descriptors.rs

Repository: PerryTS/perry

Length of output: 10428


🏁 Script executed:

# Look for build_accessor_descriptor and build_data_descriptor to see how they use PropertyAttrs
rg -B5 -A15 "fn build_(accessor|data)_descriptor" crates/perry-runtime/src/object/descriptors.rs

Repository: PerryTS/perry

Length of output: 1858


PropertyAttrs for accessor descriptors should omit the writable flag.

ECMAScript accessor descriptors only have configurable and enumerable attributes; writable applies to data descriptors only. While the runtime correctly ignores the writable bit when building the descriptor object via build_accessor_descriptor(), storing it in PropertyAttrs is semantically misleading. Consider using PropertyAttrs::new(false, false, true) instead, or introduce an accessor-specific variant to clarify intent.

🤖 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/error.rs` around lines 1089 - 1093, The
PropertyAttrs being passed to set_property_attrs for the "stack" property
descriptor currently uses true as the first argument (writable flag), but
accessor descriptors in ECMAScript should not include a writable attribute since
that applies only to data descriptors. Change the PropertyAttrs::new call to use
false as the first argument instead of true, so it reads
PropertyAttrs::new(false, false, true), to accurately reflect that this is an
accessor descriptor with only configurable and enumerable attributes.

Comment thread crates/perry-runtime/src/object/global_this.rs
Comment thread crates/perry-runtime/src/object/global_this.rs Outdated
Comment thread crates/perry-runtime/src/util_inherits.rs
- type_analysis: a user class's DECLARED .message/.name field type wins over
  the Error-instance String assumption.
- console.rs: apply JS property-key coercion in js_console_method_by_value
  (console[0] → "0"; Symbol key → miss).
- global_this: tighten the depd new Function template match — require exactly
  6 args and the five verbatim parameter names before returning the native
  thunk, so an unrelated dynamic Function body can't be misclassified.
- global_this: ERROR_CONSTRUCTOR_PTR is now thread-local (each agent's realm)
  instead of a process-global atomic — a cross-thread Error ctor /
  prepareStackTrace pointer could be foreign/freed (same reason globalThis is
  per-thread).
- util_inherits: only synthesize .prototype for callable native-module exports
  (require('stream')); a plain non-callable object now fails util.inherits as
  Node does instead of silently passing.
- rustfmt pass.
@proggeramlug proggeramlug merged commit c8dcf87 into main Jun 14, 2026
13 of 15 checks passed
@proggeramlug proggeramlug deleted the feat/nextjs-walls-31plus branch June 14, 2026 07:58
proggeramlug pushed a commit that referenced this pull request Jun 14, 2026
Rolls up the issue-fix batch merged on top of 0.5.1165 (#5102, #5103,
#5105, #5106, #5107, #5108, #5109, #5110, #5112, #5117). See CHANGELOG
for the per-PR breakdown.
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