Repository navigation
Cross-module new of an exported class with no own constructor and a runtime-value parent drops the constructor args (effect PlatformError.SystemError) — OpenCode v1.18.30 runtime wall #10258
Description
Summary
new ImportedClass(args) from another module drops the constructor arguments when ImportedClass has no own constructor and its extends clause is a runtime value (an IIFE-returned class read through a namespace re-export). The instance ends up with no own properties. Constructing the same class inside its defining module works, and so does an exported class that declares constructor(a) { super(a) }.
This is the current runtime wall of OpenCode v1.18.30 (tracker #10107). effect v4's effect/PlatformError declares export class SystemError extends Data.Error { get message() {…} } (Data.Error = core.Error, an IIFE-returned class Base extends YieldableError { constructor(args) { super(args?.message, …); Object.assign(this, args) } }). When a missing file is read, SystemError loses _tag / module / method, so Effect.catchReason("PlatformError", "NotFound", …) in OpenCode's FSUtil.readFileStringSafe does not match, the failure becomes a defect, and every command dies with Error: Unexpected error / [object Object] right after loading path=~/.config/opencode/config.json.
Repro (4 files, no dependencies)
// core.ts
export const YieldableError = (function () { class YieldableError extends globalThis.Error {}; return YieldableError })()
export const Error = (function () {
return class Base extends YieldableError {
constructor(args?: any) { super(args?.message); if (args) Object.assign(this, args) }
}
})()
// data.ts
import * as core from "./core"
export const Error = core.Error
// platform2.ts
import * as Data from "./data"
export class A_getter extends (Data.Error as any) { get message() { return "A" } }
export class B_nogetter extends (Data.Error as any) {}
export class C_method extends (Data.Error as any) { describe() { return "C" } }
class D_local_getter extends (Data.Error as any) { get message() { return "D" } }
export const makeD = (o: any) => new D_local_getter(o)
export class E_getter_other extends (Data.Error as any) { get other() { return "E" } }
export class F_getter_ctor extends (Data.Error as any) { constructor(a: any) { super(a) } get message() { return "F" } }
// main2.ts
import { A_getter, B_nogetter, C_method, makeD, E_getter_other, F_getter_ctor } from "./platform2"
import * as Data from "./data"
const t = (name: string, f: () => any) => { const e = f(); console.log(name, JSON.stringify({ tag: e?._tag, m: e?.module, keys: Object.keys(e).filter((k) => !k.startsWith("__perry_cap")), msg: String(e?.message) })) }
const o = () => ({ _tag: "NotFound", module: "FS" })
t("A exported + get message()", () => new A_getter(o()))
t("B exported, empty", () => new B_nogetter(o()))
t("C exported + method", () => new C_method(o()))
t("D local + get message()", () => makeD(o()))
t("E exported + get other()", () => new E_getter_other(o()))
t("F exported + ctor + get message()", () => new F_getter_ctor(o()))
class M_main_getter extends (Data.Error as any) { get message() { return "M" } }
t("M main-module + get message()", () => new M_main_getter(o()))bun 1.3.14:
A exported + get message() {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":"A"}
B exported, empty {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":""}
C exported + method {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":""}
D local + get message() {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":"D"}
E exported + get other() {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":""}
F exported + ctor + get message() {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":"F"}
M main-module + get message() {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":"M"}
perry 0.5.1564 (Linux x86_64):
A exported + get message() {"keys":[],"msg":""}
B exported, empty {"keys":[],"msg":""}
C exported + method {"keys":[],"msg":""}
D local + get message() {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":"D"}
E exported + get other() {"keys":[],"msg":""}
F exported + ctor + get message() {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":"F"}
M main-module + get message() {"tag":"NotFound","m":"FS","keys":["_tag","module"],"msg":"M"}
Related divergences seen while reducing (same area, possibly separate fixes)
class E extends Error { constructor(a) { super(a?.message) } }witha.message === undefined: perrynew E({}).message === "undefined"(node/bun""), and that ownmessageshadows a subclassget message().- Instances of
Data.TaggedError(tag)subclasses expose internal keys like__perry_cap_23m0000b400ee61inObject.keys(node/bun: only the user fields). fs.promises.readFile(missing)resolvesundefined(node rejects withENOENT), andfs.readFileSync(missing)does not throw.
Activity
proggeramlug commentedon Sep 14, 2026
Fix: PR #10259. Root cause: the defining module synthesizes the no-ctor class's constructor with an 8-slot forwarding band (parent arity unresolvable), while the importer declared it with 0 params (own constructor only), so
new ImportedClass(args)passed onlythis. The importer now derives the arity from the same rule. The four-module repro above prints bun's output with the patched compiler. The three related divergences listed in the issue body (Errorsuper(undefined)message,__perry_cap_*keys inObject.keys,fs.promises.readFile/readFileSyncon a missing file) are NOT addressed by that PR.proggeramlug commentedon Sep 14, 2026
Ready for review: #10265 (supersedes #10259; tracker #10107).
The defining module and every importer now use one resolved constructor-arity contract, including object-cache keys. Resolvable two-parameter parents retain arity 2; member, member+getter, named-import, and cast runtime heritage preserve their arguments.
All requested build-host tests and checks pass. Both xerr probes match Bun byte-for-byte. The complete PlatformError E1–E5/R1–R4 output matches Bun on cold and cached builds, including E1 keys
["_tag","module","method","pathOrDescriptor"]andR2 catchReason NotFound "caught". The warm build reused all 289 module objects with zero misses. The full OpenCode compile remains with the coordinator.fix(compile): imported default ctor arity matches the synthesized for…