Skip to content

runtime: serve Object.prototype-inherited globalThis members on bare-identifier dispatch (#6652) - #6656

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/6652-global-proto-members
Jul 19, 2026
Merged

runtime: serve Object.prototype-inherited globalThis members on bare-identifier dispatch (#6652)#6656
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/6652-global-proto-members

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Wall #6 of the pi coding-agent bring-up (tracker #6564; prior walls #6593, #6604, #6644, #6649, #6651 all fixed).

Root cause

Node resolves a bare hasOwnProperty through the scope chain to the global object, which inherits Object.prototype.hasOwnProperty — so hasOwnProperty.call(o, k) works. In perry, arm_ident.rs's unknown-identifier lowering behaved differently depending on position:

  • bare read / typeof / call-callee: routed through the js_global_get_or_throw_unresolved by-name runtime lookup — already correct (serves inherited members, identity preserved).
  • member-OBJECT position (ctx.unresolved_ident_as_global, set by lower_member): collapsed the ident to the bare GlobalGet(0) sentinel — which IS globalThis — discarding the identifier name entirely. hasOwnProperty.call(o, k) therefore read globalThis.call (undefined) and threw TypeError: value is not a function. Same lost-name bug made a runtime-created global's member access read the member off globalThis instead (myGlobal.propglobalThis.prop).

Trigger: @babel/types/lib/definitions/placeholders.js (hasOwnProperty.call(o, t4) || (o[t4] = []), pi-bundle.mjs:203852; 14 hasOwnProperty + 2 bare-toString sites) during pi-native module init.

Fix

crates/perry-hir/src/lower/lower_expr/arm_ident.rs: route all unknown identifiers through the existing js_global_get_or_throw_unresolved runtime lookup — member-object position included. The lookup resolves through js_object_get_field_by_name on globalThis, which serves own AND Object.prototype-inherited members with identity preserved, and still throws the spec ReferenceError: X is not defined on a true miss (now correctly localized to the base identifier instead of a wrong member read). Known globals keep the GlobalGet(0) sentinel and their intrinsic member routing (Math.max, Date.now, …) — untouched.

The compile warning now describes the real behavior:
unknown identifier 'X' — assuming global; resolved by name on globalThis (incl. Object.prototype-inherited members) at runtime.

Receiver semantics (verified against node v26.3.0, both ESM/strict and sloppy CJS): a bare CALL of such an identifier gets this = undefined — the global environment record's WithBaseObject is undefined; builtins don't coerce. toString()"[object Undefined]", hasOwnProperty("x")TypeError: Cannot convert undefined or null to object. Perry already matched this on the call-callee path; unchanged.

"bare reads lower to 0": no longer true for unknown identifiers — they never lower to the bare sentinel anymore. (A bare VALUE read of a known global that has no value-form, e.g. const q = queueMicrotask, keeps its pre-existing behavior — out of scope here.)

Tests

  • crates/perry/tests/issue_6652_global_proto_inherited_members.rs — the @babel/types shape + extraction/identity/typeof/bare-call-this-undefined/in-function/ReferenceError-miss, and the runtime-created-global member-access case; expected output is node v26's byte-for-byte.
  • test-parity/node-suite/globals/global-object-prototype-inherited-members.ts — live node-vs-perry parity fixture (10 sections incl. valueOf/isPrototypeOf/propertyIsEnumerable); verified stdout+stderr+rc identical to node --experimental-strip-types.
  • cargo test -p perry-runtime --lib -- --test-threads=1: 1426 passed, 0 failed.
  • perry-hir/perry-codegen lib tests green; at-risk integration tests (with_implicit_global_fallback, issue_5253_construct_reference_source_location, issue_forward_captured_lets_comma_decl, issue_4858, issue_5459, issue_5989, issue_6560_bun_globals, module_destructured_globals) all green. Pre-existing failures NOT from this change (fail identically on the branch base): c262_parity::logical_property_assignment_short_circuits_the_store_4586, and two perry-codegen test targets that don't compile (i64_spec_ternary_recursion, perry_builtin_name_collision — stale CompileOptions.namespace_reexport_named_imports field).

pi bring-up status

Full pi recompile with this fix: wall #6 is down — pi-native --version/--help get past pi-bundle.mjs:203852 (the pre-fix binary still throws value is not a function there). Module init now advances to a NEW, later wall #7: TypeError: Method Set.prototype.add called on incompatible receiver (builtin-subclass family — the bundle's var GaxiosInterceptorManager = class extends Set {} CJS-factory class expression is the prime suspect, adjacent to the known class extends Array gap #6232). Will be filed separately with the debug-symbols line.

Stacks on #6653 (branched from fix/6651-createrequire-allowlist-family); the diff shown includes those commits until #6653 (and its parents) merge.

Fixes #6652

https://claude.ai/code/session_01JuiiePQfrXhAFD9fuCygB9

Summary by CodeRabbit

  • Bug Fixes
    • Improved runtime resolution of bare global identifiers when used in member-access position.
    • Fixed handling of inherited Object.prototype members (method identity, dispatch, and error behavior) to match Node.
    • Ensured truly unresolved identifiers throw the expected ReferenceError.
    • Preserved access to globals created at runtime.
    • Improved diagnostic string previews to truncate safely at UTF-8 character boundaries.
  • Tests
    • Added regression tests for global Object.prototype inherited members, function-scope behavior, runtime-created globals, and exact error messaging parity.

@coderabbitai

coderabbitai Bot commented Jul 19, 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: fbaa0856-49d9-4ca7-b2c5-ccdea1575a07

📥 Commits

Reviewing files that changed from the base of the PR and between 27e7342 and 56d291a.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/value/dynamic_arith.rs

📝 Walkthrough

Walkthrough

Unknown identifiers in member-object position now resolve at runtime through globalThis, including inherited Object.prototype members. Regression tests compare these behaviors with Node output, and diagnostic string previews now truncate safely at UTF-8 boundaries.

Changes

Global resolution

Layer / File(s) Summary
Unknown identifier lookup
crates/perry-hir/src/lower/lower_expr/arm_ident.rs
Runtime unresolved-global lookup is retained for member-object identifiers, with a targeted warning replacing the previous generic warning.
Global lookup regression coverage
crates/perry/tests/issue_6652_global_proto_inherited_members.rs, test-parity/node-suite/globals/global-object-prototype-inherited-members.ts
Tests cover inherited Object.prototype members, bare-call receiver behavior, function-scope access, runtime-created globals, and ReferenceError output.

Diagnostic preview safety

Layer / File(s) Summary
UTF-8-safe operand descriptions
crates/perry-runtime/src/value/dynamic_arith.rs
Mixed-operand string previews truncate at a valid UTF-8 character boundary within the 80-byte limit.

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

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main runtime change for inherited Object.prototype members in bare-identifier dispatch.
Description check ✅ Passed The description covers the bug, fix, related issue, and tests, but it doesn't use the required template headings or checklist format.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

🧹 Nitpick comments (2)
crates/perry/tests/issue_6652_global_proto_inherited_members.rs (1)

1-135: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider moving this coverage to a cargo-test-visible unit test.

As per path instructions, prefer acceptance coverage in cargo-test-visible unit tests because integration suites under crates/*/tests/*.rs do not run on every PR. While retrieved learnings suggest that tests executing the full compile → link → run path belong in integration tests, please verify if this coverage can be adequately represented as a unit test to ensure it runs on every PR.

🤖 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/tests/issue_6652_global_proto_inherited_members.rs` around lines
1 - 135, Move the regression coverage from the integration test functions
object_prototype_inherited_globals_match_node and
runtime_created_global_member_access_matches_node into a cargo-test-visible
unit-test location, if the behavior can be represented without the full
compile-and-run harness. Preserve assertions for inherited global lookup,
runtime-created global member access, receiver semantics, and
unresolved-identifier errors; retain integration coverage only where unit
testing cannot exercise the required compiler/runtime path.

Sources: Path instructions, Learnings

tests/test_class_expr_capture_refresh_6604.sh (1)

154-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Capture test_bin's exit status explicitly before comparing output.

RUN_OUTPUT=$(./test_bin 2>&1) doesn't check the run's exit code. With set -e active, a crash (segfault, panic, etc. — the very failure mode this regression guards against, per the header's "TypeError: value is not a function" note) aborts the whole script at that line, skipping the descriptive FAIL: ... block and leaving a bare non-zero exit with no diagnostic. Capturing $? (or temporarily disabling set -e around the call) would let the existing FAIL branch report a crash the same way it reports a stale-snapshot mismatch.

🧪 Suggested fix
-RUN_OUTPUT=$(./test_bin 2>&1)
+set +e
+RUN_OUTPUT=$(./test_bin 2>&1)
+RUN_EXIT=$?
+set -e
+if [ "$RUN_EXIT" -ne 0 ]; then
+  echo "FAIL: test_bin exited with status $RUN_EXIT"
+  echo "$RUN_OUTPUT"
+  exit 1
+fi

As per path instructions, **/*.sh files should check the harness exit code directly rather than relying on output alone.

🤖 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 `@tests/test_class_expr_capture_refresh_6604.sh` around lines 154 - 166,
Capture test_bin’s exit status immediately after the RUN_OUTPUT command while
avoiding set -e aborting the script, then include that status in the existing
PASS/FAIL decision. Update the comparison around RUN_OUTPUT and EXPECTED so a
nonzero test_bin exit follows the descriptive FAIL path, while successful
matching output still reports PASS.

Source: Path instructions

🤖 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/value/dynamic_arith.rs`:
- Around line 45-46: Update the string limiting logic in the dynamic arithmetic
diagnostic path to cap the value at 80 Unicode characters rather than truncating
at byte offset 80. Preserve the existing string conversion and ensure multi-byte
UTF-8 content cannot trigger a panic.
- Around line 59-67: Update throw_mix_bigint and its diagnostic path to root
both NaN-boxed operands with crate::gc::RuntimeHandleScope before calling
describe_mix_operand. After each potentially allocating description call, reload
the corresponding operand from its rewritten handle so the second description
cannot use an evacuated pointer; preserve the existing diagnostic output and
non-diagnostic behavior.
- Around line 718-721: Update js_dynamic_ushr to use
crate::gc::RuntimeHandleScope and root the original operands and the result of
to_numeric(a) across calls that may execute user code. Ensure b remains rooted
before calling to_numeric(a), and both converted values remain rooted before
both_bigint_or_throw(a, b) and subsequent arithmetic.

---

Nitpick comments:
In `@crates/perry/tests/issue_6652_global_proto_inherited_members.rs`:
- Around line 1-135: Move the regression coverage from the integration test
functions object_prototype_inherited_globals_match_node and
runtime_created_global_member_access_matches_node into a cargo-test-visible
unit-test location, if the behavior can be represented without the full
compile-and-run harness. Preserve assertions for inherited global lookup,
runtime-created global member access, receiver semantics, and
unresolved-identifier errors; retain integration coverage only where unit
testing cannot exercise the required compiler/runtime path.

In `@tests/test_class_expr_capture_refresh_6604.sh`:
- Around line 154-166: Capture test_bin’s exit status immediately after the
RUN_OUTPUT command while avoiding set -e aborting the script, then include that
status in the existing PASS/FAIL decision. Update the comparison around
RUN_OUTPUT and EXPECTED so a nonzero test_bin exit follows the descriptive FAIL
path, while successful matching output still reports PASS.
🪄 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: 2062d353-7a4c-4b21-8c1b-83d36e9cf4a5

📥 Commits

Reviewing files that changed from the base of the PR and between d01ec0b and 8dde6fb.

📒 Files selected for processing (24)
  • crates/perry-codegen/src/codegen/module_globals_emit.rs
  • crates/perry-codegen/src/lower_call/native_table/node_core/module_sea_tls_test.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower/lower_expr/arm_class.rs
  • crates/perry-hir/src/lower/lower_expr/arm_ident.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_patterns.rs
  • crates/perry-runtime/src/module_require.rs
  • crates/perry-runtime/src/node_submodules/mod.rs
  • crates/perry-runtime/src/process.rs
  • crates/perry-runtime/src/process/node_module.rs
  • crates/perry-runtime/src/typedarray/bigint.rs
  • crates/perry-runtime/src/value/dynamic_arith.rs
  • crates/perry/tests/createrequire_builtin_modules.rs
  • crates/perry/tests/issue_6652_global_proto_inherited_members.rs
  • crates/perry/tests/module_destructured_globals.rs
  • test-parity/node-suite/bigint/arithmetic/mixed-operand-errors.ts
  • test-parity/node-suite/globals/global-object-prototype-inherited-members.ts
  • test-parity/node-suite/node-core/module-destructure-function-visibility.ts
  • test-parity/node-suite/object/class-expr-capture-refresh.ts
  • tests/test_class_expr_capture_refresh_6604.sh

Comment thread crates/perry-runtime/src/value/dynamic_arith.rs Outdated
Comment thread crates/perry-runtime/src/value/dynamic_arith.rs
Comment thread crates/perry-runtime/src/value/dynamic_arith.rs
…identifier dispatch (PerryTS#6652)

Sloppy/global bare identifiers that resolve to Object.prototype-inherited
members of the global object (hasOwnProperty, toString, valueOf, ...) threw
'TypeError: value is not a function' in member-object position:
arm_ident's unknown-identifier lowering collapsed the ident to the bare
GlobalGet(0) sentinel (which IS globalThis), discarding the identifier
name, so hasOwnProperty.call(o, k) dispatched '.call' against globalThis.
Node resolves the bare ident through the scope chain to the global object,
which inherits Object.prototype.hasOwnProperty.

Fix: route ALL unknown identifiers through the existing
js_global_get_or_throw_unresolved by-name runtime lookup — member-object
position included — which serves globalThis' own and inherited members
with identity preserved and still throws the spec ReferenceError on a
true miss. Known globals keep the GlobalGet(0) sentinel and their
intrinsic member routing. The compile warning text now describes the new
behavior.

Also fixes runtime-created globals in member position (myGlobal.prop read
globalThis.prop pre-fix) as the same lost-name bug.

Trigger: @babel/types/lib/definitions/placeholders.js
(hasOwnProperty.call(o, t4), 14 sites + 2 bare-toString sites in the pi
bundle) during pi-native module init — pi bring-up wall PerryTS#6.

Fixes PerryTS#6652

Claude-Session: https://claude.ai/code/session_01JuiiePQfrXhAFD9fuCygB9
@proggeramlug
proggeramlug force-pushed the fix/6652-global-proto-members branch from 8dde6fb to 27e7342 Compare July 19, 2026 02:38
@proggeramlug

Copy link
Copy Markdown
Contributor Author

CodeRabbit findings: the two dynamic_arith GC-rooting items were already fixed (169456b, merged via #6650 — this branch picked them up on rebase, hence the ✅ marks). The remaining live one — byte-index String::truncate(80) panicking on multi-byte UTF-8 boundaries in the diag preview — is fixed in 56d291a (char-boundary-safe cap).

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.

compile: bare hasOwnProperty.call(...) (Object.prototype member via globalThis) throws 'value is not a function' — pi wall #6

1 participant