Skip to content

fix(hir,runtime): #5989 — Next.js cacheComponents boot-chain subset (strict global-builtin assignment + Date-extension construct + URLSearchParams shape-check SIGSEGV)#6014

Merged
proggeramlug merged 6 commits into
mainfrom
fix/5989-global-builtin-assignment-clean
Jul 5, 2026

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #5988 (async-step deadlock) — hardens the boot chain behind #5989's "dynamic RSC render evaporates silently."

Once #5988 gave the render a clean async graph, the request stopped hanging and started failing forward, which unmasked a chain of pre-existing compiler/runtime bugs that Next.js 16's cacheComponents pipeline hits before it can render. Each fix here has a node-identical minimal repro; none are async-related.

Scope note: while this PR was in review, main merged #6015 — a revert of #5874 ("Improve Zod compatibility support") because #5874 regressed 322 test262 tests. That revert removed the bind_fresh_class_value / ClassExprFresh class-lowering machinery that two of this work's HIR class-forward-capture fixes were built on. Those two (fn-nested sibling-class forward captures; CJS export-getter heritage-class forward captures) are deferred — they'll be re-derived against the post-revert base once #5874's zod-compat infrastructure is re-landed cleanly (they may not even be needed without that machinery). This PR is the subset that is independent of #5874 and verified on current main.

The five fixes

  1. fix(hir,runtime) — strict assignment to an existing global builtin is a property write, not a ReferenceError. Strict-mode X = v with no lexical binding threw unconditionally; per spec (PutValue resolving to the global environment) an existing global property is a normal write. Next's cacheComponents extensions install their dynamic-IO clock interceptor via strict CJS Date = createDate(Date) — so every boot printed "Failed to install Date class extension" (the install's catch{} swallowed the throw) and clock-based dynamic detection never armed. New runtime helper js_global_assign_existing_or_throw (presence-probe + write-back, mirroring js_global_update); both HIR assignment arms emit it in strict mode via a shared helper; absent bindings still throw the named ReferenceError.

  2. fix(runtime) — make the Date-extension wrapper shape construct real Dates. Three coupled gaps made every construction through the installed wrapper an unbranded "Invalid Date": (a) the reified Reflect.construct VALUE was a no-op stub returning undefined — the wrapper calls it through a captured const construct = Reflect.construct binding; (b) Reflect.construct(Date, args, newTarget) fell to the generic construct tail (plain object + thunk); (c) identify_global_builtin_constructor recovered names by walking globalThis, which breaks the instant the global is reassigned (the extension's whole point). Fixed with a real reflect_construct_thunk, a Date arm honoring GetPrototypeFromConstructor, and a direct thunk→name map for dedicated builtin thunks. Verified on the post-revert base: the Reflect.construct(Date, arguments, new.target) case the wrapper uses now matches node.

  3. fix(runtime) — validate keys_array before deref in shape_is_url_search_params. The fix(runtime): #5961 — URLSearchParams methods reachable through dynamic access #5964 receiver gate validated the object header but dereferenced keys_array raw; a GC_TYPE_OBJECT receiver reached mid-transition carries a non-heap word there, and reading (*keys_arr).length SIGSEGV'd on the first request (config.js method dispatch probes arbitrary receivers through this shape check). Require the eager GC_TYPE_ARRAY layout specifically (an object's own key list is always eager; per CodeRabbit, reject GC_TYPE_LAZY_ARRAY rather than read its non-matching layout) via the same try_read_gc_header check (Segmentation fault.: console.log(dayjs(1749820051142).format()); #5429/fix(runtime): validate spread sources; full handle band + no type-pun on the dynamic set path #5943 family).

Plus the two CodeRabbit review fixes folded in (lazy-array rejection above; shared strict_global_assign_existing_or_throw helper to dedupe the two assignment arms).

Validation (on current main, post-#6015)

  • New integration tests (crates/perry/tests/issue_5989_strict_global_builtin_assignment.rs): the exact Next.js date.js wrapper shape installs + intercepts (value path), and absent bindings still throw the named ReferenceError.
  • Date-extension probe: installs + constructs real Dates (proto-constructor case byte-identical to node); URLSearchParams byte-identical to node.
  • Gap spot-check (class/date/reflect suite): 21/23 byte-identical vs node — zero new regressions (the 2 failures are pre-existing on origin/main, confirmed by A/B).
  • cargo fmt, GC store-site inventory, address-classification audit, file-size gate all clean.

What still blocks 8/8 (tracked in #5989)

The deferred class-forward-capture fixes are what got the compiled zod bundle past config-schema validation at boot on the #5874 base. Without #5874's machinery on current main, that path is different and needs re-assessment. Boot-chain diagnostics, repro recipes (require("next/dist/server/config-schema") reproduces the boot-blocking layers in ~2 min without a bundle compile), and the full findings are on #5989.

Summary by CodeRabbit

  • New Features

    • Improved support for Reflect.construct, including correct handling of the optional new-target behavior.
    • Added stricter handling for global assignment in strict mode, allowing existing globals to be updated when present.
  • Bug Fixes

    • Fixed Date construction so instances are created with the correct branding and prototype behavior.
    • Improved stability when validating URL search parameter internals, reducing the risk of crashes during dynamic checks.
    • Strict assignments to missing names still throw the expected ReferenceError.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a strict-mode runtime helper for assigning to existing global bindings (throwing ReferenceError otherwise), wires HIR lowering to use it, implements a real Reflect.construct thunk with Date-specific newTarget handling and direct builtin constructor identification, and hardens URL search params array validation.

Changes

Strict-mode global assignment semantics

Layer / File(s) Summary
Runtime helper declaration and implementation
crates/perry-codegen/src/runtime_decls/strings.rs, crates/perry-runtime/src/error.rs
Declares and implements js_global_assign_existing_or_throw, checking globalThis for an existing binding, writing the value if present, or throwing a ReferenceError otherwise; includes an LTO keepalive anchor.
HIR lowering call sites
crates/perry-hir/src/lower/expr_assign.rs, crates/perry-hir/src/lower/lower_expr/assignment.rs, crates/perry-hir/src/lower/lower_expr/helpers.rs, crates/perry-hir/src/lower/lower_expr.rs, crates/perry-hir/src/lower/mod.rs
Adds strict_global_assign_existing_or_throw helper and wires both strict-mode undeclared-identifier assignment paths to call it instead of unconditionally throwing.
Regression tests
crates/perry/tests/issue_5989_strict_global_builtin_assignment.rs
Adds a compile-and-run test harness plus tests for assignment to an existing global (Date) and to an absent binding (expects ReferenceError).

Reflect.construct real implementation and Date construction via newTarget

Layer / File(s) Summary
Reflect.construct thunk implementation and wiring
crates/perry-runtime/src/object/global_this/bigint_promise.rs, crates/perry-runtime/src/object/global_this.rs, crates/perry-runtime/src/object/global_this/install_static.rs
Adds reflect_construct_thunk delegating to js_reflect_construct, re-exports it, and installs it in place of the previous noop thunk for Reflect.construct.
Date construction via newTarget and builtin constructor identification
crates/perry-runtime/src/object/class_registry/construct.rs, crates/perry-runtime/src/object/class_registry/class_meta.rs
Adds a Date-specific branch applying GetPrototypeFromConstructor(newTarget) in construction, and direct func_ptr-to-name mapping for recognized builtin constructors.

URL search params safety check

Layer / File(s) Summary
Guarded keys_arr type check
crates/perry-runtime/src/url/search_params.rs
Splits the null/length check and validates the GC header type via try_read_gc_header before dereferencing keys_arr.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HIRLowering
  participant js_global_assign_existing_or_throw
  participant globalThis

  HIRLowering->>js_global_assign_existing_or_throw: call(name, value)
  js_global_assign_existing_or_throw->>globalThis: probe existing binding/property
  alt exists
    js_global_assign_existing_or_throw->>globalThis: write value
    js_global_assign_existing_or_throw-->>HIRLowering: return value
  else absent
    js_global_assign_existing_or_throw-->>HIRLowering: throw ReferenceError
  end
Loading
sequenceDiagram
  participant globalThis
  participant reflect_construct_thunk
  participant js_reflect_construct

  globalThis->>reflect_construct_thunk: captured Reflect.construct call
  reflect_construct_thunk->>js_reflect_construct: target, args_like, new_target
  js_reflect_construct-->>reflect_construct_thunk: constructed value
  reflect_construct_thunk-->>globalThis: return constructed value
Loading

Related issues: #5989 (strict global builtin assignment, Reflect.construct)

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: perry-runtime maintainers, perry-hir lowering maintainers

Poem

A rabbit taps the global door,
"Is Date still there?" it checks once more.
If yes, it writes; if no, it throws,
A ReferenceError, as spec-law goes.
Reflect now builds with newTarget's grace,
And URL params guard their keys in place. 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title is specific and clearly matches the main changes, though it is longer than ideal.
Description check ✅ Passed It includes a clear summary, concrete change list, related issue context, and validation, though the checklist and screenshots sections are not filled.
✨ 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 fix/5989-global-builtin-assignment-clean

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.

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

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/url/search_params.rs (1)

849-859: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Materialize lazy arrays before the .length read
GC_TYPE_LAZY_ARRAY does not share the eager ArrayHeader layout, so (*keys_arr).length can read the wrong field or fault. Use the same materialization/length helper used elsewhere before touching keys_arr, or keep lazy arrays out of this path.

🤖 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/url/search_params.rs` around lines 849 - 859, The
`keys_arr` handling in `search_params.rs` reads `(*keys_arr).length` even when
the object may be a `GC_TYPE_LAZY_ARRAY`, which can access the wrong layout.
Update this branch to use the same lazy-array materialization or safe length
helper used elsewhere in the runtime before any length or element access, or
reject lazy arrays here entirely; keep the existing `try_read_gc_header` and
`js_array_get_f64` flow tied to the materialized array path.
🧹 Nitpick comments (1)
crates/perry-hir/src/lower/expr_assign.rs (1)

450-474: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct lowering; consider deduplicating with lower_expr/assignment.rs.

The strict-mode branch here is functionally sound and matches the runtime helper's contract. However, the same ~20-line block (comment + Expr::Call construction) is duplicated verbatim in crates/perry-hir/src/lower/lower_expr/assignment.rs (Lines 44-67). Extracting a small shared helper (e.g., fn strict_global_assign_existing_or_throw(name: String, value: Box<Expr>) -> Expr) would prevent the two call sites from drifting apart.

♻️ Suggested extraction
pub(crate) fn strict_global_assign_existing_or_throw(name: String, value: Box<Expr>) -> Expr {
    Expr::Call {
        callee: Box::new(Expr::ExternFuncRef {
            name: "js_global_assign_existing_or_throw".to_string(),
            param_types: vec![Type::Any, Type::Any],
            return_type: Type::Any,
        }),
        args: vec![Expr::String(name), *value],
        type_args: vec![],
        byte_offset: 0,
    }
}
🤖 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/expr_assign.rs` around lines 450 - 474, The
strict-mode global assignment lowering in Expr::Call is duplicated between
expr_assign.rs and lower_expr/assignment.rs, so extract the shared construction
into a helper and reuse it from both call sites. Create a small shared function
around js_global_assign_existing_or_throw that takes the identifier name and RHS
value, returns the Expr::Call, and update the strict branch in the ExprAssign
lowering and the assignment lowering to call that helper so the two
implementations cannot drift.
🤖 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/block.rs`:
- Around line 121-145: The forward stub for heritage classes is being registered
under the raw class identifier, while the class-body lowering path resolves and
looks up the renamed symbol, so collisions can leave captures pointing at an
undefined stub. Update the class-declaration handling in block lowering to use
the resolved class name consistently when creating the forward boxed local and
inserting into lexical_forward_decls, matching the lookup logic used in
body_stmt lowering. Make sure the fix keeps the superclass-only gating and
existing scope check behavior intact.

---

Outside diff comments:
In `@crates/perry-runtime/src/url/search_params.rs`:
- Around line 849-859: The `keys_arr` handling in `search_params.rs` reads
`(*keys_arr).length` even when the object may be a `GC_TYPE_LAZY_ARRAY`, which
can access the wrong layout. Update this branch to use the same lazy-array
materialization or safe length helper used elsewhere in the runtime before any
length or element access, or reject lazy arrays here entirely; keep the existing
`try_read_gc_header` and `js_array_get_f64` flow tied to the materialized array
path.

---

Nitpick comments:
In `@crates/perry-hir/src/lower/expr_assign.rs`:
- Around line 450-474: The strict-mode global assignment lowering in Expr::Call
is duplicated between expr_assign.rs and lower_expr/assignment.rs, so extract
the shared construction into a helper and reuse it from both call sites. Create
a small shared function around js_global_assign_existing_or_throw that takes the
identifier name and RHS value, returns the Expr::Call, and update the strict
branch in the ExprAssign lowering and the assignment lowering to call that
helper so the two implementations cannot drift.
🪄 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: 3cfdcfcb-07f9-4a46-874a-9d0f5328b204

📥 Commits

Reviewing files that changed from the base of the PR and between 76cc2b9 and 58c7204.

📒 Files selected for processing (14)
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/lower/expr_assign.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower/lower_expr/assignment.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_decl/body_stmt.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-runtime/src/object/class_registry/class_meta.rs
  • crates/perry-runtime/src/object/class_registry/construct.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/bigint_promise.rs
  • crates/perry-runtime/src/object/global_this/install_static.rs
  • crates/perry-runtime/src/url/search_params.rs
  • crates/perry/tests/issue_5989_strict_global_builtin_assignment.rs

Comment on lines +121 to +145
if let ast::Stmt::Decl(ast::Decl::Class(class_decl)) = stmt {
let name = class_decl.ident.sym.to_string();
// Heritage classes ONLY: a class with a superclass binds its
// runtime value through a fresh-class-value `Let`
// (`ClassExprFresh` / dynamic parent), so earlier closures must
// capture the boxed local. A plain class's value IS the static
// `ClassRef` — pre-registering a local for it would make the
// class's OWN body references (e.g. render-result.js's
// `static #_ = this.EMPTY = new RenderResult(...)` self-construct
// during class initialization) resolve to the still-undefined
// stub instead of the class itself.
if class_decl.class.super_class.is_some() && seen_closure_refs.contains(&name) {
let already_in_scope = ctx
.locals
.lookup_index_in_scope(&name, body_entry_locals_len)
.is_some();
if !already_in_scope {
let span_lo = class_decl.ident.span.lo.0;
let id = ctx.define_local(name, Type::Any);
ctx.var_hoisted_ids.insert(id);
forward_boxed_ids.push(id);
ctx.lexical_forward_decls.insert(span_lo, id);
}
}
}

@coderabbitai coderabbitai Bot Jul 5, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Trace ordering: pre_register_forward_captured_lets vs maybe_rename_colliding_class,
# and how body_stmt resolves the class local for reuse.
rg -nP '\b(pre_register_forward_captured_lets|maybe_rename_colliding_class|resolve_class_name|lookup_local_in_current_scope)\b' crates/perry-hir/src

Repository: PerryTS/perry

Length of output: 4301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant lowering flow and name-resolution helpers.
sed -n '1035,1110p' crates/perry-hir/src/lower_decl/block.rs
printf '\n--- body_stmt.rs ---\n'
sed -n '260,430p' crates/perry-hir/src/lower_decl/body_stmt.rs
printf '\n--- context.rs ---\n'
sed -n '400,440p' crates/perry-hir/src/lower/context.rs
printf '\n--- expr_function.rs ---\n'
sed -n '1000,1095p' crates/perry-hir/src/lower/expr_function.rs

Repository: PerryTS/perry

Length of output: 22458


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- crates/perry-hir/src/lower_decl/block.rs (960-1115) ---\n'
sed -n '960,1115p' crates/perry-hir/src/lower_decl/block.rs
printf '\n--- crates/perry-hir/src/lower_decl/body_stmt.rs (390-430) ---\n'
sed -n '390,430p' crates/perry-hir/src/lower_decl/body_stmt.rs
printf '\n--- crates/perry-hir/src/lower_decl/mod.rs ---\n'
sed -n '1,80p' crates/perry-hir/src/lower_decl/mod.rs

Repository: PerryTS/perry

Length of output: 242


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- crates/perry-hir/src/lower_decl/block.rs (960-1115) ---'
sed -n '960,1115p' crates/perry-hir/src/lower_decl/block.rs
printf '%s\n' ''
printf '%s\n' '--- crates/perry-hir/src/lower_decl/body_stmt.rs (390-430) ---'
sed -n '390,430p' crates/perry-hir/src/lower_decl/body_stmt.rs

Repository: PerryTS/perry

Length of output: 12677


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '430,520p' crates/perry-hir/src/lower_decl/body_stmt.rs

Repository: PerryTS/perry

Length of output: 5714


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' crates/perry-hir/src/lower/lower_expr/arm_ident.rs

Repository: PerryTS/perry

Length of output: 11665


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -nP '\b(lookup_class_captures|RegisterClassCaptures|append_new_args_stmt|class_captures|pending_classes|class.name)\b' crates/perry-hir/src/lower_decl crates/perry-hir/src/lower

Repository: PerryTS/perry

Length of output: 50369


Use the resolved class name for the forward stub too
crates/perry-hir/src/lower_decl/block.rs:121-145 pre-registers the stub under the raw class name, but the class-body path in crates/perry-hir/src/lower_decl/body_stmt.rs:405 looks up the resolved name. For a colliding renamed heritage class (XX$0), the earlier capture keeps pointing at the raw stub and stays undefined.

🤖 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/block.rs` around lines 121 - 145, The forward
stub for heritage classes is being registered under the raw class identifier,
while the class-body lowering path resolves and looks up the renamed symbol, so
collisions can leave captures pointing at an undefined stub. Update the
class-declaration handling in block lowering to use the resolved class name
consistently when creating the forward boxed local and inserting into
lexical_forward_decls, matching the lookup logic used in body_stmt lowering.
Make sure the fix keeps the superclass-only gating and existing scope check
behavior intact.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 033ecf8: require GC_TYPE_ARRAY specifically and reject GC_TYPE_LAZY_ARRAY (an object's own keys_array is always eager; a real URLSearchParams shape never has a lazy one). Also extracted the duplicated strict-assign lowering into a shared helper (2a81795).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
git log --oneline -5 --all | head -20 || true
git show --stat 033ecf857 2>/dev/null | head -30 || echo "commit 033ecf857 not found"
git show --stat 2a8179534 2>/dev/null | head -30 || echo "commit 2a8179534 not found"

Length of output: 2527


Confirmed: these two commits address different review threads (the shape_is_url_search_params SIGSEGV hardening and the duplicated strict-assign lowering), not this thread's concern about pre_register_forward_captured_lets using the raw class name instead of the resolved/renamed name for the forward-captured heritage-class stub in crates/perry-hir/src/lower_decl/block.rs (Lines 121–145).

@proggeramlug Thanks for the update — nice cleanup on both. To confirm: those two commits (033ecf8, 2a81795) don't touch crates/perry-hir/src/lower_decl/block.rs, so this particular finding about using ctx.resolve_class_name(...) for the forward stub (to match the lookup in body_stmt.rs around Line 405) is still open. Let me know if you'd like me to hold this thread separately or if it's already tracked elsewhere.

proggeramlug pushed a commit that referenced this pull request Jul 5, 2026
…s keys_array gate

CodeRabbit: a lazy array does not share the eager ArrayHeader layout, so
reading (*keys_arr).length / js_array_get_f64 on one would access the
wrong field. An object's own key list is always an eager GC_TYPE_ARRAY
(the pre-fix code read .length directly, proving it is never lazy here),
so require GC_TYPE_ARRAY specifically and reject lazy arrays — a real
URLSearchParams shape never has a lazy keys_array, so returning false is
correct.
proggeramlug pushed a commit that referenced this pull request Jul 5, 2026
…helper

CodeRabbit: the ~20-line strict-mode global-assignment lowering was
duplicated verbatim in both assignment arms (expr_assign.rs and
lower_expr/assignment.rs). Extract to a shared pub(crate) helper in
lower_expr/helpers.rs so the two call sites can't drift.
Ralph Küpper added 6 commits July 5, 2026 09:47
…ltin is a property write, not a ReferenceError

Strict-mode 'X = v' with no lexical binding for X lowered to an
unconditional ReferenceError in both assignment arms. Per spec (PutValue
on a reference that resolves to the global environment), an EXISTING
global property is a normal property write; only a genuinely absent
binding throws.

Next.js 16's cacheComponents node-environment extensions install their
dynamic-IO clock interceptor exactly this way — strict CJS
'Date = createDate(Date)' — so every server boot printed "Failed to
install `Date` class extension" (the install's catch{} swallowed the
throw) and time-read dynamic detection never armed, one layer of the
dynamic-RSC route hang (#5989). Perry's globalThis machinery already
handles the write and bare-identifier reads observe the override
(verified byte-identical to node); only the assignment lowering blocked
it.

New runtime helper js_global_assign_existing_or_throw does the presence
probe + write-back (mirroring js_global_update's shape for ++x on
globals) and throws the named ReferenceError when absent; both lowering
arms (expr_assign.rs lower_assignment_target + lower_expr/assignment.rs)
now emit it in strict mode. RHS-before-throw evaluation order preserved.
Integration tests cover the Next.js date.js shape end-to-end and the
absent-name ReferenceError.
…l_search_params

The receiver gate (#5964) validates the OBJECT header, but keys_array
was dereferenced raw — a GC_TYPE_OBJECT reached mid-transition (or with
a typed layout) can carry a non-heap word there, and reading
(*keys_arr).length SIGSEGV'd during Next.js request handling (config.js
method dispatch probes arbitrary receivers through this shape check;
first request killed the server). Gate keys_array with the same
try_read_gc_header check (#5429/#5943 family).
…s keys_array gate

CodeRabbit: a lazy array does not share the eager ArrayHeader layout, so
reading (*keys_arr).length / js_array_get_f64 on one would access the
wrong field. An object's own key list is always an eager GC_TYPE_ARRAY
(the pre-fix code read .length directly, proving it is never lazy here),
so require GC_TYPE_ARRAY specifically and reject lazy arrays — a real
URLSearchParams shape never has a lazy keys_array, so returning false is
correct.
…helper

CodeRabbit: the ~20-line strict-mode global-assignment lowering was
duplicated verbatim in both assignment arms (expr_assign.rs and
lower_expr/assignment.rs). Extract to a shared pub(crate) helper in
lower_expr/helpers.rs so the two call sites can't drift.
… real Dates

Three coupled gaps broke Next.js 16's installed Date wrapper (every
construction through it yielded unbranded 'Invalid Date' objects):

1. The reified Reflect.construct VALUE was a no-op stub returning
   undefined — the wrapper calls it through a captured binding
   ('const construct = Reflect.construct'). Route it to the real
   js_reflect_construct (reflect_construct_thunk, mirroring
   reflect_apply_thunk).
2. Reflect.construct(Date, args, newTarget) with a distinct newTarget
   fell to the generic construct tail, which allocates a PLAIN object
   and invokes the Date thunk against it. Build the real branded Date
   and honor GetPrototypeFromConstructor like the typed-array arm.
3. identify_global_builtin_constructor recovered names via the
   globalThis singleton walk, which breaks the moment user code
   REASSIGNS the global binding (the whole point of the extension) —
   the wrapper's captured ORIGINAL constructor stopped being
   identifiable. Dedicated per-builtin thunks now map to their names
   directly, without consulting globalThis.
Syntactic Date.now() sites compile to the builtin intrinsic and bypass
an installed global override (known, separately-tracked gap) — assert
interception through a captured Date.now binding instead.
@proggeramlug proggeramlug force-pushed the fix/5989-global-builtin-assignment-clean branch from 2a81795 to 90ad524 Compare July 5, 2026 08:12
@proggeramlug proggeramlug changed the title fix(hir,runtime): #5989 — Next.js cacheComponents boot chain (strict global-builtin assignment + Date-extension construct + fn-nested class forward captures + USP shape-check SIGSEGV) fix(hir,runtime): #5989 — Next.js cacheComponents boot-chain subset (strict global-builtin assignment + Date-extension construct + URLSearchParams shape-check SIGSEGV) Jul 5, 2026
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Retargeted to the #5874-independent subset. While this PR was in review, main merged #6015 (revert of #5874 "Improve Zod compatibility support", which regressed 322 test262 tests). That revert deleted the bind_fresh_class_value / ClassExprFresh machinery two of the original commits built on (the fn-nested + CJS-export-getter class-forward-capture fixes), so the full set no longer rebases. This PR now carries the 5 fixes that are independent of #5874 (strict global-builtin assignment, Date-extension construct trio, URLSearchParams SIGSEGV guard + both review fixes), rebased onto current main and re-validated: builds green, fmt/audits clean, integration tests pass, Date extension installs+constructs (proto-constructor matches node), 21/23 class/date/reflect gap (2 pre-existing failures, zero new regressions). The two deferred class-forward-capture fixes will be re-derived against the post-revert base once #5874's zod-compat infrastructure re-lands cleanly — tracked on #5989.

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

🤖 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-runtime/src/object/class_registry/construct.rs`:
- Around line 912-928: The heap-pointer check in construct.rs is using a raw
address threshold that can still admit small handle values before the unsafe
GcHeader dereference. Update the has_user_proto logic to reuse the canonical
is_plausible_heap_addr(raw) guard, or an equivalent handle-band plus heap-range
check, before reading hdr in the Object/Array prototype detection path. Keep the
existing is_fn fast path and the hdr.obj_type match, but ensure the unsafe deref
only happens for plausible heap addresses.
🪄 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: 515daca8-3ff1-4f5d-8fa7-da30cf441b57

📥 Commits

Reviewing files that changed from the base of the PR and between 2a81795 and 90ad524.

📒 Files selected for processing (14)
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/lower/expr_assign.rs
  • crates/perry-hir/src/lower/lower_expr.rs
  • crates/perry-hir/src/lower/lower_expr/assignment.rs
  • crates/perry-hir/src/lower/lower_expr/helpers.rs
  • crates/perry-hir/src/lower/mod.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-runtime/src/object/class_registry/class_meta.rs
  • crates/perry-runtime/src/object/class_registry/construct.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/bigint_promise.rs
  • crates/perry-runtime/src/object/global_this/install_static.rs
  • crates/perry-runtime/src/url/search_params.rs
  • crates/perry/tests/issue_5989_strict_global_builtin_assignment.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/perry-hir/src/lower/lower_expr.rs
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/perry-runtime/src/object/global_this/install_static.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/lower/lower_expr/assignment.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-hir/src/lower/lower_expr/helpers.rs
  • crates/perry-hir/src/lower/mod.rs
  • crates/perry-runtime/src/object/class_registry/class_meta.rs
  • crates/perry-runtime/src/object/global_this/bigint_promise.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-hir/src/lower/expr_assign.rs
  • crates/perry-runtime/src/url/search_params.rs
  • crates/perry/tests/issue_5989_strict_global_builtin_assignment.rs

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 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-runtime/src/object/class_registry/construct.rs`:
- Around line 912-928: The heap-pointer check in construct.rs is using a raw
address threshold that can still admit small handle values before the unsafe
GcHeader dereference. Update the has_user_proto logic to reuse the canonical
is_plausible_heap_addr(raw) guard, or an equivalent handle-band plus heap-range
check, before reading hdr in the Object/Array prototype detection path. Keep the
existing is_fn fast path and the hdr.obj_type match, but ensure the unsafe deref
only happens for plausible heap addresses.
🪄 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: 515daca8-3ff1-4f5d-8fa7-da30cf441b57

📥 Commits

Reviewing files that changed from the base of the PR and between 2a81795 and 90ad524.

📒 Files selected for processing (14)
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/lower/expr_assign.rs
  • crates/perry-hir/src/lower/lower_expr.rs
  • crates/perry-hir/src/lower/lower_expr/assignment.rs
  • crates/perry-hir/src/lower/lower_expr/helpers.rs
  • crates/perry-hir/src/lower/mod.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-runtime/src/object/class_registry/class_meta.rs
  • crates/perry-runtime/src/object/class_registry/construct.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/bigint_promise.rs
  • crates/perry-runtime/src/object/global_this/install_static.rs
  • crates/perry-runtime/src/url/search_params.rs
  • crates/perry/tests/issue_5989_strict_global_builtin_assignment.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/perry-hir/src/lower/lower_expr.rs
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/perry-runtime/src/object/global_this/install_static.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/lower/lower_expr/assignment.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-hir/src/lower/lower_expr/helpers.rs
  • crates/perry-hir/src/lower/mod.rs
  • crates/perry-runtime/src/object/class_registry/class_meta.rs
  • crates/perry-runtime/src/object/global_this/bigint_promise.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-hir/src/lower/expr_assign.rs
  • crates/perry-runtime/src/url/search_params.rs
  • crates/perry/tests/issue_5989_strict_global_builtin_assignment.rs
🛑 Comments failed to post (1)
crates/perry-runtime/src/object/class_registry/construct.rs (1)

912-928: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the handle-band / valid-obj-ptr helpers and their module path used by is_closure_ptr.
rg -nP 'pub (fn|const fn) (is_handle_band|is_valid_obj_ptr)\b' crates/perry-runtime/src
# Check whether any dynamic "prototype" value can carry a handle-band pointer tag.
rg -nP 'closure_(set|get)_dynamic_prop' crates/perry-runtime/src -C2 | rg -n 'prototype'

Repository: PerryTS/perry

Length of output: 15629


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- construct.rs around the cited branch ---'
sed -n '890,935p' crates/perry-runtime/src/object/class_registry/construct.rs

echo
echo '--- value/addr_class.rs helpers ---'
sed -n '1,180p' crates/perry-runtime/src/value/addr_class.rs

echo
echo '--- closure/dynamic_props.rs is_closure_ptr context ---'
rg -n 'pub fn is_closure_ptr|is_valid_obj_ptr|is_handle_band|SIGSEGV|Headers' crates/perry-runtime/src/closure/dynamic_props.rs -n -C 4

echo
echo '--- prototype_objects.rs relevant prototype handling ---'
sed -n '1,80p' crates/perry-runtime/src/object/class_registry/prototype_objects.rs

echo
echo '--- any direct use of dp.is_pointer in construct.rs nearby ---'
rg -n 'is_pointer\(\)|has_user_proto|ensure_function_prototype_object|closure_get_dynamic_prop' crates/perry-runtime/src/object/class_registry/construct.rs -n -C 3

Repository: PerryTS/perry

Length of output: 24698


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'is_plausible_heap_addr\(' crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 1391


Use the canonical heap-pointer guard here. dp.is_pointer() can still be a small handle value, and raw >= GC_HEADER_SIZE + 0x1000 doesn’t exclude the handle band, so a fetch/Headers-style id can fault on the GcHeader read. Reuse is_plausible_heap_addr(raw) (or the same handle-band + heap-range check) before the unsafe deref.

🤖 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/construct.rs` around lines 912
- 928, The heap-pointer check in construct.rs is using a raw address threshold
that can still admit small handle values before the unsafe GcHeader dereference.
Update the has_user_proto logic to reuse the canonical
is_plausible_heap_addr(raw) guard, or an equivalent handle-band plus heap-range
check, before reading hdr in the Object/Array prototype detection path. Keep the
existing is_fn fast path and the hdr.obj_type match, but ensure the unsafe deref
only happens for plausible heap addresses.

@proggeramlug proggeramlug merged commit a23be4f into main Jul 5, 2026
16 of 17 checks passed
@proggeramlug proggeramlug deleted the fix/5989-global-builtin-assignment-clean branch July 5, 2026 09:13
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