Repository navigation
Cross-module instanceof returns true for unrelated imported classes #585
Description
Repro
// lib_a.ts
export class A { ax = 1 }
export class B { bx = 2 }
// main.ts
import { A, B } from './lib_a.js';
const a = new A();
console.log(a instanceof A); // true (correct)
console.log(a instanceof B); // false (correct in this minimal case)That minimal case actually works. But the bug surfaces when the value is an Object.assign'd instance of a class imported from one module, checked against an unrelated class imported from a different module:
// surfaces while compiling drizzle-orm 0.45.2 via perry.compilePackages
function pgTableWithSchema(name) {
const rawTable = new PgTable(name); // PgTable lives in pg-core/table.js
return Object.assign(rawTable, { /* cols */ });
}
const users = pgTableWithSchema('users');
// drizzle-orm/entity.js's `is()` brand check
import { Subquery } from '../../subquery.js'; // unrelated class, separate module
console.log(users instanceof Subquery); // perry: TRUE (WRONG); node/bun: FALSEIn the actual drizzle run via perry, all three of these return true:
users instanceof Subqueryusers instanceof PgViewBaseusers instanceof SQL
Why it matters for #488
drizzle-orm's is(value, kind) brand-check (in entity.js) short-circuits with if (value instanceof type) return true;. With this bug, is(table, Subquery) returns true unconditionally. drizzle's from() builder then takes the Subquery branch, reads table._.selectedFields, and crashes with TypeError: Cannot read properties of undefined (reading 'selectedFields').
This is the dominant query-side blocker for #488 (drizzle + @perryts/postgres acceptance test).
Where I'd start digging
crates/perry-codegen/src/expr.rs:5498 Expr::InstanceOf — for the cross-module case, falls back to cid = 0 when ctx.class_ids.get(ty) misses (line 5526-5539). Then js_instanceof(value, 0) is called.
crates/perry-runtime/src/object.rs:3806 js_instanceof — at line 4047-4050:
let obj_class_id = (*obj_ptr).class_id;
if obj_class_id == class_id { return true_val; }If both obj_class_id and the requested class_id happen to be 0 (e.g. an Object.assign'd table whose class_id metadata was lost + an unresolved cross-module class name), 0 == 0 returns true.
Probable fix surfaces:
- Codegen: ensure cross-module
Subquery/PgViewBase/SQLresolve to a non-zero class id (likely working — seeimported_class_stubsregistration incodegen.rs:462). Verify they're being threaded through correctly whencompilePackagesis used vs. ordinary.tsimports. - Runtime: never return true for
class_id == 0— that should be an "unknown class" sentinel that always returns false. - Codegen:
Object.assign(rawTable, { ... })may lose the rawTable's class_id; verify the result still has the correct ObjectHeader.class_id.
Workaround
Patch drizzle's entity.js to skip the instanceof short-circuit and rely on the entityKind chain walk:
// if (value instanceof type) return true; // skip on perryWith this patch the chain walk also fails (A[sym] returns undefined for static Symbol-keyed fields — separate issue), but the path advances to the next blocker (#TBD: thenables). So is() is the gate.
Refs
Blocks #488. Surfaced from #420 sub-issue acceptance run on v0.5.684.
Activity
test(harness): tier 3 fixtures — ws-echo, axios-get, drizzle-sqlite, …