mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
`entries.map((entry) => async () => …)` (package-manager's concurrency task lists, `(() => Promise<…>)[]`) was refused: `.map` rejected a `func` return with the same badType as a `void` return. A closure is a supported array element. The closure array literal `[() => 1]` already lowers to a `func`-element array, where the value is one ScrClosure for its whole life, stored by reference with the element kind's `_v` adapter and `scr_closure_trace_v`. The map helper builds the same array, so the `func` half of the fence goes at both `.map` sites (plain receiver and union-of-arrays receiver). The `void` fence stays: `void[]` has no element, because a void return value is erased before it could be written. Corpus 8415-map-closure-elements pins the `.map`-produced arrays of closures: `Array<() => Promise<string>>` and `Array<() => string>`, closure identity through `indexOf`/`includes`, an inline `.map`, and `Promise.all(tasks.map((task) => task()))`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
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-map-closure-"));
|
|
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 });
|
|
}
|
|
}
|
|
|
|
test("a map callback answering a closure produces an array of functions", async () => {
|
|
const result = await lower(`
|
|
interface Entry { source: string; scope: "user" | "project" }
|
|
function tasksFor(entries: Entry[]): Array<() => Promise<string>> {
|
|
return entries.map((entry) => async () => \`\${entry.scope}:\${entry.source}\`);
|
|
}
|
|
function readers(entries: Entry[]): (() => string)[] {
|
|
return entries.map((entry) => () => entry.source);
|
|
}
|
|
console.log(tasksFor([]).length, readers([]).length);
|
|
export {};
|
|
`);
|
|
if (!result.ok) throw new Error(`fenced: ${result.codes.join(", ")}`);
|
|
expect(validateModule(result.module)).toEqual([]);
|
|
// The element kind is the closure type itself: a function value is one
|
|
// ScrClosure stored by reference, like a closure array literal.
|
|
for (const name of ["tasksFor", "readers"]) {
|
|
const fn = result.module.functions.find((candidate) => candidate.name === name);
|
|
expect(fn, name).toBeDefined();
|
|
expect(fn!.returnType.kind, name).toBe("array");
|
|
if (fn!.returnType.kind === "array") expect(fn!.returnType.elem.kind, name).toBe("func");
|
|
}
|
|
});
|
|
|
|
test("an inline map result of closures stays inline", async () => {
|
|
const result = await lower(`
|
|
interface Entry { source: string }
|
|
const entries: Entry[] = [{ source: "a" }];
|
|
const first = entries.map((entry) => (() => entry.source.length));
|
|
console.log(first[0]!());
|
|
export {};
|
|
`);
|
|
if (!result.ok) throw new Error(`fenced: ${result.codes.join(", ")}`);
|
|
expect(validateModule(result.module)).toEqual([]);
|
|
});
|
|
|
|
test("a void-returning map callback still has no element to store", async () => {
|
|
const result = await lower(`
|
|
const items = [1, 2];
|
|
const out = items.map((n) => {
|
|
console.log(n);
|
|
});
|
|
console.log(out.length);
|
|
export {};
|
|
`);
|
|
// `void[]` stays unrepresentable: a void value is erased before it could
|
|
// be written. Only the CLOSURE element became an ordinary element.
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.codes).toEqual(["SC2009"]);
|
|
});
|