Skip to content

[codex] fix Three-style native class semantics#5225

Open
andrewtdiz wants to merge 6 commits into
PerryTS:mainfrom
andrewtdiz:codex/fix-three-native-class-semantics-20260615
Open

[codex] fix Three-style native class semantics#5225
andrewtdiz wants to merge 6 commits into
PerryTS:mainfrom
andrewtdiz:codex/fix-three-native-class-semantics-20260615

Conversation

@andrewtdiz

@andrewtdiz andrewtdiz commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix the remaining Three-style native class semantics on top of current main. The source-visible/symlinked relative import resolution piece is already present in the current base via #5210, so this PR is rebased over it and avoids re-adding duplicate resolver tests.

This PR carries:

  • TypeScript declare class props no longer become runtime instance fields or static slots.
  • Cross-module inherited accessor metadata is propagated into HIR lowering and native subclass codegen.
  • Three-like regressions for multilevel constructor state and inherited accessors.

Root Causes

  • Imported inherited accessors were not seeded into HIR lowering, so assignments such as this.image = ... could bypass inherited setters.
  • TypeScript declare class props were treated as runtime slots, shadowing constructor-created state or defaults.
  • The original downstream failure also depended on temp-entry relative imports through /var; that resolver issue is now in the base branch via [codex] Fix symlinked relative import resolution #5210.

Impact

  • Fixes PerspectiveCamera inherited Object3D state when combined with the current base resolver fix.
  • Fixes DataTexture image/default/override/version behavior.
  • Keeps the symlink/source-visible import regression covered through the current base while this PR covers the remaining class semantics.

Caveat

The remaining downstream DataTexture.needsUpdate.sourceFlag smoke check is a setter-only Source.needsUpdate oracle, not a Perry runtime failure.

Validation

  • cargo fmt --check
  • git diff --check
  • cargo build -p perry
  • cargo test -p perry lexical_path_tests
  • cargo test -p perry symlinked_entry_resolves_relative_imports_from_lexical_path
  • cargo test -p perry js_import_scan_follows_bare_dot_and_dotdot_specifiers
  • cargo build --release -p perry-runtime
  • PERRY_BIN=/Users/andrew/.codex/worktrees/05e0/perry/target/debug/perry tests/test_symlinked_entry_imported_class.sh
  • PERRY_BIN=/Users/andrew/.codex/worktrees/05e0/perry/target/debug/perry tests/test_xmod_imported_multilevel_ctor_state.sh
  • PERRY_BIN=/Users/andrew/.codex/worktrees/05e0/perry/target/debug/perry tests/test_imported_inherited_accessor_datatexture.sh
  • PERRY_CLI=/Users/andrew/.codex/worktrees/05e0/perry/target/debug/perry PERRY_RUNTIME_DIR=/Users/andrew/.codex/worktrees/05e0/perry/target/release npm run perry:smoke from /Users/andrew/Documents/perry-three

Downstream acceptance: imported-super, two-arg Math, public Three prototype, and public Three camera passed; public Three texture is 24/25 with only DataTexture.needsUpdate.sourceFlag remaining.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed cross-module imported class getter/setter dispatch, including correct behavior through inheritance chains.
    • Improved prototype accessor matching so accessor methods route correctly during assignments.
    • Ensured TypeScript declare static class properties aren’t treated as runtime statics.
    • Added support for additional cluster EventEmitter-style APIs (e.g., on, addListener, once, emit, removeListener, removeAllListeners).
  • Tests
    • Added regression coverage for imported inherited accessors and constructor-state preservation across multi-level inheritance.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds end-to-end cross-module class accessor support: a new ClassAccessorNames type consolidates getter/setter tracking; CompilationContext gains a cross_module_class_accessors map; bootstrap collects and propagates accessor names across the inheritance graph; HIR lowering APIs are extended to accept and seed these names; codegen synthesizes getter/setter stub functions for imported class stubs; and class_field_global_index gains a cycle-guarded inheritance-chain check to prevent packed-field reads for accessor-backed properties. declare-annotated ClassProp members are also excluded from static-field registration and main lowering passes. Additionally, native dispatch is extended to recognize and lower EventEmitter-style method calls on the cluster module's default export.

Changes

Cross-module accessor pipeline

Layer / File(s) Summary
ClassAccessorNames type and module setup
crates/perry-hir/src/class_accessors.rs, crates/perry-hir/src/lib.rs, crates/perry-hir/src/lower/lowering_context.rs
Introduces ClassAccessorNames struct with getter_names and setter_names vectors, plus helper methods (is_empty, contains_any, insert_getter, insert_setter, extend_from). Registers module in lib.rs and imports type in lowering_context for use in class accessor registry storage.
LoweringContext accessor registry API updates
crates/perry-hir/src/lower/context.rs
Updates register_class_accessor_names to accept ClassAccessorNames directly instead of Vec<String>. Updates lookup_class_accessor_names return type to Option<&ClassAccessorNames>. Adds new seed_imported_class_accessors method to pre-populate accessor names from cross-module dependencies.
CompilationContext field and cross-module accessor collection
crates/perry/src/commands/compile/types.rs, crates/perry/src/commands/compile/bootstrap.rs
Adds cross_module_class_accessors: HashMap<String, ClassAccessorNames> field to CompilationContext (initialized empty). rerun_collect_with_class_field_types builds accessor_map and parent_map, runs a fixpoint propagation loop to inherit parent accessors to child classes, and seeds the context with both field types and accessor names before module re-collection.
Module lowering API extensions
crates/perry-hir/src/lower/lower_module_fn.rs
Extends lower_module_with_class_id_types_seed_and_entry and lower_module_full function signatures with imported_class_accessors: Option<&HashMap<...>> parameter. During lowering context initialization, seeds the context via ctx.seed_imported_class_accessors(...) when accessors are provided. Tightens static property registration to exclude declare-marked fields.
Class declaration/expression lowering: declare filtering and accessor refactoring
crates/perry-hir/src/lower_decl/class_decl.rs
Filters declare-annotated ClassProp members from static-field registration and main member lowering for both class declarations and expressions. Introduces runtime_instance_accessor_names helper to build ClassAccessorNames from non-static instance getter/setter Methods and PrivateMethod accessors. Refactors constructor-body accessor tracking from HashSet<String> to ClassAccessorNames, using insert_getter/insert_setter for collection, extend_from for parent unions, contains_any for conflict checks, and passes ClassAccessorNames directly to registration calls.
Expression assignment accessor check refactor
crates/perry-hir/src/lower/expr_assign.rs
Refactors prototype accessor-name matching in lower_assignment_target to use a local class_has_accessor helper that combines ctx.lookup_class_accessor_names(class_name).is_some_and(...) with names.contains_any(method_name), aligning with the new ClassAccessorNames API for both direct and aliased prototype-method dispatch paths.
Threading imported_class_accessors into module lowering
crates/perry/src/commands/compile/collect_modules.rs
Derives imported_class_accessors from ctx.cross_module_class_accessors (or None when empty) and passes it to perry_hir::lower_module_full alongside the existing imported_class_fields.
Codegen stub synthesis and field-vs-accessor dispatch guard
crates/perry-codegen/src/codegen/mod.rs, crates/perry-codegen/src/type_analysis_class_fields.rs
Synthesizes empty-bodied perry_hir::Function objects for each imported getter/setter name and populates stub Class.getters/Class.setters by zipping names with synthesized functions. Adds accessor_in_chain cycle-guarded helper that walks the extends_name chain; class_field_global_index returns None immediately when a matching accessor is found, routing access through prototype dispatch instead of packed-slot reads.
Test fixtures and regression tests
test-files/_helpers/three_like_descriptor_bases.ts, test-files/test_three_like_native_class_descriptors.ts, tests/test_imported_inherited_accessor_datatexture.sh, tests/test_xmod_imported_multilevel_ctor_state.sh
Adds Object3D, Camera, and Texture helper classes with descriptor-based and getter/setter patterns. Adds static test file validating descriptor inheritance and inherited getter/setter routing across subclasses without creating unintended own properties. Adds two bash regression tests: one verifying inherited cross-module accessor semantics and edge cases (DataTextureLike extending BaseTexture with accessor aliasing and static-vs-instance pollution checks), one verifying multi-level constructor state replay across three imported class levels (PerspectiveLike → CameraBase → BaseObject).

Cluster module EventEmitter-style native dispatch

Layer / File(s) Summary
Node cluster EventEmitter method dispatch table
crates/perry-codegen/src/lower_call/native_table/node_misc.rs
Adds NativeModSig entries to NODE_MISC_ROWS for cluster listener/emit-style methods (on, addListener, once, prependListener, prependOnceListener, emit, eventNames, listenerCount, removeListener, off, removeAllListeners), each mapped to corresponding js_cluster_* runtime symbols with argument and return type conventions.
Cluster module method recognition and lowering
crates/perry-hir/src/lower/expr_call/native_module.rs
Introduces is_cluster_default_event_emitter_method helper recognizing EventEmitter-style method names. Extends try_native_module_methods dispatch: when calling cluster module member with imported default and method name matches the helper, lowers directly to Expr::NativeMethodCall for static cluster dispatch.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

🐇 Hop, hop across the module lines,
Accessors wired through getter vines!
No packed-slot reads where setters dwell,
The inheritance chain now works quite well.
Declare fields skip the runtime race —
The rabbit stamps each stub in place! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing Three-style native class semantics, which aligns with the PR's primary objective of addressing inherited accessor metadata and TypeScript declare property handling.
Description check ✅ Passed The PR description provides comprehensive coverage of all required sections: Summary, Changes (implicit via root causes and impact), Related issue context, Test plan with specific validation commands, and complete Checklist compliance with workspace version/metadata guidelines.
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 unit tests (beta)
  • Create PR with unit tests

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

@andrewtdiz andrewtdiz force-pushed the codex/fix-three-native-class-semantics-20260615 branch from 609ec79 to 7ad6506 Compare June 16, 2026 02:03
@andrewtdiz andrewtdiz marked this pull request as ready for review June 16, 2026 02:40

@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/src/commands/compile/types.rs`:
- Around line 623-627: The cross_module_class_accessors field uses Vec<String>
which loses the distinction between getter-only, setter-only, and getter+setter
accessors, causing incorrect semantics downstream in the stub/dispatch path.
Replace the Vec<String> with a data structure that explicitly tracks getter and
setter capabilities separately (such as a struct with separate getter_names and
setter_names fields or similar). Update all code that populates and consumes
this HashMap to store and use the getter/setter information from separate sets
rather than a single flattened string vector, ensuring each accessor kind is
preserved and propagated independently throughout the HIR lowering process.
🪄 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: 16ad032c-f8ca-4ec2-987f-cb3e03c7c906

📥 Commits

Reviewing files that changed from the base of the PR and between 07aba52 and 7ad6506.

📒 Files selected for processing (12)
  • crates/perry-codegen/src/codegen/mod.rs
  • crates/perry-codegen/src/type_analysis_class_fields.rs
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/lower_module_fn.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry/src/commands/compile/bootstrap.rs
  • crates/perry/src/commands/compile/collect_modules.rs
  • crates/perry/src/commands/compile/types.rs
  • test-files/_helpers/three_like_descriptor_bases.ts
  • test-files/test_three_like_native_class_descriptors.ts
  • tests/test_imported_inherited_accessor_datatexture.sh
  • tests/test_xmod_imported_multilevel_ctor_state.sh

Comment thread crates/perry/src/commands/compile/types.rs Outdated

@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-hir/src/lower_decl/class_decl.rs (1)

1003-1034: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep static accessors out of instance accessor metadata.

Both paths can register static get/set x as an instance accessor name. That makes constructor field inference skip this.x = ... and makes C.prototype.x = ... look accessor-backed even when only the constructor has a static accessor. Filter to runtime instance accessors before inserting.

Suggested direction
-                ast::ClassMember::Method(m)
-                    if matches!(m.kind, ast::MethodKind::Getter | ast::MethodKind::Setter) =>
+                ast::ClassMember::Method(m)
+                    if !m.is_static
+                        && m.function.body.is_some()
+                        && matches!(m.kind, ast::MethodKind::Getter | ast::MethodKind::Setter) =>
                 {
-                ast::ClassMember::PrivateMethod(m)
-                    if matches!(m.kind, ast::MethodKind::Getter | ast::MethodKind::Setter) =>
+                ast::ClassMember::PrivateMethod(m)
+                    if !m.is_static
+                        && m.function.body.is_some()
+                        && matches!(m.kind, ast::MethodKind::Getter | ast::MethodKind::Setter) =>
                 {

For the class-expression path, avoid deriving the registry from the unqualified getters / setters vectors unless those vectors can distinguish static from instance accessors; otherwise collect from class.body with the same !is_static && body.is_some() filter.

Also applies to: 1678-1687

🤖 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 1003 - 1034, Both
the Method and PrivateMethod blocks are registering static getters/setters as
instance accessors without filtering. Add a check to exclude static accessors
before inserting into the accessor_names registry. For the Method block handling
ast::ClassMember::Method, verify that the method is not static (check
m.is_static or equivalent property) before calling insert_getter or
insert_setter. Apply the same filtering logic at the second location mentioned
(lines 1678-1687) which handles class expressions. Only instance accessors
(where is_static is false) should be registered in the accessor_names to prevent
constructor field inference issues and incorrect prototype assignment detection.
🤖 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/expr_assign.rs`:
- Around line 646-648: The accessor guard using lookup_class_accessor_names only
protects against mis-lowering accessor writes for direct C.prototype.x
assignments, but the aliased prototype path (where a variable holds a reference
to C.prototype before assignment) still incorrectly returns ProtoOwner::Class.
Apply the same accessor name checking logic to the code path that handles
aliased prototype assignments to ensure that when an accessor name is detected,
both direct and aliased assignments are treated consistently and not mis-lowered
as prototype monkey-patches.

---

Outside diff comments:
In `@crates/perry-hir/src/lower_decl/class_decl.rs`:
- Around line 1003-1034: Both the Method and PrivateMethod blocks are
registering static getters/setters as instance accessors without filtering. Add
a check to exclude static accessors before inserting into the accessor_names
registry. For the Method block handling ast::ClassMember::Method, verify that
the method is not static (check m.is_static or equivalent property) before
calling insert_getter or insert_setter. Apply the same filtering logic at the
second location mentioned (lines 1678-1687) which handles class expressions.
Only instance accessors (where is_static is false) should be registered in the
accessor_names to prevent constructor field inference issues and incorrect
prototype assignment detection.
🪄 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: 03460be8-5a48-4a6f-96cc-1144c9fc5423

📥 Commits

Reviewing files that changed from the base of the PR and between 7ad6506 and 8a30448.

📒 Files selected for processing (10)
  • crates/perry-hir/src/class_accessors.rs
  • crates/perry-hir/src/lib.rs
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/expr_assign.rs
  • crates/perry-hir/src/lower/lower_module_fn.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry/src/commands/compile/bootstrap.rs
  • crates/perry/src/commands/compile/types.rs
  • tests/test_imported_inherited_accessor_datatexture.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_imported_inherited_accessor_datatexture.sh

Comment thread crates/perry-hir/src/lower/expr_assign.rs Outdated

@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-hir/src/lower/expr_call/native_module.rs`:
- Around line 1498-1504: In the cluster default event emitter method dispatch
block where is_cluster_default_event_emitter_method returns true, the code
constructs an Expr::NativeMethodCall that uses the non-normalized module_name
for the module field. However, the dispatch table in the codegen layer expects
the normalized module name ("cluster" instead of "node:cluster"). Change line
1503 in the Expr::NativeMethodCall construction from module:
module_name.to_string() to module: normalized_module.to_string() to ensure the
emitted module name matches the dispatch table keys, matching the pattern used
in similar dispatch logic elsewhere in the file.
🪄 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: 214ae6df-ecba-4ff9-a65c-05d722e6fbf2

📥 Commits

Reviewing files that changed from the base of the PR and between 805ae05 and 20c317c.

📒 Files selected for processing (4)
  • crates/perry-codegen-arkts/src/tests.rs
  • crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs
  • crates/perry-codegen/src/lower_call/native_table/node_misc.rs
  • crates/perry-hir/src/lower/expr_call/native_module.rs

Comment on lines +1498 to +1504
if normalized_module == "cluster"
&& matches!(imported_method, Some("default"))
&& is_cluster_default_event_emitter_method(&method_name)
{
return Ok(Ok(Expr::NativeMethodCall {
module: module_name.to_string(),
class_name: 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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lookup_native_module definition(s) =="
rg -n -C3 '\bfn\s+lookup_native_module\b|\blookup_native_module\s*\(' crates/perry-hir/src

echo
echo "== cluster/native module registration sites =="
rg -n -C3 '"cluster"|"node:cluster"|default' crates/perry-hir/src crates/perry/src

echo
echo "== cluster rows in native dispatch table =="
rg -n -C2 'module:\s*"cluster"|module:\s*"node:cluster"' crates/perry-codegen/src/lower_call/native_table

echo
echo "== native_module_lookup normalization rules =="
rg -n -C6 'fn\s+native_module_lookup|let\s+normalized\s*=\s*match\s+module' crates/perry-codegen/src/lower_call/native_module_dispatch.rs

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Lines 1498-1510 of native_module.rs ==="
sed -n '1498,1510p' crates/perry-hir/src/lower/expr_call/native_module.rs

echo
echo "=== Lines 1490-1520 context ==="
sed -n '1490,1520p' crates/perry-hir/src/lower/expr_call/native_module.rs

echo
echo "=== Cluster table rows in node_misc.rs ==="
rg -n "cluster" crates/perry-codegen/src/lower_call/native_table/node_misc.rs | head -20

echo
echo "=== Check if normalized_module is used in the returned Expr ==="
sed -n '1498,1520p' crates/perry-hir/src/lower/expr_call/native_module.rs | grep -E "normalized_module|module_name"

Repository: PerryTS/perry

Length of output: 3611


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for cluster module registration/import ==="
rg -n -B3 -A3 'cluster.*node:cluster|node:cluster.*cluster' crates/perry-hir/src crates/perry/src | head -50

echo
echo "=== Look for cluster in tests or module setup ==="
rg -n 'lookup_native_module.*cluster|cluster.*lookup_native_module' crates/perry-hir/src/lower/tests.rs

echo
echo "=== Where are native modules registered (builtin_modules) ==="
fd -type f -name '*.rs' crates/perry-hir/src/context | head -5

echo
echo "=== Search for where cluster/node:cluster gets registered ==="
rg -n 'cluster|node_module' crates/perry-hir/src/context.rs | grep -i cluster | head -20

Repository: PerryTS/perry

Length of output: 910


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find context.rs and see native module registration ==="
rg -n 'fn.*lookup_native_module|register.*native.*module' crates/perry-hir/src/context.rs | head -10

echo
echo "=== Look at context.rs to find builtin_modules or native_modules ==="
rg -n 'builtin_modules|native_modules|"cluster"' crates/perry-hir/src/context.rs | head -30

echo
echo "=== Check what module_name values are passed to lookup_native_module in the call site ==="
sed -n '1450,1500p' crates/perry-hir/src/lower/expr_call/native_module.rs | grep -B20 'lookup_native_module' | tail -30

Repository: PerryTS/perry

Length of output: 206


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find where LoweringContext is defined ==="
fd -type f -name '*.rs' crates/perry-hir/src | xargs grep -l 'struct LoweringContext'

echo
echo "=== Look for lookup_native_module implementation ==="
rg -n 'fn lookup_native_module' crates/perry-hir/src -A 10 | head -40

echo
echo "=== Check the full context around lines 1470-1480 in native_module.rs to see where module_name comes from ==="
sed -n '1470,1490p' crates/perry-hir/src/lower/expr_call/native_module.rs

Repository: PerryTS/perry

Length of output: 294


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find where LoweringContext is defined ==="
rg -n 'struct LoweringContext' crates/perry-hir/src | head -5

echo
echo "=== Look for lookup_native_module implementation ==="
rg -n 'fn lookup_native_module' crates/perry-hir/src -A 15 | head -60

echo
echo "=== Check full context around lines 1470-1495 in native_module.rs to see where module_name comes from ==="
sed -n '1470,1495p' crates/perry-hir/src/lower/expr_call/native_module.rs

Repository: PerryTS/perry

Length of output: 3100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check native_modules field definition and registration ==="
rg -n 'native_modules.*Vec|register_native_module|native_modules.push' crates/perry-hir/src/lower/context.rs | head -30

echo
echo "=== Look for where native modules are initially populated ==="
rg -n 'native_modules.*=.*vec!|insert.*native.*module' crates/perry-hir/src/lower/context.rs | head -20

echo
echo "=== Search for where cluster specifically gets registered as native module ==="
rg -n 'cluster' crates/perry-hir/src/lower/context.rs | head -20

echo
echo "=== Check context.rs around where native_modules is populated ==="
sed -n '1,150p' crates/perry-hir/src/lower/context.rs | tail -80

Repository: PerryTS/perry

Length of output: 273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Look at register_native_module function ==="
sed -n '1037,1070p' crates/perry-hir/src/lower/context.rs

echo
echo "=== Search for all register_native_module calls to see the pattern ==="
rg -n 'register_native_module' crates/perry-hir/src --context 2 | head -100

echo
echo "=== Specifically look for cluster registration ==="
rg -n 'register_native_module.*cluster|cluster.*register_native_module' crates/perry-hir/src -B2 -A2

Repository: PerryTS/perry

Length of output: 7576


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Look at module_decl.rs around line 240 to see how native_module is determined ==="
sed -n '200,260p' crates/perry-hir/src/lower/module_decl.rs

echo
echo "=== Look for resolve_import_source or similar that might normalize module names ==="
rg -n 'resolve.*import|import.*source' crates/perry-hir/src/lower/module_decl.rs | head -20

Repository: PerryTS/perry

Length of output: 4585


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Verify: does source from import statement remain as 'node:cluster' or get stripped? ==="
sed -n '230,245p' crates/perry-hir/src/lower/module_decl.rs

echo
echo "=== Check what 'source' variable is at the registration point (line 242) ==="
sed -n '160,200p' crates/perry-hir/src/lower/module_decl.rs | grep -A30 'fn.*default_import'

Repository: PerryTS/perry

Length of output: 1384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find where source variable comes from in default_import branch ==="
sed -n '100,240p' crates/perry-hir/src/lower/module_decl.rs | tail -80 | head -50

echo
echo "=== Confirm: is source the raw import string 'node:cluster' or normalized? ==="
rg -n 'ImportDecl|source.*=' crates/perry-hir/src/lower/module_decl.rs | grep -B5 -A5 'default_import' | head -30

Repository: PerryTS/perry

Length of output: 3465


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Look at the function handling default imports to see where source comes from ==="
sed -n '350,450p' crates/perry-hir/src/lower/module_decl.rs

echo
echo "=== Check is_native_module or similar to see if source gets normalized anywhere ==="
rg -n 'is_native_module|is_node_core_module' crates/perry-hir/src/lower/module_decl.rs -B3 -A3 | head -40

Repository: PerryTS/perry

Length of output: 7953


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find is_native_module definition ==="
rg -n 'fn is_native_module' crates/perry-hir/src -A 10

echo
echo "=== Search for is_native_module in context.rs or helpers ==="
rg -n 'is_native_module' crates/perry-hir/src/lower/context.rs -B2 -A10 | head -30

echo
echo "=== Check where the default import registration happens and what 'source' value is ==="
sed -n '330,380p' crates/perry-hir/src/lower/module_decl.rs

Repository: PerryTS/perry

Length of output: 6298


Fix module name canonicalization in the cluster default method dispatch.

Line 1503 emits module: module_name.to_string(), but the dispatch table in crates/perry-codegen/src/lower_call/native_table/node_misc.rs is keyed as module: "cluster".

When the user writes import cluster from "node:cluster", lookup_native_module returns ("node:cluster", None), so module_name = "node:cluster". The code then checks normalized_module == "cluster" (which correctly strips the prefix), but emits module: "node:cluster" instead of the normalized form, causing dispatch table lookup failures.

Change line 1503 from:

module: module_name.to_string(),

to:

module: normalized_module.to_string(),

This matches the pattern used elsewhere in the file (e.g., lines 1519–1525) and ensures the emitted module name aligns with the codegen dispatch table keys.

🤖 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_call/native_module.rs` around lines 1498 -
1504, In the cluster default event emitter method dispatch block where
is_cluster_default_event_emitter_method returns true, the code constructs an
Expr::NativeMethodCall that uses the non-normalized module_name for the module
field. However, the dispatch table in the codegen layer expects the normalized
module name ("cluster" instead of "node:cluster"). Change line 1503 in the
Expr::NativeMethodCall construction from module: module_name.to_string() to
module: normalized_module.to_string() to ensure the emitted module name matches
the dispatch table keys, matching the pattern used in similar dispatch logic
elsewhere in the file.

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