Repository navigation
Description
Perry: 0.5.359 native (macOS aarch64). Discovered while integrating the now-fixed #233 + #235 into our @perryts/mongodb driver bench harness — the bench's workload (in bench/src/workload.ts) takes a Driver interface as a parameter and calls driver.findOne(...) on it. After #235 was fixed, the call started silently returning undefined and the impl's method body was never invoked. Bisected to module layout.
The trigger: when a consumer function lives in a module that only type-imports the interface (no value-side import of any class implementing it), Perry compiles the call site without any dispatch entry, and await d.method() resolves to undefined. The impl's method body is never entered. Adding any value-side import of an implementing class to the consumer module — even an unused export const _ref = Impl — restores correct dispatch.
Bun 1.3.12 and Node 25.8.0 both run the broken repro correctly (the impl method is invoked, the awaited value is the real result).
Repro — 4 files
driver.ts:
export interface Driver {
findOne(db: string, col: string, filter: any): Promise<any>;
close(): Promise<void>;
}impl.ts:
import { MongoClient } from "@perryts/mongodb";
import type { Driver } from "./driver.ts";
export class Impl implements Driver {
client: MongoClient | null = null;
async findOne(db: string, col: string, filter: any): Promise<any> {
console.log(' [Impl.findOne] CALLED');
if (this.client === null) {
this.client = await MongoClient.connect("mongodb://127.0.0.1:27017");
}
return this.client.db(db).collection(col).findOne(filter);
}
async close(): Promise<void> {
if (this.client !== null) { this.client.close(); this.client = null; }
}
}workload.ts — only type-imports Driver:
import type { Driver } from "./driver.ts";
export async function consume(d: Driver): Promise<void> {
const m = await d.findOne('sample_mflix', 'movies', { title: 'The Great Train Robbery' });
console.log('consume: m typeof=' + typeof m + ' isUndef=' + (m === undefined));
await d.close();
}main.ts:
import { Impl } from "./impl.ts";
import { consume } from "./workload.ts";
async function main() { await consume(new Impl()); }
main().catch((e: any) => console.error('ERR:', e));Actual (Perry 0.5.359)
consume: m typeof=undefined isUndef=true
The [Impl.findOne] CALLED log never appears — the impl's method body is not invoked.
Expected (Bun 1.3.12, Node 25.8.0 both produce this)
[Impl.findOne] CALLED
consume: m typeof=object isUndef=false
The fix that restores correct dispatch
Add any value-side import of Impl to workload.ts. Even an unused re-export works:
import type { Driver } from "./driver.ts";
import { Impl } from "./impl.ts";
export const _ref: typeof Impl = Impl; // <-- this single line is enough
export async function consume(d: Driver): Promise<void> {
// unchanged
const m = await d.findOne('sample_mflix', 'movies', { title: 'The Great Train Robbery' });
console.log('consume: m typeof=' + typeof m + ' isUndef=' + (m === undefined));
await d.close();
}With that single value-import, the impl method is invoked correctly. The fix needs to be a class that implements the interface — importing an unrelated class (e.g., class Unrelated { x: number = 0 }) does not help.
Bisection
Same Driver + Impl source, varied module layout:
| Layout | Result |
|---|---|
| All in one file (interface + impl + consumer + main) | works |
| Interface in own file, impl + consumer + main in second file | works |
| 3 files: interface, impl, main (consumer in main) | works |
| 4 files: interface, impl, consumer, main (matches our bench) | broken |
Same 4 files, but workload.ts value-imports Impl |
works |
Same 4 files, but workload.ts value-imports an unrelated class |
broken |
So the trigger is specifically: a consumer module in a separate file from main, where the consumer module only knows the interface as a type.
Likely cause
Looks like Perry's per-module method-table generation only emits dispatch entries for an interface in modules that value-import a class implementing it. Modules that only type-import the interface get no dispatch table for it, so calls compile to a no-op resolution that yields undefined.
Notes
- Same Perry source compilation flow as Array.push from inside an async function silently caps at 16 elements when the array is a function parameter #233 / Async miscompile: an extra synchronous statement before await MongoClient.connect() drops the next await silently #235 (no special flags, no AOT-vs-JIT difference observed).
- Reproduces on every run — fully deterministic.
- The bench harness (https://github.com/PerryTS/mongodb if/when published — currently local) follows exactly the 4-file layout above:
bench/src/driver.ts(interface),bench/src/adapters/perryts.ts(impl),bench/src/workload.ts(consumer, type-only import),bench/run-perryts.ts(main). - Workaround applied in the bench: add
import { PerryTSDriver } from './adapters/perryts.ts'; export const _ref = PerryTSDriver;toworkload.ts. Ugly but immediate.
Activity
fix(compile): PerryTS#240 type-only-imported interface drops dispatch…
fix(codegen): #309 Effect 34 GB OOM via inheritance-chain cycle break…