mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Choose comptime union arms by discriminant and refuse recursive records
A comptime record value whose union has a discriminant takes the arm its discriminant field names, so a value that fits two arms with overlapping shapes gets that arm's tag and layout instead of the first arm it fits. comptimeBakeable reports a record type reachable from its own fields as not bakeable instead of recursing without end, so such a comptime result reports SC1090, and the message names unions and that limit.
This commit is contained in:
@@ -76,7 +76,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr
|
||||
"SC1090",
|
||||
expr,
|
||||
`baking comptime results of type '${lowerer.fmt(target)}' into the binary ` +
|
||||
`(only number, string, boolean, arrays, and records lower to literals)`,
|
||||
`(number, string, boolean, and arrays, records and unions of those lower to literals; a record type reachable from its own fields does not)`,
|
||||
);
|
||||
}
|
||||
// 5. The host executes the extracted callback under a finite budget.
|
||||
@@ -112,25 +112,36 @@ function comptimeFailureDetail(error: unknown): string {
|
||||
* expression: number/string/boolean, arrays and record shapes of those,
|
||||
* and unions of those with null or undefined (the mixed rows of a JSON
|
||||
* table: `(string | number)[][]`), nested. Closures, class instances,
|
||||
* promises, dyn, and void cannot be baked. */
|
||||
* promises, dyn, void, and recursive record types (a record reachable
|
||||
* from its own fields) cannot be baked. */
|
||||
export function comptimeBakeable(lowerer: Lowerer, t: IrType): boolean {
|
||||
return bakeableWithin(lowerer, t, new Set());
|
||||
}
|
||||
|
||||
/** comptimeBakeable below the records in `open`, whose fields are being
|
||||
* checked: reaching one of them again means the type is recursive. */
|
||||
function bakeableWithin(lowerer: Lowerer, t: IrType, open: Set<string>): boolean {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "string":
|
||||
case "bool":
|
||||
return true;
|
||||
case "array":
|
||||
return lowerer.comptimeBakeable(t.elem);
|
||||
return bakeableWithin(lowerer, t.elem, open);
|
||||
case "record": {
|
||||
const shape = lowerer.shapes.get(t.shapeId);
|
||||
return !!shape && shape.fields.every((f) => lowerer.comptimeBakeable(f.type));
|
||||
if (!shape || open.has(t.shapeId)) return false;
|
||||
open.add(t.shapeId);
|
||||
const bakeable = shape.fields.every((f) => bakeableWithin(lowerer, f.type, open));
|
||||
open.delete(t.shapeId);
|
||||
return bakeable;
|
||||
}
|
||||
case "union": {
|
||||
const arms = lowerer.unions.get(t.unionId)?.arms;
|
||||
return (
|
||||
arms !== undefined &&
|
||||
arms.every(
|
||||
(arm) => isUnitType(arm) || (arm.kind !== "union" && comptimeBakeable(lowerer, arm)),
|
||||
(arm) => isUnitType(arm) || (arm.kind !== "union" && bakeableWithin(lowerer, arm, open)),
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -139,6 +150,30 @@ export function comptimeBakeable(lowerer: Lowerer, t: IrType): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** The tag of the arm of union `unionId` that `value` bakes into, or -1.
|
||||
* A record value whose discriminant field holds one of the union's
|
||||
* discriminant values takes that case's arm, since arms with overlapping
|
||||
* shapes can both fit it. Any other value takes the first arm it fits, in
|
||||
* the union's arm order: a JSON value fits exactly one scalar arm, and
|
||||
* array or record arms are told apart by their contents. */
|
||||
function comptimeUnionArm(lowerer: Lowerer, unionId: string, value: unknown): number {
|
||||
const def = lowerer.unions.get(unionId);
|
||||
if (!def) return -1;
|
||||
const discriminant = def.discriminant;
|
||||
if (
|
||||
discriminant !== undefined &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
const key = (value as Record<string, unknown>)[discriminant.field];
|
||||
const named = discriminant.cases.find((c) => c.values.some((v) => v === key));
|
||||
if (named !== undefined)
|
||||
return comptimeFits(lowerer, value, def.arms[named.tag]!) ? named.tag : -1;
|
||||
}
|
||||
return def.arms.findIndex((arm) => comptimeFits(lowerer, value, arm));
|
||||
}
|
||||
|
||||
/** Whether `value` bakes into the bakeable type `t` without a mismatch:
|
||||
* the arm choice of a union literal. Records are exact, as in
|
||||
* comptimeValueToIr; an absent field reads as undefined. */
|
||||
@@ -168,9 +203,7 @@ function comptimeFits(lowerer: Lowerer, value: unknown, t: IrType): boolean {
|
||||
);
|
||||
}
|
||||
case "union":
|
||||
return (lowerer.unions.get(t.unionId)?.arms ?? []).some((arm) =>
|
||||
comptimeFits(lowerer, value, arm),
|
||||
);
|
||||
return comptimeUnionArm(lowerer, t.unionId, value) >= 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -313,11 +346,8 @@ export function comptimeValueToIr(
|
||||
return { kind: "recordLit", fields, type: expected, loc };
|
||||
}
|
||||
case "union": {
|
||||
// The first arm the value fits, in the union's arm order: a JSON
|
||||
// value fits exactly one scalar arm, and array or record arms are
|
||||
// told apart by their contents.
|
||||
const arms = lowerer.unions.get(expected.unionId)!.arms; // bakeable-checked
|
||||
const tag = arms.findIndex((arm) => comptimeFits(lowerer, value, arm));
|
||||
const tag = comptimeUnionArm(lowerer, expected.unionId, value);
|
||||
if (tag < 0) fail(describeComptimeValue(value));
|
||||
const arm = arms[tag]!;
|
||||
const payload: IrExpr = isUnitType(arm)
|
||||
|
||||
@@ -43,3 +43,20 @@ const meta = comptime(() => {
|
||||
return { kind: "meta", tags, count: tags.length };
|
||||
});
|
||||
console.log(meta.kind, meta.tags.join("|"), meta.count);
|
||||
|
||||
// A discriminated union whose arms both fit a "labeled" value without a
|
||||
// label: the discriminant picks the arm.
|
||||
type Item = { kind: "plain"; v: number } | { kind: "labeled"; v: number; label?: string };
|
||||
const items: Item[] = comptime(
|
||||
() =>
|
||||
[
|
||||
{ kind: "plain", v: 1 },
|
||||
{ kind: "labeled", v: 2 },
|
||||
{ kind: "labeled", v: 3, label: "x" },
|
||||
] as Item[],
|
||||
);
|
||||
for (const item of items) {
|
||||
if (item.kind === "labeled") console.log("labeled", item.v, item.label);
|
||||
else console.log("plain", item.v);
|
||||
}
|
||||
console.log(JSON.stringify(items));
|
||||
|
||||
@@ -20,3 +20,10 @@ const nested = comptime(() => comptime(() => 1));
|
||||
const eventually = comptime(async () => 1);
|
||||
|
||||
const lied = comptime(() => 1 as unknown as string);
|
||||
|
||||
// A record type reachable from its own fields has no literal form.
|
||||
interface Chain {
|
||||
value: number;
|
||||
next: Chain | null;
|
||||
}
|
||||
const chain: Chain = comptime(() => ({ value: 1, next: null }) as Chain);
|
||||
|
||||
@@ -45,4 +45,11 @@ comptime.ts:22:23 - error SC1110: comptime evaluation failed: expected 'string'
|
||||
21 |
|
||||
22 | const lied = comptime(() => 1 as unknown as string);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
23 |
|
||||
23 |
|
||||
|
||||
comptime.ts:29:22 - error SC1090: baking comptime results of type '{ next: null | ...; value: number }' into the binary (number, string, boolean, and arrays, records and unions of those lower to literals; a record type reachable from its own fields does not) is not supported yet
|
||||
|
||||
28 | }
|
||||
29 | const chain: Chain = comptime(() => ({ value: 1, next: null }) as Chain);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
30 |
|
||||
Reference in New Issue
Block a user