Repository navigation
When calling a function across modules, calling methods of the passed-in object instance within the function does not take effect. #392
Copy link
Copy link
Closed
Labels
bugConfirmed defect or regressionConfirmed defect or regression
Description
What happened
As title
What you expected
it should works
Minimal reproduction
Multiple files:
perry-repro-ecs-direct.ts
import { processCommands } from "./perry-repro-modules/commands";
import { Changeset } from "./perry-repro-modules/shared";
const changeset = new Changeset();
processCommands([{ type: "set", componentType: 1, component: { x: 1 } }], changeset);
console.log("adds size", changeset.adds.size);
console.log("has component", changeset.adds.has(1));perry-repro-modules/commands.ts:
export function processCommands(
commands: { type: string; componentType: number; component: unknown }[],
changeset: { set(componentType: number, component: unknown): void },
): void {
for (const command of commands) {
if (command.type === "set") {
changeset.set(command.componentType, command.component);
}
}
}perry-repro-modules/shared.ts:
export class Changeset {
readonly adds = new Map<number, unknown>();
set(componentType: number, component: unknown): void {
this.adds.set(componentType, component);
}
}Command you ran:
perry perry-repro-ecs-direct.ts && ./perry-repro-ecs-directoutput:
adds size 0
has component false
expected:
adds size 1
has component true
Environment
- Perry version:
- Host OS:
- Target:
- Rust toolchain (if building from source):
- Installed via:
Diagnostic output
Perry Doctor
Environment Checks
──────────────────
✓ perry version: 0.5.455
✓ update status: up to date
✓ clang (LLVM codegen): clang version 22.1.3
✓ system linker (cc): cc (GCC) 15.2.1 20260209
✓ runtime library: /home/codehz/Projects/perry/target/release/libperry_runtime.a
⚠ project config (perry.toml): not found - run: perry init
All critical checks passed with some warnings.
Anything else
Found a compile/link bug
if I rename those files as a.ts, b.ts, c.ts and rewrite a.ts to
import { processCommands } from "./b.ts";
import { Changeset } from "./c.ts";
const changeset = new Changeset();
processCommands([{ type: "set", componentType: 1, component: { x: 1 } }], changeset);
console.log("adds size", changeset.adds.size);
console.log("has component", changeset.adds.has(1));
I got a link error like
Collecting modules...
Compile package: @codehz/pipeline
Found 3 module(s): 3 native, 0 JavaScript
Generating code...
Wrote object file: a_ts.o
Wrote object file: b_ts.o
Wrote object file: c_ts.o
Linking (runtime-only)...
/usr/bin/ld: a_ts.o: in function `main':
perry_llvm_19546_1777593693465021409.ll:(.text+0x71): undefined reference to `b_ts__init'
/usr/bin/ld: perry_llvm_19546_1777593693465021409.ll:(.text+0x76): undefined reference to `c_ts__init'
/usr/bin/ld: perry_llvm_19546_1777593693465021409.ll:(.text+0x10f): undefined reference to `c_ts__Changeset_constructor'
/usr/bin/ld: perry_llvm_19546_1777593693465021409.ll:(.text+0x255): undefined reference to `perry_fn_b_ts__processCommands'
collect2: 错误:ld 返回 1
Error: Linking failed
its seems to be cache poisoning... same content file hit same cache in .perry-cache
objdump --syms b_ts.o
b_ts.o: 文件格式 elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 perry_llvm_18891_1777593312870011022.ll
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l .rodata.cst8 0000000000000000 .LCPI0_0
0000000000000008 l .rodata.cst8 0000000000000000 .LCPI0_1
0000000000000000 l O .rodata.cst4 0000000000000004 perry_null_guard_zero
0000000000000048 l O .bss 0000000000000008 perry_repro_modules_commands_ts_.str.3.handle
0000000000000005 l O .rodata.str1.1 0000000000000004 .Lperry_repro_modules_commands_ts_.str.1.bytes
0000000000000030 l O .bss 0000000000000008 perry_repro_modules_commands_ts_.str.0.handle
0000000000000038 l O .bss 0000000000000008 perry_repro_modules_commands_ts_.str.1.handle
0000000000000040 l O .bss 0000000000000008 perry_repro_modules_commands_ts_.str.2.handle
0000000000000010 l .rodata.cst8 0000000000000000 .LCPI1_0
0000000000000018 l .rodata.cst8 0000000000000000 .LCPI1_1
0000000000000020 l .rodata.cst8 0000000000000000 .LCPI2_0
0000000000000000 l O .rodata.str1.1 0000000000000005 .Lperry_repro_modules_commands_ts_.str.0.bytes
0000000000000009 l O .rodata.str1.1 000000000000000e .Lperry_repro_modules_commands_ts_.str.2.bytes
0000000000000017 l O .rodata.str1.1 000000000000000a .Lperry_repro_modules_commands_ts_.str.3.bytes
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 g F .text 0000000000000353 perry_fn_perry_repro_modules_commands_ts__processCommands
0000000000000000 *UND* 0000000000000000 js_shadow_frame_push
0000000000000000 *UND* 0000000000000000 js_shadow_slot_set
0000000000000000 *UND* 0000000000000000 js_value_length_f64
0000000000000000 *UND* 0000000000000000 js_object_get_field_by_name_f64
0000000000000000 *UND* 0000000000000000 js_native_call_method
0000000000000000 *UND* 0000000000000000 js_array_get_f64
0000000000000000 *UND* 0000000000000000 js_object_get_field_ic_miss
0000000000000000 *UND* 0000000000000000 js_get_string_pointer_unified
0000000000000000 *UND* 0000000000000000 js_string_equals
0000000000000000 *UND* 0000000000000000 js_shadow_frame_pop
0000000000000360 g F .text 0000000000000353 __perry_wrap_perry_fn_perry_repro_modules_commands_ts__processCommands
00000000000006c0 g F .text 0000000000000009 __perry_wrap_perry_unknown_func
00000000000006d0 g F .text 0000000000000005 perry_repro_modules_commands_ts__init
00000000000006e0 g F .text 00000000000000b6 __perry_init_strings_perry_repro_modules_commands_ts
0000000000000000 *UND* 0000000000000000 js_string_from_bytes
0000000000000000 *UND* 0000000000000000 js_nanbox_string
0000000000000000 *UND* 0000000000000000 js_gc_register_global_root
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugConfirmed defect or regressionConfirmed defect or regression
Activity
proggeramlug commentedon May 1, 2026
Fixed in v0.5.464 (commit 6dbf9db — auto-closed by the commit message's
closes #392reference).Root cause: same-module method calls dispatched correctly through the codegen's static idispatch tower (
lower_call.rsenumeratesctx.classesto find implementors and emits a class_id check). Cross-module calls fell through to the runtime'sjs_native_call_method, which DOES support vtable dispatch viaCLASS_VTABLE_REGISTRY— but the codegen NEVER emitted anyjs_register_class_method(...)calls. The FFI was declared but no caller invoked it; method registration was effectively dead code.Fix in
crates/perry-codegen/src/codegen.rs::emit_string_pool: after the existingjs_register_class_parentblock, emit onejs_register_class_method(class_id, name_ptr, name_len, func_ptr, param_count)call per (class, method) pair for every class DEFINED in the current module (skipping imported class stubs viamethod.body.is_empty()). Pre-intern method names into the string pool beforeemit_string_poolruns since it takes&strings.Verified: your literal 3-file repro now prints
adds size 1 / has component true(was0 / false). Regression testtest-files/test_issue_392_cross_module_method_dispatch.tsmatchesnode --experimental-strip-typesbyte-for-byte. Parity 190/190 — no regressions.Side benefit: this also fixes the more general class-method dispatch path for any code where the codegen of the calling site can't see the receiver's class definition (e.g. methods called through generic typed parameters, methods on objects from
Record<string, MyClass>lookups, etc.).codehz commentedon May 1, 2026
@proggeramlug can you fix the cache issue? if two file has exact same content, it will cached but due to the path is different, the generated symbol is different which can cause unexpected problem
codehz commentedon May 1, 2026
@proggeramlug also another example hit same bug (tested in perry 0.5.464)
perry-min/buffer-min.tsperry-min/changeset.tsperry-min/process.tsperry-min/main-buffer-min.tscommand:
fix(codegen): closes #392 followup — route Map/Set field mutations on…
fix(codegen,hir): closes #446 — bind class-method PropertyGet + flow …