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 }