mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Walk a for..of over an iterable beside never[]
`for (const name of registry.get(key) ?? [])` (mcp's server-tool registry)
types the head `Set<string> | never[]` and had no lowering: the iterable
union rule knows Iterable/generator unions and the indexed numeric arms, so
the loop fell through to the generic SC2001 fence on the union type.
`never[]` is the empty array literal's own type: its only value is the empty
array, which yields no iterations. `T[] | never[]` reduces in the checker
(never is assignable to T), so the reachable shape is exactly one inhabited
non-array iterable beside `never[]`. That loop is the existing container walk
on the arm the runtime tag selects:
if (unionIsTag(head, setArm)) { <lowerForOfSet on unionNarrow(head)> }
for Set, Map and string — no new representation, and the empty-array arm is
an empty `else`. A union of two inhabited iterables
(`Set<string> | string[]`) still fences: both arms hold values and their
walks yield different element kinds through different protocols.
Corpus 8416-iterable-never-array-arm pins Set/Map/string registries with
present and missing keys, a live set mutated between loops, and an explicit
`Set<string> | never[]` annotation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c3954d4dd4
commit
54cd19e52a
@@ -10632,6 +10632,72 @@ function isUntypedIterable(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
return lowerer.mapTypeOf(type)?.kind === "dyn";
|
||||
}
|
||||
|
||||
/** `for (const x of map.get(k) ?? [])`: the receiver's checker type is one
|
||||
* non-array iterable beside `never[]` — the array literal's type, whose only
|
||||
* value is the empty array (`never` is uninhabited), so that arm yields
|
||||
* nothing at all. The loop walks the iterable arm under its tag test and the
|
||||
* empty-array arm runs zero iterations. An ARRAY iterable never needs this:
|
||||
* `T[] | never[]` reduces in the checker to `T[]`, never being assignable to
|
||||
* T. A union of two inhabited iterables has no single element type and keeps
|
||||
* its existing fence. */
|
||||
function lowerForOfEmptyArrayArm(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.ForOfStatement,
|
||||
iterable: IrExpr,
|
||||
iterSrc: ts.Expression,
|
||||
labels?: string[],
|
||||
): IrStmt | null {
|
||||
if (iterable.type.kind !== "union" || stmt.awaitModifier !== undefined) return null;
|
||||
const { checker } = lowerer;
|
||||
const widened = lowerer.typeOf(iterSrc);
|
||||
if (!widened.isUnionType()) return null;
|
||||
const parts = ts.constituentTypes(widened);
|
||||
if (parts.length !== 2) return null;
|
||||
const neverArrays = parts.filter((part) => {
|
||||
if (!checker.isArrayType(part)) return false;
|
||||
const elem = checker.getTypeArguments(part as ts.TypeReference)[0];
|
||||
return elem !== undefined && (elem.flags & ts.TypeFlags.Never) !== 0;
|
||||
});
|
||||
if (neverArrays.length !== 1) return null;
|
||||
const other = parts.find((part) => part !== neverArrays[0]);
|
||||
if (other === undefined) return null;
|
||||
const armT = lowerer.mapTypeOf(other);
|
||||
if (armT === null) return null;
|
||||
if (armT.kind !== "set" && armT.kind !== "map" && armT.kind !== "string") return null;
|
||||
const tag = lowerer.armTag(iterable.type.unionId, armT);
|
||||
if (tag < 0) return null;
|
||||
const loc = locOf(stmt);
|
||||
const narrowed: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId: iterable.type.unionId,
|
||||
tag,
|
||||
value: iterable,
|
||||
type: armT,
|
||||
loc,
|
||||
};
|
||||
const loop: IrStmt =
|
||||
armT.kind === "set"
|
||||
? lowerForOfSet(lowerer, stmt, narrowed, armT)
|
||||
: armT.kind === "map"
|
||||
? lowerForOfMap(lowerer, stmt, narrowed, armT)
|
||||
: lowerForOfString(lowerer, stmt, narrowed, labels);
|
||||
return {
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: iterable.type.unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value: iterable,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [loop],
|
||||
else_: null,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
// Labels consume HERE (nested statements must never see them). The
|
||||
// plain array/string paths carry them; the container/matchAll/stdin
|
||||
@@ -10915,6 +10981,10 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
const iterableUnion = iterableUnionOf(lowerer, iterable.type);
|
||||
if (iterableUnion) return lowerForOfIterable(lowerer, stmt, iterable, iterableUnion, labels);
|
||||
}
|
||||
{
|
||||
const emptyArrayArm = lowerForOfEmptyArrayArm(lowerer, stmt, iterable, iterSrc, labels);
|
||||
if (emptyArrayArm) return emptyArrayArm;
|
||||
}
|
||||
let tupleSource: {
|
||||
shapeId: string;
|
||||
fields: { name: string; type: IrType }[];
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
import type { IrModule } from "../src/ir/ir.js";
|
||||
|
||||
/** Lower one program to IR; a fence answers its diagnostic codes instead. */
|
||||
async function lower(
|
||||
source: string,
|
||||
): Promise<{ ok: true; module: IrModule } | { ok: false; codes: string[] }> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-iterable-union-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok) return { ok: false, codes: result.diagnostics.map((d) => d.code) };
|
||||
return { ok: true, module: deserializeModule(await readFile(outPath, "utf8")) };
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Every IR node kind present in the module, as a set. */
|
||||
function nodeKinds(module: IrModule): Set<string> {
|
||||
const kinds = new Set<string>();
|
||||
const visit = (value: unknown): void => {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) visit(item);
|
||||
return;
|
||||
}
|
||||
if (value === null || typeof value !== "object") return;
|
||||
const node = value as { kind?: unknown };
|
||||
if (typeof node.kind === "string") kinds.add(node.kind);
|
||||
for (const child of Object.values(value)) visit(child);
|
||||
};
|
||||
visit(module.functions);
|
||||
return kinds;
|
||||
}
|
||||
|
||||
test("a for-of over an iterable beside never[] walks the iterable on its tag", async () => {
|
||||
const result = await lower(`
|
||||
function setTotal(registry: Map<string, Set<string>>, key: string): number {
|
||||
let total = 0;
|
||||
for (const name of registry.get(key) ?? []) total += name.length;
|
||||
return total;
|
||||
}
|
||||
function mapTotal(registry: Map<string, Map<string, number>>, key: string): number {
|
||||
let total = 0;
|
||||
for (const [name, value] of registry.get(key) ?? []) total += name.length + value;
|
||||
return total;
|
||||
}
|
||||
function stringTotal(registry: Map<string, string>, key: string): number {
|
||||
let total = 0;
|
||||
for (const ch of registry.get(key) ?? []) total += ch.length;
|
||||
return total;
|
||||
}
|
||||
console.log(setTotal(new Map(), "k"), mapTotal(new Map(), "k"), stringTotal(new Map(), "k"));
|
||||
export {};
|
||||
`);
|
||||
if (!result.ok) throw new Error(`fenced: ${result.codes.join(", ")}`);
|
||||
expect(validateModule(result.module)).toEqual([]);
|
||||
const kinds = nodeKinds(result.module);
|
||||
// The loop is guarded by the arm's tag; the empty-array arm yields nothing.
|
||||
expect(kinds.has("unionIsTag")).toBe(true);
|
||||
expect(kinds.has("unionNarrow")).toBe(true);
|
||||
});
|
||||
|
||||
test("a union of two inhabited iterables still has no common iteration", async () => {
|
||||
const result = await lower(`
|
||||
function total(registry: Map<string, Set<string> | string[]>, key: string): number {
|
||||
let sum = 0;
|
||||
for (const item of registry.get(key) ?? []) sum += item.length;
|
||||
return sum;
|
||||
}
|
||||
console.log(total(new Map(), "k"));
|
||||
export {};
|
||||
`);
|
||||
// Both arms can hold values, and the two walks yield at different element
|
||||
// kinds through different protocols: the union keeps its fence.
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.codes).toEqual(["SC2001"]);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8419-iterable-never-array-arm.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// `for (const x of registry.get(key) ?? [])`: the receiver's type is the
|
||||
// iterable beside `never[]` — the array literal's type, whose only value is
|
||||
// the empty array, so that arm runs zero iterations.
|
||||
function setLength(registry: Map<string, Set<string>>, key: string): number {
|
||||
let total = 0;
|
||||
for (const name of registry.get(key) ?? []) total += name.length;
|
||||
return total;
|
||||
}
|
||||
|
||||
function mapTotal(registry: Map<string, Map<string, number>>, key: string): number {
|
||||
let total = 0;
|
||||
for (const [name, value] of registry.get(key) ?? []) total += name.length + value;
|
||||
return total;
|
||||
}
|
||||
|
||||
function stringLength(registry: Map<string, string>, key: string): number {
|
||||
let total = 0;
|
||||
for (const ch of registry.get(key) ?? []) total += ch.length;
|
||||
return total;
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const sets = new Map<string, Set<string>>();
|
||||
sets.set("a", new Set(["xy", "z"]));
|
||||
console.log("set", setLength(sets, "a"), setLength(sets, "missing"));
|
||||
sets.get("a")?.add("q");
|
||||
console.log("set live", setLength(sets, "a"));
|
||||
|
||||
const maps = new Map<string, Map<string, number>>();
|
||||
maps.set("a", new Map([["xy", 3]]));
|
||||
console.log("map", mapTotal(maps, "a"), mapTotal(maps, "missing"));
|
||||
|
||||
const strings = new Map<string, string>();
|
||||
strings.set("a", "xyz");
|
||||
console.log("string", stringLength(strings, "a"), stringLength(strings, "missing"));
|
||||
|
||||
// The same shape through an explicit union annotation.
|
||||
const empty: Set<string> | never[] = sets.get("missing") ?? [];
|
||||
let seen = 0;
|
||||
for (const item of empty) seen += item.length;
|
||||
console.log("annotated", seen);
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3000, "san": 4100 }
|
||||
Reference in New Issue
Block a user