From d04b7a5cf2e30fa875cb5bd3bcd52ec3a45d3463 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 21:59:15 -0500 Subject: [PATCH] Read ThreadSlots values with the layout they are stored with --- .../src/frontend/lowering/thread-slots.ts | 9 +++- .../5730-thread-slots-widened-values.ts.json | 6 +++ .../5730-thread-slots-widened-values.ts | 45 +++++++++++++++++++ tests/diagnostics/thread-slots-fences.ts | 5 +++ .../__snapshots__/thread-slots-fences.ts.txt | 11 ++++- .../5730-thread-slots-widened-values.ts.json | 1 + 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5730-thread-slots-widened-values.ts.json create mode 100644 tests/corpus/5730-thread-slots-widened-values.ts create mode 100644 tests/harness/corpus-costs/5730-thread-slots-widened-values.ts.json diff --git a/packages/compiler/src/frontend/lowering/thread-slots.ts b/packages/compiler/src/frontend/lowering/thread-slots.ts index 50b5ecf09..8227c6bd0 100644 --- a/packages/compiler/src/frontend/lowering/thread-slots.ts +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -22,6 +22,7 @@ import { threadSlotsMemberOf, } from "../experimental-import.js"; import * as ts from "../ts7/adapter.js"; +import { withUndefinedArm } from "../type-mapper.js"; import type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; import { locOf } from "../program.js"; @@ -238,10 +239,16 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): const args = [name, receiver, key]; switch (member) { case "get": { + // The slots hold values in the layout of the type the constructor registered (`family.value`, + // which `set` stores with). The receiver's annotation can name a structurally wider value + // type, so the value read is converted to the call's type like any value flowing into it. const resultTs = lowerer.typeOf(expr); const result = lowerer.mapTypeOf(resultTs); if (result === null) lowerer.badType(expr, resultTs); - return { kind: "intrinsic", name: "slots.get", args, type: result!, loc }; + const stored = withUndefinedArm(family.value, lowerer.unions); + if (stored === null) lowerer.badType(expr, resultTs); + const read: IrExpr = { kind: "intrinsic", name: "slots.get", args, type: stored!, loc }; + return typeKey(stored!) === typeKey(result!) ? read : lowerer.coerceInto(expr, read, result!); } case "has": case "delete": diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5730-thread-slots-widened-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5730-thread-slots-widened-values.ts.json new file mode 100644 index 000000000..001c7ea32 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5730-thread-slots-widened-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5730-thread-slots-widened-values.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5730-thread-slots-widened-values.ts b/tests/corpus/5730-thread-slots-widened-values.ts new file mode 100644 index 000000000..88a1803e7 --- /dev/null +++ b/tests/corpus/5730-thread-slots-widened-values.ts @@ -0,0 +1,45 @@ +import { ThreadSlots } from "@scriptc/experimental"; + +// A ThreadSlots const annotated with a structurally wider value type than +// its constructor's: the values are stored with the constructor's layout and +// read back through the annotation's. Node.js prints the same. + +class Key { + readonly id: number; + constructor(id: number) { + this.id = id; + } +} + +interface Counted { + count: number; + label: string; +} + +interface Labeled { + label: string; +} + +interface Tagged { + label: string; + tag?: string; +} + +const counted: ThreadSlots = new ThreadSlots("widened.counted"); +const tagged: ThreadSlots = new ThreadSlots("widened.tagged"); + +const first = new Key(1); +const second = new Key(2); +const value: Counted = { count: 7, label: "seven" }; +counted.set(first, value); +const read = counted.get(first)!; +console.log(read.label, counted.get(second), counted.has(first)); + +tagged.set(second, { count: 2, label: "two" } as Counted); +const t = tagged.get(second); +console.log(t?.label, t?.tag, tagged.get(first)); + +function labelOf(slots: Labeled | undefined): string { + return slots === undefined ? "none" : slots.label; +} +console.log(labelOf(counted.get(first)), labelOf(counted.get(second))); diff --git a/tests/diagnostics/thread-slots-fences.ts b/tests/diagnostics/thread-slots-fences.ts index 7ec71db34..ed985c803 100644 --- a/tests/diagnostics/thread-slots-fences.ts +++ b/tests/diagnostics/thread-slots-fences.ts @@ -27,3 +27,8 @@ function local(slots: ThreadSlots, key: Key): State | undefined { const ctor = ThreadSlots; const getter = first.get; console.log(named, second, shaped, callbacks, local(first, new Key()), ctor, getter); + +// A read through an annotated value type the stored values can't be converted +// to: they are stored as records, which a read as the class can't convert. +const records: ThreadSlots = new ThreadSlots("fences.records"); +console.log(records.get(new Key())); diff --git a/tests/harness/__snapshots__/thread-slots-fences.ts.txt b/tests/harness/__snapshots__/thread-slots-fences.ts.txt index 566ddfba6..9d4648c27 100644 --- a/tests/harness/__snapshots__/thread-slots-fences.ts.txt +++ b/tests/harness/__snapshots__/thread-slots-fences.ts.txt @@ -59,4 +59,13 @@ thread-slots-fences.ts:28:16 - error SC2020: 'ThreadSlots.get (@scriptc/experime | ^~~~~~~~~ 29 | console.log(named, second, shaped, callbacks, local(first, new Key()), ctor, getter); - hint: call slots.get(...) directly \ No newline at end of file + hint: call slots.get(...) directly + +thread-slots-fences.ts:34:13 - error SC2003: union conversion has no unambiguous layout mapping: expected 'State | undefined', got '{ count: number } | undefined' (each source arm needs an identical destination arm, a unique compatible record/array layout, or a shared literal discriminant selecting compatible layouts) + + 33 | const records: ThreadSlots = new ThreadSlots("fences.records"); + 34 | console.log(records.get(new Key())); + | ^~~~~~~~~~~~~~~~~~~~~~ + 35 | + + hint: narrow the value to a SINGLE arm first (a discriminant check, or '!== undefined'/'!== null' for unit arms); a plain arm value widens into any union that contains it, and a whole union widens into any union whose arms include the source's \ No newline at end of file diff --git a/tests/harness/corpus-costs/5730-thread-slots-widened-values.ts.json b/tests/harness/corpus-costs/5730-thread-slots-widened-values.ts.json new file mode 100644 index 000000000..444a9b634 --- /dev/null +++ b/tests/harness/corpus-costs/5730-thread-slots-widened-values.ts.json @@ -0,0 +1 @@ +{ "plain": 2849, "san": 4164 }