Merge main

This commit is contained in:
Chris Tate
2026-10-26 22:04:34 -05:00
17 changed files with 1325 additions and 213 deletions
@@ -39,6 +39,8 @@ import {
warmNativeCaches,
} from "./native-toolchain.js";
import { splitLlvmProgram } from "./llvm/split.js";
import { implicitToolchainFingerprints } from "./native/compiler-fingerprint.js";
import { fingerprintDependencyPaths } from "./native/dependency-files.js";
const scratch: string[] = [];
const TEST_CACHE_IDENTITY = "cc-cache-tests-v1";
@@ -672,6 +674,29 @@ test.skipIf(clangExecutable === undefined)(
},
);
test.skipIf(clangExecutable === undefined)(
"the implicit toolchain probe leaves out a header the compiler rejects",
async () => {
// A system header that the runtime spells, shadowed by one that is an
// #error, as clang 18's <riscv_vector.h> is outside RISC-V. The probe
// still covers the other headers.
const dir = await mkdtemp(join(tmpdir(), "scriptc-probe-header-"));
scratch.push(dir);
await mkdir(join(dir, "sys"));
const rejected = join(dir, "sys", "auxv.h");
await writeFile(rejected, '#error "not for this target"\n');
const includes = await implicitDependencyProbeIncludes(runtimeSrcDir());
expect(includes).toContain("<sys/auxv.h>");
const fingerprints = await implicitToolchainFingerprints(
{ argv: [clangExecutable!, "-isystem", dir], targetArgs: [], target: null },
"probe-rejected-header",
);
const dependencies = fingerprintDependencyPaths(fingerprints.complete);
expect(dependencies).not.toContain(rejected);
expect(dependencies.some((path) => path.endsWith("/stdatomic.h"))).toBe(true);
},
);
test.skipIf(
process.platform === "win32" ||
clangExecutable === undefined ||
@@ -399,11 +399,8 @@ async function implicitToolchainFingerprintsFresh(
];
const [dependencyResults, linker, assembler, compilerInvocation] = await Promise.all([
Promise.all(
sources.map((source) =>
execFileAsync(compiler, [...probeArgs, source], {
cwd: probeDir,
maxBuffer: 16 * 1024 * 1024,
}),
sources.map((source, index) =>
probeIncludeDependencies(compiler, probeArgs, probeDir, source, sourceGroups[index]!),
),
),
execFileAsync(compiler, [...prefix, "-print-prog-name=ld"], { cwd: probeDir }),
@@ -423,14 +420,7 @@ async function implicitToolchainFingerprintsFresh(
const toolSpellings = [linker.stdout.trim(), assemblerSpelling].filter(
(value, index, all) => value !== "" && all.indexOf(value) === index,
);
const sourceSet = new Set(sources);
const dependencyPaths = [
...new Set(
dependencyResults
.flatMap((dependencies) => parseMakeDependencies(dependencies.stdout))
.filter((path) => !sourceSet.has(path)),
),
].sort();
const dependencyPaths = [...new Set(dependencyResults.flat())].sort();
probe = {
compilerIdentity: compilerIdentity ?? `<unresolved>\0${compiler}`,
compilerInvocation: normalizedProbeInvocation(compilerInvocation, probeDir),
@@ -513,6 +503,51 @@ async function implicitToolchainFingerprintsFresh(
};
}
/** Headers the dependency probe preprocesses at a time when it probes them
* one by one. */
const SINGLE_HEADER_PROBES = 8;
/** The files the compiler reads to preprocess `source`, a probe source that
* includes `includes`, apart from the probe's own sources. When the compiler
* rejects `source`, for example for a header it finds that is an #error
* outside its target (<riscv_vector.h> through clang 18 without the RISC-V
* vector extension), each header is preprocessed on its own, and a header
* that the compiler rejects on its own is left out, as an absent one is.
* Throws the compiler's error when it rejects every header. */
async function probeIncludeDependencies(
compiler: string,
probeArgs: readonly string[],
probeDir: string,
source: string,
includes: readonly string[],
): Promise<string[]> {
const dependencies = async (path: string): Promise<string[]> => {
const result = await execFileAsync(compiler, [...probeArgs, path], {
cwd: probeDir,
maxBuffer: 16 * 1024 * 1024,
});
return parseMakeDependencies(result.stdout).filter((dependency) => dependency !== path);
};
try {
return await dependencies(source);
} catch (error) {
const found: string[][] = [];
for (let start = 0; start < includes.length; start += SINGLE_HEADER_PROBES) {
const batch = includes.slice(start, start + SINGLE_HEADER_PROBES);
const results = await Promise.all(
batch.map(async (include, offset) => {
const single = `${source.slice(0, -2)}-${start + offset}.c`;
await writeFile(single, implicitDependencyIncludeDirective(include));
return dependencies(single).catch(() => null);
}),
);
for (const result of results) if (result !== null) found.push(result);
}
if (found.length === 0) throw error;
return found.flat();
}
}
const stableImplicitToolchainMemos = new Map<string, Promise<ImplicitToolchainFingerprints>>();
export function implicitToolchainFingerprints(
driver: Pick<CcDriver, "argv" | "targetArgs" | "target">,
@@ -1,7 +1,8 @@
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
import {
BOOL,
DYN,
F64,
STRING,
canConvertToDyn,
funcOf,
@@ -16,7 +17,7 @@ import {
import { transformStmtList } from "../../ir/traverse.js";
import { genResultRecord } from "../type-mapper.js";
import type { DispatchSites } from "./dispatch-sites.js";
import type { Lowerer } from "./lowerer.js";
import type { Lowerer, UntypedLoopSite } from "./lowerer.js";
type GeneratorType = Extract<IrType, { kind: "generator" }>;
type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
@@ -401,6 +402,310 @@ export class GeneratorDynamicDispatch {
});
changed = true;
}
return this.stepUntypedLoops(lowerer, functions) || changed;
}
/** The generator types a loop over an untyped iterable can step natively:
* sync generators for `for … of`, and both kinds for `for await` (a sync
* generator through the async-from-sync rules). Insertion order, so a
* type keeps its index as later waves add types. */
private loopTypes(lowerer: Lowerer, site: UntypedLoopSite): [string, GeneratorType][] {
const out: [string, GeneratorType][] = [];
for (const [key, type] of this.generators) {
if (type.async === true && !site.async) continue;
const result = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions);
if (result === null) continue;
// next() with no argument, as the protocol calls it.
const channel = type.nextT;
if (channel.kind !== "undefinedT" && channel.kind !== "void" && channel.kind !== "dyn")
continue;
if (
!canConvertToDyn(
result,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
)
continue;
out.push([key, type]);
}
return out;
}
/** Loops over untyped iterables step the program's own generators on the
* typed path. Once the loop has its iterator and next method, a kind local
* records which generator type the iterator is, when its next method is
* the generator's own (`%dyn.generator.method.next`); each step then
* resumes the generator directly and boxes only the value, where the
* protocol would box the result record and its promise and read them
* back. Every other iterator, and every early exit (return() through the
* protocol), keeps the dynamic path. The awaits are the protocol's: a
* `for await` over an async generator awaits next()'s promise, and over a
* sync generator awaits once for the result and, reached through
* Symbol.iterator, once for the value. */
private stepUntypedLoops(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
if (this.generators.size === 0 || lowerer.untypedLoops.size === 0) return false;
// The loop reads next through the getter only once it exists.
if (!this.properties.has("next")) return false;
let changed = false;
for (const fn of functions) {
if (this.generated.has(fn)) continue;
const byKind = new Map<string, UntypedLoopSite>();
const byValue = new Map<string, UntypedLoopSite>();
for (const local of fn.locals) {
const site = lowerer.untypedLoops.get(local);
if (site === undefined) continue;
byKind.set(site.kind, site);
byValue.set(site.value, site);
}
if (byKind.size === 0) continue;
fn.body = transformStmtList(fn.body, {
expr: (expr) => expr,
stmt: (stmt) => {
if (stmt.kind !== "varDecl" || stmt.init === null) return stmt;
const opened = byKind.get(stmt.localId);
if (opened !== undefined) {
const types = this.loopTypes(lowerer, opened);
const init = this.loopKind(opened, types, stmt.loc);
if (this.loopKindCount(stmt.init) === types.length) return stmt;
changed = true;
return { ...stmt, init };
}
const site = byValue.get(stmt.localId);
if (site === undefined) return stmt;
const types = this.loopTypes(lowerer, site);
// The protocol step is the innermost else of the type chain.
let fallback = stmt.init;
let present = 0;
while (
fallback.kind === "ternary" &&
fallback.cond.kind === "bin" &&
fallback.cond.left.kind === "varRef" &&
fallback.cond.left.localId === site.kind
) {
fallback = fallback.else_;
present++;
}
if (present === types.length) return stmt;
let init = fallback;
for (let i = types.length - 1; i >= 0; i--) {
const [, type] = types[i]!;
const record = `${site.kind}.r${i + 1}`;
init = {
kind: "ternary",
cond: {
kind: "bin",
op: "===",
left: varRef(site.kind, F64, stmt.loc),
right: numLit(i + 1, stmt.loc),
type: BOOL,
loc: stmt.loc,
},
then: this.loopStep(lowerer, fn, site, type, record, stmt.loc),
else_: init,
type: DYN,
loc: stmt.loc,
};
}
changed = true;
return { ...stmt, init };
},
});
}
return changed;
}
/** How many generator types a loop's kind initializer tests: the chain
* inside its own-method test (loopKind). */
private loopKindCount(init: IrExpr): number {
if (init.kind !== "ternary") return 0;
let count = 0;
for (let chain = init.then; chain.kind === "ternary"; chain = chain.else_) count++;
return count;
}
/** The loop's kind: the 1-based index of the generator type its iterator
* is, when the next method it read is that generator's own, else 0. */
private loopKind(
site: UntypedLoopSite,
types: readonly [string, GeneratorType][],
loc: IrExpr["loc"],
): IrExpr {
const own: IrExpr = {
kind: "libCall",
fn: "dyn.sameValue",
args: [
varRef(site.next, DYN, loc),
{
kind: "dynFrom",
value: {
kind: "closure",
fnName: "%dyn.generator.method.next",
captures: [],
type: funcOf([DYN], DYN),
loc,
},
type: DYN,
loc,
},
],
type: BOOL,
loc,
};
let kind: IrExpr = numLit(0, loc);
for (let i = types.length - 1; i >= 0; i--)
kind = {
kind: "ternary",
cond: {
kind: "libCall",
fn: "dyn.typedRefIs",
args: [
varRef(site.iterator, DYN, loc),
{ kind: "strLit", value: types[i]![0], type: STRING, loc },
],
type: BOOL,
loc,
},
then: numLit(i + 1, loc),
else_: kind,
type: F64,
loc,
};
return types.length === 0
? kind
: { kind: "ternary", cond: own, then: kind, else_: numLit(0, loc), type: F64, loc };
}
/** One step of a loop over an untyped iterable that is a `type`
* generator: sets the loop's done flag and answers the value, boxed. */
private loopStep(
lowerer: Lowerer,
fn: IrFunction,
site: UntypedLoopSite,
type: GeneratorType,
recordId: string,
loc: IrExpr["loc"],
): IrExpr {
const gen: IrExpr = { kind: "dynCheck", value: varRef(site.iterator, DYN, loc), type, loc };
const setDone = (value: IrExpr): IrStmt => ({ kind: "assign", localId: site.done, value, loc });
const doneRef = varRef(site.done, BOOL, loc);
const result = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions)!;
if (!fn.locals.some((local) => local.id === recordId))
fn.locals.push({ id: recordId, name: recordId, type: result, mutable: false });
const field = (name: "value" | "done", fieldType: IrType): IrExpr => ({
kind: "recordGet",
obj: varRef(recordId, result, loc),
shapeId: result.shapeId,
field: name,
type: fieldType,
loc,
});
const valueType = lowerer.shapes
.get(result.shapeId)!
.fields.find((f) => f.name === "value")!.type;
const boxed: IrExpr = { kind: "dynFrom", value: field("value", valueType), type: DYN, loc };
const awaitDyn = (value: IrExpr): IrExpr => ({
kind: "libCall",
fn: "async.awaitDyn",
args: [value],
type: DYN,
loc,
});
const resumed: IrExpr = {
kind: "genResume",
mode: "next",
gen,
arg: null,
type: type.async === true ? { kind: "promise", inner: result } : result,
loc,
};
if (!site.async)
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: recordId, init: resumed, loc },
setDone(field("done", BOOL)),
],
result: {
kind: "ternary",
cond: doneRef,
then: dynUndefinedExpr(loc),
else_: boxed,
type: DYN,
loc,
},
type: DYN,
loc,
};
if (type.async === true)
return {
kind: "seqExpr",
stmts: [
{
kind: "varDecl",
localId: recordId,
init: { kind: "awaitExpr", value: resumed, type: result, loc },
loc,
},
setDone(field("done", BOOL)),
],
result: {
kind: "ternary",
cond: doneRef,
then: dynUndefinedExpr(loc),
else_: boxed,
type: DYN,
loc,
},
type: DYN,
loc,
};
// for await over a sync generator: the result awaits once. When the
// loop reached it through Symbol.iterator, the async-from-sync iterator
// also awaits the value (the final one too); a sync generator that an
// object's Symbol.asyncIterator returns passes the value as it is, as
// the protocol does.
const fromSync: IrExpr =
site.sync === null
? { kind: "boolLit", value: false, type: BOOL, loc }
: varRef(site.sync, BOOL, loc);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: recordId, init: resumed, loc },
setDone(field("done", BOOL)),
{ kind: "exprStmt", expr: awaitDyn(dynUndefinedExpr(loc)), loc },
],
result: {
kind: "ternary",
cond: doneRef,
then: {
kind: "ternary",
cond: fromSync,
then: {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: awaitDyn(boxed), loc }],
result: dynUndefinedExpr(loc),
type: DYN,
loc,
},
else_: dynUndefinedExpr(loc),
type: DYN,
loc,
},
else_: {
kind: "ternary",
cond: fromSync,
then: awaitDyn(boxed),
else_: boxed,
type: DYN,
loc,
},
type: DYN,
loc,
},
type: DYN,
loc,
};
}
}
@@ -11853,6 +11853,7 @@ function lowerForOfDyn(
const iterator = lowerer.declareHiddenLocal("%dofIterator", DYN);
const next = lowerer.declareHiddenLocal("%dofNext", DYN);
const fast = lowerer.declareHiddenLocal("%dofNative", BOOL);
const generator = lowerer.declareHiddenLocal("%dofGenerator", F64);
const done = lowerer.declareHiddenLocal("%dofDone", BOOL);
done.mutable = true;
const value = lowerer.declareHiddenLocal("%dofValue", DYN);
@@ -11862,6 +11863,15 @@ function lowerForOfDyn(
needsClose.mutable = true;
const error = lowerer.declareHiddenLocal("%dofError", CAUGHT);
const syncIterator = asyncLoop ? lowerer.declareHiddenLocal("%dofSync", BOOL) : null;
lowerer.untypedLoops.set(generator, {
kind: generator.id,
value: value.id,
iterator: iterator.id,
next: next.id,
done: done.id,
async: asyncLoop,
sync: syncIterator?.id ?? null,
});
const awaitDyn = (value: IrExpr): IrExpr => ({
kind: "libCall",
fn: "async.awaitDyn",
@@ -12136,6 +12146,14 @@ function lowerForOfDyn(
: iteratorCanStep(varRef(iterator.id, DYN, loc), varRef(next.id, DYN, loc), loc),
loc,
},
// Which of the program's generator types the iterator is, once
// generator-dynamic-dispatch.ts knows them; 0 keeps the protocol.
{
kind: "varDecl",
localId: generator.id,
init: { kind: "numLit", value: 0, type: F64, loc },
loc,
},
{
kind: "varDecl",
localId: done.id,
@@ -2068,6 +2068,21 @@ function byName(fns: readonly IrFunction[]): IrFunction[] {
);
}
/** A loop over an untyped iterable (lowerForOfDyn). `kind` is a local the
* loop sets once it has opened the iterator: 0 for the dynamic protocol,
* or the 1-based index of the program's generator type it steps natively.
* `value` is the per-step local whose initializer takes one step. */
export interface UntypedLoopSite {
kind: string;
value: string;
iterator: string;
next: string;
done: string;
/** `for await`; `sync` names its async-from-sync flag. */
async: boolean;
sync: string | null;
}
/** The program module a createRequire binding requires. */
type CreateRequireTarget = NonNullable<ReturnType<typeof createRequireProgramModuleOf>>;
@@ -3591,6 +3606,11 @@ export class Lowerer {
* values other than fresh literals (noteUntypedIntegrity). */
untypedIntegrity = false;
readonly liftedFns: IrFunction[] = [];
/** `for … of` and `for await` loops over untyped iterables, by their
* generator-kind local (generator-dynamic-dispatch.ts steps the program's
* own generators there on the typed path). Local ids repeat across
* functions; the local itself is in its function's locals. */
readonly untypedLoops = new Map<IrLocal, UntypedLoopSite>();
/** global-values.ts's answer per JavaScript source: the receiver of its
* first read of the global object by a computed name, or null. */
readonly computedGlobalReads = new Map<ts.SourceFile, ts.Expression | null>();
@@ -2069,3 +2069,252 @@ export function walkTypeFields(node: IrType, walker: IrFieldWalker, skipType: bo
}
}
}
/** A field ir.ts declares that this file does not read. */
type FieldsNotRead<T, Read extends PropertyKey> = Exclude<keyof T, Read>;
/** The string or object values a field read as a scalar can hold. */
type ValuesNotRead<T> = Extract<NonNullable<T>, string | object>;
/** Fails type checking unless `T` is never: run `pnpm generate:ir-fields`. */
type Unread<T extends never> = T;
/** Every walked object type's fields as ir.ts declared them when this file
* was generated. */
export type IrFieldsCurrent = [
Unread<FieldsNotRead<Extract<IrExpr, { kind: "arrIntrinsic" }>, "kind" | "method" | "receiver" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "arrayGet" }>, "kind" | "arr" | "index" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "arrayHas" }>, "kind" | "arr" | "index" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "arrayLit" }>, "kind" | "elems" | "spreads" | "capacity" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "arrayLit" }>["capacity"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "arrayNewLen" }>, "kind" | "length" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "arrayState" }>, "kind" | "arr" | "index" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "assignExpr" }>, "kind" | "localId" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "awaitExpr" }>, "kind" | "value" | "type" | "adopt" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "awaitExpr" }>["adopt"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "awaitUnionExpr" }>, "kind" | "value" | "promiseTag" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "awaitUnionExpr" }>["promiseTag"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "bin" }>, "kind" | "op" | "left" | "right" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "boolLit" }>, "kind" | "value" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "boolLit" }>["value"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "bytesIntrinsic" }>, "kind" | "method" | "receiver" | "args" | "type" | "loc" | "invalidNaN">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "bytesIntrinsic" }>["invalidNaN"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "bytesNew" }>, "kind" | "source" | "from" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "bytesNew" }>["from"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "call" }>, "kind" | "callee" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "callValue" }>, "kind" | "callee" | "receiver" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "caughtCheck" }>, "kind" | "value" | "className" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "caughtNarrow" }>, "kind" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "caughtTest" }>, "kind" | "value" | "test" | "className" | "negated" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "caughtTest" }>["negated"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "caughtToDyn" }>, "kind" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "chainRecv" }>, "kind" | "id" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "classRef" }>, "kind" | "className" | "captures" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "closure" }>, "kind" | "fnName" | "captures" | "adapts" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "closure" }>["adapts"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "downcast" }>, "kind" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynArrLit" }>, "kind" | "elems" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrExpr, { kind: "dynCall" }>["spreads"]>, readonly unknown[]>[number]>, object>, "arg" | "what">>,
Unread<ValuesNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrExpr, { kind: "dynCall" }>["spreads"]>, readonly unknown[]>[number]>, object>["arg"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynCall" }>, "kind" | "callee" | "receiver" | "calleeName" | "calleeNameValue" | "args" | "spreads" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynCheck" }>, "kind" | "value" | "preserveRefs" | "adopt" | "exact" | "inferredSite" | "origin" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynCheck" }>["preserveRefs"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynCheck" }>["adopt"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynCheck" }>["exact"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynCheck" }>["origin"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynDestrCheck" }>, "kind" | "value" | "spelling" | "firstProp" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynFrom" }>, "kind" | "value" | "fnName" | "fnLength" | "liveRef" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynFrom" }>["fnLength"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynFrom" }>["liveRef"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynHasKey" }>, "kind" | "key" | "negated" | "value" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynHasKey" }>["negated"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynInvoke" }>, "kind" | "recv" | "method" | "calleeName" | "calleeNameValue" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynIterN" }>, "kind" | "value" | "count" | "notIterableMessage" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynIterN" }>["count"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynKeyGet" }>, "kind" | "key" | "optional" | "value" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynKeyGet" }>["optional"]>>,
Unread<FieldsNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrExpr, { kind: "dynObjLit" }>["fields"]>, readonly unknown[]>[number]>, object>, "key" | "value">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynObjLit" }>, "kind" | "fields" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynScalarEq" }>, "kind" | "left" | "right" | "negated" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynScalarEq" }>["negated"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "dynTest" }>, "kind" | "test" | "bytesElem" | "negated" | "value" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "dynTest" }>["negated"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "ffiCall" }>, "kind" | "import" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "fieldAbsent" }>, "kind" | "unionId" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "fieldGet" }>, "kind" | "obj" | "className" | "field" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "fieldIncDec" }>, "kind" | "op" | "prefix" | "obj" | "className" | "field" | "fieldDyn" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "fieldIncDec" }>["prefix"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "fieldIncDec" }>["fieldDyn"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "genResume" }>, "kind" | "mode" | "gen" | "arg" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "incDec" }>, "kind" | "op" | "prefix" | "localId" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "incDec" }>["prefix"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "instanceOf" }>, "kind" | "value" | "className" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "instanceOfValue" }>, "kind" | "value" | "classValue" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "intrinsic" }>, "kind" | "name" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "jsonStringify" }>, "kind" | "value" | "indent" | "circular" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "jsonStringify" }>["circular"]>>,
Unread<FieldsNotRead<Extract<NonNullable<Extract<IrExpr, { kind: "libCall" }>["assertOptions"]>, object>, "diff" | "skipPrototype">>,
Unread<ValuesNotRead<Extract<NonNullable<Extract<IrExpr, { kind: "libCall" }>["assertOptions"]>, object>["skipPrototype"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "libCall" }>, "kind" | "fn" | "args" | "type" | "loc" | "prototypeIdentityOnly" | "realTimer" | "assertOptions">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "libCall" }>["prototypeIdentityOnly"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "libCall" }>["realTimer"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "logical" }>, "kind" | "op" | "left" | "right" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "mapIntrinsic" }>, "kind" | "method" | "receiver" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrExpr, { kind: "mapNew" }>["seed"]>, readonly unknown[]>[number]>, object>, "key" | "value">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "mapNew" }>, "kind" | "seed" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "moduleNsRef" }>, "kind" | "moduleId" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "new" }>, "kind" | "className" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "newPromise" }>, "kind" | "executor" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "newValue" }>, "kind" | "callee" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "nullish" }>, "kind" | "left" | "right" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "numLit" }>, "kind" | "value" | "spelling" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "numLit" }>["value"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "optChain" }>, "kind" | "id" | "receiver" | "body" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "orDefault" }>, "kind" | "left" | "right" | "retag" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "promiseVoidWiden" }>, "kind" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "promiseWithResolvers" }>, "kind" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrExpr, { kind: "recordClone" }>["overrides"]>, readonly unknown[]>[number]>, object>, "name" | "value">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "recordClone" }>, "kind" | "source" | "overrides" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "recordGet" }>, "kind" | "obj" | "shapeId" | "field" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "recordHas" }>, "kind" | "obj" | "shapeId" | "field" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "recordKeyGet" }>, "kind" | "obj" | "shapeId" | "key" | "overflowOnly" | "missNaN" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "recordKeyGet" }>["overflowOnly"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "recordKeyGet" }>["missNaN"]>>,
Unread<FieldsNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrExpr, { kind: "recordLit" }>["fields"]>, readonly unknown[]>[number]>, object>, "name" | "value" | "overflow">>,
Unread<ValuesNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrExpr, { kind: "recordLit" }>["fields"]>, readonly unknown[]>[number]>, object>["overflow"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "recordLit" }>, "kind" | "fields" | "nullProto" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "recordLit" }>["nullProto"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "recordNullProto" }>, "kind" | "obj" | "shapeId" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "recordOvfHas" }>, "kind" | "obj" | "shapeId" | "key" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "recordOvfKeys" }>, "kind" | "obj" | "shapeId" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "regexIntrinsic" }>, "kind" | "method" | "receiver" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "regexLit" }>, "kind" | "pattern" | "flags" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "selfRef" }>, "kind" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "seqExpr" }>, "kind" | "stmts" | "result" | "generatorDelegate" | "diverges" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "seqExpr" }>["generatorDelegate"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "seqExpr" }>["diverges"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "setIntrinsic" }>, "kind" | "method" | "receiver" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "setNew" }>, "kind" | "seed" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "strCmp" }>, "kind" | "op" | "left" | "right" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "strConcat" }>, "kind" | "left" | "right" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "strEq" }>, "kind" | "negated" | "left" | "right" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "strEq" }>["negated"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "strIntrinsic" }>, "kind" | "method" | "receiver" | "args" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "strLit" }>, "kind" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "templateStrings" }>, "kind" | "key" | "cooked" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "ternary" }>, "kind" | "cond" | "then" | "else_" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "toBool" }>, "kind" | "operand" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "toString" }>, "kind" | "operand" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unary" }>, "kind" | "op" | "operand" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unionDisc" }>, "kind" | "unionId" | "field" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unionEq" }>, "kind" | "unionId" | "negated" | "sameValue" | "left" | "right" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "unionEq" }>["negated"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "unionEq" }>["sameValue"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unionFuncEq" }>, "kind" | "unionId" | "tag" | "union" | "func" | "negated" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "unionFuncEq" }>["tag"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "unionFuncEq" }>["negated"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unionIsTag" }>, "kind" | "unionId" | "tag" | "negated" | "value" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "unionIsTag" }>["tag"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "unionIsTag" }>["negated"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unionKeyGet" }>, "kind" | "unionId" | "key" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unionNarrow" }>, "kind" | "unionId" | "tag" | "value" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "unionNarrow" }>["tag"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unionRetype" }>, "kind" | "fromId" | "unionId" | "tags" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unionWrap" }>, "kind" | "unionId" | "tag" | "value" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "unionWrap" }>["tag"]>>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "unitLit" }>, "kind" | "unit" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "upcast" }>, "kind" | "value" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "varRef" }>, "kind" | "localId" | "type" | "loc">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "virtualCall" }>, "kind" | "className" | "method" | "args" | "type" | "loc" | "targets">>,
Unread<FieldsNotRead<Extract<NonNullable<Extract<IrExpr, { kind: "yieldExpr" }>["captureCompletion"]>, object>, "returnType">>,
Unread<FieldsNotRead<Extract<IrExpr, { kind: "yieldExpr" }>, "kind" | "value" | "awaited" | "delegated" | "captureCompletion" | "type" | "loc">>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "yieldExpr" }>["awaited"]>>,
Unread<ValuesNotRead<Extract<IrExpr, { kind: "yieldExpr" }>["delegated"]>>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "arrayDelete" }>, "kind" | "arr" | "index" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "arraySet" }>, "kind" | "arr" | "index" | "value" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "arraySetLength" }>, "kind" | "arr" | "length" | "loc" | "pop" | "reserve">>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "arraySetLength" }>["pop"]>>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "arraySetLength" }>["reserve"]>>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "arraySetUndefined" }>, "kind" | "arr" | "index" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "assign" }>, "kind" | "localId" | "value" | "initializes" | "loc">>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "assign" }>["initializes"]>>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "block" }>, "kind" | "body" | "labels" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "break" }>, "kind" | "label" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "bytesSet" }>, "kind" | "arr" | "index" | "value" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "continue" }>, "kind" | "label" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "doWhile" }>, "kind" | "body" | "cond" | "labels" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "exprStmt" }>, "kind" | "expr" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "fieldSet" }>, "kind" | "obj" | "className" | "field" | "value" | "sloppy" | "define" | "attributesChecked" | "loc">>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "fieldSet" }>["sloppy"]>>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "fieldSet" }>["define"]>>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "fieldSet" }>["attributesChecked"]>>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "for" }>, "kind" | "init" | "cond" | "update" | "body" | "labels" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "forOf" }>, "kind" | "localId" | "iterable" | "body" | "labels" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "if" }>, "kind" | "cond" | "then" | "else_" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "recordKeyDelete" }>, "kind" | "obj" | "shapeId" | "key" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "recordKeySet" }>, "kind" | "obj" | "shapeId" | "key" | "value" | "overflowOnly" | "loc">>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "recordKeySet" }>["overflowOnly"]>>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "recordSet" }>, "kind" | "obj" | "shapeId" | "field" | "value" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "rethrow" }>, "kind" | "localId" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "return" }>, "kind" | "value" | "awaited" | "loc">>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "return" }>["awaited"]>>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "runtimeFence" }>, "kind" | "code" | "message" | "loc">>,
Unread<FieldsNotRead<Extract<NonNullable<Extract<NonNullable<Extract<IrStmt, { kind: "switch" }>["cases"]>, readonly unknown[]>[number]>, object>, "test" | "body">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "switch" }>, "kind" | "disc" | "cases" | "labels" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "throw" }>, "kind" | "value" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "tryCatch" }>, "kind" | "tryBody" | "catchBody" | "catchLocalId" | "finallyBody" | "suppressFinallyErrors" | "dropFinallyErrors" | "loc">>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "tryCatch" }>["suppressFinallyErrors"]>>,
Unread<ValuesNotRead<Extract<IrStmt, { kind: "tryCatch" }>["dropFinallyErrors"]>>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "varDecl" }>, "kind" | "localId" | "init" | "loc">>,
Unread<FieldsNotRead<Extract<IrStmt, { kind: "while" }>, "kind" | "cond" | "body" | "labels" | "loc">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "array" }>, "kind" | "elem" | "optionalElements">>,
Unread<ValuesNotRead<Extract<IrType, { kind: "array" }>["optionalElements"]>>,
Unread<FieldsNotRead<Extract<IrType, { kind: "bigint" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "bool" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "bytes" }>, "kind" | "elem">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "caught" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "child" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "childStream" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "childWriter" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "classval" }>, "kind" | "className">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "cryptoHash" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "cryptoHmac" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "date" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "dgramSocket" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "dir" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "dyn" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "f64" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "fileHandle" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "fsWatcher" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "func" }>, "kind" | "params" | "ret" | "rest" | "restAbi" | "argumentsAll">>,
Unread<ValuesNotRead<Extract<IrType, { kind: "func" }>["rest"]>>,
Unread<ValuesNotRead<Extract<IrType, { kind: "func" }>["argumentsAll"]>>,
Unread<FieldsNotRead<Extract<IrType, { kind: "generator" }>, "kind" | "async" | "yieldT" | "retT" | "nextT">>,
Unread<ValuesNotRead<Extract<IrType, { kind: "generator" }>["async"]>>,
Unread<FieldsNotRead<Extract<IrType, { kind: "http2Session" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "http2Stream" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "httpClientReq" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "httpReq" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "httpRes" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "jsval" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "map" }>, "kind" | "key" | "value">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "moduleNs" }>, "kind" | "moduleId">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "netServer" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "netSocket" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "nullT" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "object" }>, "kind" | "className">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "procStream" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "promise" }>, "kind" | "inner">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "record" }>, "kind" | "shapeId">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "regex" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "searchParams" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "secureCtx" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "set" }>, "kind" | "elem">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "spawnRes" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "stats" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "string" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "symbol" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "testCtx" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "undefinedT" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "union" }>, "kind" | "unionId">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "url" }>, "kind">>,
Unread<FieldsNotRead<Extract<IrType, { kind: "void" }>, "kind">>,
];
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8380-untyped-generator-loops.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8381-untyped-generator-stages.mjs"
],
"diags": []
}
+11 -3
View File
@@ -1518,7 +1518,11 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) {
(void)to;
scr_trap("scriptc: internal error: native stack switch reached on WASI\n");
#elif defined(SCR_ASAN_FIBERS)
void **save = from_fiber ? &from_fiber->fake_stack : &scr_main_fake_stack;
/* A finished fiber leaves for the last time: a NULL slot has
* AddressSanitizer free its fake stack, which otherwise stays mapped
* until exit. */
void **save = from_fiber ? (from_fiber->done ? NULL : &from_fiber->fake_stack)
: &scr_main_fake_stack;
const void *bottom = to_fiber ? to_fiber->stack : NULL;
size_t size = to_fiber ? SCR_FIBER_STACK : 0;
__sanitizer_start_switch_fiber(save, bottom, size);
@@ -1527,10 +1531,13 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) {
#else
swapcontext(from, to);
#endif
/* Back in this frame: the context that saved into `save` above runs
* again, whichever fiber `scr_current` names (a finished fiber returns
* to its resumer with no fiber to switch to). */
const void *old_bottom;
size_t old_size;
__sanitizer_finish_switch_fiber(
scr_current ? scr_current->fake_stack : scr_main_fake_stack, &old_bottom, &old_size);
__sanitizer_finish_switch_fiber(from_fiber ? from_fiber->fake_stack : scr_main_fake_stack,
&old_bottom, &old_size);
#elif defined(SCR_FAST_CTX)
scr_ctx_swap(from, to);
#else
@@ -2228,6 +2235,7 @@ static void scr_prefix_trampoline(void) {
scr_als_active = &f->als;
entry(f, argpack);
scr_current = carrier;
carrier->done = true; /* its stack is freed once the spawner is back */
scr_switch(&carrier->ctx, carrier->return_to, NULL);
/* unreachable */
}
+22 -11
View File
@@ -706,31 +706,42 @@ static inline size_t scr_utf16_units_short(const char *data, size_t len) {
}
/* A fresh concatenation (no head, the template-literal shape): numbers
* format straight into the result, which reserves scr_f64_to_str's whole
* 32-byte buffer for each. */
* format into a stack buffer first, so the result is allocated at its exact
* length. */
#define SCR_F64_TEXT_MAX 32
static __attribute__((noinline)) ScrStr *scr_str_concat_fresh(ScrStr *const *parts,
const double *nums, size_t count) {
size_t reserve = 0;
char texts[SCR_STR_MIXED_MAX * SCR_F64_TEXT_MAX];
size_t text_len[SCR_STR_MIXED_MAX];
size_t len = 0, used = 0;
bool ascii = true;
for (size_t i = 0; i < count; i++) {
size_t n = parts[i] ? parts[i]->len : SCR_F64_TEXT_MAX;
if (n > SCR_STR_MAX_CAP - reserve) scr_oom();
reserve += n;
if (parts[i]) ascii &= parts[i]->ascii;
size_t n;
if (parts[i]) {
n = parts[i]->len;
ascii &= parts[i]->ascii;
} else {
n = scr_f64_to_str(nums[i], texts + used);
text_len[i] = n;
used += n;
}
if (n > SCR_STR_MAX_CAP - len) scr_oom();
len += n;
}
ScrStr *out = scr_str_take_spare(reserve);
if (!out) out = scr_str_alloc(reserve, reserve);
ScrStr *out = scr_str_take_spare(len);
if (!out) out = scr_str_alloc(len, len);
char *w = out->data;
const char *text = texts;
for (size_t i = 0; i < count; i++) {
if (parts[i]) {
scr_copy_bytes(w, parts[i]->data, parts[i]->len);
w += parts[i]->len;
} else {
w += scr_f64_to_str(nums[i], w);
scr_copy_bytes(w, text, text_len[i]);
w += text_len[i];
text += text_len[i];
}
}
size_t len = (size_t)(w - out->data);
out->len = len;
out->data[len] = '\0';
out->ascii = ascii;
+7
View File
@@ -88,6 +88,13 @@ test("string methods match Node on committed oracle cases", async () => {
expect(r.stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/);
});
// A template literal of strings and numbers allocates exactly its length:
// numbers format before the result is sized, not into a reserved maximum.
test("template-literal concatenation allocates its exact length", async () => {
const { stderr } = await execFileAsync(bin, ["--concat-capacity"]);
expect(stderr.trim()).toBe("0 concat capacity failures");
});
// repeat(count) with count < 0 or Infinity is Node's catchable RangeError
// ("Invalid count value: -1"); the runtime leaves it pending.
test.each([
+35
View File
@@ -982,7 +982,42 @@ static void ordering_asserts(void) {
}
}
/* Template-literal concatenations (no head) of strings and numbers: the
* result holds exactly its bytes, whatever the numbers format to. */
static int check_concat_capacity(void) {
ScrStr *a = scr_str_new("f", 1), *b = scr_str_new("_d", 2), *c = scr_str_new("", 0);
struct {
ScrStr *parts[4];
double nums[4];
size_t count;
const char *expected;
} cases[] = {
{{a, NULL, b, NULL}, {0, 7, 0, 2230}, 4, "f7_d2230"},
{{NULL, a}, {-1.5e-7, 0}, 2, "-1.5e-7f"},
{{NULL, NULL, NULL}, {1.7976931348623157e308, 0.1, -0.0}, 3, "1.7976931348623157e+3080.10"},
{{c, NULL, c}, {0, 42, 0}, 3, "42"},
};
int failed = 0;
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) {
ScrStr *r = scr_str_concat_mixed(NULL, cases[i].parts, cases[i].nums, cases[i].count);
size_t want = strlen(cases[i].expected);
if (r->len != want || memcmp(r->data, cases[i].expected, want) != 0 || r->data[want] != '\0' ||
r->cap != r->len) {
fprintf(stderr, "concat case %zu: got \"%.*s\" len %zu cap %zu, want \"%s\" len %zu\n", i,
(int)r->len, r->data, r->len, (size_t)r->cap, cases[i].expected, want);
failed++;
}
scr_str_release(r);
}
scr_str_release(a);
scr_str_release(b);
scr_str_release(c);
fprintf(stderr, "%d concat capacity failures\n", failed);
return failed != 0;
}
int main(int argc, char **argv) {
if (argc > 1 && strcmp(argv[1], "--concat-capacity") == 0) return check_concat_capacity();
if (argc > 1 && strncmp(argv[1], "--crash-repeat", 14) == 0) {
ScrStr *s = scr_str_new("ab", 2);
double count = strcmp(argv[1], "--crash-repeat-inf") == 0
+264 -183
View File
@@ -14,202 +14,283 @@ const require = createRequire(join(root, "package.json"));
const ts = require("typescript");
const irPath = join(root, "packages/compiler/src/ir/ir.ts");
const target = join(root, "packages/compiler/src/ir/fields.generated.ts");
const samePath = (a, b) => a.replace(/\\/g, "/") === b.replace(/\\/g, "/");
const program = ts.createProgram([irPath], { strict: true, target: ts.ScriptTarget.ES2022 });
const checker = program.getTypeChecker();
const source = program.getSourceFile(irPath);
const aliases = new Map();
ts.forEachChild(source, (node) => {
if (ts.isTypeAliasDeclaration(node)) aliases.set(node.name.text, node);
});
/** The text of fields.generated.ts for `irText`, the source of ir.ts. */
export function generateIrFields(irText = readFileSync(irPath, "utf8")) {
const options = { strict: true, target: ts.ScriptTarget.ES2022 };
const host = ts.createCompilerHost(options);
const readSourceFile = host.getSourceFile;
// ir.ts is read from `irText`; the files it imports from disk.
host.getSourceFile = (file, languageVersion, ...rest) =>
samePath(file, irPath)
? ts.createSourceFile(file, irText, languageVersion)
: readSourceFile.call(host, file, languageVersion, ...rest);
const program = ts.createProgram([irPath], options, host);
const checker = program.getTypeChecker();
const source = program.getSourceFile(irPath);
const aliases = new Map();
ts.forEachChild(source, (node) => {
if (ts.isTypeAliasDeclaration(node)) aliases.set(node.name.text, node);
});
/** Whether a value of `type` can be a string. */
function holdsString(type) {
if (type.isUnion()) return type.types.some(holdsString);
return (type.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
}
/** Whether every value of `type` is a string. */
function onlyString(type) {
if (type.isUnion()) return type.types.every(onlyString);
return (type.flags & ts.TypeFlags.StringLike) !== 0;
}
/** Each kind's string fields, in declaration order, merged over the unions:
* a kind both unions declare lists the fields of both. */
const kinds = new Map();
for (const name of ["IrExpr", "IrStmt"]) {
const alias = aliases.get(name);
const union = checker.getTypeAtLocation(alias.name);
for (const member of union.isUnion() ? union.types : [union]) {
const kindSymbol = member.getProperty("kind");
const kindType = checker.getTypeOfSymbolAtLocation(kindSymbol, alias);
if (!kindType.isStringLiteral()) throw new Error(`${name} member without a literal kind`);
const fields = kinds.get(kindType.value) ?? [];
for (const property of member.getProperties()) {
const type = checker.getTypeOfSymbolAtLocation(property, alias);
if (!holdsString(type)) continue;
const known = fields.find((field) => field.name === property.name);
const always = onlyString(type) && (property.flags & ts.SymbolFlags.Optional) === 0;
if (known === undefined) fields.push({ name: property.name, always });
else known.always &&= always;
}
kinds.set(kindType.value, fields);
/** Whether a value of `type` can be a string. */
function holdsString(type) {
if (type.isUnion()) return type.types.some(holdsString);
return (type.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
}
}
const lines = [
"// Generated from packages/compiler/src/ir/ir.ts by scripts/generate-ir-fields.mjs.",
"// Run `pnpm generate:ir-fields` after changing the fields of an IR node.",
'import type { IrExpr, IrStmt, IrType } from "./ir.js";',
"",
"/** Calls `visit` with each string field of `node` (including `kind`) and its",
" * name: the string values `for (const key in node)` finds on a node, read",
" * by name in the order ir.ts declares them. */",
"export function eachStringField(",
" node: IrExpr | IrStmt,",
" visit: (value: string, field: string) => void,",
"): void {",
" switch (node.kind) {",
];
for (const [kind, fields] of [...kinds].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
lines.push(` case ${JSON.stringify(kind)}: {`);
for (const { name, always } of fields) {
const read = `node.${name}`;
if (always) lines.push(` visit(${read}, ${JSON.stringify(name)});`);
else
lines.push(` if (typeof ${read} === "string") visit(${read}, ${JSON.stringify(name)});`);
/** Whether every value of `type` is a string. */
function onlyString(type) {
if (type.isUnion()) return type.types.every(onlyString);
return (type.flags & ts.TypeFlags.StringLike) !== 0;
}
lines.push(" return;", " }");
}
lines.push(" }", "}", "");
// The deep walkers: every field of every object under an IR node, as a
// recursive key walk finds them, read by name.
const members = new Map();
for (const name of ["IrExpr", "IrStmt", "IrType"]) {
const union = checker.getTypeAtLocation(aliases.get(name).name);
members.set(name, new Set(union.isUnion() ? union.types : [union]));
}
const nullish = ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void;
const scalar =
ts.TypeFlags.NumberLike |
ts.TypeFlags.BooleanLike |
ts.TypeFlags.BigIntLike |
ts.TypeFlags.ESSymbolLike;
/** The walker an IR union member belongs to, or null. */
function irKind(type) {
for (const [name, set] of members) if (set.has(type)) return name;
return null;
}
let temp = 0;
/** Lines that walk `expr` of `type`: strings to `walker.string`, IR nodes to
* their walkers, arrays by element and other objects by field. */
function walkLines(expr, type, indent, depth) {
if (depth > 12) throw new Error(`field type nests too deep at ${expr}`);
const parts = type.isUnion() ? type.types : [type];
const present = parts.filter((part) => (part.flags & nullish) === 0);
const optional = present.length !== parts.length;
const strings = present.filter((part) => (part.flags & ts.TypeFlags.StringLike) !== 0);
const objects = present.filter((part) => (part.flags & (ts.TypeFlags.StringLike | scalar)) === 0);
for (const part of present)
if ((part.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0)
throw new Error(`untyped field ${expr}`);
const body = [];
if (objects.length > 0) {
const kinds = new Set(objects.map(irKind));
if (kinds.size === 1 && !kinds.has(null)) {
const [kind] = kinds;
body.push(`walk${kind.slice(2)}Fields(${expr}, walker, skipType);`);
} else if (objects.every((part) => checker.isArrayType(part))) {
if (objects.length !== 1) throw new Error(`several array types at ${expr}`);
const item = `item${temp++}`;
body.push(`for (const ${item} of ${expr}) {`);
body.push(...walkLines(item, checker.getTypeArguments(objects[0])[0], " ", depth + 1));
body.push("}");
} else if (kinds.has(null) && objects.length === 1) {
for (const property of checker.getPropertiesOfType(objects[0])) {
if (property.name === "loc") continue;
const fieldType = checker.getTypeOfSymbol(property);
const field = `${expr}.${property.name}`;
const lines = walkLines(field, fieldType, "", depth + 1);
if (lines.length === 0) continue;
if (property.name === "type")
body.push("if (!skipType) {", ...lines.map((l) => ` ${l}`), "}");
else body.push(...lines);
/** Each kind's string fields, in declaration order, merged over the unions:
* a kind both unions declare lists the fields of both. */
const kinds = new Map();
for (const name of ["IrExpr", "IrStmt"]) {
const alias = aliases.get(name);
const union = checker.getTypeAtLocation(alias.name);
for (const member of union.isUnion() ? union.types : [union]) {
const kindSymbol = member.getProperty("kind");
const kindType = checker.getTypeOfSymbolAtLocation(kindSymbol, alias);
if (!kindType.isStringLiteral()) throw new Error(`${name} member without a literal kind`);
const fields = kinds.get(kindType.value) ?? [];
for (const property of member.getProperties()) {
const type = checker.getTypeOfSymbolAtLocation(property, alias);
if (!holdsString(type)) continue;
const known = fields.find((field) => field.name === property.name);
const always = onlyString(type) && (property.flags & ts.SymbolFlags.Optional) === 0;
if (known === undefined) fields.push({ name: property.name, always });
else known.always &&= always;
}
} else throw new Error(`mixed object types at ${expr}: ${checker.typeToString(type)}`);
kinds.set(kindType.value, fields);
}
}
let lines = body;
if (strings.length > 0 && objects.length > 0)
lines = [
`if (typeof ${expr} === "string") walker.string(${expr});`,
...(body.length > 0 ? [`else {`, ...body.map((l) => ` ${l}`), "}"] : []),
];
else if (strings.length > 0) lines = [`walker.string(${expr});`];
if (lines.length > 0 && optional && !(strings.length > 0 && objects.length > 0))
lines = [
`if (${expr} !== undefined && ${expr} !== null) {`,
...lines.map((l) => ` ${l}`),
"}",
];
else if (lines.length > 0 && optional)
lines = [
`if (${expr} !== undefined && ${expr} !== null) {`,
...lines.map((l) => ` ${l}`),
"}",
];
return lines.map((line) => indent + line);
}
lines.push(
"/** What a field walk reports (walkExprFields): each expression and",
" * statement node before its fields, and each string a field holds. */",
"export interface IrFieldWalker {",
" node(node: IrExpr | IrStmt): void;",
" string(value: string): void;",
"}",
"",
);
for (const name of ["IrExpr", "IrStmt", "IrType"]) {
const short = name.slice(2);
const nodeCall = name === "IrType" ? [] : [" walker.node(node);"];
lines.push(
`/** Walks every field of \`node\` but \`loc\` (and \`type\` with \`skipType\`), and`,
" * every field of each object under it: the strings and nodes a recursive",
" * `for (const key in value)` walk with the same skips finds, read by name in",
" * the order ir.ts declares the fields. */",
`export function walk${short}Fields(node: ${name}, walker: IrFieldWalker, skipType: boolean): void {`,
...nodeCall,
const lines = [
"// Generated from packages/compiler/src/ir/ir.ts by scripts/generate-ir-fields.mjs.",
"// Run `pnpm generate:ir-fields` after changing the fields of an IR node.",
'import type { IrExpr, IrStmt, IrType } from "./ir.js";',
"",
"/** Calls `visit` with each string field of `node` (including `kind`) and its",
" * name: the string values `for (const key in node)` finds on a node, read",
" * by name in the order ir.ts declares them. */",
"export function eachStringField(",
" node: IrExpr | IrStmt,",
" visit: (value: string, field: string) => void,",
"): void {",
" switch (node.kind) {",
);
const byKind = new Map();
for (const member of members.get(name)) {
const kind = checker.getTypeOfSymbol(member.getProperty("kind")).value;
if (byKind.has(kind)) throw new Error(`${name} kind ${kind} declared twice`);
byKind.set(kind, member);
}
for (const [kind, member] of [...byKind].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
];
for (const [kind, fields] of [...kinds].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
lines.push(` case ${JSON.stringify(kind)}: {`);
for (const property of member.getProperties()) {
if (property.name === "loc") continue;
const fieldType = checker.getTypeOfSymbol(property);
const field = `node.${property.name}`;
const walk = walkLines(field, fieldType, "", 0);
if (walk.length === 0) continue;
if (property.name === "type")
lines.push(" if (!skipType) {", ...walk.map((l) => ` ${l}`), " }");
else lines.push(...walk.map((l) => ` ${l}`));
for (const { name, always } of fields) {
const read = `node.${name}`;
if (always) lines.push(` visit(${read}, ${JSON.stringify(name)});`);
else
lines.push(
` if (typeof ${read} === "string") visit(${read}, ${JSON.stringify(name)});`,
);
}
lines.push(" return;", " }");
}
lines.push(" }", "}", "");
}
const text = lines.join("\n");
if (process.argv.includes("--check")) {
if (readFileSync(target, "utf8") !== text)
throw new Error("fields.generated.ts is stale; run node scripts/generate-ir-fields.mjs");
} else writeFileSync(target, text);
// The deep walkers: every field of every object under an IR node, as a
// recursive key walk finds them, read by name.
const members = new Map();
for (const name of ["IrExpr", "IrStmt", "IrType"]) {
const union = checker.getTypeAtLocation(aliases.get(name).name);
members.set(name, new Set(union.isUnion() ? union.types : [union]));
}
const nullish = ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void;
const scalar =
ts.TypeFlags.NumberLike |
ts.TypeFlags.BooleanLike |
ts.TypeFlags.BigIntLike |
ts.TypeFlags.ESSymbolLike;
/** Whether every value of `type` is a number, boolean, bigint, symbol, null
* or undefined. A record of such fields walks to nothing too, but it is an
* object: its own fields are checked instead. */
function onlyScalar(type) {
const parts = type.isUnion() ? type.types : [type];
return parts.every((part) => (part.flags & (scalar | nullish)) !== 0);
}
/** The walker an IR union member belongs to, or null. */
function irKind(type) {
for (const [name, set] of members) if (set.has(type)) return name;
return null;
}
let temp = 0;
/** The fields each walked object type declares, for the type-level check at
* the end of the file: `typeExpr` names the object type, `fields` every
* field it had when the file was generated and `scalars` the fields that held
* no string and no object then. */
const checks = [];
/** Records `property list` of `object` (named `typeExpr`) for the check. */
function noteFields(typeExpr, properties, scalarNames) {
checks.push({
typeExpr,
fields: properties.map((property) => property.name),
scalars: scalarNames,
});
}
/** Lines that walk `expr` of `type` (named `typeExpr` in the generated file):
* strings to `walker.string`, IR nodes to their walkers, arrays by element and
* other objects by field. */
function walkLines(expr, type, typeExpr, indent, depth) {
if (depth > 12) throw new Error(`field type nests too deep at ${expr}`);
const parts = type.isUnion() ? type.types : [type];
const present = parts.filter((part) => (part.flags & nullish) === 0);
const optional = present.length !== parts.length;
const strings = present.filter((part) => (part.flags & ts.TypeFlags.StringLike) !== 0);
const objects = present.filter(
(part) => (part.flags & (ts.TypeFlags.StringLike | scalar)) === 0,
);
for (const part of present)
if ((part.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0)
throw new Error(`untyped field ${expr}`);
const body = [];
if (objects.length > 0) {
const kinds = new Set(objects.map(irKind));
if (kinds.size === 1 && !kinds.has(null)) {
const [kind] = kinds;
body.push(`walk${kind.slice(2)}Fields(${expr}, walker, skipType);`);
} else if (objects.every((part) => checker.isArrayType(part))) {
if (objects.length !== 1) throw new Error(`several array types at ${expr}`);
const item = `item${temp++}`;
body.push(`for (const ${item} of ${expr}) {`);
const itemType = `Extract<NonNullable<${typeExpr}>, readonly unknown[]>[number]`;
body.push(
...walkLines(item, checker.getTypeArguments(objects[0])[0], itemType, " ", depth + 1),
);
body.push("}");
} else if (kinds.has(null) && objects.length === 1) {
const recordType = `Extract<NonNullable<${typeExpr}>, object>`;
const properties = checker.getPropertiesOfType(objects[0]);
const scalarNames = [];
for (const property of properties) {
if (property.name === "loc") continue;
const fieldType = checker.getTypeOfSymbol(property);
const field = `${expr}.${property.name}`;
const fieldTypeExpr = `${recordType}[${JSON.stringify(property.name)}]`;
const lines = walkLines(field, fieldType, fieldTypeExpr, "", depth + 1);
if (onlyScalar(fieldType)) scalarNames.push(property.name);
if (lines.length === 0) continue;
if (property.name === "type")
body.push("if (!skipType) {", ...lines.map((l) => ` ${l}`), "}");
else body.push(...lines);
}
noteFields(recordType, properties, scalarNames);
} else throw new Error(`mixed object types at ${expr}: ${checker.typeToString(type)}`);
}
let lines = body;
if (strings.length > 0 && objects.length > 0)
lines = [
`if (typeof ${expr} === "string") walker.string(${expr});`,
...(body.length > 0 ? [`else {`, ...body.map((l) => ` ${l}`), "}"] : []),
];
else if (strings.length > 0) lines = [`walker.string(${expr});`];
if (lines.length > 0 && optional && !(strings.length > 0 && objects.length > 0))
lines = [
`if (${expr} !== undefined && ${expr} !== null) {`,
...lines.map((l) => ` ${l}`),
"}",
];
else if (lines.length > 0 && optional)
lines = [
`if (${expr} !== undefined && ${expr} !== null) {`,
...lines.map((l) => ` ${l}`),
"}",
];
return lines.map((line) => indent + line);
}
lines.push(
"/** What a field walk reports (walkExprFields): each expression and",
" * statement node before its fields, and each string a field holds. */",
"export interface IrFieldWalker {",
" node(node: IrExpr | IrStmt): void;",
" string(value: string): void;",
"}",
"",
);
for (const name of ["IrExpr", "IrStmt", "IrType"]) {
const short = name.slice(2);
const nodeCall = name === "IrType" ? [] : [" walker.node(node);"];
lines.push(
`/** Walks every field of \`node\` but \`loc\` (and \`type\` with \`skipType\`), and`,
" * every field of each object under it: the strings and nodes a recursive",
" * `for (const key in value)` walk with the same skips finds, read by name in",
" * the order ir.ts declares the fields. */",
`export function walk${short}Fields(node: ${name}, walker: IrFieldWalker, skipType: boolean): void {`,
...nodeCall,
" switch (node.kind) {",
);
const byKind = new Map();
for (const member of members.get(name)) {
const kind = checker.getTypeOfSymbol(member.getProperty("kind")).value;
if (byKind.has(kind)) throw new Error(`${name} kind ${kind} declared twice`);
byKind.set(kind, member);
}
for (const [kind, member] of [...byKind].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
lines.push(` case ${JSON.stringify(kind)}: {`);
const memberType = `Extract<${name}, { kind: ${JSON.stringify(kind)} }>`;
const scalarNames = [];
for (const property of member.getProperties()) {
if (property.name === "loc") continue;
const fieldType = checker.getTypeOfSymbol(property);
const field = `node.${property.name}`;
const fieldTypeExpr = `${memberType}[${JSON.stringify(property.name)}]`;
const walk = walkLines(field, fieldType, fieldTypeExpr, "", 0);
if (onlyScalar(fieldType)) scalarNames.push(property.name);
if (walk.length === 0) continue;
if (property.name === "type")
lines.push(" if (!skipType) {", ...walk.map((l) => ` ${l}`), " }");
else lines.push(...walk.map((l) => ` ${l}`));
}
lines.push(" return;", " }");
noteFields(memberType, member.getProperties(), scalarNames);
}
lines.push(" }", "}", "");
}
// Type checking fails below when ir.ts gives a walked object a field this
// file does not know, or lets a field that held no string and no object hold
// one before this file is regenerated.
const quoted = (names) => names.map((name) => JSON.stringify(name)).join(" | ");
lines.push(
"/** A field ir.ts declares that this file does not read. */",
"type FieldsNotRead<T, Read extends PropertyKey> = Exclude<keyof T, Read>;",
"/** The string or object values a field read as a scalar can hold. */",
"type ValuesNotRead<T> = Extract<NonNullable<T>, string | object>;",
"/** Fails type checking unless `T` is never: run `pnpm generate:ir-fields`. */",
"type Unread<T extends never> = T;",
"",
"/** Every walked object type's fields as ir.ts declared them when this file",
" * was generated. */",
"export type IrFieldsCurrent = [",
);
for (const { typeExpr, fields, scalars } of checks) {
lines.push(
` Unread<FieldsNotRead<${typeExpr}, ${fields.length > 0 ? quoted(fields) : "never"}>>,`,
);
for (const scalarName of scalars)
lines.push(` Unread<ValuesNotRead<${typeExpr}[${JSON.stringify(scalarName)}]>>,`);
}
lines.push("];", "");
return lines.join("\n");
}
if (process.argv[1] !== undefined && samePath(process.argv[1], fileURLToPath(import.meta.url))) {
const text = generateIrFields();
if (process.argv.includes("--check")) {
if (readFileSync(target, "utf8") !== text)
throw new Error("fields.generated.ts is stale; run node scripts/generate-ir-fields.mjs");
} else writeFileSync(target, text);
}
@@ -0,0 +1,162 @@
// Untyped for-of and for await loops over generator objects: values,
// completion values, early exits, errors, other iterables through the same
// loop, and the microtask order of each step.
function* numbers(n) {
for (let i = 0; i < n; i++) yield i;
return "done";
}
function* mixed() {
yield "a";
yield { k: 1 };
yield undefined;
yield [1, 2];
yield Promise.resolve(7);
return 99;
}
async function* asyncNumbers(n) {
for (let i = 0; i < n; i++) yield i * 10;
return "async done";
}
async function* asyncMixed() {
yield "x";
yield Promise.resolve(8);
yield null;
}
function collect(it) {
const out = [];
for (const x of it) out.push(x);
return out;
}
async function collectAsync(it) {
const out = [];
for await (const x of it) out.push(x);
return out;
}
const show = (v) => JSON.stringify(v, (k, x) => (x instanceof Promise ? "promise" : x === undefined ? "undef" : x));
async function main() {
// The same loops over generators, arrays, Maps, strings and custom iterators.
console.log("sync", show(collect(numbers(4))), show(collect(mixed())));
console.log("sync other", show(collect([1, 2])), show(collect(new Map([[1, 2]]))), show(collect("hé")));
const custom = {
[Symbol.iterator]() {
let i = 0;
return { next: () => ({ value: i * i, done: i++ > 2 }) };
},
};
console.log("sync custom", show(collect(custom)));
console.log("async", show(await collectAsync(asyncNumbers(3))), show(await collectAsync(asyncMixed())));
console.log("async over sync", show(await collectAsync(mixed())), show(await collectAsync(numbers(3))));
console.log("async other", show(await collectAsync([Promise.resolve(1), 2])), show(await collectAsync(custom)));
const asyncCustom = {
[Symbol.asyncIterator]() {
let i = 0;
return { next: async () => ({ value: `c${i}`, done: i++ > 1 }) };
},
};
console.log("async custom", show(await collectAsync(asyncCustom)));
// Symbol.asyncIterator returning a sync generator: each result is awaited,
// its value is not, so a yielded promise reaches the loop as a promise.
const viaAsyncIterator = { [Symbol.asyncIterator]: () => mixed() };
const seen = [];
for await (const x of viaAsyncIterator) seen.push(x instanceof Promise ? `promise ${await x}` : x);
console.log("async iterator sync", show(seen));
// A generator that has started keeps its position; one that finished stays done.
const started = numbers(5);
started.next();
console.log("started", show(collect(started)), show(started.next()));
// Early exits close the generator: finally blocks run once.
function* guarded(label) {
try {
yield 1;
yield 2;
} finally {
console.log(label, "finally");
}
}
const pick = (v) => v;
for (const x of pick(guarded("break"))) {
console.log("break at", x);
break;
}
try {
for (const x of pick(guarded("throw"))) throw new Error(`body ${x}`);
} catch (e) {
console.log("caught", e.message);
}
const g = pick(guarded("after"));
for (const x of g) {
if (x === 1) break;
}
console.log("after break", show(g.next()));
async function* aguarded(label) {
try {
yield 1;
yield 2;
} finally {
console.log(label, "finally");
}
}
for await (const x of pick(aguarded("abreak"))) {
console.log("abreak at", x);
break;
}
for await (const x of pick(guarded("sync abreak"))) {
console.log("sync abreak at", x);
break;
}
// Errors from the generator body reach the loop.
function* failing() {
yield 1;
throw new TypeError("generator failed");
}
async function* afailing() {
yield 1;
throw new RangeError("async generator failed");
}
try {
for (const x of pick(failing())) console.log("failing got", x);
} catch (e) {
console.log("caught", e.name, e.message);
}
try {
for await (const x of pick(afailing())) console.log("afailing got", x);
} catch (e) {
console.log("caught", e.name, e.message);
}
// Microtask order: each step against a promise chain ticking alongside.
const ticker = (label, n) => {
let p = Promise.resolve();
for (let i = 0; i < n; i++) p = p.then(() => console.log(label, "tick", i));
return p;
};
const consume = async (label, it) => {
for await (const x of it) console.log(label, "value", x);
console.log(label, "end");
};
await Promise.all([consume("sync", pick(numbers(3))), ticker("sync", 10)]);
await Promise.all([consume("async", pick(asyncNumbers(3))), ticker("async", 10)]);
await Promise.all([consume("promises", pick(mixed())), ticker("promises", 14)]);
// Stages chained through untyped parameters.
async function* double(src) {
for await (const x of src) yield x * 2;
}
async function* keepOdd(src) {
for await (const x of src) if (x % 4 !== 0) yield x;
}
let sum = 0;
for await (const x of keepOdd(double(asyncNumbers(50)))) sum += x;
console.log("stages", sum);
let syncSum = 0;
function* inc(src) {
for (const x of src) yield x + 1;
}
for (const x of inc(inc(numbers(100)))) syncSum += x;
console.log("sync stages", syncSum);
}
main();
@@ -0,0 +1,82 @@
// Untyped stages chained through generator objects: a sync generator source
// consumed with for await, async generator transforms, an async function
// consumer, and the same stages over arrays and custom iterables.
function run(source, transform, destination) {
return destination(transform(source()));
}
const N = 2000;
const total = await run(
function* () {
for (let i = 0; i < N; i++) yield i;
},
async function* (src) {
for await (const n of src) yield n * 2;
},
async function (src) {
let sum = 0;
for await (const n of src) sum += n;
return sum;
},
);
console.log("total", total);
async function* map(it, f) {
for await (const x of it) yield await f(x);
}
async function* take(it, n) {
if (n <= 0) return;
for await (const x of it) {
yield x;
if (--n === 0) return;
}
}
async function* source(n) {
try {
for (let i = 0; i < n; i++) yield i;
} finally {
console.log("source closed");
}
}
async function toArray(it) {
const out = [];
for await (const x of it) out.push(x);
return out;
}
const half = async (x) => x / 2;
console.log(await toArray(take(map(source(10), half), 3)));
console.log(await toArray(map([1, 2, 3], half)));
console.log(await toArray(map(new Set(["a", "b"]), async (s) => s.toUpperCase())));
const words = {
*[Symbol.iterator]() {
yield "x";
yield "y";
},
};
console.log(await toArray(map(words, async (s) => s + s)));
// A stage that switches iterables between calls of the same loop.
function* letters() {
yield "p";
yield "q";
}
async function* numbersAsync() {
yield 1;
yield 2;
}
const sources = [letters(), numbersAsync(), ["z"], letters()];
for (const s of sources) console.log(await toArray(s));
// Sync stages: generator to generator through untyped parameters.
function* inc(src) {
for (const x of src) yield x + 1;
}
function* evens(src) {
for (const x of src) if (x % 2 === 0) yield x;
}
function* range(n) {
for (let i = 0; i < n; i++) yield i;
return "range done";
}
let s = 0;
for (const x of evens(inc(range(1000)))) s += x;
console.log("sync", s, [...inc([10, 20])], Array.from(evens(range(7))));
+60
View File
@@ -0,0 +1,60 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import ts from "typescript";
import { expect, test } from "vitest";
import { generateIrFields } from "../../scripts/generate-ir-fields.mjs";
const compilerDir = join(import.meta.dirname, "../../packages/compiler");
const irPath = join(compilerDir, "src/ir/ir.ts");
const generatedPath = join(compilerDir, "src/ir/fields.generated.ts");
const irText = readFileSync(irPath, "utf8");
const arrayGet = '{ kind: "arrayGet"; arr: IrExpr; index: IrExpr; type: IrType; loc: SrcLoc }';
/** ir.ts with `field` added to the arrayGet node. */
function withArrayGetField(field: string): string {
expect(irText).toContain(arrayGet);
return irText.replace(arrayGet, arrayGet.replace("loc: SrcLoc }", `loc: SrcLoc; ${field} }`));
}
/** The type errors of `generated` as fields.generated.ts, with `ir` as ir.ts,
* under the compiler package's options. */
function generatedErrors(ir: string, generated: string): string[] {
const parsed = ts.getParsedCommandLineOfConfigFile(
join(compilerDir, "tsconfig.json"),
{ noEmit: true, incremental: false },
{ ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} },
)!;
const host = ts.createCompilerHost(parsed.options);
const readSourceFile = host.getSourceFile;
host.getSourceFile = (file, languageVersion, ...rest) =>
file === irPath
? ts.createSourceFile(file, ir, languageVersion)
: file === generatedPath
? ts.createSourceFile(file, generated, languageVersion)
: readSourceFile.call(host, file, languageVersion, ...rest);
const program = ts.createProgram([generatedPath], parsed.options, host);
return ts
.getPreEmitDiagnostics(program, program.getSourceFile(generatedPath))
.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
}
test("a nested record of scalar fields passes the checks once the readers are regenerated", () => {
const ir = withArrayGetField("counts?: { hits: number; misses: number | undefined }");
expect(generatedErrors(ir, generateIrFields(ir))).toEqual([]);
});
test("the record's own fields are checked: an added field or a widened scalar fails with its name", () => {
const generated = generateIrFields(
withArrayGetField("counts?: { hits: number; misses: number | undefined }"),
);
const added = generatedErrors(
withArrayGetField("counts?: { hits: number; misses: number | undefined; label: string }"),
generated,
);
expect(added).toEqual([`Type '"label"' does not satisfy the constraint 'never'.`]);
const widened = generatedErrors(
withArrayGetField("counts?: { hits: number | string; misses: number | undefined }"),
generated,
);
expect(widened).toEqual([`Type 'string' does not satisfy the constraint 'never'.`]);
});
+3 -1
View File
@@ -59,7 +59,9 @@ test("the production layout and metadata stage lowers statically", async () => {
});
for (const backend of ["llvm"] as const) {
test(`native LLVM layout and metadata emission (${backend})`, async () => {
// The sanitized dev build of the stage takes 170 to 300 s of CI time beside
// its shard's other tests.
test(`native LLVM layout and metadata emission (${backend})`, { timeout: 600_000 }, async () => {
const dir = mkdtempSync(
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-emission-"),
);