Skip to content

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

@proggeramlug

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)

  1. class E extends Error { constructor(a) { super(a?.message) } } with a.message === undefined: perry new E({}).message === "undefined" (node/bun ""), and that own message shadows a subclass get message().
  2. Instances of Data.TaggedError(tag) subclasses expose internal keys like __perry_cap_23m0000b400ee61 in Object.keys (node/bun: only the user fields).
  3. fs.promises.readFile(missing) resolves undefined (node rejects with ENOENT), and fs.readFileSync(missing) does not throw.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions