Read ThreadSlots values with the layout they are stored with

This commit is contained in:
Chris Tate
2026-10-19 21:59:15 -05:00
parent 4bbf31b61f
commit d04b7a5cf2
6 changed files with 75 additions and 2 deletions
@@ -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":
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5730-thread-slots-widened-values.ts"
],
"diags": []
}
@@ -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<Key, Labeled> = new ThreadSlots<Key, Counted>("widened.counted");
const tagged: ThreadSlots<Key, Tagged> = new ThreadSlots<Key, Counted>("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)));
+5
View File
@@ -27,3 +27,8 @@ function local(slots: ThreadSlots<Key, State>, 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<Key, State> = new ThreadSlots<Key, { count: number }>("fences.records");
console.log(records.get(new Key()));
@@ -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
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<Key, State> = new ThreadSlots<Key, { count: number }>("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
@@ -0,0 +1 @@
{ "plain": 2849, "san": 4164 }