fix: accept readonly tuples as Set seeds (#384)

- Accept supported readonly tuples in static Set constructors while preserving single evaluation and insertion order.
- Add differential coverage and refresh constructor diagnostics.

Fixes #373

Co-authored-by: Hagege Ruben <20857346+HagegeR@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-09-22 21:50:04 -05:00
committed by GitHub
co-authored by Hagege Ruben
parent e12856c1b4
commit cc44afb4ee
6 changed files with 60 additions and 11 deletions
@@ -5323,12 +5323,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
}
lowerer.badType(expr, tsType);
}
// `new Set<T>()`: Map's sibling. The SEEDED form lowers for any
// T[]-typed argument — literal or variable, T already a legal
// element type — as construct + bulk add (duplicates collapse,
// insertion order preserved, exactly JS). Non-array seeds (another
// Set, general iterables) keep the fence. Unsupported element types
// are named specifically.
// `new Set<T>()`: Map's sibling. The SEEDED form lowers for arrays
// and fixed tuples of legal elements — literal or variable — as
// construct + bulk add (duplicates collapse, insertion order
// preserved, exactly JS). Other iterables keep the fence.
// Unsupported element types are named specifically.
// `new WeakMap()` / `new WeakSet()` in JAVASCRIPT sources: no weak
// container exists in the value model, but harness code constructs
// one unconditionally and touches it only on paths tests don't
@@ -5379,6 +5378,24 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// Any other lowered kind falls through to the named fence
// below — never a mistyped seed into the validator.
}
// Fixed tuples use record storage, not T[] storage. Snapshot
// their legal elements into an array before bulk insertion;
// the helper takes the tuple as one argument, so an effectful
// seed expression is evaluated exactly once.
if (argIr?.kind === "record") {
const shape = lowerer.shapes.get(argIr.shapeId);
if (shape?.tuple && shape.fields.every((field) => typeEquals(field.type, mapped.elem))) {
const seedType = { kind: "array" as const, elem: mapped.elem };
const helper = lowerer.tupleArrayWidthHelper(argIr.shapeId, seedType, loc);
if (helper) {
const tuple = lowerer.lowerExpr(argNode);
if (typeEquals(tuple.type, argIr)) {
const seed: IrExpr = { kind: "call", callee: helper, args: [tuple], type: seedType, loc };
return { kind: "setNew", seed, type: mapped, loc };
}
}
}
}
}
}
// JavaScript's identity-Set idiom: `new Set([setTimeout, atob,
@@ -5410,7 +5427,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
lowerer.noLowering(
"new Set(values)",
expr,
"construct the Set empty and add() each value — only an array of " +
"construct the Set empty and add() each value — only an array or fixed tuple of " +
"already-legal elements (string or number) seeds a Set",
);
}
@@ -6002,8 +6002,9 @@ export class Lowerer {
tupleArrayWidthHelper(fromId: string, toT: IrType & { kind: "array" }, loc: SrcLoc): string | null {
const from = this.shapes.get(fromId);
if (!from || !from.tuple) return null;
const fields = [...from.fields].sort((a, b) => Number(a.name) - Number(b.name));
const lifts: WidthLift[] = [];
for (const f of from.fields) {
for (const f of fields) {
const lift = this.widthLiftPlan(f.type, toT.elem);
if (!lift) return null;
lifts.push(lift);
@@ -6025,7 +6026,7 @@ export class Lowerer {
kind: "return",
value: {
kind: "arrayLit",
elems: from.fields.map((f, i) =>
elems: fields.map((f, i) =>
this.applyWidthLift(
lifts[i]!,
{ kind: "recordGet", obj: t, shapeId: fromId, field: f.name, type: f.type, loc },
@@ -6510,6 +6510,12 @@
],
"diags": []
},
"<repo>/tests/corpus/2947-set-readonly-seeds.ts": {
"order": [
"<repo>/tests/corpus/2947-set-readonly-seeds.ts"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
+25
View File
@@ -0,0 +1,25 @@
const words = ["pear", "apple", "pear"] as const;
const named = new Set<string>(words);
console.log(named.size, named.has("apple"), [...named].join(","));
const inferred = new Set(words);
console.log(inferred.size, inferred.has("pear"), [...inferred].join(","));
const readonlyWords: readonly string[] = ["north", "south", "north"];
const fromReadonlyArray = new Set(readonlyWords);
console.log(fromReadonlyArray.size, [...fromReadonlyArray].join(","));
const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 3] as const;
const fromTuple = new Set<number>(numbers);
console.log(fromTuple.size, [...fromTuple].join(","));
let seedCalls = 0;
function makeSeed() {
seedCalls++;
return ["once", "twice", "once"] as const;
}
const fromCall = new Set<string>(makeSeed());
console.log(seedCalls, fromCall.size, [...fromCall].join(","));
const direct = new Set(["red", "blue", "red"] as const);
console.log(direct.size, [...direct].join(","));
+1 -1
View File
@@ -5,7 +5,7 @@ sets.ts:8:16 - error SC2020: 'new Set(values)' is part of the standard library t
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
9 |
hint: construct the Set empty and add() each value — only an array of already-legal elements (string or number) seeds a Set
hint: construct the Set empty and add() each value — only an array or fixed tuple of already-legal elements (string or number) seeds a Set
sets.ts:12:7 - error SC2009: values of type 'Set<boolean>' cannot be compiled: the Set shape is supported, but elements are limited to numbers and strings — 'boolean' is outside that domain
@@ -134,7 +134,7 @@ stdlib-fence.ts:30:12 - error SC2020: 'new Set(values)' is part of the standard
| ^~~~~~~~~~~~~~~~~~~~~~~~
31 | // (new Error / TypeError / RangeError / SyntaxError now LOWER — the error
hint: construct the Set empty and add() each value — only an array of already-legal elements (string or number) seeds a Set
hint: construct the Set empty and add() each value — only an array or fixed tuple of already-legal elements (string or number) seeds a Set
stdlib-fence.ts:34:7 - error SC2020: 'AggregateError' is part of the standard library types but has no scriptc lowering yet