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.
This commit is contained in:
Chris Tate
2026-10-19 22:44:28 -05:00
parent 0afea2639d
commit 064b072f94
4 changed files with 75 additions and 1 deletions
+3 -1
View File
@@ -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");
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6063-union-class-field-checks.ts"
],
"diags": []
}
@@ -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<void> {
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));
}
@@ -0,0 +1 @@
{ "plain": 4152, "san": 6474 }