From 064b072f94bc88c3003d195f08c500595f4eb151 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 22:44:28 -0500 Subject: [PATCH] Match class capsules in union fields of checked unions A checked conversion to a union of object types tests each member's fields with the members' matchers. For a field of type `C | undefined` holding a class instance, the union's matcher found the instance's capsule, which is not a capsule of the union itself, and answered no (or matched the instance's plain-object view, which no class arm accepts), so no member matched and the conversion threw a TypeError. workerData read as such a union failed this way in every worker. A union now matches capsules of other types through its arms, where the class arm accepts the instance. --- packages/compiler/src/backend/llvm/dyn.ts | 4 +- .../6063-union-class-field-checks.ts.json | 6 ++ tests/corpus/6063-union-class-field-checks.ts | 65 +++++++++++++++++++ .../6063-union-class-field-checks.ts.json | 1 + 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6063-union-class-field-checks.ts.json create mode 100644 tests/corpus/6063-union-class-field-checks.ts create mode 100644 tests/harness/corpus-costs/6063-union-class-field-checks.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index cf7578c02..ae4f12fb8 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -762,7 +762,9 @@ export class LlDyn { B.terminate(`ret i1 true`); B.startBlock(lNext); } - if (t.kind !== "dyn") { + // A union matches a capsule of another type through its arms below: a + // class capsule in a `C | undefined` field matches the `C` arm. + if (t.kind !== "dyn" && t.kind !== "union") { const view = this.viewFn(wrapped); this.host.declare(`declare void @scr_dyn_release_v(ptr)`); const kind = this.kindOf(B, "%d"); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6063-union-class-field-checks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6063-union-class-field-checks.ts.json new file mode 100644 index 000000000..9e48b2910 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6063-union-class-field-checks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6063-union-class-field-checks.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6063-union-class-field-checks.ts b/tests/corpus/6063-union-class-field-checks.ts new file mode 100644 index 000000000..426182b28 --- /dev/null +++ b/tests/corpus/6063-union-class-field-checks.ts @@ -0,0 +1,65 @@ +// A checked conversion of a dynamic value to a union of object types picks +// the member whose fields match, including a field of type `C | undefined` +// that holds an instance of the class C: workerData read as such a union, +// and an unknown value asserted to it, give the instance or undefined. +import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads"; +import { publish } from "@scriptc/threads"; + +class Job { + readonly name: string; + constructor(name: string) { + this.name = name; + } +} + +interface WithJob { + readonly kind: "job"; + readonly job: Job | undefined; +} +interface Plain { + readonly kind: "plain"; + readonly note: string; +} +interface Counted { + readonly kind: "counted"; + readonly count: number; + readonly last: Job | undefined; +} +type Data = WithJob | Plain | Counted; + +function describe(data: Data): string { + switch (data.kind) { + case "job": + return data.job === undefined ? "job none" : `job ${data.job.name}`; + case "plain": + return `plain ${data.note}`; + case "counted": + return `counted ${data.count} ${data.last?.name ?? "none"}`; + } +} + +function run(data: Data): Promise { + return new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { workerData: data }); + worker.on("message", (message) => console.log("worker:", message)); + worker.on("error", (error) => console.log("worker failed:", String(error))); + worker.on("exit", () => resolve()); + }); +} + +if (isMainThread) { + const values: unknown[] = [ + { kind: "job", job: new Job("local") }, + { kind: "job", job: undefined }, + { kind: "plain", note: "text" }, + { kind: "counted", count: 2, last: new Job("tail") }, + ]; + for (const value of values) console.log("local:", describe(value as Data)); + const shared = publish(new Job("shared")); + await run({ kind: "job", job: shared }); + await run({ kind: "job", job: undefined }); + await run({ kind: "plain", note: "worker" }); + await run({ kind: "counted", count: 3, last: shared }); +} else { + parentPort!.postMessage(describe(workerData as Data)); +} diff --git a/tests/harness/corpus-costs/6063-union-class-field-checks.ts.json b/tests/harness/corpus-costs/6063-union-class-field-checks.ts.json new file mode 100644 index 000000000..bba381595 --- /dev/null +++ b/tests/harness/corpus-costs/6063-union-class-field-checks.ts.json @@ -0,0 +1 @@ +{ "plain": 4152, "san": 6474 }