mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Merge main
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
- **EventEmitter:** Literal event names use fixed argument tuples.
|
||||
Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion.
|
||||
Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported.
|
||||
An emitter reached through an untyped value shares one listener list with its typed uses, so untyped registrations and emits reach typed listeners and typed emits reach untyped listeners.
|
||||
An untyped `emit` of an event that has a listener with typed parameters converts its arguments to the event's typed tuple before any listener runs, and throws a `TypeError` when they do not convert.
|
||||
Through an untyped value, `listeners()` and `rawListeners()` of an event with a listener that has typed parameters are unsupported, and `emit('error')` of an object that is not an Error with no `error` listener is unsupported.
|
||||
Untyped `newListener` and `removeListener` listeners receive `undefined` in place of a listener registered with typed parameters.
|
||||
Reading EventEmitter methods as properties through an untyped value (`typeof emitter.listeners`) answers `undefined`.
|
||||
`events.once` accepts EventEmitter and stream instances with a literal event name and an optional object-literal `{ signal }`; a stream's `data` event, other options values, and EventTarget emitters are unsupported.
|
||||
Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported.
|
||||
|
||||
@@ -5,4 +5,7 @@
|
||||
A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code.
|
||||
`Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs.
|
||||
Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`.
|
||||
Functions support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible`; the operations apply to the function's own properties (`name`, `length`, `prototype`, and the ones a program assigns).
|
||||
Class constructors are not supported.
|
||||
A write to a read-only property of a checked dictionary or a function throws a `TypeError` whose message omits Node's `'x' of object '#<Object>'` (or `of function '…'`) suffix.
|
||||
A JavaScript class field that is first created after its instance became non-extensible is added instead of throwing.
|
||||
|
||||
@@ -5,7 +5,8 @@ Bytes the build did not see, including bytes produced at run time, reject with a
|
||||
`scriptc coverage` lists the modules a build finds, with their size, whether they validate, and the WebAssembly proposals they use.
|
||||
|
||||
Translated modules can use the WebAssembly 2.0 feature set: the MVP plus sign-extension operators, non-trapping float-to-int conversions, bulk memory, multi-value, mutable globals, reference types, and extended constant expressions.
|
||||
Modules that use SIMD, threads, exception handling, tail calls, typed function references or GC, memory64, or multiple memories are not translated, so loading them rejects with the `CompileError` above; packages that fall back to a scalar build when that happens, such as undici and es-module-lexer, take their fallback.
|
||||
Modules that use SIMD, threads, exception handling, tail calls, typed function references or GC, memory64, or multiple memories are not translated, so loading them rejects with a `CompileError` that names the proposal the module uses, and `WebAssembly.validate` answers `false` for them; packages that fall back to a scalar build when that happens, such as undici and es-module-lexer, take their fallback.
|
||||
Invalid modules the build finds reject with a `CompileError` that includes the validation error.
|
||||
|
||||
Supported: `WebAssembly.compile`, `instantiate` (with bytes or a `Module`), `validate`, `new WebAssembly.Module`, `new WebAssembly.Instance`, `new WebAssembly.Memory`, `new WebAssembly.Table`, `new WebAssembly.Global`, `WebAssembly.Module.imports`, `exports` and `customSections`, and `instanceof` for every class; the error classes `CompileError`, `LinkError` and `RuntimeError`, constructed with or without `new`; `globalThis.WebAssembly` and its `compile`, `instantiate` and `validate` functions as values.
|
||||
Instances expose exported functions, memories (`buffer`, `grow`), tables (`get`, `set`, `grow`, `length`) and globals (`value`, `valueOf`).
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
import { precompiledRuntimePackTarget } from "../../compiler/src/startup-cache.js";
|
||||
|
||||
// The first optimized executable build of an invocation prepares its input in a frontend process
|
||||
// (prepareExecutableInputInChild in the compiler's index.ts) when the compiler runs from its built
|
||||
// files, as the CLI's bootstrap does. Later builds of the invocation, and builds with
|
||||
// SCRIPTC_FRONTEND_PROCESS=0, prepare it in the compiler's own process.
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../../..");
|
||||
const bootstrap = join(repoRoot, "packages/cli/dist/bootstrap.js");
|
||||
const compilerApi = pathToFileURL(join(repoRoot, "packages/compiler/dist/index.js")).href;
|
||||
const helperTarget = precompiledRuntimePackTarget();
|
||||
|
||||
interface Build {
|
||||
code: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
async function build(
|
||||
entry: string,
|
||||
outPath: string,
|
||||
cacheRoot: string,
|
||||
inProcess: boolean,
|
||||
extra: { args?: string[]; env?: NodeJS.ProcessEnv } = {},
|
||||
): Promise<Build> {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
SCRIPTC_CACHE_DIR: cacheRoot,
|
||||
SCRIPTC_TIMING: "1",
|
||||
...extra.env,
|
||||
};
|
||||
delete env["SCRIPTC_NO_CACHE"];
|
||||
if (inProcess) env["SCRIPTC_FRONTEND_PROCESS"] = "0";
|
||||
else delete env["SCRIPTC_FRONTEND_PROCESS"];
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
process.execPath,
|
||||
[bootstrap, "build", entry, "-o", outPath, ...(extra.args ?? [])],
|
||||
{ env, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
return { code: 0, stdout, stderr };
|
||||
} catch (error) {
|
||||
const failed = error as { code?: number; stdout?: string; stderr?: string };
|
||||
return { code: failed.code ?? 1, stdout: failed.stdout ?? "", stderr: failed.stderr ?? "" };
|
||||
}
|
||||
}
|
||||
|
||||
/** The build's output without its timing lines, whose times differ between runs. */
|
||||
const withoutTiming = (text: string): string =>
|
||||
text
|
||||
.split("\n")
|
||||
.filter((line) => !line.startsWith("scriptc timing") && !line.startsWith("scriptc lowering"))
|
||||
.join("\n");
|
||||
|
||||
test.skipIf(helperTarget === null)(
|
||||
"the frontend process builds the executable, diagnostics and cache entries the in-process build does",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-frontend-process-"));
|
||||
try {
|
||||
const src = join(dir, "src");
|
||||
await mkdir(src);
|
||||
await writeFile(
|
||||
join(src, "shapes.ts"),
|
||||
"export const area = (w: number, h: number): number => w * h;\n",
|
||||
);
|
||||
await writeFile(
|
||||
join(src, "main.ts"),
|
||||
'import { area } from "./shapes.js";\nconsole.log(area(3, 4), [1, 2, 3].map((x) => x * 2).join(","));\n',
|
||||
);
|
||||
const entry = join(src, "main.ts");
|
||||
const out = (name: string): string => join(dir, name, "program");
|
||||
await mkdir(join(dir, "child"));
|
||||
await mkdir(join(dir, "inline"));
|
||||
|
||||
const child = await build(entry, out("child"), join(dir, "cache-child"), false);
|
||||
const inline = await build(entry, out("inline"), join(dir, "cache-inline"), true);
|
||||
expect(child.code).toBe(0);
|
||||
expect(inline.code).toBe(0);
|
||||
expect(child.stderr).toContain('"phase":"frontend-process"');
|
||||
expect(inline.stderr).not.toContain('"phase":"frontend-process"');
|
||||
expect(await readFile(out("child"))).toEqual(await readFile(out("inline")));
|
||||
expect(await readFile(join(dir, "child", "main.ll"), "utf8")).toBe(
|
||||
await readFile(join(dir, "inline", "main.ll"), "utf8"),
|
||||
);
|
||||
expect((await execFileAsync(out("child"))).stdout).toBe("12 2,4,6\n");
|
||||
// The request and result files are removed.
|
||||
expect(
|
||||
(await readdir(join(dir, "child"))).filter((name) => name.includes("frontend")),
|
||||
).toEqual([]);
|
||||
|
||||
// A repeat finds the executable the frontend process's build published. An edit rebuilds in
|
||||
// the compiler's own process.
|
||||
const repeat = await build(entry, out("child"), join(dir, "cache-child"), false);
|
||||
expect(repeat.code).toBe(0);
|
||||
expect(repeat.stderr).not.toContain("scriptc lowering");
|
||||
expect(repeat.stderr).not.toContain('"phase":"frontend-process"');
|
||||
await writeFile(
|
||||
join(src, "shapes.ts"),
|
||||
"export const area = (w: number, h: number): number => w * h + 1;\n",
|
||||
);
|
||||
const edited = await build(entry, out("child"), join(dir, "cache-child"), false);
|
||||
expect(edited.code).toBe(0);
|
||||
expect(edited.stderr).toContain("scriptc lowering");
|
||||
expect(edited.stderr).not.toContain('"phase":"frontend-process"');
|
||||
expect((await execFileAsync(out("child"))).stdout).toBe("13 2,4,6\n");
|
||||
|
||||
// Without the executable cache entries (as for a program whose frontend inputs cannot be
|
||||
// cached), the rebuild still runs in the compiler's own process: the first build recorded
|
||||
// its native program object.
|
||||
await rm(join(dir, "cache-child", "early-exe"), { recursive: true, force: true });
|
||||
await rm(join(dir, "cache-child", "early-exe-route"), { recursive: true, force: true });
|
||||
const uncached = await build(entry, out("child"), join(dir, "cache-child"), false);
|
||||
expect(uncached.code).toBe(0);
|
||||
expect(uncached.stderr).toContain("scriptc lowering");
|
||||
expect(uncached.stderr).not.toContain('"phase":"frontend-process"');
|
||||
expect((await execFileAsync(out("child"))).stdout).toBe("13 2,4,6\n");
|
||||
|
||||
// A program the frontend rejects reports the same diagnostics either way. The first build of
|
||||
// the invocation in an empty cache runs the frontend process.
|
||||
await writeFile(join(src, "main.ts"), 'const n: number = "not a number";\nconsole.log(n);\n');
|
||||
const childError = await build(entry, out("child"), join(dir, "cache-child-error"), false);
|
||||
const inlineError = await build(entry, out("inline"), join(dir, "cache-inline"), true);
|
||||
expect(childError.stderr).toContain('"phase":"frontend-process"');
|
||||
expect(childError.code).not.toBe(0);
|
||||
expect(childError.code).toBe(inlineError.code);
|
||||
expect(withoutTiming(childError.stderr)).toBe(withoutTiming(inlineError.stderr));
|
||||
expect(withoutTiming(childError.stderr)).toContain("not a number");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
|
||||
test.skipIf(helperTarget === null)(
|
||||
"a compile called from evaluated code runs its frontend process once",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-frontend-process-eval-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
await writeFile(entry, 'console.log("from eval");\n');
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`import { compile } from ${JSON.stringify(compilerApi)};
|
||||
const result = await compile(process.argv[1], { outDir: process.argv[2], outPath: process.argv[3] });
|
||||
console.log("compiled", result.ok);`,
|
||||
entry,
|
||||
dir,
|
||||
join(dir, "program"),
|
||||
],
|
||||
{
|
||||
env: { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache"), SCRIPTC_TIMING: "1" },
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
expect(stdout).toBe("compiled true\n");
|
||||
expect(stderr).toContain('"phase":"frontend-process"');
|
||||
expect((await execFileAsync(join(dir, "program"))).stdout).toBe("from eval\n");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
|
||||
test.skipIf(helperTarget === null)(
|
||||
"a build with --provenance-sources compiles the attested sources in the frontend process",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-frontend-process-provenance-"));
|
||||
try {
|
||||
const fixture = join(repoRoot, "tests/fixtures/provenance");
|
||||
const entry = join(fixture, "cases/greet/main.ts");
|
||||
const extra = {
|
||||
args: ["--provenance-sources"],
|
||||
env: { SCRIPTC_PROVENANCE_MANIFEST: join(fixture, "manifest.json") },
|
||||
};
|
||||
await mkdir(join(dir, "child"));
|
||||
await mkdir(join(dir, "inline"));
|
||||
const child = await build(
|
||||
entry,
|
||||
join(dir, "child", "greet"),
|
||||
join(dir, "cache"),
|
||||
false,
|
||||
extra,
|
||||
);
|
||||
const inline = await build(
|
||||
entry,
|
||||
join(dir, "inline", "greet"),
|
||||
join(dir, "cache"),
|
||||
true,
|
||||
extra,
|
||||
);
|
||||
expect(child.code).toBe(0);
|
||||
expect(inline.code).toBe(0);
|
||||
expect(child.stderr).toContain('"phase":"frontend-process"');
|
||||
const childLlvm = await readFile(join(dir, "child", "main.ll"), "utf8");
|
||||
expect(childLlvm).toBe(await readFile(join(dir, "inline", "main.ll"), "utf8"));
|
||||
// Without the flag, the program compiles the published package instead.
|
||||
await mkdir(join(dir, "published"));
|
||||
const published = await build(
|
||||
entry,
|
||||
join(dir, "published", "greet"),
|
||||
join(dir, "cache"),
|
||||
true,
|
||||
);
|
||||
expect(published.code).toBe(0);
|
||||
expect(await readFile(join(dir, "published", "main.ll"), "utf8")).not.toBe(childLlvm);
|
||||
expect((await execFileAsync(join(dir, "child", "greet"))).stdout).toBe(
|
||||
"hello, world\nHELLO, COMPILER!\nhello, chain!\n",
|
||||
);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
|
||||
test.skipIf(helperTarget === null)(
|
||||
"concurrent first builds of entries with the same name in one output directory keep their own LLVM",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-frontend-process-stem-"));
|
||||
try {
|
||||
const out = join(dir, "out");
|
||||
await mkdir(out);
|
||||
const entries = ["first", "second"].map((name) => join(dir, name, "main.ts"));
|
||||
for (const [index, entry] of entries.entries()) {
|
||||
await mkdir(join(entry, ".."));
|
||||
// Different programs with the same stem write the same public main.ll.
|
||||
const lines = Array.from({ length: 200 }, (_, line) => `console.log("${index}-${line}");`);
|
||||
await writeFile(entry, `${lines.join("\n")}\n`);
|
||||
}
|
||||
const builds = await Promise.all(
|
||||
entries.map((entry, index) =>
|
||||
build(entry, join(out, `program-${index}`), join(dir, `cache-${index}`), false),
|
||||
),
|
||||
);
|
||||
for (const [index, result] of builds.entries()) {
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stderr).toContain('"phase":"frontend-process"');
|
||||
const { stdout } = await execFileAsync(join(out, `program-${index}`));
|
||||
expect(stdout.split("\n")[0]).toBe(`${index}-0`);
|
||||
}
|
||||
// The frontend process's private copies of the LLVM text are removed.
|
||||
expect((await readdir(out)).filter((name) => name.includes("frontend"))).toEqual([]);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
@@ -2128,6 +2128,10 @@ declare module "fs/promises" {
|
||||
options: import("node:fs").GlobOptionsWithFileTypes,
|
||||
): AsyncIterableIterator<import("node:fs").Dirent>;
|
||||
export function rm(path: string): Promise<void>;
|
||||
export function rm(
|
||||
path: string,
|
||||
options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number },
|
||||
): Promise<void>;
|
||||
export function stat(path: string): Promise<import("node:fs").Stats>;
|
||||
export function statfs(
|
||||
path: string | Uint8Array | URL,
|
||||
|
||||
@@ -44,3 +44,15 @@ uint8_t scriptc_native_private_directory(const uint8_t *bytes, size_t size, uint
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* The runtime's full sweep after a phase's data died (scr_rc.h). */
|
||||
void scr_collect_cycles_phase(void);
|
||||
|
||||
/* Reclaims the cycles a finished compiler phase leaves behind. The
|
||||
* frontend's syntax trees and program state refer to each other, so
|
||||
* reference counting frees almost none of them when the frontend is
|
||||
* disposed, and the collector's schedule alone may not run a full pass
|
||||
* before the build ends. */
|
||||
void scriptc_native_collect_cycles(void) {
|
||||
scr_collect_cycles_phase();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"ffi_format": 6,
|
||||
"functions": [
|
||||
{ "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" }
|
||||
{ "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" },
|
||||
{ "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { DynamicFieldStores } from "../ir/int32-slots.js";
|
||||
import type { IrModule, IrType } from "../ir/ir.js";
|
||||
import {
|
||||
DYN,
|
||||
@@ -34,8 +35,11 @@ import { computeCycleMutability, fieldKey } from "./cycle-mutability.js";
|
||||
* subclass and retain touches the cycle header, so header presence must be
|
||||
* uniform across an extends tree): a unit is cycle-capable iff ANY member
|
||||
* is — every backend uses the same grouping. */
|
||||
export function computeTraced(mod: IrModule): { shapes: Set<string>; unions: Set<string> } {
|
||||
const { shapes, unions } = analyzeCycles(mod, false);
|
||||
export function computeTraced(
|
||||
mod: IrModule,
|
||||
stores?: DynamicFieldStores,
|
||||
): { shapes: Set<string>; unions: Set<string> } {
|
||||
const { shapes, unions } = analyzeCycles(mod, false, stores);
|
||||
return { shapes, unions };
|
||||
}
|
||||
|
||||
@@ -88,6 +92,7 @@ export function explainCycle(mod: IrModule, key: string): CycleExplanation | nul
|
||||
function analyzeCycles(
|
||||
mod: IrModule,
|
||||
labels: boolean,
|
||||
stores?: DynamicFieldStores,
|
||||
): {
|
||||
shapes: Set<string>;
|
||||
unions: Set<string>;
|
||||
@@ -120,7 +125,7 @@ function analyzeCycles(
|
||||
const lifetimeRoots = new Set(mod.contracts?.programLifetime ?? []);
|
||||
const programLifetime = (className: string): boolean =>
|
||||
lifetimeRoots.size > 0 && lifetimeRoots.has(rootOf(className));
|
||||
const mutability = computeCycleMutability(mod, unitOfClass);
|
||||
const mutability = computeCycleMutability(mod, unitOfClass, stores);
|
||||
const shapeDefs = [
|
||||
...classes.map((c) => ({
|
||||
key: `object:${c.name}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js";
|
||||
import { RUNTIME_EMITTER_CLASS, RUNTIME_STREAM_CLASSES, typeKey } from "../ir/ir.js";
|
||||
import { dynamicFieldStores } from "../ir/int32-slots.js";
|
||||
import { dynamicFieldStores, type DynamicFieldStores } from "../ir/int32-slots.js";
|
||||
import { everyExprChild, everyStmtChild, everyTypeChild } from "../ir/traverse.js";
|
||||
|
||||
/** Which shape fields can be written after their object escapes.
|
||||
@@ -72,6 +72,7 @@ export function fieldKey(unit: string, field: string): string {
|
||||
export function computeCycleMutability(
|
||||
mod: IrModule,
|
||||
unitOfClass: (className: string) => string,
|
||||
stores?: DynamicFieldStores,
|
||||
): CycleMutability {
|
||||
const result: CycleMutability = {
|
||||
all: false,
|
||||
@@ -100,7 +101,7 @@ export function computeCycleMutability(
|
||||
// intrinsics other than publish (which only marks objects immortal), the
|
||||
// intern-table operations (which store published values) and ThreadSlots
|
||||
// reads stay conservative.
|
||||
const dynStores = dynamicFieldStores(mod);
|
||||
const dynStores = stores ?? dynamicFieldStores(mod);
|
||||
const dynamicStores = dynStores.all || dynStores.names.size > 0;
|
||||
result.emptyPropertyBags = !dynamicStores;
|
||||
const boundaryWrites = (e: IrExpr): boolean =>
|
||||
|
||||
@@ -31,7 +31,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.handleProps ? { handleProps: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.dynBufferNum ? { dynBufferNum: true } : {}),
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
...(features.builtinValues ? { builtinValues: true } : {}),
|
||||
events: features.processEvents,
|
||||
emitter: features.emitter,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Static immutability facts the backend uses to drop run-time immutability
|
||||
* checks (see computeConstructionFacts). Pure functions of the IR module. */
|
||||
import type { DynamicFieldStores } from "../ir/int32-slots.js";
|
||||
import type { IrExpr, IrModule, IrStmt } from "../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
|
||||
import {
|
||||
@@ -44,7 +45,10 @@ export class ImmutableFields {
|
||||
}
|
||||
}
|
||||
|
||||
export function computeImmutableFields(mod: IrModule): ImmutableFields | null {
|
||||
export function computeImmutableFields(
|
||||
mod: IrModule,
|
||||
stores?: DynamicFieldStores,
|
||||
): ImmutableFields | null {
|
||||
if (mod.lib !== undefined) return null;
|
||||
const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const));
|
||||
const roots = new Map<string, string>();
|
||||
@@ -58,7 +62,7 @@ export function computeImmutableFields(mod: IrModule): ImmutableFields | null {
|
||||
return cur;
|
||||
};
|
||||
const unitOfClass = (name: string): string => `object:${rootOf(name)}`;
|
||||
const mutability = computeCycleMutability(mod, unitOfClass);
|
||||
const mutability = computeCycleMutability(mod, unitOfClass, stores);
|
||||
return mutability.all ? null : new ImmutableFields(mutability, unitOfClass);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export async function compilePackedLibrary(
|
||||
assert: options.assert ?? false,
|
||||
inspect: options.inspect ?? false,
|
||||
dynInvoke: options.dynInvoke ?? false,
|
||||
...(options.dynBuffer === true ? { dynBuffer: true } : {}),
|
||||
...(options.dynTier === true ? { dynTier: true } : {}),
|
||||
dc: false,
|
||||
dynAsync: false,
|
||||
|
||||
@@ -269,8 +269,9 @@ export function cycTypeDescriptor(name: string, trace: string, free: string, kin
|
||||
* expression) in `%o`, aborting on OOM, with the RC-audit allocation note
|
||||
* on the out-of-line path. With `cyc`, the object carries a collector
|
||||
* header (scr_cyc_alloc's contract). `stored` is the number of leading
|
||||
* object bytes the caller stores unconditionally right after (at least the
|
||||
* refcount word), which a recycled block need not clear. Labels use the
|
||||
* object bytes the caller stores unconditionally right after (the refcount
|
||||
* word, or the 4-byte header of a compact class instance), which a recycled
|
||||
* block need not clear; it is a multiple of 4 and at most `size`. Labels use the
|
||||
* `sa.` prefix; the lines end in an open block.
|
||||
*/
|
||||
export function emitObjectAlloc(
|
||||
@@ -350,7 +351,7 @@ function allocLines(
|
||||
` store ptr %sa.next, ptr %sa.fp`,
|
||||
` %sa.z = getelementptr inbounds i8, ptr %sa.b, i64 ${keep}`,
|
||||
` %sa.zn = sub i64 %sa.n, ${keep}`,
|
||||
` call void @llvm.memset.p0.i64(ptr align 8 %sa.z, i8 0, i64 %sa.zn, i1 false)`,
|
||||
` call void @llvm.memset.p0.i64(ptr align ${keep % 8 === 0 ? 8 : 4} %sa.z, i8 0, i64 %sa.zn, i1 false)`,
|
||||
` br label %sa.fast`,
|
||||
`sa.bump: ; fresh reservation memory is already zero`,
|
||||
` %sa.c1 = add i64 %sa.c, 1`,
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js";
|
||||
import { buildClassGraph, classMembershipIntervals, emitClassMembershipHelper } from "./classes.js";
|
||||
import {
|
||||
buildClassGraph,
|
||||
classIdLoadLines,
|
||||
classMembershipIntervals,
|
||||
emitClassMembershipHelper,
|
||||
type LlClassMeta,
|
||||
} from "./classes.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import {
|
||||
mangleClassNew,
|
||||
mangleClassReleaseDirect,
|
||||
mangleClassRelease,
|
||||
mangleClassRetain,
|
||||
mangleClassStruct,
|
||||
mangleClassTrace,
|
||||
mangleRecordRelease,
|
||||
} from "../mangle.js";
|
||||
@@ -129,18 +136,19 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
|
||||
expect(body(mangleClassRetain(cls)), cls).toMatch(/entry:\n {2}ret ptr %o\n}/);
|
||||
expect(body(mangleClassRelease(cls)), cls).toMatch(/entry:\n {2}ret void\n}/);
|
||||
// new: no collector header (no descriptor, no scr_cyc_alloc) and the
|
||||
// program-lifetime count 2^31 under the class id (Node's preorder number
|
||||
// 0 + 1, Leaf's 1 + 1), not SIZE_MAX (which means published).
|
||||
// 32-bit header: the tag 0x70000000 and the class id (Node's preorder
|
||||
// number 0 + 1, Leaf's 1 + 1), without a count.
|
||||
const nw = body(mangleClassNew(cls));
|
||||
const id = cls === "Node" ? 1 : 2;
|
||||
expect(nw, cls).toContain(`store i64 ${id * 2 ** 32 + 2 ** 31}, ptr %o.vt`);
|
||||
expect(nw, cls).not.toContain("store i64 -1, ptr %o");
|
||||
expect(nw, cls).toContain(`store i32 ${0x70000000 + id}, ptr %o.vt`);
|
||||
expect(nw, cls).not.toContain("store i64");
|
||||
expect(nw, cls).not.toContain("scr_cyc_alloc");
|
||||
expect(llvm, cls).not.toContain(`@${mangleClassTrace(cls)}.cyctype`);
|
||||
// The RC audit (sanitize builds) learns that the program keeps
|
||||
// program-lifetime objects; other builds emit nothing for it.
|
||||
expect(nw, cls).toContain("call void @scr_obj_lifetime_note(ptr %o)");
|
||||
}
|
||||
expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, ptr }`);
|
||||
const plain = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false });
|
||||
expect(plain).not.toContain("scr_obj_lifetime_note");
|
||||
// The mortal Owner keeps its header, counts and possible-root step.
|
||||
@@ -149,6 +157,84 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
|
||||
expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp");
|
||||
});
|
||||
|
||||
test("ProgramLifetime classes pack their fields after a 32-bit header", () => {
|
||||
const node = { kind: "object" as const, className: "Node" };
|
||||
const leaf = { kind: "object" as const, className: "Leaf" };
|
||||
const BOOL = { kind: "bool" as const };
|
||||
const nodeFields = [
|
||||
{ name: "parent", type: node },
|
||||
{ name: "flag", type: BOOL },
|
||||
{ name: "weight", type: F64 },
|
||||
];
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
|
||||
classes: [
|
||||
{ name: "Node", fields: nodeFields, methods: [], loc },
|
||||
{
|
||||
name: "Leaf",
|
||||
base: "Node",
|
||||
fields: [...nodeFields, { name: "next", type: leaf }, { name: "mark", type: BOOL }],
|
||||
methods: [],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
contracts: { programLifetime: ["Node"] },
|
||||
};
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
// Fields go widest first, each at the first offset that fits it. Node's
|
||||
// weight (an int32 slot: nothing stores a fraction) takes the word the
|
||||
// header shares (24 bytes, not 32); Leaf keeps Node's offsets and puts its
|
||||
// bool next to Node's (32 bytes, not 48).
|
||||
expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, ptr, i8 }`);
|
||||
expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, ptr, i8, i8, ptr }`);
|
||||
// Without the contract, the layout is the declared one.
|
||||
const mortal = emitLlvmModule({ ...mod, contracts: {} }, { pointerBits: 64 });
|
||||
expect(mortal).toContain(`%${mangleClassStruct("Leaf")} = type { i64, ptr, i8, i32, ptr, i8 }`);
|
||||
});
|
||||
|
||||
test("a recycled block is cleared from the end of a ProgramLifetime instance's 32-bit header", () => {
|
||||
const BOOL = { kind: "bool" as const };
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
|
||||
classes: [
|
||||
{ name: "Marker", fields: [], methods: [], loc },
|
||||
{ name: "Toggle", fields: [{ name: "on", type: BOOL }], methods: [], loc },
|
||||
],
|
||||
contracts: { programLifetime: ["Marker", "Toggle"] },
|
||||
};
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
for (const name of ["Marker", "Toggle"]) {
|
||||
const start = llvm.indexOf(`define internal ptr @${mangleClassNew(name)}(`);
|
||||
const body = llvm.slice(start, llvm.indexOf("\n}\n", start));
|
||||
// The constructor stores the 4-byte header itself; a 4-byte instance
|
||||
// (Marker) clears nothing, and Toggle's field at offset 4 is cleared.
|
||||
expect(body).toContain("%sa.z = getelementptr inbounds i8, ptr %sa.b, i64 4");
|
||||
expect(body).toContain("%sa.zn = sub i64 %sa.n, 4");
|
||||
expect(body).toContain("call void @llvm.memset.p0.i64(ptr align 4 %sa.z,");
|
||||
}
|
||||
});
|
||||
|
||||
test("invariant class id reads of compact instances leave out the published bit", () => {
|
||||
const group = ", !invariant.group !{}";
|
||||
const compact = (post: number) => ({ compact: true, root: { post } }) as unknown as LlClassMeta;
|
||||
// Publication sets the header word's top bit; ids below 2^16 are read
|
||||
// from the low 16 bits, which never change after construction.
|
||||
expect(classIdLoadLines(compact(40), "%o", "%id", group)).toEqual([
|
||||
" %id.h = load i16, ptr %o, !invariant.group !{}",
|
||||
" %id = zext i16 %id.h to i32",
|
||||
]);
|
||||
// Wider ids, and reads without the metadata, mask the whole word.
|
||||
const masked = [" %id.w = load i32, ptr %o", " %id = and i32 %id.w, 268435455"];
|
||||
expect(classIdLoadLines(compact(0xffff), "%o", "%id", group)).toEqual(masked);
|
||||
expect(classIdLoadLines(compact(40), "%o", "%id", "")).toEqual(masked);
|
||||
});
|
||||
|
||||
test("published ProgramLifetime instances use the published mark every other object uses", () => {
|
||||
const node = { kind: "object" as const, className: "Node" };
|
||||
const leaf = { kind: "object" as const, className: "Leaf" };
|
||||
@@ -212,19 +298,25 @@ test("published ProgramLifetime instances use the published mark every other obj
|
||||
contracts: { programLifetime: ["Node"] },
|
||||
};
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
// The walker marks each instance like a mortal object (SIZE_MAX, logged so
|
||||
// a refusal restores the program-lifetime count); untraced, so it never
|
||||
// leaves a collector buffer.
|
||||
// The walker marks each instance by the top bit of its 32-bit header,
|
||||
// logging the word so a refusal restores it; untraced, so it never leaves
|
||||
// a collector buffer.
|
||||
expect(llvm).toContain("call void @scr_publish(ptr");
|
||||
// Kind 4 (an object) | 0x100 (the rc word keeps a class id: the mark
|
||||
// writes only the count).
|
||||
expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 260\)/);
|
||||
// The frozen-object guard of the store tests the count against UINT32_MAX:
|
||||
// an unpublished instance (2^31) stays writable.
|
||||
// Kind 4 (an object) | 0x200 (the 32-bit header: the mark sets its top
|
||||
// bit).
|
||||
expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 516\)/);
|
||||
const plain = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false });
|
||||
expect(plain).toContain("%mpub = or i32 %mrc, -2147483648");
|
||||
// Pushing a reference skips one published before: a set top bit.
|
||||
expect(llvm).toMatch(/%immortal0 = icmp slt i32 %rc0, 0/);
|
||||
// The walker dispatches on the class id in the header's low 28 bits.
|
||||
expect(llvm).toContain("%id = and i32 %idw, 268435455");
|
||||
// The frozen-object guard of the store tests the top bit: an unpublished
|
||||
// instance stays writable.
|
||||
const store =
|
||||
llvm.split(/^define /m).find((fn) => fn.includes("call void @scr_throw_published_field(")) ??
|
||||
"";
|
||||
expect(store).toMatch(/icmp eq i32 %\S+, -1/);
|
||||
expect(store).toMatch(/icmp slt i32 %\S+, 0/);
|
||||
expect(store).toContain("call void @scr_publish(ptr");
|
||||
// 32-bit targets use 2^31.
|
||||
const llvm32 = emitLlvmModule(mod, { pointerBits: 32 });
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "../mangle.js";
|
||||
import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import { INLINE_UNION_FIELD } from "./inline-unions.js";
|
||||
import { NullableRefFields } from "./nullable-fields.js";
|
||||
import type { Int32Slots } from "../../ir/int32-slots.js";
|
||||
import {
|
||||
@@ -88,6 +89,15 @@ export class LlClassMeta {
|
||||
* bits, and the hierarchy's vtables are one array indexed by the id
|
||||
* (mangleVtArray). */
|
||||
idInRc = false;
|
||||
/** A ProgramLifetime class with the 32-bit header (assignCompactLayouts):
|
||||
* the i32 at offset 0 is compactHeaderWord, with the class id in place of
|
||||
* the rc word's high half, and the fields are packed after it. */
|
||||
compact = false;
|
||||
/** Compact classes: the struct's elements by offset (the header, field
|
||||
* -1, then the fields by their index in def.fields). */
|
||||
compactElems: { offset: number; size: number; align: number; type: string; field: number }[] = [];
|
||||
/** Compact classes: each field's GEP index, by its index in def.fields. */
|
||||
compactSlots: number[] = [];
|
||||
|
||||
constructor(readonly def: IrClassDef) {
|
||||
this.root = this;
|
||||
@@ -371,6 +381,10 @@ export function classSlotWordIndex(meta: LlClassMeta): number {
|
||||
/** The GEP index where a class's own fields start: after the header words
|
||||
* and the slot word. */
|
||||
export function classFieldBase(meta: LlClassMeta): number {
|
||||
if (meta.compact)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: compact class ${meta.def.name} packs its fields`,
|
||||
);
|
||||
return headerEnd(meta) + (meta.threadSlot ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -387,7 +401,12 @@ export function classFieldIndex(meta: LlClassMeta, field: string): { index: numb
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: unknown field ${field} on class ${meta.def.name}`,
|
||||
);
|
||||
return { index: classFieldBase(meta) + idx, type: meta.def.fields[idx]!.type };
|
||||
return { index: classFieldSlot(meta, idx), type: meta.def.fields[idx]!.type };
|
||||
}
|
||||
|
||||
/** The GEP index of the field at `idx` in `meta.def.fields`. */
|
||||
export function classFieldSlot(meta: LlClassMeta, idx: number): number {
|
||||
return meta.compact ? meta.compactSlots[idx]! : classFieldBase(meta) + idx;
|
||||
}
|
||||
|
||||
/** Lines that release the ThreadSlots values of the object `%o` before it
|
||||
@@ -472,8 +491,7 @@ export function classPreLines(
|
||||
): string[] {
|
||||
if (meta.idInRc)
|
||||
return [
|
||||
` ${pre}.idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
|
||||
` ${pre}.id = load i32, ptr ${pre}.idp`,
|
||||
...classIdLoadLines(meta, obj, `${pre}.id`, ""),
|
||||
` ${pre}.n = add i32 ${pre}.id, -1 ; the class id is the preorder number + 1`,
|
||||
` ${pre} = zext i32 ${pre}.n to ${sizeType}`,
|
||||
];
|
||||
@@ -499,14 +517,39 @@ export function classIdVtLines(
|
||||
const vtt = `%${mangleVtStruct(root.def.name)}`;
|
||||
const n = root.post - root.pre + 1;
|
||||
return [
|
||||
` ${t}idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
|
||||
` ${t}id = load i32, ptr ${t}idp${group}`,
|
||||
...classIdLoadLines(meta, `${obj}`, `${t}id`, group),
|
||||
` ${t}ix = sub i32 ${t}id, ${root.pre + 1}`,
|
||||
` ${t}ix64 = zext i32 ${t}ix to i64`,
|
||||
` ${vt} = getelementptr inbounds [${n} x ${vtt}], ptr @${mangleVtArray(root.def.name)}, i64 0, i64 ${t}ix64`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Lines that load the class id of the LlClassMeta.idInRc instance `obj`
|
||||
* into the i32 `id`: the high half of the rc word, or the low 28 bits of a
|
||||
* compact instance's header word (compactHeaderWord). `group` is the
|
||||
* metadata of the load (`, !invariant.group !{}` for a load LLVM may treat
|
||||
* as invariant), and it only goes on bytes that never change: publication
|
||||
* sets the top bit of a compact header word, so a compact instance's id is
|
||||
* then read as the word's low 16 bits, which hold the whole id when the
|
||||
* hierarchy's ids are below 2^16, and otherwise as the masked word without
|
||||
* the metadata. */
|
||||
export function classIdLoadLines(
|
||||
meta: LlClassMeta,
|
||||
obj: string,
|
||||
id: string,
|
||||
group: string,
|
||||
): string[] {
|
||||
if (meta.compact) {
|
||||
if (group !== "" && meta.root.post + 1 <= 0xffff)
|
||||
return [` ${id}.h = load i16, ptr ${obj}${group}`, ` ${id} = zext i16 ${id}.h to i32`];
|
||||
return [` ${id}.w = load i32, ptr ${obj}`, ` ${id} = and i32 ${id}.w, ${COMPACT_ID_MASK}`];
|
||||
}
|
||||
return [
|
||||
` ${id}p = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
|
||||
` ${id} = load i32, ptr ${id}p${group}`,
|
||||
];
|
||||
}
|
||||
|
||||
/** The byte offset of the class id in the rc word of an instance that
|
||||
* keeps it there (LlClassMeta.idInRc): the high half of the little-endian
|
||||
* 64-bit word. The count is the i32 at offset 0. */
|
||||
@@ -522,6 +565,134 @@ export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string {
|
||||
return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString();
|
||||
}
|
||||
|
||||
/** The 32-bit header of a compact class instance (LlClassMeta.compact):
|
||||
* COMPACT_HEADER_TAG, the class id (classIdRcWord's preorder number + 1, 0
|
||||
* outside a hierarchy) in the low 28 bits, and COMPACT_PUBLISHED once
|
||||
* publication marks the object. No other object's count reaches
|
||||
* 0x70000000, so the runtime reads a word of 0xf0000000 and up as published
|
||||
* for every object (scr_rc.h SCR_RC_PUBLISHED). */
|
||||
export const COMPACT_HEADER_TAG = 0x70000000;
|
||||
export const COMPACT_PUBLISHED = 0x80000000;
|
||||
export const COMPACT_ID_MASK = 0x0fffffff;
|
||||
|
||||
/** The leading bytes of a new instance that its allocation function stores
|
||||
* right after allocating it (emitObjectAlloc's `stored`), which a recycled
|
||||
* block therefore need not clear: a compact instance's 4-byte header word
|
||||
* (its fields start at offset 4), the 8-byte rc word, or the rc word and
|
||||
* the vtable word of a hierarchy member that keeps its class in a vtable. */
|
||||
export function storedHeaderBytes(meta: LlClassMeta): number {
|
||||
if (meta.compact) return 4;
|
||||
return meta.hierarchy && !meta.idInRc ? 16 : 8;
|
||||
}
|
||||
|
||||
/** The header word a compact instance is created with. */
|
||||
export function compactHeaderWord(meta: LlClassMeta): number {
|
||||
return COMPACT_HEADER_TAG | (meta.hierarchy ? meta.pre + 1 : 0);
|
||||
}
|
||||
|
||||
/** Byte size and alignment of a class field's LLVM storage type. */
|
||||
function fieldStorage(type: string): { size: number; align: number } {
|
||||
switch (type) {
|
||||
case "i8":
|
||||
return { size: 1, align: 1 };
|
||||
case "i32":
|
||||
return { size: 4, align: 4 };
|
||||
case "ptr":
|
||||
case "double":
|
||||
return { size: 8, align: 8 };
|
||||
case INLINE_UNION_FIELD:
|
||||
return { size: 16, align: 8 };
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: class field storage ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The LLVM storage type of each of a class's flattened fields. */
|
||||
export function classFieldTypes(host: ShapeHost, cls: IrClassDef, int32?: Int32Slots): string[] {
|
||||
// Whole-program int32 fields (int32-slots.ts) store the exact i32.
|
||||
return cls.fields.map((f) =>
|
||||
int32?.isField(cls.name, f.name)
|
||||
? "i32"
|
||||
: llFieldType(f.type, host.inlineUnions.classField(cls.name, f.name)),
|
||||
);
|
||||
}
|
||||
|
||||
/** Gives the classes `compact` admits (whole hierarchies: the test is the
|
||||
* root's) the 32-bit header and packed fields. A class keeps its base's
|
||||
* field offsets, and places each of its own fields, widest first, at the
|
||||
* first offset after the header that fits it: in a hole another field's
|
||||
* alignment left, in the base's tail padding, or at the end. So 32-bit and
|
||||
* smaller fields share 8-byte words. The struct lists the header and the
|
||||
* fields by offset, which LLVM's layout of the type reproduces (checked). */
|
||||
export function assignCompactLayouts(
|
||||
metaMap: ReadonlyMap<string, LlClassMeta>,
|
||||
compact: (root: LlClassMeta) => boolean,
|
||||
fieldTypes: (cls: IrClassDef) => string[],
|
||||
): void {
|
||||
const byPre = [...metaMap.values()].sort((a, b) => a.pre - b.pre);
|
||||
for (const meta of byPre) {
|
||||
if (meta.def.runtime === true || !compact(meta.root)) continue;
|
||||
const base = meta.base;
|
||||
const inherited = base?.compactElems ?? [
|
||||
{ offset: 0, size: 4, align: 4, type: "i32", field: -1 },
|
||||
];
|
||||
const baseCount = base?.def.fields.length ?? 0;
|
||||
if (base !== null && (!base.compact || !sameFieldPrefix(base.def, meta.def)))
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: compact class ${meta.def.name} does not extend its base's layout`,
|
||||
);
|
||||
const elems = inherited.map((e) => ({ ...e }));
|
||||
const types = fieldTypes(meta.def);
|
||||
const own = types
|
||||
.map((type, field) => ({ type, field, ...fieldStorage(type) }))
|
||||
.slice(baseCount)
|
||||
.sort((a, b) => b.size - a.size || a.field - b.field);
|
||||
for (const f of own) {
|
||||
const sorted = [...elems].sort((a, b) => a.offset - b.offset);
|
||||
let at = -1;
|
||||
let cursor = 0;
|
||||
for (const e of sorted) {
|
||||
const start = alignTo(cursor, f.align);
|
||||
if (start + f.size <= e.offset) {
|
||||
at = start;
|
||||
break;
|
||||
}
|
||||
cursor = Math.max(cursor, e.offset + e.size);
|
||||
}
|
||||
if (at < 0) at = alignTo(cursor, f.align);
|
||||
elems.push({ offset: at, size: f.size, align: f.align, type: f.type, field: f.field });
|
||||
}
|
||||
elems.sort((a, b) => a.offset - b.offset);
|
||||
// LLVM places each element at the next offset its alignment allows.
|
||||
let end = 0;
|
||||
for (const e of elems) {
|
||||
if (alignTo(end, e.align) !== e.offset)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: compact layout of ${meta.def.name} is not LLVM's`,
|
||||
);
|
||||
end = e.offset + e.size;
|
||||
}
|
||||
const slots = new Array<number>(types.length).fill(-1);
|
||||
elems.forEach((e, index) => {
|
||||
if (e.field >= 0) slots[e.field] = index;
|
||||
});
|
||||
meta.compact = true;
|
||||
meta.compactElems = elems;
|
||||
meta.compactSlots = slots;
|
||||
}
|
||||
}
|
||||
|
||||
function alignTo(n: number, align: number): number {
|
||||
return Math.ceil(n / align) * align;
|
||||
}
|
||||
|
||||
function sameFieldPrefix(base: IrClassDef, cls: IrClassDef): boolean {
|
||||
return (
|
||||
base.fields.length <= cls.fields.length &&
|
||||
base.fields.every((f, i) => cls.fields[i]!.name === f.name)
|
||||
);
|
||||
}
|
||||
|
||||
function undefFieldInits(
|
||||
host: ClassHost,
|
||||
meta: LlClassMeta,
|
||||
@@ -589,12 +760,17 @@ export function emitClassShapes(
|
||||
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
// Whole-program int32 fields (int32-slots.ts) store the exact i32.
|
||||
const fieldTys = cls.fields.map((f) =>
|
||||
int32?.isField(cls.name, f.name)
|
||||
? "i32"
|
||||
: llFieldType(f.type, host.inlineUnions.classField(cls.name, f.name)),
|
||||
);
|
||||
if (meta.compact) {
|
||||
typeDefs.push(
|
||||
`%${mangleClassStruct(cls.name)} = type { ${meta.compactElems.map((e) => e.type).join(", ")} } ` +
|
||||
`; class ${cls.name} (32-bit header) { ${meta.compactElems
|
||||
.slice(1)
|
||||
.map((e) => llvmCommentText(cls.fields[e.field]!.name))
|
||||
.join("; ")} }`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const fieldTys = classFieldTypes(host, cls, int32);
|
||||
// Subclasses of a runtime base embed its remaining prefix (the state
|
||||
// and display-name slots) between the vtable word and the field list;
|
||||
// stream subclasses add the stream-state pointer — upcasts to
|
||||
@@ -718,7 +894,7 @@ export function emitClassShapes(
|
||||
const traced = host.tracedShapes.has(`object:${cls.name}`);
|
||||
const runtimePrefix = runtimePrefixOf(meta);
|
||||
const isStreamRooted = streamRooted(meta);
|
||||
const fieldIndex = (i: number): number => classFieldBase(meta) + i;
|
||||
const fieldIndex = (i: number): number => classFieldSlot(meta, i);
|
||||
// Teardown, trace and gcFree see each slot's storage type: a nullable
|
||||
// class field holds the instance pointer (NULL for the unit arm), and
|
||||
// every class retain/release/visit entry point is NULL-tolerant.
|
||||
@@ -851,30 +1027,41 @@ export function emitClassShapes(
|
||||
``,
|
||||
);
|
||||
// Direct release (the vtable entry): the class's own teardown. The
|
||||
// caller already handled NULL/immortal.
|
||||
const reld: string[] = [
|
||||
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`,
|
||||
`entry:`,
|
||||
` %rc = load ${count}, ptr %o`,
|
||||
` %n = sub ${count} %rc, 1`,
|
||||
` store ${count} %n, ptr %o`,
|
||||
` %dead = icmp eq ${count} %n, 0`,
|
||||
` br i1 %dead, label %free, label %${traced ? "root" : "done"}`,
|
||||
`free:`,
|
||||
];
|
||||
if (traced) reld.push(...cycleDeadLines(host, "teardown"));
|
||||
if (bounded) {
|
||||
const destroy = `${mangleClassReleaseDirect(cls.name)}_destroy`;
|
||||
const destroyBody: string[] = [];
|
||||
teardown(destroyBody);
|
||||
defs.push(...destroyDefinitions(destroy, destroyBody, chained), ``);
|
||||
host.declare(`declare void @scr_rc_destroy(ptr, ptr)`);
|
||||
reld.push(` call void @scr_rc_destroy(ptr %o, ptr @${destroy})`);
|
||||
} else teardown(reld);
|
||||
reld.push(` br label %done`);
|
||||
if (traced) reld.push(`root:`, ...cycleRootLines(host, "done"));
|
||||
reld.push(`done:`, ` ret void`, `}`, ``);
|
||||
defs.push(...reld);
|
||||
// caller already handled NULL/immortal. A compact instance has no
|
||||
// count: nothing releases it.
|
||||
if (meta.compact) {
|
||||
defs.push(
|
||||
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name} (immortal)`,
|
||||
`entry:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
} else {
|
||||
const reld: string[] = [
|
||||
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`,
|
||||
`entry:`,
|
||||
` %rc = load ${count}, ptr %o`,
|
||||
` %n = sub ${count} %rc, 1`,
|
||||
` store ${count} %n, ptr %o`,
|
||||
` %dead = icmp eq ${count} %n, 0`,
|
||||
` br i1 %dead, label %free, label %${traced ? "root" : "done"}`,
|
||||
`free:`,
|
||||
];
|
||||
if (traced) reld.push(...cycleDeadLines(host, "teardown"));
|
||||
if (bounded) {
|
||||
const destroy = `${mangleClassReleaseDirect(cls.name)}_destroy`;
|
||||
const destroyBody: string[] = [];
|
||||
teardown(destroyBody);
|
||||
defs.push(...destroyDefinitions(destroy, destroyBody, chained), ``);
|
||||
host.declare(`declare void @scr_rc_destroy(ptr, ptr)`);
|
||||
reld.push(` call void @scr_rc_destroy(ptr %o, ptr @${destroy})`);
|
||||
} else teardown(reld);
|
||||
reld.push(` br label %done`);
|
||||
if (traced) reld.push(`root:`, ...cycleRootLines(host, "done"));
|
||||
reld.push(`done:`, ` ret void`, `}`, ``);
|
||||
defs.push(...reld);
|
||||
}
|
||||
} else {
|
||||
const freeBody: string[] = [];
|
||||
teardown(freeBody);
|
||||
@@ -927,10 +1114,22 @@ export function emitClassShapes(
|
||||
host,
|
||||
sizeOf,
|
||||
traced && !lifetime ? { type: cycType } : null,
|
||||
meta.hierarchy && !meta.idInRc ? 16 : 8,
|
||||
storedHeaderBytes(meta),
|
||||
),
|
||||
);
|
||||
if (meta.idInRc) {
|
||||
if (meta.compact && !meta.hierarchy)
|
||||
nw.push(` store i32 ${compactHeaderWord(meta)}, ptr %o ; no count (compactHeaderWord)`);
|
||||
else if (meta.compact) {
|
||||
// The header word: the class id and no count (compactHeaderWord).
|
||||
// Only publication writes it again, setting COMPACT_PUBLISHED in the
|
||||
// top byte, so the class id reads that carry !invariant.group read
|
||||
// the low 16 bits only (classIdLoadLines).
|
||||
host.declare(`declare ptr @llvm.launder.invariant.group.p0(ptr)`);
|
||||
nw.push(
|
||||
` %o.vt = call ptr @llvm.launder.invariant.group.p0(ptr %o)`,
|
||||
` store i32 ${compactHeaderWord(meta)}, ptr %o.vt ; class ${llvmCommentText(cls.name)} is ${meta.pre}`,
|
||||
);
|
||||
} else if (meta.idInRc) {
|
||||
// The class id in the high half of the rc word, the count in the low
|
||||
// half (classIdRcWord). It never changes while the object lives:
|
||||
// retains, releases and the runtime write only the low half.
|
||||
|
||||
@@ -3245,7 +3245,8 @@ export class LlDyn {
|
||||
/* ── the dyn ToString pair (dynToStrHelper, ported) ────────────────── */
|
||||
|
||||
/** Node's String() over a dyn value — sc_ds (+1 result) over the
|
||||
* recursive sc_ds_buf walker. Borrowed operand; never throws. */
|
||||
* recursive sc_ds_buf walker. Borrowed operand; an exception that an
|
||||
* object's toString or valueOf throws stays pending. */
|
||||
dynToStrHelper(): string {
|
||||
if (this.dynToStrFn) return this.dynToStrFn;
|
||||
const name = "sc_ds";
|
||||
@@ -3348,9 +3349,9 @@ export class LlDyn {
|
||||
B.startBlock(labels.get(DYN_KIND.ARR)!);
|
||||
{
|
||||
// Array.prototype.toString: join(",") — null/undefined ELEMENTS
|
||||
// print empty (unlike top level), nested arrays flatten.
|
||||
// print empty (unlike top level), nested arrays flatten. The length
|
||||
// is read once, as join reads it.
|
||||
const n = this.lenOf(B, "%d");
|
||||
const items = this.itemsOf(B, "%d");
|
||||
this.i64Loop(B, "ds.ar", n, (i, brNext) => {
|
||||
const nz = B.tmp();
|
||||
B.line(`${nz} = icmp ugt ${this.S} ${i}, 0`);
|
||||
@@ -3361,7 +3362,22 @@ export class LlDyn {
|
||||
B.line(`call void @scr_jb_putc(ptr %b, i8 44)`);
|
||||
B.br(lel);
|
||||
B.startBlock(lel);
|
||||
const e = this.itemAt(B, items, i);
|
||||
// Each index reads the element as join does (scr_dyn_arr_at, +1):
|
||||
// from the current storage, undefined past the current length,
|
||||
// and through the prototype for a hole. An element's toString
|
||||
// can append to, shrink or reallocate the array; a getter that
|
||||
// throws answers NULL with the exception pending.
|
||||
host.declare(`declare ptr @scr_dyn_arr_at(ptr, double)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const index = B.tmp();
|
||||
B.line(`${index} = uitofp ${this.S} ${i} to double`);
|
||||
const e = B.tmp();
|
||||
B.line(`${e} = call ptr @scr_dyn_arr_at(ptr %d, double ${index})`);
|
||||
const missing = B.tmp();
|
||||
B.line(`${missing} = icmp eq ptr ${e}, null`);
|
||||
const lGot = B.newLabel("ds.gt");
|
||||
B.condBr(missing, done, lGot);
|
||||
B.startBlock(lGot);
|
||||
const ek = this.kindOf(B, e);
|
||||
const isU = B.tmp();
|
||||
const isN = B.tmp();
|
||||
@@ -3369,20 +3385,30 @@ export class LlDyn {
|
||||
B.line(`${isU} = icmp eq i32 ${ek}, ${DYN_KIND.UNDEF}`);
|
||||
B.line(`${isN} = icmp eq i32 ${ek}, ${DYN_KIND.NULL}`);
|
||||
B.line(`${unit} = or i1 ${isU}, ${isN}`);
|
||||
const lSkip = B.newLabel("ds.sk");
|
||||
const lRec = B.newLabel("ds.rc");
|
||||
const lSkip = B.newLabel("ds.sk");
|
||||
B.condBr(unit, lSkip, lRec);
|
||||
B.startBlock(lSkip);
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${e})`);
|
||||
brNext();
|
||||
B.startBlock(lRec);
|
||||
B.line(`call void @sc_ds_buf(ptr %b, ptr ${e})`);
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${e})`);
|
||||
// The first exception an element's conversion throws ends the
|
||||
// conversion; later elements are not read.
|
||||
const thrown = B.tmp();
|
||||
for (const line of host.pendingTestLines(thrown)) B.line(line.trimStart());
|
||||
const lMore = B.newLabel("ds.mo");
|
||||
B.condBr(thrown, done, lMore);
|
||||
B.startBlock(lMore);
|
||||
});
|
||||
B.br(done);
|
||||
}
|
||||
B.startBlock(labels.get(DYN_KIND.OBJ)!);
|
||||
{
|
||||
// The checked-dynamic tree's error encoding renders Error.prototype.toString;
|
||||
// plain objects are "[object Object]".
|
||||
// other objects convert with ToPrimitive's string hint: a toString or
|
||||
// valueOf of their own or of their prototypes, else "[object Object]".
|
||||
const marker = this.objGetLit(B, "%d", "%error");
|
||||
const isErr = B.tmp();
|
||||
B.line(`${isErr} = icmp ne ptr ${marker}, null`);
|
||||
@@ -3390,8 +3416,21 @@ export class LlDyn {
|
||||
const lPlain = B.newLabel("ds.pl");
|
||||
B.condBr(isErr, lErr, lPlain);
|
||||
B.startBlock(lPlain);
|
||||
this.puts(B, "%b", "[object Object]");
|
||||
B.br(done);
|
||||
{
|
||||
// Null leaves the exception the conversion threw pending.
|
||||
host.declare(`declare ptr @scr_dyn_string_coerce_js(ptr)`);
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
const text = B.tmp();
|
||||
B.line(`${text} = call ptr @scr_dyn_string_coerce_js(ptr %d)`);
|
||||
const converted = B.tmp();
|
||||
B.line(`${converted} = icmp ne ptr ${text}, null`);
|
||||
const lPut = B.newLabel("ds.pp");
|
||||
B.condBr(converted, lPut, done);
|
||||
B.startBlock(lPut);
|
||||
this.putScrStr(B, "%b", text);
|
||||
B.line(`call void @scr_str_release(ptr ${text})`);
|
||||
B.br(done);
|
||||
}
|
||||
B.startBlock(lErr);
|
||||
// Error.prototype.toString, reading name and message through the
|
||||
// prototype. Null leaves the exception its conversion threw pending.
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
vAdapters,
|
||||
} from "./shapes.js";
|
||||
import { emitLlvmLayouts } from "./layouts.js";
|
||||
import { moduleRuntimeFeatures } from "../../ir/runtime-features.js";
|
||||
import { emitArraySearchHelpers } from "./array-search.js";
|
||||
import { emitClassMembershipHelper } from "./classes.js";
|
||||
import { type RegexMatcherCode, compileRegexMatcher } from "./regex-matchers.js";
|
||||
@@ -138,7 +137,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// outlive the RC audit — release it with the globals (the runtime ABI’s
|
||||
// sc_release_globals tail). Only when the dispatch unit is even
|
||||
// linked (defineProps is the only writer).
|
||||
const runtimeFeatures = moduleRuntimeFeatures(emitter.mod);
|
||||
const runtimeFeatures = emitter.runtimeFeatures;
|
||||
const fnValueProps = [...emitter.fnValues];
|
||||
if (fnValueProps.length > 0) emitter.declare(`declare void @scr_box_release(ptr)`);
|
||||
if (emitter.classObjs.size > 0)
|
||||
|
||||
@@ -54,11 +54,7 @@ import {
|
||||
VOID,
|
||||
classJsName,
|
||||
} from "../../ir/ir.js";
|
||||
import {
|
||||
moduleRuntimeFeatures,
|
||||
moduleUsesDynBuiltinStream,
|
||||
moduleUsesMockTimers,
|
||||
} from "../../ir/runtime-features.js";
|
||||
import { moduleRuntimeFeatures, type RuntimeFeatures } from "../../ir/runtime-features.js";
|
||||
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
|
||||
import { type DenseArrayLoop } from "./dense-array-loops.js";
|
||||
import { findInitializedByteLoopBindings } from "./byte-windows.js";
|
||||
@@ -264,7 +260,9 @@ import {
|
||||
emitLibCall,
|
||||
} from "./lib-dispatch.js";
|
||||
import {
|
||||
assignCompactLayouts,
|
||||
buildClassGraph,
|
||||
classFieldTypes,
|
||||
classMembershipIntervals,
|
||||
classFieldIndex,
|
||||
classPreLines,
|
||||
@@ -879,13 +877,16 @@ export class LlEmitter {
|
||||
/** The program calls mock.timers members (moduleUsesMockTimers): timer
|
||||
* calls emit scr_test.c's mock-aware entry points. */
|
||||
readonly mockTimers: boolean;
|
||||
private schedulesInContextValue: boolean | undefined = undefined;
|
||||
private runtimeFeaturesValue: RuntimeFeatures | undefined;
|
||||
/** The module's runtime features (moduleRuntimeFeatures), found with one
|
||||
* walk of the module the first time any of them is read. */
|
||||
get runtimeFeatures(): RuntimeFeatures {
|
||||
return (this.runtimeFeaturesValue ??= moduleRuntimeFeatures(this.mod));
|
||||
}
|
||||
/** Whether timer and tick scheduling goes through the *_als entry
|
||||
* points (RuntimeFeatures asyncContext). */
|
||||
get schedulesInContext(): boolean {
|
||||
if (this.schedulesInContextValue === undefined)
|
||||
this.schedulesInContextValue = moduleRuntimeFeatures(this.mod).asyncContext;
|
||||
return this.schedulesInContextValue;
|
||||
return this.runtimeFeatures.asyncContext;
|
||||
}
|
||||
/** Emitted ref-kind resolve thunks for new Promise, interned per inner
|
||||
* typeKey → thunk symbol. */
|
||||
@@ -901,10 +902,9 @@ export class LlEmitter {
|
||||
/** Identity-preserving static→dyn capsules for program classes and Web
|
||||
* APIs whose values remain directly observable. */
|
||||
readonly liveDynRefAdapters = new Map<string, LlStreamTypedRefAdapter>();
|
||||
private dynBuiltinStreamUse: boolean | undefined;
|
||||
/** See moduleUsesDynBuiltinStream. */
|
||||
get usesDynBuiltinStream(): boolean {
|
||||
return (this.dynBuiltinStreamUse ??= moduleUsesDynBuiltinStream(this.mod));
|
||||
return this.runtimeFeatures.dynBuiltinStream;
|
||||
}
|
||||
/** Runtime-arm dispatchers for live union values. */
|
||||
readonly liveDynUnionRefAdapters = new Map<string, string>();
|
||||
@@ -1141,7 +1141,7 @@ export class LlEmitter {
|
||||
) {
|
||||
this.stackTraces = observesStacks(mod);
|
||||
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
|
||||
this.mockTimers = moduleUsesMockTimers(mod);
|
||||
this.mockTimers = this.runtimeFeatures.mockTimers;
|
||||
this.debug =
|
||||
options.debugSources === undefined
|
||||
? null
|
||||
@@ -1154,14 +1154,18 @@ export class LlEmitter {
|
||||
);
|
||||
this.constantNumericTables = findConstantNumericTables(mod);
|
||||
this.initializerBindings = findInitializerBindings(mod);
|
||||
this.int32Slots = analyzeInt32Slots(mod);
|
||||
// One walk finds the dynamic field stores every analysis below consults.
|
||||
const dynamicStores = dynamicFieldStores(mod);
|
||||
this.int32Slots = analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings: this.initializerBindings,
|
||||
});
|
||||
this.liveViewsReadOnly =
|
||||
mod.lib === undefined && !dynamicStores.all && dynamicStores.names.size === 0;
|
||||
this.int32Abis = this.debug === null ? findInt32Abis(mod, this.int32Slots) : new Map();
|
||||
this.immortalGlobals = new Set(mod.contracts?.immortalGlobals ?? []);
|
||||
this.programLifetime = new Set(mod.contracts?.programLifetime ?? []);
|
||||
this.finalFields = analyzeFinalFields(mod);
|
||||
this.finalFields = analyzeFinalFields(mod, false, dynamicStores);
|
||||
this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
|
||||
this.ffiExtendNarrowIntegers =
|
||||
options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
|
||||
@@ -1231,7 +1235,7 @@ export class LlEmitter {
|
||||
this.unframedFunctions = this.stackTraces
|
||||
? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes)
|
||||
: new Set();
|
||||
this.immutableFields = computeImmutableFields(mod);
|
||||
this.immutableFields = computeImmutableFields(mod, dynamicStores);
|
||||
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
|
||||
this.indirectMayThrow = mt.indirect || mod.workers === true;
|
||||
for (const cls of mod.classes ?? []) {
|
||||
@@ -1356,7 +1360,7 @@ export class LlEmitter {
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
|
||||
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
|
||||
this.lazyCaptures = new LazyCaptures(this.fnByName);
|
||||
const traced = computeTraced(mod);
|
||||
const traced = computeTraced(mod, dynamicStores);
|
||||
this.tracedShapes = traced.shapes;
|
||||
this.tracedUnions = traced.unions;
|
||||
// A shape union's value is whichever record it holds, and the collector
|
||||
@@ -1492,6 +1496,15 @@ export class LlEmitter {
|
||||
classDefinitions: (cls, sizeOf) => regions.classDefs(cls, sizeOf),
|
||||
};
|
||||
}
|
||||
// ProgramLifetime hierarchies count nothing: their instances get the
|
||||
// 32-bit header and packed fields (classes.ts LlClassMeta.compact),
|
||||
// except where regions count them or a slot word follows the header.
|
||||
if (this.sizeType === "i64" && mod.lifetimeRegions !== true)
|
||||
assignCompactLayouts(
|
||||
this.classMeta,
|
||||
(root) => programLifetimeClass(this.shapeHost, root) && !root.threadSlot,
|
||||
(cls) => classFieldTypes(this.shapeHost, cls, this.int32Slots),
|
||||
);
|
||||
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
|
||||
const meta = this.classMeta.get(name);
|
||||
if (!meta) break; // hand-written IR without the builtin defs: no stamps
|
||||
@@ -5114,6 +5127,7 @@ export class LlEmitter {
|
||||
post: meta.post,
|
||||
runtimeRooted: meta.root.def.runtime === true,
|
||||
idInRc: meta.idInRc,
|
||||
compact: meta.compact,
|
||||
// Inline scalar-union fields are plain bits: nothing to push.
|
||||
fields: meta.def.fields.flatMap((f) =>
|
||||
this.inlineUnions.classField(className, f.name)
|
||||
@@ -5154,15 +5168,21 @@ export class LlEmitter {
|
||||
|
||||
/** Branches on "`obj` is published" (rc == SIZE_MAX, or a count of
|
||||
* UINT32_MAX in the low half for `count` i32: classes.ts
|
||||
* LlClassMeta.idInRc); leaves the builder in the cold block and answers
|
||||
* the continuation label. */
|
||||
private emitPublishedTest(obj: string, count: "i32" | "i64" = this.sizeType): string {
|
||||
* LlClassMeta.idInRc, or the top bit of a compact header word for
|
||||
* "compact": classes.ts COMPACT_PUBLISHED); leaves the builder in the cold
|
||||
* block and answers the continuation label. */
|
||||
private emitPublishedTest(obj: string, count: "i32" | "i64" | "compact" = this.sizeType): string {
|
||||
const B = this.B;
|
||||
const rc = B.tmp(),
|
||||
frozen = B.tmp(),
|
||||
expected = B.tmp();
|
||||
B.line(`${rc} = load ${count}, ptr ${obj}`);
|
||||
B.line(`${frozen} = icmp eq ${count} ${rc}, -1`);
|
||||
if (count === "compact") {
|
||||
B.line(`${rc} = load i32, ptr ${obj}`);
|
||||
B.line(`${frozen} = icmp slt i32 ${rc}, 0`);
|
||||
} else {
|
||||
B.line(`${rc} = load ${count}, ptr ${obj}`);
|
||||
B.line(`${frozen} = icmp eq ${count} ${rc}, -1`);
|
||||
}
|
||||
this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
|
||||
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`);
|
||||
const cold = B.newLabel("pub.frozen"),
|
||||
@@ -5203,7 +5223,10 @@ export class LlEmitter {
|
||||
if (className !== null && !published.classGuarded(className)) return;
|
||||
const B = this.B;
|
||||
const meta = className === null ? undefined : this.classMeta.get(className);
|
||||
const ok = this.emitPublishedTest(obj, meta?.idInRc === true ? "i32" : this.sizeType);
|
||||
const ok = this.emitPublishedTest(
|
||||
obj,
|
||||
meta?.compact === true ? "compact" : meta?.idInRc === true ? "i32" : this.sizeType,
|
||||
);
|
||||
let owner: string;
|
||||
if (meta === undefined) owner = this.cstr("Object");
|
||||
else if (!meta.hierarchy) owner = this.cstr(meta.def.jsName ?? meta.def.name);
|
||||
|
||||
@@ -29,7 +29,7 @@ import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { DYN_BUFFER_NUM_METHODS } from "../../ir/runtime-features.js";
|
||||
import { DYN_BUFFER_METHODS } from "../../ir/runtime-features.js";
|
||||
|
||||
export function emitDynamicExpr(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -234,11 +234,9 @@ export function emitDynamicExpr(
|
||||
});
|
||||
argsPtr = arr;
|
||||
}
|
||||
// Fixed-width Buffer method names take scr_bytes_dyn.c's entry, which
|
||||
// Buffer.prototype method names take their scr_bytes_dyn.c entry, which
|
||||
// runs them on Buffer receivers and dispatches any other receiver.
|
||||
const invoke = DYN_BUFFER_NUM_METHODS.has(e.method)
|
||||
? "scr_dyn_invoke_buffer_num"
|
||||
: "scr_dyn_invoke_prepared";
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared";
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
let name = host.cstr(e.calleeName);
|
||||
if (e.calleeNameValue !== undefined) {
|
||||
@@ -1038,6 +1036,29 @@ export function emitDynamicExpr(
|
||||
B.line(`${n} = xor i1 ${t}, true`);
|
||||
return { name: n, type: e.type };
|
||||
}
|
||||
if (e.test === "dynamic") {
|
||||
// A REF payload whose retain adapter is the dyn adapter (sc_cd's
|
||||
// by-reference arm).
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
const kp = B.tmp(),
|
||||
k = B.tmp(),
|
||||
isRef = B.tmp(),
|
||||
rp = B.tmp(),
|
||||
r = B.tmp(),
|
||||
isDyn = B.tmp(),
|
||||
t = B.tmp();
|
||||
B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 1`);
|
||||
B.line(`${k} = load i32, ptr ${kp}`);
|
||||
B.line(`${isRef} = icmp eq i32 ${k}, 4`);
|
||||
B.line(`${rp} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 5`);
|
||||
B.line(`${r} = load ptr, ptr ${rp}`);
|
||||
B.line(`${isDyn} = icmp eq ptr ${r}, @scr_dyn_retain_v`);
|
||||
B.line(`${t} = and i1 ${isRef}, ${isDyn}`);
|
||||
if (e.negated !== true) return { name: t, type: e.type };
|
||||
const n = B.tmp();
|
||||
B.line(`${n} = xor i1 ${t}, true`);
|
||||
return { name: n, type: e.type };
|
||||
}
|
||||
if (e.test === "object") {
|
||||
host.declare(`declare zeroext i1 @scr_caught_is_object(ptr)`);
|
||||
const t = B.tmp();
|
||||
|
||||
@@ -853,7 +853,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "emitter.emitFlex") {
|
||||
if (e.fn === "emitter.emitFlex" || e.fn === "emitter.emitAny") {
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const n = args.length - 2;
|
||||
let payload = "null";
|
||||
@@ -867,10 +867,57 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
}
|
||||
payload = arr;
|
||||
}
|
||||
host.declare(`declare zeroext i1 @scr_emitter_emit_flex(ptr, ptr, ptr, ${host.sizeType})`);
|
||||
const sym = e.fn === "emitter.emitAny" ? "scr_emitter_emit_any" : "scr_emitter_emit_flex";
|
||||
host.declare(`declare zeroext i1 @${sym}(ptr, ptr, ptr, ${host.sizeType})`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call i1 @scr_emitter_emit_flex(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${payload}, ${host.sizeType} ${n})`,
|
||||
`${t} = call i1 @${sym}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${payload}, ${host.sizeType} ${n})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "emitter.emitMixed") {
|
||||
// (recv, name, k, ...k dyn arguments, ...converted tuple): the dyn
|
||||
// arguments ride an exact-length array like emitFlex, the tuple the
|
||||
// pointer-classed variadic tail like emitter.emit. Borrowed; may throw.
|
||||
const count = e.args[2]!;
|
||||
const k = count.kind === "numLit" ? count.value : 0;
|
||||
const args = [e.args[0]!, e.args[1]!, ...e.args.slice(3)].map((a) => host.emitExpr(a));
|
||||
let payload = "null";
|
||||
if (k > 0) {
|
||||
const arr = B.slot();
|
||||
B.entryAllocas.push(`${arr} = alloca [${k} x ptr]`);
|
||||
for (let i = 0; i < k; i++) {
|
||||
const slot = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds [${k} x ptr], ptr ${arr}, i64 0, i64 ${i}`);
|
||||
B.line(`store ptr ${args[i + 2]!.name}, ptr ${slot}`);
|
||||
}
|
||||
payload = arr;
|
||||
}
|
||||
const tuple = args.slice(2 + k).map((a) => {
|
||||
const ty = host.llType(a.type);
|
||||
if (ty === "double" || ty === "i1") {
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty} ; EventEmitter scalar argument`);
|
||||
B.line(`store ${ty} ${a.name}, ptr ${slot}`);
|
||||
return `ptr ${slot}`;
|
||||
}
|
||||
return `ptr ${a.name}`;
|
||||
});
|
||||
host.declare(
|
||||
`declare zeroext i1 @scr_emitter_emit_mixed(ptr, ptr, ptr, ${host.sizeType}, ...)`,
|
||||
);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call zeroext i1 (ptr, ptr, ptr, ${host.sizeType}, ...) @scr_emitter_emit_mixed(` +
|
||||
[
|
||||
`ptr ${args[0]!.name}`,
|
||||
`ptr ${args[1]!.name}`,
|
||||
`ptr ${payload}`,
|
||||
`${host.sizeType} ${k}`,
|
||||
...tuple,
|
||||
].join(", ") +
|
||||
`)`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
|
||||
@@ -793,6 +793,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fsp.rename": "scr_fsp_rename",
|
||||
"fsp.readdir": "scr_fsp_readdir",
|
||||
"fsp.rm": "scr_fsp_rm",
|
||||
"fsp.rmOpts": "scr_fsp_rm_opts",
|
||||
"fsp.rmRetry": "scr_fsp_rm_opts_retry",
|
||||
"fsp.stat": "scr_fsp_stat",
|
||||
"fsp.realpath": "scr_fsp_realpath",
|
||||
"fsp.lstat": "scr_fsp_lstat",
|
||||
@@ -1070,6 +1072,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"emitter.off": "scr_emitter_off",
|
||||
"emitter.offDyn": "scr_emitter_off_dyn",
|
||||
"emitter.onFlex": "scr_emitter_on_flex",
|
||||
"emitter.onAny": "scr_emitter_on_any",
|
||||
"emitter.checkListener": "scr_emitter_check_listener",
|
||||
"emitter.removeAll": "scr_emitter_remove_all",
|
||||
"emitter.emitError": "scr_emitter_emit_error",
|
||||
@@ -1078,6 +1081,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"emitter.countDyn": "scr_emitter_listener_count_dyn",
|
||||
"emitter.names": "scr_emitter_event_names",
|
||||
"emitter.listeners": "scr_emitter_listeners",
|
||||
"emitter.listenersDyn": "scr_emitter_listeners_dyn",
|
||||
"emitter.hasTyped": "scr_emitter_has_typed",
|
||||
"emitter.setMax": "scr_emitter_set_max",
|
||||
"emitter.setMaxChk": "scr_emitter_set_max_chk",
|
||||
"emitter.setDefaultMaxChk": "scr_emitter_set_default_max_chk",
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { IrClassDef, IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../.
|
||||
import { mapOf, STRING, typeKey } from "../../ir/ir.js";
|
||||
import { mangleClassStruct, mangleRecordStruct } from "../mangle.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { COMPACT_ID_MASK } from "./classes.js";
|
||||
import { shapeTagFn } from "./nullable-unions.js";
|
||||
import type { InlineUnionFields } from "./inline-unions.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -66,6 +67,8 @@ export interface PublishClassInfo {
|
||||
/** Instances keep their class id in the rc word (classes.ts
|
||||
* LlClassMeta.idInRc). */
|
||||
idInRc: boolean;
|
||||
/** Instances have the 32-bit header (classes.ts LlClassMeta.compact). */
|
||||
compact: boolean;
|
||||
/** The class's own and inherited fields: GEP index and storage type. */
|
||||
fields: { name: string; index: number; type: IrType }[];
|
||||
}
|
||||
@@ -76,6 +79,11 @@ const PUB_KIND_UNION = 5;
|
||||
/** SCR_PUB_CLASS_ID, or'ed into a kind: the object keeps its class id
|
||||
* in the high half of its rc word (classes.ts LlClassMeta.idInRc). */
|
||||
const PUB_CLASS_ID = 0x100;
|
||||
/** SCR_PUB_COMPACT, or'ed into a kind: the object has the 32-bit header
|
||||
* (classes.ts compactHeaderWord), whose top bit is the mark. */
|
||||
const PUB_COMPACT = 0x200;
|
||||
/** COMPACT_PUBLISHED as an i32 constant. */
|
||||
const COMPACT_MARK = "-2147483648";
|
||||
/** SCR_PUB_LOG_COUNT (scr_publish.c), in a log entry's `buf`: `rc` is the
|
||||
* count of the low half. */
|
||||
const PUB_LOG_COUNT = "-2147483648";
|
||||
@@ -223,7 +231,11 @@ export class PublishWalkers {
|
||||
// hold time-value bits that no walker may push.
|
||||
if (def.arms.some((arm) => scalarRefusal(arm)))
|
||||
throw new LlvmUnsupportedError(`publish of ${typeKey(t)}`, loc);
|
||||
const live = arms.flatMap((a) => (a.fn === null ? [] : [{ tag: a.tag, fn: a.fn }]));
|
||||
const live = arms.flatMap((a) =>
|
||||
a.fn === null
|
||||
? []
|
||||
: [{ tag: a.tag, fn: a.fn, compact: this.compactRef(def.arms[a.tag]!) }],
|
||||
);
|
||||
if (this.host.shapeUnion(t)) {
|
||||
// The value is the record itself (or a unit encoding): no box to
|
||||
// mark; the arm's walker marks and walks the record.
|
||||
@@ -256,7 +268,7 @@ export class PublishWalkers {
|
||||
for (const a of live)
|
||||
lines.push(
|
||||
`arm${a.tag}:`,
|
||||
...this.push("%slot", a.fn, `u${a.tag}`),
|
||||
...this.push("%slot", a.fn, `u${a.tag}`, a.compact),
|
||||
` br label %done`,
|
||||
);
|
||||
}
|
||||
@@ -316,18 +328,25 @@ export class PublishWalkers {
|
||||
const lines = [
|
||||
head,
|
||||
`entry:`,
|
||||
...(info.idInRc
|
||||
...(info.compact
|
||||
? [
|
||||
` %idp = getelementptr inbounds i8, ptr %o, i64 4 ; the class id (classes.ts classIdRcWord)`,
|
||||
` %id = load i32, ptr %idp`,
|
||||
` %idw = load i32, ptr %o ; the class id (classes.ts compactHeaderWord)`,
|
||||
` %id = and i32 %idw, ${COMPACT_ID_MASK}`,
|
||||
` %idpre = add i32 %id, -1`,
|
||||
` %pre = zext i32 %idpre to ${this.host.sizeType}`,
|
||||
]
|
||||
: [
|
||||
` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`,
|
||||
` %vt = load ptr, ptr %vtp`,
|
||||
` %pre = load ${this.host.sizeType}, ptr %vt`,
|
||||
]),
|
||||
: info.idInRc
|
||||
? [
|
||||
` %idp = getelementptr inbounds i8, ptr %o, i64 4 ; the class id (classes.ts classIdRcWord)`,
|
||||
` %id = load i32, ptr %idp`,
|
||||
` %idpre = add i32 %id, -1`,
|
||||
` %pre = zext i32 %idpre to ${this.host.sizeType}`,
|
||||
]
|
||||
: [
|
||||
` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`,
|
||||
` %vt = load ptr, ptr %vtp`,
|
||||
` %pre = load ${this.host.sizeType}, ptr %vt`,
|
||||
]),
|
||||
` switch ${this.host.sizeType} %pre, label %other [ ${cases.map((c) => `${this.host.sizeType} ${c.pre}, label %c${c.pre}`).join(" ")} ]`,
|
||||
];
|
||||
for (const c of cases)
|
||||
@@ -363,6 +382,7 @@ export class PublishWalkers {
|
||||
path,
|
||||
loc,
|
||||
info.idInRc,
|
||||
info.compact,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -374,23 +394,26 @@ export class PublishWalkers {
|
||||
* targets), and an object with a collector header leaves its candidate
|
||||
* buffer through scr_cyc_pub_take. Anything else calls scr_pub_mark, as
|
||||
* does every mark of an RC-audit build. */
|
||||
private markLines(cyc: boolean, kind: number, classId = false): string[] {
|
||||
private markLines(cyc: boolean, kind: number, classId = false, compact = false): string[] {
|
||||
const flags = compact ? PUB_COMPACT : classId ? PUB_CLASS_ID : 0;
|
||||
const call = (name: string): string[] => [
|
||||
` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${classId ? kind | PUB_CLASS_ID : kind})`,
|
||||
` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${kind | flags})`,
|
||||
` br i1 %${name}, label %walk, label %done`,
|
||||
];
|
||||
if (this.host.rcAudit === true) return call("m");
|
||||
const S = this.host.sizeType;
|
||||
// An object that keeps its class id in the rc word is marked and
|
||||
// logged through its count, the low half.
|
||||
const C = classId ? "i32" : S;
|
||||
// A compact object's header word is logged whole (the restore writes
|
||||
// the low 32 bits back) and marked by its top bit.
|
||||
const C = classId || compact ? "i32" : S;
|
||||
const field = (name: string, slot: number): string[] => [
|
||||
` %${name}p = getelementptr inbounds ptr, ptr %ctx, i32 ${slot}`,
|
||||
` %${name} = load ${S === "i64" ? "i64" : "i32"}, ptr %${name}p`,
|
||||
];
|
||||
const lines = [
|
||||
` %mrc = load ${C}, ptr %o`,
|
||||
` %mimm = icmp eq ${C} %mrc, -1`,
|
||||
compact ? ` %mimm = icmp slt i32 %mrc, 0` : ` %mimm = icmp eq ${C} %mrc, -1`,
|
||||
` br i1 %mimm, label %done, label %mtest`,
|
||||
`mtest:`,
|
||||
` %mrefp = getelementptr inbounds ptr, ptr %ctx, i32 6`,
|
||||
@@ -408,7 +431,7 @@ export class PublishWalkers {
|
||||
ok = "%mok2";
|
||||
}
|
||||
lines.push(` br i1 ${ok}, label %mfast, label %mslow`, `mslow:`, ...call("ms"), `mfast:`);
|
||||
let buf = classId ? String(PUB_LOG_COUNT) : "0";
|
||||
let buf = classId || compact ? String(PUB_LOG_COUNT) : "0";
|
||||
if (cyc) {
|
||||
// ScrCycHdr.buf, the word before the object: its top bit while the
|
||||
// object is a buffered candidate.
|
||||
@@ -443,7 +466,9 @@ export class PublishWalkers {
|
||||
` store i32 ${buf}, ptr %mebuf`,
|
||||
` %mlogged1 = add ${S} %mlogged, 1`,
|
||||
` store ${S} %mlogged1, ptr %mloggedp`,
|
||||
` store ${C} -1, ptr %o`,
|
||||
...(compact
|
||||
? [` %mpub = or i32 %mrc, ${COMPACT_MARK}`, ` store i32 %mpub, ptr %o`]
|
||||
: [` store ${C} -1, ptr %o`]),
|
||||
` br label %walk`,
|
||||
);
|
||||
return lines;
|
||||
@@ -453,15 +478,18 @@ export class PublishWalkers {
|
||||
* immortal references (the parent links of a tree, values published
|
||||
* before) are the ones scr_pub_push skips: they are tested here, without
|
||||
* the call. The lines end in a fresh block labelled `pushed${id}`. */
|
||||
private push(v: string, fn: string, id: string): string[] {
|
||||
private push(v: string, fn: string, id: string, compact = false): string[] {
|
||||
return [
|
||||
` %null${id} = icmp eq ptr ${v}, null`,
|
||||
` br i1 %null${id}, label %pushed${id}, label %live${id}`,
|
||||
`live${id}:`,
|
||||
// The count (the low half of a 64-bit rc word) is UINT32_MAX for an
|
||||
// immortal object of every kind (scr_rc.h SCR_RC_COUNT).
|
||||
// immortal object of every kind (scr_rc.h SCR_RC_COUNT), and a
|
||||
// compact object's header word has its top bit set once published.
|
||||
` %rc${id} = load i32, ptr ${v}`,
|
||||
` %immortal${id} = icmp eq i32 %rc${id}, -1`,
|
||||
compact
|
||||
? ` %immortal${id} = icmp slt i32 %rc${id}, 0`
|
||||
: ` %immortal${id} = icmp eq i32 %rc${id}, -1`,
|
||||
` br i1 %immortal${id}, label %pushed${id}, label %push${id}`,
|
||||
`push${id}:`,
|
||||
...this.pushLines(v, fn, id),
|
||||
@@ -497,6 +525,12 @@ export class PublishWalkers {
|
||||
];
|
||||
}
|
||||
|
||||
/** Whether references of type `t` are compact class instances. */
|
||||
private compactRef(t: IrType): boolean {
|
||||
const arm = this.host.nullableArm(t) ?? t;
|
||||
return arm.kind === "object" && this.host.classInfo(arm.className).compact;
|
||||
}
|
||||
|
||||
private objectBody(
|
||||
head: string,
|
||||
struct: string,
|
||||
@@ -505,8 +539,14 @@ export class PublishWalkers {
|
||||
path: string,
|
||||
loc: SrcLoc,
|
||||
classId = false,
|
||||
compact = false,
|
||||
): string[] {
|
||||
const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT, classId), `walk:`];
|
||||
const lines = [
|
||||
head,
|
||||
`entry:`,
|
||||
...this.markLines(traced, PUB_KIND_OBJECT, classId, compact),
|
||||
`walk:`,
|
||||
];
|
||||
let n = 0;
|
||||
for (const member of members) {
|
||||
const fn = this.walker(member.type, `${path}.${member.name}`, loc);
|
||||
@@ -519,7 +559,7 @@ export class PublishWalkers {
|
||||
lines.push(
|
||||
` %f${n} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${member.index} ; ${llvmComment(member.name)}`,
|
||||
` %v${n} = load ptr, ptr %f${n}`,
|
||||
...this.push(`%v${n}`, fn, `${n}`),
|
||||
...this.push(`%v${n}`, fn, `${n}`, this.compactRef(member.type)),
|
||||
);
|
||||
n++;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
/* The WebAssembly modules translated into a program (IrModule.wasmModules):
|
||||
* each module's translation (wasm/translate.ts) and the registry the
|
||||
* runtime looks modules up in (scr_wasm_registry, scr_wasm_abi.h). The
|
||||
* each module's translation (wasm/translate.ts), the registry the runtime
|
||||
* looks modules up in (scr_wasm_registry, scr_wasm_abi.h), and the table of
|
||||
* modules the build saw but does not translate (scr_wasm_untranslated). The
|
||||
* translations join the program module, so the native helper optimizes,
|
||||
* partitions and caches them with it. Programs that do not use the
|
||||
* WebAssembly API carry no wasmModules and emit nothing here. */
|
||||
import type { IrModule } from "../../ir/ir.js";
|
||||
import type { IrModule, IrWasmUntranslated } from "../../ir/ir.js";
|
||||
import { decodeBase64 } from "../../wasm/base64.js";
|
||||
import { translateWasmModule, WasmTranslateError } from "../../wasm/translate.js";
|
||||
import {
|
||||
cBytes,
|
||||
translateWasmModule,
|
||||
utf8Bytes,
|
||||
WasmTranslateError,
|
||||
} from "../../wasm/translate.js";
|
||||
|
||||
function hexBytes(hex: string): Uint8Array {
|
||||
const out = new Uint8Array(hex.length / 2);
|
||||
@@ -28,11 +34,38 @@ export function wasmBoundsChecks(options: {
|
||||
return /windows|mingw|cygwin|win32|msvc/.test(target);
|
||||
}
|
||||
|
||||
/** The top-level lines of every translated module plus the registry.
|
||||
* `declared` holds the declarations the module already carries; runtime
|
||||
* entry points it lacks are declared here. A module the translator
|
||||
* cannot translate is left out of the registry, so loading it at run
|
||||
* time rejects with the CompileError for untranslated bytes. */
|
||||
/** scr_wasm_untranslated: hash, byte length and reason per module. */
|
||||
function emitUntranslated(untranslated: readonly IrWasmUntranslated[]): string[] {
|
||||
if (untranslated.length === 0)
|
||||
return [`@scr_wasm_untranslated = constant { i64, ptr } { i64 0, ptr null }`];
|
||||
const out: string[] = [];
|
||||
const entries: string[] = [];
|
||||
for (let index = 0; index < untranslated.length; index++) {
|
||||
const module = untranslated[index]!;
|
||||
const reason = new Uint8Array(utf8Bytes(module.reason));
|
||||
out.push(
|
||||
`@sc_wasm_untranslated_reason${index} = private unnamed_addr constant [${reason.length} x i8] c"${cBytes(reason, 0, reason.length)}"`,
|
||||
);
|
||||
const digest = hexBytes(module.sha256);
|
||||
const sha: string[] = [];
|
||||
for (let i = 0; i < 32; i++) sha.push(`i8 ${digest[i]!}`);
|
||||
entries.push(
|
||||
`{ [32 x i8], i64, ptr, i64 } { [32 x i8] [${sha.join(", ")}], i64 ${module.size}, ptr @sc_wasm_untranslated_reason${index}, i64 ${reason.length} }`,
|
||||
);
|
||||
}
|
||||
out.push(
|
||||
`@sc_wasm_untranslated_modules = private constant [${entries.length} x { [32 x i8], i64, ptr, i64 }] [${entries.join(", ")}]`,
|
||||
`@scr_wasm_untranslated = constant { i64, ptr } { i64 ${entries.length}, ptr @sc_wasm_untranslated_modules }`,
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The top-level lines of every translated module plus the registry and
|
||||
* the untranslated table. `declared` holds the declarations the module
|
||||
* already carries; runtime entry points it lacks are declared here. A
|
||||
* module the translator cannot translate joins the untranslated table, so
|
||||
* loading it at run time rejects with a CompileError that names the
|
||||
* translator's reason. */
|
||||
export function emitWasmModules(
|
||||
mod: IrModule,
|
||||
declared: ReadonlySet<string>,
|
||||
@@ -43,6 +76,7 @@ export function emitWasmModules(
|
||||
const out: string[] = [];
|
||||
const decls = new Set(declared);
|
||||
const descriptors: string[] = [];
|
||||
const untranslated = [...(mod.wasmUntranslated ?? [])];
|
||||
for (const module of modules) {
|
||||
const bytes = decodeBase64(module.bytes);
|
||||
if (bytes === null) continue;
|
||||
@@ -53,8 +87,9 @@ export function emitWasmModules(
|
||||
customSections: mod.wasmCustomSections === true,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof WasmTranslateError) continue;
|
||||
throw error;
|
||||
if (!(error instanceof WasmTranslateError)) throw error;
|
||||
untranslated.push({ sha256: module.sha256, size: bytes.length, reason: error.message });
|
||||
continue;
|
||||
}
|
||||
for (const decl of translation.decls) {
|
||||
if (decls.has(decl)) continue;
|
||||
@@ -72,6 +107,7 @@ export function emitWasmModules(
|
||||
`@scr_wasm_registry = constant { i64, ptr } { i64 ${descriptors.length}, ptr @sc_wasm_registry_modules }`,
|
||||
);
|
||||
}
|
||||
out.push(...emitUntranslated(untranslated));
|
||||
out.push("");
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { createHash, type Hash } from "node:crypto";
|
||||
import { closeSync, constants, openSync, readSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { access, chmod, mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import { access, chmod, copyFile, mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import {
|
||||
@@ -73,9 +73,31 @@ export interface NativeCodegenArtifact {
|
||||
|
||||
const resolvedHelperCache = new Map<string, Promise<ResolvedHelper>>();
|
||||
|
||||
/** LLVM text already written to `file`, of `bytes` bytes, which a build passes on without reading it. */
|
||||
export interface LlvmFile {
|
||||
readonly file: string;
|
||||
readonly bytes: number;
|
||||
}
|
||||
|
||||
/** LLVM text as a string, as parts to concatenate, or as a file. */
|
||||
export type LlvmInput = string | readonly string[] | LlvmFile;
|
||||
|
||||
export function isLlvmFile(llvm: LlvmInput): llvm is LlvmFile {
|
||||
return typeof llvm === "object" && !Array.isArray(llvm);
|
||||
}
|
||||
|
||||
/** The size of `llvm` in bytes. */
|
||||
export function llvmInputBytes(llvm: LlvmInput): number {
|
||||
if (isLlvmFile(llvm)) return llvm.bytes;
|
||||
return (typeof llvm === "string" ? [llvm] : llvm).reduce(
|
||||
(bytes, part) => bytes + Buffer.byteLength(part),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
export interface NativeCodegenOptions {
|
||||
outputPath: string;
|
||||
llvm: string | readonly string[];
|
||||
llvm: LlvmInput;
|
||||
outputKind: NativeCodegenOutputKind;
|
||||
sourcePath: string;
|
||||
optimization?: "0" | "1" | "2" | "3" | "s" | "z";
|
||||
@@ -278,14 +300,31 @@ async function resolveHelper(
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** Adds the bytes of `file` to `hash`, as `hash.update` of the text written there would. */
|
||||
function updateHashFromFile(hash: Hash, file: string): void {
|
||||
const fd = openSync(file, "r");
|
||||
try {
|
||||
const buffer = Buffer.allocUnsafe(4 * 1024 * 1024);
|
||||
for (;;) {
|
||||
const read = readSync(fd, buffer, 0, buffer.length, null);
|
||||
if (read === 0) break;
|
||||
hash.update(buffer.subarray(0, read));
|
||||
}
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
function cacheKey(
|
||||
options: NativeCodegenOptions,
|
||||
target: NativeTargetSpec,
|
||||
helper: HelperIdentity,
|
||||
): string {
|
||||
const hash = createHash("sha256").update("scriptc-native-codegen-v1\0");
|
||||
for (const part of typeof options.llvm === "string" ? [options.llvm] : options.llvm)
|
||||
hash.update(part);
|
||||
if (isLlvmFile(options.llvm)) updateHashFromFile(hash, options.llvm.file);
|
||||
else
|
||||
for (const part of typeof options.llvm === "string" ? [options.llvm] : options.llvm)
|
||||
hash.update(part);
|
||||
return hash
|
||||
.update("\0")
|
||||
.update(JSON.stringify(target))
|
||||
@@ -424,7 +463,8 @@ export async function emitNativeArtifact(
|
||||
// concurrent build of another entry that has the same output stem.
|
||||
const input = privateSiblingPath(options.outputPath, "native-input");
|
||||
try {
|
||||
await writeLlvmSource(input, options.llvm, 0o600);
|
||||
if (isLlvmFile(options.llvm)) await copyFile(options.llvm.file, input);
|
||||
else await writeLlvmSource(input, options.llvm, 0o600);
|
||||
await invoke(helper.binaryPath, [
|
||||
"emit",
|
||||
"--input",
|
||||
|
||||
@@ -58,9 +58,9 @@ export interface NativeLinkFeatures {
|
||||
/** events.once with options: scr_events_once_signal.c. Absent (not
|
||||
* false) for programs without it. */
|
||||
eventsOnceSignal?: boolean;
|
||||
/** Fixed-width Buffer methods called on checked-dynamic values link
|
||||
/** Buffer.prototype methods called on checked-dynamic values link
|
||||
* scr_bytes_dyn.c; absent (false) for programs without them. */
|
||||
dynBufferNum?: boolean;
|
||||
dynBuffer?: boolean;
|
||||
/** Builtin functions read as values (Buffer.byteLength, Buffer.alloc,
|
||||
* fs.realpathSync) link scr_builtin_values.c; absent (false) otherwise. */
|
||||
builtinValues?: boolean;
|
||||
|
||||
@@ -123,9 +123,9 @@ export interface CcOptions {
|
||||
/** events.once with options (events.onceSignal): compiles
|
||||
* scr_events_once_signal.c. */
|
||||
eventsOnceSignal?: boolean;
|
||||
/** Fixed-width Buffer methods called on checked-dynamic values:
|
||||
/** Buffer.prototype methods called on checked-dynamic values:
|
||||
* compiles scr_bytes_dyn.c. */
|
||||
dynBufferNum?: boolean;
|
||||
dynBuffer?: boolean;
|
||||
/** Builtin functions read as values: compiles scr_builtin_values.c. */
|
||||
builtinValues?: boolean;
|
||||
/** The program constructs EventTarget/Event/CustomEvent values or
|
||||
@@ -324,6 +324,9 @@ export interface LibArchiveOptions {
|
||||
threadInstances?: boolean;
|
||||
/** IR-detected link gates (the compileC precedent, refusal-narrowed). */
|
||||
dynInvoke?: boolean;
|
||||
/** Buffer.prototype methods on checked-dynamic values: scr_bytes_dyn.c,
|
||||
* with dynInvoke. */
|
||||
dynBuffer?: boolean;
|
||||
/** See CompileOptions.dynTier: the scr_js_* units. */
|
||||
dynTier?: boolean;
|
||||
regex?: boolean;
|
||||
|
||||
@@ -180,8 +180,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"),
|
||||
unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"),
|
||||
unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"),
|
||||
// Fixed-width Buffer methods called on checked-dynamic values.
|
||||
unit((u) => u.opts.dynBufferNum === true && u.opts.dynInvoke, "scr_bytes_dyn.c"),
|
||||
// Buffer.prototype methods called on checked-dynamic values.
|
||||
unit((u) => u.opts.dynBuffer === true && u.opts.dynInvoke, "scr_bytes_dyn.c"),
|
||||
// Builtin functions read as values in JavaScript dependencies.
|
||||
unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"),
|
||||
unit((u) => u.opts.dc, "scr_dc.c"),
|
||||
|
||||
@@ -112,6 +112,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
...(opts.assert && opts.bigint ? ["scr_bigint_assert.c"] : []),
|
||||
...(opts.emitter ? ["scr_events_emitter.c", "scr_dyn_handle.c"] : []),
|
||||
...(opts.dynInvoke ? ["scr_dyn_invoke.c"] : []),
|
||||
...(opts.dynInvoke && opts.dynBuffer ? ["scr_bytes_dyn.c"] : []),
|
||||
...(opts.dynTier === true ? ["scr_js_value.c", "scr_js_object.c"] : []),
|
||||
...(opts.zlib ? ["scr_zlib.c"] : []),
|
||||
...(opts.copying ? ["scr_copying.c"] : []),
|
||||
|
||||
@@ -88,7 +88,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
handleProps: boolean;
|
||||
dynForAwait: boolean;
|
||||
eventsOnceSignal: boolean;
|
||||
dynBufferNum: boolean;
|
||||
dynBuffer: boolean;
|
||||
builtinValues: boolean;
|
||||
warningReplace: boolean;
|
||||
stdioExtended: boolean;
|
||||
@@ -286,7 +286,7 @@ export function effectiveRuntimeFeatures(
|
||||
handleProps: features.handleProps === true,
|
||||
dynForAwait: features.dynForAwait === true,
|
||||
eventsOnceSignal: features.eventsOnceSignal === true,
|
||||
dynBufferNum: features.dynBufferNum === true,
|
||||
dynBuffer: features.dynBuffer === true,
|
||||
builtinValues: features.builtinValues === true,
|
||||
warningReplace: features.warningReplace === true,
|
||||
stdioExtended: features.stdioExtended === true,
|
||||
|
||||
@@ -13,9 +13,11 @@ import {
|
||||
} from "../backend/native-toolchain.js";
|
||||
import { installDarwinDebugSymbols, readDarwinDebugSymbols } from "../backend/debug-symbols.js";
|
||||
import {
|
||||
hasEarlyExecutableNativeBuild,
|
||||
publishEarlyExecutableCache,
|
||||
readEarlyExecutableCache,
|
||||
readRoutedExecutableCache,
|
||||
recordEarlyExecutableNativeBuild,
|
||||
type EarlyExecutableCacheOptions,
|
||||
type EarlyExecutableNativeFeatures,
|
||||
} from "./executable-cache.js";
|
||||
@@ -596,3 +598,38 @@ test("the route index still isolates eager and deferred native environments", as
|
||||
)?.executableRestored,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("a recorded native build is found by its invocation", async () => {
|
||||
const f = await fixture();
|
||||
expect(await hasEarlyExecutableNativeBuild(null, f.options)).toBe(false);
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, f.options)).toBe(false);
|
||||
await recordEarlyExecutableNativeBuild(null, f.options);
|
||||
await recordEarlyExecutableNativeBuild(f.root, f.options);
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, f.options)).toBe(true);
|
||||
// The record does not depend on the compiler implementation or the native environment.
|
||||
const rebuilt: EarlyExecutableCacheOptions = {
|
||||
...f.options,
|
||||
implementation: "b".repeat(64),
|
||||
nativeEnvironment: "another driver",
|
||||
};
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, rebuilt)).toBe(true);
|
||||
expect(
|
||||
await hasEarlyExecutableNativeBuild(f.root, {
|
||||
...f.options,
|
||||
outPath: join(f.options.outDir, "other"),
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, { ...f.options, sanitize: true })).toBe(false);
|
||||
// Publishing an executable does not record a native build.
|
||||
const other = { ...f.options, outPath: join(f.options.outDir, "published") };
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(f.source));
|
||||
await publishEarlyExecutableCache(f.root, other, {
|
||||
llvmPath: f.llvmPath,
|
||||
irPath: f.irPath,
|
||||
native,
|
||||
executableRestored: false,
|
||||
frontend: tracker.snapshot(),
|
||||
});
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, other)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
handleProps?: boolean;
|
||||
dynForAwait?: boolean;
|
||||
eventsOnceSignal?: boolean;
|
||||
dynBufferNum?: boolean;
|
||||
dynBuffer?: boolean;
|
||||
builtinValues?: boolean;
|
||||
events: boolean;
|
||||
emitter: boolean;
|
||||
@@ -338,6 +338,39 @@ function routePath(
|
||||
return join(root, "early-exe-route", routeKey(options));
|
||||
}
|
||||
|
||||
function nativeBuildMarkerPath(
|
||||
root: string,
|
||||
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
||||
): string {
|
||||
return join(root, "early-exe-native-build", routeKey(options));
|
||||
}
|
||||
|
||||
/** Records that a build of the invocation `options` emitted its native program object, so the
|
||||
* native helper's partition cache holds its partitions. Unlike the route, the record does not
|
||||
* depend on the build publishing its executable. */
|
||||
export async function recordEarlyExecutableNativeBuild(
|
||||
root: string | null,
|
||||
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
||||
): Promise<void> {
|
||||
if (root === null) return;
|
||||
const marker = nativeBuildMarkerPath(root, options);
|
||||
await mkdir(dirname(marker), { recursive: true, mode: 0o700 });
|
||||
await writeFile(marker, "", { mode: 0o600 });
|
||||
}
|
||||
|
||||
/** Whether an earlier build of the invocation `options` recorded its native program object
|
||||
* (`recordEarlyExecutableNativeBuild`). False when there is no cache. */
|
||||
export async function hasEarlyExecutableNativeBuild(
|
||||
root: string | null,
|
||||
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
||||
): Promise<boolean> {
|
||||
if (root === null) return false;
|
||||
return stat(nativeBuildMarkerPath(root, options)).then(
|
||||
(info) => info.isFile(),
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
function routeIntegrity(stamp: Omit<EarlyExecutableRouteStamp, "integrity">): string {
|
||||
return createHash("sha256")
|
||||
.update("early-executable-route-stamp-v2\0")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CompileFailure, CompileRequestOptions } from "../compile-types.js";
|
||||
import type { FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { frontendDisposed } from "../frontend/dispose-hook.js";
|
||||
import {
|
||||
checkerPanicDiag,
|
||||
npmStaticRefusalDiag,
|
||||
@@ -112,6 +113,7 @@ export function prepareExecutableModule(
|
||||
}
|
||||
} finally {
|
||||
fe.dispose();
|
||||
frontendDisposed();
|
||||
timing("frontend-dispose");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { frontendProcessExecArgv } from "./frontend-process-args.js";
|
||||
|
||||
test("the frontend process keeps heap limits and loaders", () => {
|
||||
expect(
|
||||
frontendProcessExecArgv([
|
||||
"--max-old-space-size=8192",
|
||||
"--stack-size=4000",
|
||||
"--import",
|
||||
"tsx",
|
||||
"-r",
|
||||
"./setup.cjs",
|
||||
"--conditions=development",
|
||||
"--enable-source-maps",
|
||||
]),
|
||||
).toEqual([
|
||||
"--max-old-space-size=8192",
|
||||
"--stack-size=4000",
|
||||
"--import",
|
||||
"tsx",
|
||||
"-r",
|
||||
"./setup.cjs",
|
||||
"--conditions=development",
|
||||
"--enable-source-maps",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the frontend process drops evaluated code, inspectors, watch and test runner options", () => {
|
||||
expect(
|
||||
frontendProcessExecArgv([
|
||||
"--max-old-space-size=8192",
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
"await compile(process.argv[1], {})",
|
||||
]),
|
||||
).toEqual(["--max-old-space-size=8192"]);
|
||||
expect(frontendProcessExecArgv(["-e", "code", "-p", "code", "-pe", "code"])).toEqual([]);
|
||||
expect(
|
||||
frontendProcessExecArgv(["--eval=code", "--print=code", "--input-type", "module"]),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
frontendProcessExecArgv([
|
||||
"--inspect",
|
||||
"--inspect-brk=9229",
|
||||
"--inspect-port",
|
||||
"9230",
|
||||
"--watch",
|
||||
"--watch-path=src",
|
||||
"--test",
|
||||
"--test-name-pattern",
|
||||
"build",
|
||||
"--test-reporter=spec",
|
||||
"--expose-gc",
|
||||
]),
|
||||
).toEqual(["--expose-gc"]);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/** Options of the compiler's own process that the frontend process (frontend-process.ts) must not
|
||||
* receive, with the options among them that take their value as the next argument: the code of
|
||||
* --eval and --print would run in place of the frontend process's entry, and the inspector, watch
|
||||
* and test runner options belong to the process that was started with them. */
|
||||
const OPTIONS_WITH_VALUES = new Set([
|
||||
"-e",
|
||||
"--eval",
|
||||
"-p",
|
||||
"--print",
|
||||
"--input-type",
|
||||
"--inspect-port",
|
||||
"--debug-port",
|
||||
"--watch-path",
|
||||
"--test-concurrency",
|
||||
"--test-coverage-branches",
|
||||
"--test-coverage-exclude",
|
||||
"--test-coverage-functions",
|
||||
"--test-coverage-include",
|
||||
"--test-coverage-lines",
|
||||
"--test-global-setup",
|
||||
"--test-isolation",
|
||||
"--test-name-pattern",
|
||||
"--test-reporter",
|
||||
"--test-reporter-destination",
|
||||
"--test-shard",
|
||||
"--test-skip-pattern",
|
||||
"--test-timeout",
|
||||
]);
|
||||
const OPTIONS = new Set(["-i", "--interactive", "-c", "--check", "--test"]);
|
||||
const OPTION_PREFIXES = ["--inspect", "--debug-port", "--watch", "--test-", "--input-type="];
|
||||
|
||||
/** The Node.js options of the frontend process: `execArgv` without the options that run code
|
||||
* (--eval, --print), open an inspector, or watch or test, so that its own entry runs with the
|
||||
* compiler process's remaining options, such as heap limits and loaders. */
|
||||
export function frontendProcessExecArgv(execArgv: readonly string[]): string[] {
|
||||
const kept: string[] = [];
|
||||
for (let index = 0; index < execArgv.length; index++) {
|
||||
const option = execArgv[index]!;
|
||||
// A short option group such as -pe takes the code that follows it.
|
||||
if (OPTIONS_WITH_VALUES.has(option) || /^-[a-z]*[ep][a-z]*$/i.test(option)) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
OPTIONS.has(option) ||
|
||||
option.startsWith("--eval=") ||
|
||||
option.startsWith("--print=") ||
|
||||
OPTION_PREFIXES.some((prefix) => option.startsWith(prefix))
|
||||
)
|
||||
continue;
|
||||
kept.push(option);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// The frontend process of an executable build (FrontendProcess and runFrontendProcess in index.ts):
|
||||
// loads the compiler, then reads the paths of its request and result from stdin. Without them it
|
||||
// exits: the build found its executable without it.
|
||||
import { enableCompileCache } from "node:module";
|
||||
|
||||
try {
|
||||
enableCompileCache();
|
||||
} catch {
|
||||
// Bytecode caching only speeds up loading.
|
||||
}
|
||||
const { runFrontendProcess } = await import("./index.js");
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += String(chunk);
|
||||
const [requestPath, resultPath] = input.split("\n");
|
||||
if (requestPath !== undefined && requestPath !== "" && resultPath !== undefined)
|
||||
await runFrontendProcess(requestPath, resultPath);
|
||||
// The frontend's own helpers (the checker process, timers) must not keep this process alive.
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,14 @@
|
||||
/* What runs right after a build disposes its frontend. The native compiler
|
||||
* sets it to a full cycle collection (native/cli.ts): the frontend's syntax
|
||||
* trees and program state refer to each other, so disposing them leaves
|
||||
* cyclic garbage that reference counting cannot free. Under Node it is
|
||||
* unset. */
|
||||
let afterDispose: (() => void) | null = null;
|
||||
|
||||
export function setFrontendDisposedHook(hook: () => void): void {
|
||||
afterDispose = hook;
|
||||
}
|
||||
|
||||
export function frontendDisposed(): void {
|
||||
if (afterDispose !== null) afterDispose();
|
||||
}
|
||||
@@ -863,15 +863,22 @@ export function lowerFsPromisesMkdirCall(
|
||||
: { kind: "libCall", fn: "fsp.mkdirRecursive", args: [path], type, loc };
|
||||
}
|
||||
|
||||
// rmSync(p, options): literal { recursive?, force? } booleans — the
|
||||
// cleanup shape rmSync(dir, { recursive: true, force: true }) —
|
||||
// plus the maxRetries/retryDelay numbers (the tmpdir-harness shape
|
||||
// rmSync(p, options) and fs.promises.rm(p, options): literal
|
||||
// { recursive?, force? } booleans — the cleanup shape
|
||||
// rmSync(dir, { recursive: true, force: true }) — plus the
|
||||
// maxRetries/retryDelay numbers (the tmpdir-harness shape
|
||||
// rmSync(p, { maxRetries: 3, recursive: true, force: true })): those
|
||||
// lower as ordinary number expressions into the retry libCall, whose
|
||||
// runtime implements Node's linear-backoff retry on
|
||||
// EBUSY/EMFILE/ENFILE/ENOTEMPTY/EPERM. The retry-free shape keeps the
|
||||
// historical rmOptsSync lowering unchanged.
|
||||
export function lowerFsRemoveCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
// historical rmOptsSync lowering unchanged. The promise form runs the same
|
||||
// removal and rejects with its error.
|
||||
export function lowerFsRemoveCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
promise: boolean,
|
||||
): IrExpr {
|
||||
const opts = literalBoolOptions(
|
||||
expr.arguments.nodes[1]!,
|
||||
["recursive", "force"],
|
||||
@@ -879,7 +886,7 @@ export function lowerFsRemoveCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
);
|
||||
if (opts === null) {
|
||||
lowerer.noLowering(
|
||||
"rmSync with an options argument beyond { recursive, force, maxRetries, retryDelay }",
|
||||
`${promise ? "fs.promises.rm" : "rmSync"} with an options argument beyond { recursive, force, maxRetries, retryDelay }`,
|
||||
expr.arguments.nodes[1]!,
|
||||
"the recursive/force flags must be boolean literals; maxRetries/retryDelay are numbers; other options have no lowering",
|
||||
);
|
||||
@@ -888,12 +895,13 @@ export function lowerFsRemoveCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
|
||||
const recursive = boolLit(opts.bools["recursive"] === true, loc);
|
||||
const force = boolLit(opts.bools["force"] === true, loc);
|
||||
const type: IrType = promise ? { kind: "promise", inner: VOID } : VOID;
|
||||
if (opts.exprs["maxRetries"] === undefined && opts.exprs["retryDelay"] === undefined) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "fs.rmOptsSync",
|
||||
fn: promise ? "fsp.rmOpts" : "fs.rmOptsSync",
|
||||
args: [path, recursive, force],
|
||||
type: VOID,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -904,7 +912,7 @@ export function lowerFsRemoveCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
node ? lowerer.lowerExprExpecting(node, F64) : { kind: "numLit", value: dflt, type: F64, loc };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "fs.rmRetrySync",
|
||||
fn: promise ? "fsp.rmRetry" : "fs.rmRetrySync",
|
||||
args: [
|
||||
path,
|
||||
recursive,
|
||||
@@ -912,7 +920,7 @@ export function lowerFsRemoveCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
num(opts.exprs["maxRetries"], 0),
|
||||
num(opts.exprs["retryDelay"], 100),
|
||||
],
|
||||
type: VOID,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -292,7 +292,10 @@ export function lowerBuiltinModuleCall(
|
||||
return lowerFsPromisesMkdirCall(lowerer, expr, loc);
|
||||
}
|
||||
if (bi.module === "fs" && bi.member === "rmSync" && expr.arguments.nodes.length === 2) {
|
||||
return lowerFsRemoveCall(lowerer, expr, loc);
|
||||
return lowerFsRemoveCall(lowerer, expr, loc, false);
|
||||
}
|
||||
if (bi.module === "fs/promises" && bi.member === "rm" && expr.arguments.nodes.length === 2) {
|
||||
return lowerFsRemoveCall(lowerer, expr, loc, true);
|
||||
}
|
||||
// accessSync(p, mode?) and truncateSync(p, len?), with their promise
|
||||
// spellings: an omitted mode is Node's F_OK (0) and an omitted len is 0.
|
||||
|
||||
@@ -676,12 +676,14 @@ export function requireExactArityValue(
|
||||
: null;
|
||||
if (mapped && typeEquals(mapped, funcType)) return;
|
||||
// An 'any' slot (`C.prototype.m = function (a, b) {…}` through an
|
||||
// untyped receiver) boxes the value into a checked-dynamic function:
|
||||
// untyped receiver), or a slot whose type maps to a checked-dynamic
|
||||
// value (a member of a literal lowered with a generic function's type
|
||||
// parameters erased), boxes the value into a checked-dynamic function:
|
||||
// the box completes omitted arguments with undefined and checks every
|
||||
// argument into the completed ABI, so any boxable signature passes.
|
||||
if (
|
||||
target !== undefined &&
|
||||
(target.flags & ts.TypeFlags.Any) !== 0 &&
|
||||
((target.flags & ts.TypeFlags.Any) !== 0 || mapped?.kind === "dyn") &&
|
||||
canBoxFuncIntoDyn(
|
||||
funcType,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { BOOL, CAUGHT, DYN, type IrFunction, type IrType } from "../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import { classInstanceOf } from "./class-dynamic-dispatch.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Exception snapshots retain native objects. Recover their known layouts
|
||||
@@ -10,6 +20,10 @@ export class CaughtValueDispatch {
|
||||
private readonly thrownClasses = new Set<string>();
|
||||
private readonly convertedClasses = new Set<string>();
|
||||
private helper: IrFunction | null = null;
|
||||
/** The dynamic-payload helpers of class tests and extractions, by kind
|
||||
* and class (dynamicPayloads). */
|
||||
private readonly payloadHelpers = new Map<string, IrFunction>();
|
||||
private readonly payloadHelperFns = new Set<IrFunction>();
|
||||
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
|
||||
const consumers: IrFunction[] = [];
|
||||
@@ -81,4 +95,164 @@ export class CaughtValueDispatch {
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/** A thrown unknown or union value, and the runtime's own dynamic throws,
|
||||
* reach a catch binding as a dynamic payload. When an instance of class C
|
||||
* can be such a payload (C or a subclass of C crosses into unknown, the
|
||||
* `boxed` classes), `e instanceof C`, the reads it narrows and `e as C` in
|
||||
* `sites` also answer through the dynamic class test, as standalone classes
|
||||
* already do. Programs that never box C keep the native payload test. */
|
||||
dynamicPayloads(
|
||||
lowerer: Lowerer,
|
||||
sites: ReadonlySet<IrFunction>,
|
||||
boxed: ReadonlySet<string>,
|
||||
): boolean {
|
||||
if (boxed.size === 0 || sites.size === 0) return false;
|
||||
const reached = new Map<string, ClassInfo | null>();
|
||||
const reachable = (className: string | undefined): ClassInfo | null => {
|
||||
if (className === undefined) return null;
|
||||
let info = reached.get(className);
|
||||
if (info === undefined) {
|
||||
const found = lowerer.classes.get(className);
|
||||
info =
|
||||
found &&
|
||||
lowerer.inHierarchy(found) &&
|
||||
[...boxed].some((name) => name === className || lowerer.isSubclassOf(name, className))
|
||||
? found
|
||||
: null;
|
||||
reached.set(className, info);
|
||||
}
|
||||
return info;
|
||||
};
|
||||
let changed = false;
|
||||
const qualifies = (expr: IrExpr): boolean =>
|
||||
expr.kind === "caughtTest"
|
||||
? expr.test === "instanceof" && reachable(expr.className) !== null
|
||||
: (expr.kind === "caughtNarrow" || expr.kind === "caughtCheck") &&
|
||||
expr.type.kind === "object" &&
|
||||
reachable(expr.type.className) !== null;
|
||||
for (const fn of sites) {
|
||||
// The conversions themselves test the native payload.
|
||||
if (fn === this.helper || this.payloadHelperFns.has(fn)) continue;
|
||||
const found = !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !qualifies(expr) });
|
||||
if (!found) continue;
|
||||
fn.body = transformStmtList(fn.body, {
|
||||
stmt: (stmt) => stmt,
|
||||
expr: (expr) => {
|
||||
if (expr.kind === "caughtTest" && expr.test === "instanceof") {
|
||||
const info = reachable(expr.className);
|
||||
if (!info) return expr;
|
||||
changed = true;
|
||||
const call: IrExpr = {
|
||||
kind: "call",
|
||||
callee: this.payloadHelper(lowerer, "instanceof", info, expr.loc).name,
|
||||
args: [expr.value],
|
||||
type: BOOL,
|
||||
loc: expr.loc,
|
||||
};
|
||||
return expr.negated
|
||||
? { kind: "unary", op: "!", operand: call, type: BOOL, loc: expr.loc }
|
||||
: call;
|
||||
}
|
||||
if (
|
||||
(expr.kind === "caughtNarrow" || expr.kind === "caughtCheck") &&
|
||||
expr.type.kind === "object"
|
||||
) {
|
||||
const info = reachable(expr.type.className);
|
||||
if (!info) return expr;
|
||||
changed = true;
|
||||
const kind = expr.kind === "caughtNarrow" ? "narrow" : "check";
|
||||
return {
|
||||
kind: "call",
|
||||
callee: this.payloadHelper(lowerer, kind, info, expr.loc).name,
|
||||
args: [expr.value],
|
||||
type: expr.type,
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
return expr;
|
||||
},
|
||||
});
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/** `%caught.instanceof:C`, `%caught.narrow:C` and `%caught.check:C`: the
|
||||
* native payload test or extraction, and otherwise a value thrown as
|
||||
* unknown tested against C and checked into C. A check of a payload that
|
||||
* is not a C keeps caughtCheck's TypeError. */
|
||||
private payloadHelper(
|
||||
lowerer: Lowerer,
|
||||
kind: "instanceof" | "narrow" | "check",
|
||||
info: ClassInfo,
|
||||
loc: SrcLoc,
|
||||
): IrFunction {
|
||||
const className = info.def.name;
|
||||
const name = `%caught.${kind}:${className}`;
|
||||
const existing = this.payloadHelpers.get(name);
|
||||
if (existing) return existing;
|
||||
const caught = (): IrExpr => varRef("caught", CAUGHT, loc);
|
||||
const dynamic = (): IrExpr => ({ kind: "caughtToDyn", value: caught(), type: DYN, loc });
|
||||
const native: IrExpr = {
|
||||
kind: "caughtTest",
|
||||
value: caught(),
|
||||
test: "instanceof",
|
||||
className,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
// Only a value thrown as unknown can hold an instance besides a native
|
||||
// payload, which the native test answers exactly.
|
||||
const dynamicInstance = (): IrExpr => ({
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: { kind: "caughtTest", value: caught(), test: "dynamic", type: BOOL, loc },
|
||||
right: classInstanceOf(lowerer, dynamic(), info, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const type: IrType = { kind: "object", className };
|
||||
const returnType = kind === "instanceof" ? BOOL : type;
|
||||
let value: IrExpr;
|
||||
if (kind === "instanceof") {
|
||||
value = {
|
||||
kind: "ternary",
|
||||
cond: native,
|
||||
then: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
else_: dynamicInstance(),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
} else if (kind === "narrow") {
|
||||
value = {
|
||||
kind: "ternary",
|
||||
cond: native,
|
||||
then: { kind: "caughtNarrow", value: caught(), type, loc },
|
||||
else_: lowerer.coerceToExpected(dynamic(), type),
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
} else {
|
||||
value = {
|
||||
kind: "ternary",
|
||||
cond: dynamicInstance(),
|
||||
then: lowerer.coerceToExpected(dynamic(), type),
|
||||
else_: { kind: "caughtCheck", value: caught(), className, type, loc },
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const helper: IrFunction = {
|
||||
name,
|
||||
params: [{ localId: "caught", name: "caught", type: CAUGHT }],
|
||||
locals: [{ id: "caught", name: "caught", type: CAUGHT, mutable: false }],
|
||||
returnType,
|
||||
body: [{ kind: "return", value, loc }],
|
||||
loc,
|
||||
};
|
||||
this.payloadHelpers.set(name, helper);
|
||||
this.payloadHelperFns.add(helper);
|
||||
lowerer.liftedFns.push(helper);
|
||||
return helper;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ export function lowerClassDataDescriptor(
|
||||
|
||||
function descriptorGuard(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): string {
|
||||
const name = `%descriptorGuard:${info.def.name}`;
|
||||
const existing = lowerer.liftedFns.find((fn) => fn.name === name);
|
||||
const existing = lowerer.liftedFnNamed(name);
|
||||
const key = varRef("key", DYN, loc);
|
||||
const fields = new Set<string>();
|
||||
const collect = (owner: ClassInfo): void => {
|
||||
|
||||
@@ -32,6 +32,9 @@ function context() {
|
||||
unions: new Map(),
|
||||
classes: new Map(),
|
||||
liftedFns,
|
||||
liftedFnNamed: (name: string | undefined) => liftedFns.find((fn) => fn.name === name),
|
||||
subclassLinks: 0,
|
||||
constructedClassCount: () => -1,
|
||||
diags: [],
|
||||
prototypeMethodAccesses: new Map(),
|
||||
protocolDispatches: new Map(),
|
||||
|
||||
@@ -68,7 +68,11 @@ import { isClassCallback } from "./class-callbacks.js";
|
||||
import { CaughtValueDispatch } from "./caught-value-dispatch.js";
|
||||
import { refreshDescriptorGuards } from "./class-descriptors.js";
|
||||
import { ClassConstructionDispatch } from "./class-construction.js";
|
||||
import { emitterRooted } from "./lower-event-emitter.js";
|
||||
import {
|
||||
emitterRooted,
|
||||
lowerDynamicEmitterEmit,
|
||||
lowerDynamicEmitterOn,
|
||||
} from "./lower-event-emitter.js";
|
||||
import { protocolArm, protocolFieldArm } from "./class-protocol-dispatch.js";
|
||||
import { STREAM_OPERATORS } from "./lower-stream.js";
|
||||
import { deletedPropertyNames, fixedFieldFamily } from "./js-class-fields.js";
|
||||
@@ -97,6 +101,24 @@ const EMITTER_METHODS = new Set([
|
||||
"off",
|
||||
"removeListener",
|
||||
]);
|
||||
/** The rest of EventEmitter.prototype, answered through an untyped value
|
||||
* on emitters that are not streams (streams keep their own surface). */
|
||||
const EMITTER_INTROSPECTION_METHODS = new Set([
|
||||
"listenerCount",
|
||||
"eventNames",
|
||||
"removeAllListeners",
|
||||
"listeners",
|
||||
"rawListeners",
|
||||
"setMaxListeners",
|
||||
"getMaxListeners",
|
||||
]);
|
||||
/** True when the class is or extends one of the runtime stream classes. */
|
||||
function streamRooted(info: ClassInfo): boolean {
|
||||
for (let c: ClassInfo | null = info; c; c = c.base) {
|
||||
if (c.builtinStream !== undefined) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const STREAM_BOOL_PROPERTIES = new Set([
|
||||
"readable",
|
||||
"readableEnded",
|
||||
@@ -366,7 +388,7 @@ function readOnlyBagBody(body: IrStmt[], orEmpty = false): IrStmt[] {
|
||||
|
||||
export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc, readOnly = false): IrFunction {
|
||||
const name = readOnly ? READ_PROPERTIES : "%dyn.class.properties";
|
||||
const existing = lowerer.liftedFns.find((fn) => fn.name === name);
|
||||
const existing = lowerer.liftedFnNamed(name);
|
||||
if (existing) return existing;
|
||||
const helper: IrFunction = {
|
||||
name,
|
||||
@@ -762,6 +784,7 @@ export class ClassDynamicDispatch {
|
||||
const rewrite = new Set<IrFunction>();
|
||||
const toPrimitiveSites = new Set<IrFunction>();
|
||||
const hasKeySites = new Set<IrFunction>();
|
||||
const caughtSites = new Set<IrFunction>();
|
||||
const discover = (type: IrType): void => {
|
||||
const key = typeKey(type);
|
||||
if (seenTypes.has(key)) return;
|
||||
@@ -827,6 +850,13 @@ export class ClassDynamicDispatch {
|
||||
case "dynHasKey":
|
||||
hasKeySites.add(fn);
|
||||
break;
|
||||
case "caughtTest":
|
||||
if (expr.test === "instanceof") caughtSites.add(fn);
|
||||
break;
|
||||
case "caughtNarrow":
|
||||
case "caughtCheck":
|
||||
if (expr.type.kind === "object") caughtSites.add(fn);
|
||||
break;
|
||||
case "dynKeyGet":
|
||||
case "dynInvoke":
|
||||
rewrite.add(fn);
|
||||
@@ -865,6 +895,7 @@ export class ClassDynamicDispatch {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
changed = this.caughtDispatch.dynamicPayloads(lowerer, caughtSites, this.boxed) || changed;
|
||||
if (this.boxed.size === 0 && rewrite.size === 0) return changed;
|
||||
const converting = this.convertingHints(lowerer);
|
||||
const calling = this.calledConversions(lowerer);
|
||||
@@ -1003,7 +1034,7 @@ export class ClassDynamicDispatch {
|
||||
const loc = this.propertyBag.loc;
|
||||
const method = classMethodValue(lowerer, info.decl, info, "toJSON", loc);
|
||||
const prototype = method ? classPrototypeData(lowerer, info, loc) : null;
|
||||
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper);
|
||||
const helper = lowerer.liftedFnNamed(info.def.prototypeDataHelper);
|
||||
const init = helper?.body[0];
|
||||
if (
|
||||
!info.prototypeReflectionReady &&
|
||||
@@ -1223,6 +1254,13 @@ export class ClassDynamicDispatch {
|
||||
const matching = this.propertyReceivers(lowerer, true).flatMap(({ info }) => {
|
||||
if (!info || info.fields.has(method) || info.builtinError) return [];
|
||||
if (emitterRooted(lowerer, info) && EMITTER_METHODS.has(method)) return [info];
|
||||
if (
|
||||
emitterRooted(lowerer, info) &&
|
||||
!streamRooted(info) &&
|
||||
EMITTER_INTROSPECTION_METHODS.has(method) &&
|
||||
!findMethodOn(lowerer, info, method)
|
||||
)
|
||||
return [info];
|
||||
if (info.builtinStream) {
|
||||
return STREAM_METHODS.has(method) ||
|
||||
(STREAM_OPERATORS.has(method) && info.builtinStream !== "w")
|
||||
@@ -1236,6 +1274,15 @@ export class ClassDynamicDispatch {
|
||||
byMethod.set(method, matching);
|
||||
return matching;
|
||||
};
|
||||
const candidateSets = new Map<string, ReadonlySet<ClassInfo>>();
|
||||
const candidateSet = (method: string): ReadonlySet<ClassInfo> => {
|
||||
let set = candidateSets.get(method);
|
||||
if (!set) {
|
||||
set = new Set(candidates(method));
|
||||
candidateSets.set(method, set);
|
||||
}
|
||||
return set;
|
||||
};
|
||||
for (const fn of functions) {
|
||||
// Discovery already visits every expression. Preserve bodies without
|
||||
// dispatch sites instead of rebuilding their entire typed IR tree on
|
||||
@@ -2220,16 +2267,16 @@ export class ClassDynamicDispatch {
|
||||
: expr.kind === "dynInvoke"
|
||||
? expr
|
||||
: null;
|
||||
const staticCandidates =
|
||||
invoke &&
|
||||
// Static methods matter only for methods no instance answers.
|
||||
const staticCandidates = (method: string): boolean =>
|
||||
[...this.boxedConstructors].some((name) => {
|
||||
const info = lowerer.classes.get(name);
|
||||
return info && findStaticOn(lowerer, info, invoke.method)?.method;
|
||||
return info && findStaticOn(lowerer, info, method)?.method;
|
||||
});
|
||||
if (
|
||||
!invoke ||
|
||||
(candidates(invoke.method).length === 0 &&
|
||||
!staticCandidates &&
|
||||
!staticCandidates(invoke.method) &&
|
||||
!(invoke.method === "toString" && lowerer.classes.has("%Error")))
|
||||
)
|
||||
return expr;
|
||||
@@ -2501,10 +2548,13 @@ export class ClassDynamicDispatch {
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
for (const plan of this.propertyReceivers(lowerer, true)) {
|
||||
// Compute the candidates where the first plan would have, so the
|
||||
// memoized set is the same whichever plans are already answered.
|
||||
const plans = this.propertyReceivers(lowerer, true);
|
||||
const matching = plans.length ? candidateSet(dispatch.source.method) : null;
|
||||
for (const plan of plans) {
|
||||
const { info } = plan;
|
||||
if (!candidates(dispatch.source.method).includes(info) || dispatch.classes.has(plan.key))
|
||||
continue;
|
||||
if (dispatch.classes.has(plan.key) || !matching!.has(info)) continue;
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.source.loc;
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
@@ -2648,7 +2698,7 @@ export class ClassDynamicDispatch {
|
||||
if (
|
||||
plan.info !== plan.capsule ||
|
||||
dispatch.classes.has(plan.key) ||
|
||||
(!dispatch.field && !candidates(dispatch.method).includes(plan.info))
|
||||
(!dispatch.field && !candidateSet(dispatch.method).has(plan.info))
|
||||
)
|
||||
continue;
|
||||
dispatch.classes.add(plan.key);
|
||||
@@ -2738,6 +2788,8 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
for (const plan of this.propertyReceivers(lowerer, true)) {
|
||||
const { info } = plan;
|
||||
// Classes answered in an earlier pass skip the member lookups.
|
||||
if (dispatch.classes.has(plan.key)) continue;
|
||||
if (
|
||||
!isClassOwnEnumerableFieldName(dispatch.name) ||
|
||||
(!(info.builtinStream !== undefined && streamProperty(dispatch.name)) &&
|
||||
@@ -2751,7 +2803,6 @@ export class ClassDynamicDispatch {
|
||||
!findMethodOn(lowerer, info, `set:${dispatch.name}`))
|
||||
)
|
||||
continue;
|
||||
if (dispatch.classes.has(plan.key)) continue;
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.fn.loc;
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
@@ -3310,7 +3361,7 @@ export class ClassDynamicDispatch {
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const name = fn === "dyn.hasKey" ? "%dyn.class.bagHas" : "%dyn.class.bagHasComputed";
|
||||
let helper = lowerer.liftedFns.find((item) => item.name === name);
|
||||
let helper = lowerer.liftedFnNamed(name);
|
||||
if (!helper) {
|
||||
const keyType = fn === "dyn.hasKey" ? STRING : DYN;
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
@@ -3907,7 +3958,7 @@ export class ClassDynamicDispatch {
|
||||
private readPropertyBagOrEmpty(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr {
|
||||
// One call per read site: large JS programs have a branch per class and
|
||||
// property, so the absent-bag handling lives in the shared helper.
|
||||
let helper = lowerer.liftedFns.find((fn) => fn.name === READ_PROPERTIES_OR_EMPTY);
|
||||
let helper = lowerer.liftedFnNamed(READ_PROPERTIES_OR_EMPTY);
|
||||
if (!helper) {
|
||||
// Settled by finalize; until then, the read-only bag's own dispatch.
|
||||
helper = {
|
||||
@@ -4256,17 +4307,38 @@ export class ClassDynamicDispatch {
|
||||
add(root);
|
||||
}
|
||||
|
||||
/** propertyReceivers results by mode, with the class graph they were
|
||||
* computed from: the boxed classes, the registered classes, the subclass
|
||||
* links and the constructed classes only grow, so equal counts and the
|
||||
* same capsules give the same plans. */
|
||||
private readonly receiverPlans = new Map<
|
||||
string,
|
||||
{ revision: string; capsules: (ClassInfo | undefined)[]; plans: readonly PropertyReceiver[] }
|
||||
>();
|
||||
|
||||
/** A capsule retains its static type, while its object may be a subclass.
|
||||
* Preorder insertion lets later branches test the most derived layout first. */
|
||||
private propertyReceivers(
|
||||
lowerer: Lowerer,
|
||||
streams = false,
|
||||
normalize = true,
|
||||
): PropertyReceiver[] {
|
||||
): readonly PropertyReceiver[] {
|
||||
const mode = `${streams}:${normalize}`;
|
||||
const revision = `${this.boxed.size}:${lowerer.classes.size}:${lowerer.subclassLinks}:${lowerer.constructedClassCount()}`;
|
||||
const cached = this.receiverPlans.get(mode);
|
||||
if (cached?.revision === revision) {
|
||||
let i = 0;
|
||||
let same = true;
|
||||
for (const name of this.boxed)
|
||||
if (lowerer.classes.get(name) !== cached.capsules[i++]) same = false;
|
||||
if (same) return cached.plans;
|
||||
}
|
||||
const capsules: (ClassInfo | undefined)[] = [];
|
||||
const plans: PropertyReceiver[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const name of this.boxed) {
|
||||
const capsule = lowerer.classes.get(name);
|
||||
capsules.push(capsule);
|
||||
if (
|
||||
!capsule ||
|
||||
((capsule.builtinEmitter || capsule.builtinStream) && !streams) ||
|
||||
@@ -4275,10 +4347,10 @@ export class ClassDynamicDispatch {
|
||||
continue;
|
||||
const canonical = normalize && !capsule.builtinStream && !capsule.builtinEmitter;
|
||||
const visit = (info: ClassInfo): void => {
|
||||
if (
|
||||
(!canonical || !seen.has(info.def.name)) &&
|
||||
(info === capsule || lowerer.classCanBeConstructed(info))
|
||||
) {
|
||||
// A seen class was visited with its whole subtree, and a canonical
|
||||
// visit adds no plan for a seen class.
|
||||
if (canonical && seen.has(info.def.name)) return;
|
||||
if (info === capsule || lowerer.classCanBeConstructed(info)) {
|
||||
seen.add(info.def.name);
|
||||
plans.push({
|
||||
info,
|
||||
@@ -4290,6 +4362,7 @@ export class ClassDynamicDispatch {
|
||||
};
|
||||
visit(capsule);
|
||||
}
|
||||
this.receiverPlans.set(mode, { revision, capsules, plans });
|
||||
return plans;
|
||||
}
|
||||
|
||||
@@ -5055,6 +5128,66 @@ export class ClassDynamicDispatch {
|
||||
return [store, { kind: "return", value: null, loc }];
|
||||
}
|
||||
|
||||
/** EventEmitter.prototype's introspection and bookkeeping members on an
|
||||
* emitter seen through an untyped value: Node's results over the native
|
||||
* listener registry. */
|
||||
private emitterIntrospection(lowerer: Lowerer, dispatch: Dispatch, receiver: IrExpr): IrStmt[] {
|
||||
const { method, loc } = dispatch.source;
|
||||
const incoming = dispatch.source.args.map((_, i) => varRef(`p.${i + 1}`, DYN, loc));
|
||||
const ret = (value: IrExpr): IrStmt[] => [
|
||||
{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc },
|
||||
];
|
||||
const call = (fn: IrLibFn, args: IrExpr[], type: IrType): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn,
|
||||
args: [receiver, ...args],
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
// Node indexes _events with the argument itself: an omitted name is
|
||||
// the key "undefined".
|
||||
const eventName = (): IrExpr =>
|
||||
incoming.length
|
||||
? lowerer.coerceToExpected(incoming[0]!, STRING)
|
||||
: { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
switch (method) {
|
||||
case "listenerCount":
|
||||
return ret(
|
||||
incoming.length >= 2
|
||||
? call("emitter.countDyn", [eventName(), incoming[1]!], F64)
|
||||
: call("emitter.count", [eventName()], F64),
|
||||
);
|
||||
case "eventNames":
|
||||
return ret(call("emitter.names", [], { kind: "array", elem: STRING }));
|
||||
case "removeAllListeners":
|
||||
return ret(
|
||||
incoming.length
|
||||
? call(
|
||||
"emitter.removeAll",
|
||||
[eventName(), { kind: "boolLit", value: false, type: BOOL, loc }],
|
||||
receiver.type,
|
||||
)
|
||||
: call(
|
||||
"emitter.removeAll",
|
||||
[
|
||||
{ kind: "strLit", value: "", type: STRING, loc },
|
||||
{ kind: "boolLit", value: true, type: BOOL, loc },
|
||||
],
|
||||
receiver.type,
|
||||
),
|
||||
);
|
||||
case "listeners":
|
||||
case "rawListeners":
|
||||
return ret(call("emitter.listenersDyn", [eventName()], DYN));
|
||||
case "setMaxListeners":
|
||||
return ret(
|
||||
call("emitter.setMaxChk", [incoming[0] ?? dynUndefinedExpr(loc)], receiver.type),
|
||||
);
|
||||
default:
|
||||
return ret(call("emitter.getMax", [], F64));
|
||||
}
|
||||
}
|
||||
|
||||
private methodBody(
|
||||
lowerer: Lowerer,
|
||||
dispatch: Dispatch,
|
||||
@@ -5070,6 +5203,14 @@ export class ClassDynamicDispatch {
|
||||
loc,
|
||||
},
|
||||
];
|
||||
if (
|
||||
emitterRooted(lowerer, info) &&
|
||||
!streamRooted(info) &&
|
||||
EMITTER_INTROSPECTION_METHODS.has(method) &&
|
||||
!findMethodOn(lowerer, info, method)
|
||||
) {
|
||||
return this.emitterIntrospection(lowerer, dispatch, receiver);
|
||||
}
|
||||
if (
|
||||
info.builtinStream ||
|
||||
(emitterRooted(lowerer, info) &&
|
||||
@@ -5090,18 +5231,37 @@ export class ClassDynamicDispatch {
|
||||
];
|
||||
if (method === "emit") {
|
||||
if (!incoming.length) return fence();
|
||||
return ret({
|
||||
kind: "libCall",
|
||||
fn: "emitter.emitFlex",
|
||||
args: [receiver, lowerer.coerceToExpected(incoming[0]!, STRING), ...incoming.slice(1)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const event = lowerer.coerceToExpected(incoming[0]!, STRING);
|
||||
const bridged = streamRooted(info)
|
||||
? null
|
||||
: lowerDynamicEmitterEmit(lowerer, receiver, event, incoming.slice(1), loc);
|
||||
return ret(
|
||||
bridged ?? {
|
||||
kind: "libCall",
|
||||
fn: "emitter.emitAny",
|
||||
args: [receiver, event, ...incoming.slice(1)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (registering || removing) {
|
||||
if (incoming.length < 2) return fence();
|
||||
const event = lowerer.coerceToExpected(incoming[0]!, STRING);
|
||||
const cb = incoming[1]!;
|
||||
const bridged =
|
||||
registering && !streamRooted(info)
|
||||
? lowerDynamicEmitterOn(
|
||||
lowerer,
|
||||
receiver,
|
||||
event,
|
||||
cb,
|
||||
method === "once" || method === "prependOnceListener",
|
||||
method.startsWith("prepend"),
|
||||
loc,
|
||||
)
|
||||
: null;
|
||||
if (bridged) return ret(bridged);
|
||||
return [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
@@ -5113,7 +5273,7 @@ export class ClassDynamicDispatch {
|
||||
fn: registering
|
||||
? info.builtinStream
|
||||
? "stream.onDyn"
|
||||
: "emitter.onFlex"
|
||||
: "emitter.onAny"
|
||||
: "emitter.offDyn",
|
||||
args: [
|
||||
receiver,
|
||||
|
||||
@@ -426,7 +426,7 @@ function reflectedMethodValue(
|
||||
);
|
||||
}
|
||||
if (value.kind === "closure" && value.captures.length === 0) {
|
||||
const body = lowerer.liftedFns.find((fn) => fn.name === value.fnName);
|
||||
const body = lowerer.liftedFnNamed(value.fnName);
|
||||
if (body?.captures?.length === 0) delete body.captures;
|
||||
}
|
||||
const cache = staticMethod
|
||||
|
||||
@@ -172,7 +172,7 @@ export function classPrototypeData(
|
||||
}
|
||||
if (!metadataOnly && !info.prototypeObservedMethodsReady) {
|
||||
info.prototypeObservedMethodsReady = true;
|
||||
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper)!;
|
||||
const helper = lowerer.liftedFnNamed(info.def.prototypeDataHelper)!;
|
||||
const value = varRef(`%g.${info.def.prototypeDataHelper}`, DYN, loc);
|
||||
// Materialize only observed method slots. Own declarations stop lookup
|
||||
// at the correct prototype even if an ancestor is replaced later. An
|
||||
@@ -307,7 +307,7 @@ export function reflectClassPrototype(
|
||||
if (info.base.prototypeReflectionRefusal)
|
||||
return refuse("prototype reflection with this base class is not supported yet");
|
||||
}
|
||||
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper);
|
||||
const helper = lowerer.liftedFnNamed(info.def.prototypeDataHelper);
|
||||
const initialization = helper?.body[0];
|
||||
if (!helper || initialization?.kind !== "if" || info.prototypeReflectionReady) return prototype;
|
||||
info.prototypeReflectionReady = true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { generatorDrain } from "../iterator-adapters.js";
|
||||
import { identityPreservingWidening } from "../coercions/identity.js";
|
||||
import { liftedFreshSpread } from "../coercions/fresh-spread.js";
|
||||
import { checkedIterableSpread } from "../checked-iterable-spread.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import {
|
||||
dynUndefinedExpr,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
arrayOf,
|
||||
canConvertToDyn,
|
||||
funcOf,
|
||||
isRefCounted,
|
||||
isSupportedArrayElem,
|
||||
@@ -153,6 +155,32 @@ export function lowerArraySpreadItems(
|
||||
// Fresh elements (an array literal) convert into the element layout.
|
||||
source = liftedFreshSpread(lowerer, node.expression, source, arrType, loc) ?? source;
|
||||
}
|
||||
// A checked-dynamic source, or any other iterable spread into
|
||||
// checked-dynamic items, drains once through the iterator protocol
|
||||
// (with the spread call's TypeErrors), and its elements convert to
|
||||
// the element type.
|
||||
const drains =
|
||||
source.type.kind === "dyn"
|
||||
? elem.kind === "dyn" ||
|
||||
elem.kind === "f64" ||
|
||||
elem.kind === "string" ||
|
||||
elem.kind === "bool"
|
||||
: elem.kind === "dyn" &&
|
||||
!typeEquals(source.type, arrType) &&
|
||||
!(
|
||||
source.type.kind === "array" &&
|
||||
identityPreservingWidening(lowerer, source.type.elem, elem)
|
||||
) &&
|
||||
canConvertToDyn(
|
||||
source.type,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
);
|
||||
if (drains) {
|
||||
const spelling = ts.getTextOfNode(node.expression);
|
||||
const items = checkedIterableSpread(lowerer, source, spelling, loc);
|
||||
source = lowerer.coerceInto(node.expression, items, arrType);
|
||||
}
|
||||
if (!typeEquals(source.type, arrType)) {
|
||||
if (
|
||||
source.type.kind !== "array" ||
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
lowerOptionalArgument,
|
||||
lowerStaticallyUndefinedArgument,
|
||||
} from "../optional-arguments.js";
|
||||
import { lowerCheckedArrayFrom } from "./array-construction.js";
|
||||
import { lowerArraySpreadItems, lowerCheckedArrayFrom } from "./array-construction.js";
|
||||
import { lowerToNumberArgument } from "../lower-exprs.js";
|
||||
import { lowerBytesIteratorValue } from "../iterator-adapters.js";
|
||||
import { lowerArrayCallback } from "./callback-arguments.js";
|
||||
@@ -642,7 +642,7 @@ export function lowerBytesNew(
|
||||
);
|
||||
}
|
||||
|
||||
/** TypedArray.from without a mapping callback. Preserve its iterable /
|
||||
/** TypedArray.from and TypedArray.of. Preserve from's iterable /
|
||||
* array-like semantics separately from constructor length coercion. */
|
||||
export function lowerBytesStaticCall(
|
||||
lowerer: Lowerer,
|
||||
@@ -662,10 +662,15 @@ export function lowerBytesStaticCall(
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
if (access.name.text !== "from" || !ts.isIdentifier(access.expression)) return null;
|
||||
if (
|
||||
(access.name.text !== "from" && access.name.text !== "of") ||
|
||||
!ts.isIdentifier(access.expression)
|
||||
)
|
||||
return null;
|
||||
const symbol = lowerer.resolveValueSymbol(access.expression);
|
||||
const elem = symbol ? own(BYTES_CTORS, ts.symbolName(symbol)) : undefined;
|
||||
if (!elem || !lowerer.isStdlibSymbol(symbol ?? undefined)) return null;
|
||||
if (access.name.text === "of") return lowerBytesOf(lowerer, call, elem);
|
||||
if (
|
||||
call.arguments.nodes.length < 1 ||
|
||||
call.arguments.nodes.length > 3 ||
|
||||
@@ -754,6 +759,59 @@ export function lowerBytesStaticCall(
|
||||
};
|
||||
}
|
||||
|
||||
/** TypedArray.of: a new array of the arguments, each converted with ToNumber
|
||||
* in argument order after all of them are evaluated. Numbers copy directly;
|
||||
* other values (and spread items) collect into an array that goes through
|
||||
* TypedArray.from's conversion, which calls valueOf/toString the same way. */
|
||||
function lowerBytesOf(lowerer: Lowerer, call: ts.CallExpression, elem: IrBytesElem): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const nodes = call.arguments.nodes;
|
||||
const type = bytesOf(elem);
|
||||
if (nodes.some(ts.isSpreadElement)) {
|
||||
const numeric = nodes.every((node) => {
|
||||
const mapped = lowerer.mapTypeOf(
|
||||
lowerer.typeOf(ts.isSpreadElement(node) ? node.expression : node),
|
||||
);
|
||||
return ts.isSpreadElement(node)
|
||||
? mapped?.kind === "array" && mapped.elem.kind === "f64"
|
||||
: mapped?.kind === "f64";
|
||||
});
|
||||
const elemType = numeric ? F64 : DYN;
|
||||
const items = lowerArraySpreadItems(
|
||||
lowerer,
|
||||
nodes,
|
||||
elemType,
|
||||
{ kind: "array", elem: elemType },
|
||||
loc,
|
||||
);
|
||||
if (numeric) return { kind: "bytesNew", source: items, type, loc };
|
||||
return {
|
||||
kind: "bytesNew",
|
||||
source: lowerer.coerceToExpected(items, DYN),
|
||||
from: true,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const values = nodes.map((node) => lowerer.lowerExpr(node));
|
||||
if (values.every((value) => value.type.kind === "f64")) {
|
||||
return {
|
||||
kind: "bytesNew",
|
||||
source: { kind: "arrayLit", elems: values, type: arrayOf(F64), loc },
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const elems = values.map((value, i) => lowerer.coerceInto(nodes[i]!, value, DYN));
|
||||
return {
|
||||
kind: "bytesNew",
|
||||
source: lowerer.coerceToExpected({ kind: "arrayLit", elems, type: arrayOf(DYN), loc }, DYN),
|
||||
from: true,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Construct a view over the complete branded backing allocation. */
|
||||
function lowerArrayBufferView(
|
||||
lowerer: Lowerer,
|
||||
@@ -1479,6 +1537,8 @@ function lowerBufferInstanceMethod(
|
||||
if (encName === "utf16le") align = 2;
|
||||
} else if (vT?.kind === "bytes" && vT.elem === "u8") {
|
||||
needle = lowerer.lowerExpr(args[0]!);
|
||||
// A Buffer needle under utf16le also searches whole 2-byte units.
|
||||
if (encName === "utf16le") align = 2;
|
||||
} else {
|
||||
lowerer.noLowering(
|
||||
`.${name} of '${lowerer.checker.typeToString(lowerer.typeOf(args[0]!))}' values`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
|
||||
import { observeRecordLiteralRebuilds, transformExpr } from "../../ir/traverse.js";
|
||||
import { ShapeRegistry } from "../type-mapper.js";
|
||||
import {
|
||||
applyKeyOrderWitnesses,
|
||||
@@ -156,7 +157,24 @@ test("a synthesized construction pins today's table", () => {
|
||||
expect(f.order(ab)).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("a checked conversion pins today's table", () => {
|
||||
test("a checked conversion alone pins today's table", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", OPT],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc };
|
||||
// A late write to `a` would move it after `b`; values only a checked
|
||||
// conversion builds keep today's table, so parsed values in that order
|
||||
// need no source view.
|
||||
f.run([decl("p", parsed), set(ref("p", ab), "a"), observe(ref("p", ab))]);
|
||||
expect(f.order(ab)).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("a shape literals and a checked conversion both build takes the literals' order", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
@@ -166,8 +184,33 @@ test("a checked conversion pins today's table", () => {
|
||||
["a", "b"],
|
||||
);
|
||||
const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc };
|
||||
// The literal's values can enumerate in no other order; parsed values in
|
||||
// another order keep a record-source view.
|
||||
f.run([observe(f.literal(ab, ["b", "a"])), observe(parsed)]);
|
||||
expect(f.order(ab)).toEqual(["a", "b"]);
|
||||
expect(f.order(ab)).toEqual(["b", "a"]);
|
||||
});
|
||||
|
||||
test("a literal an IR rewrite rebuilds keeps its witness", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const identity = { expr: (e: IrExpr): IrExpr => e, stmt: (st: IrStmt): IrStmt => st };
|
||||
const original = f.literal(ab, ["b", "a"]);
|
||||
observeRecordLiteralRebuilds((from, to) => f.notes.follow(from, to));
|
||||
let rebuilt: IrExpr;
|
||||
try {
|
||||
rebuilt = transformExpr(original, identity);
|
||||
} finally {
|
||||
observeRecordLiteralRebuilds(null);
|
||||
}
|
||||
expect(rebuilt).not.toBe(original);
|
||||
f.run([observe(rebuilt)]);
|
||||
expect(f.order(ab)).toEqual(["b", "a"]);
|
||||
});
|
||||
|
||||
test("a spread contributes its source's order, and an override keeps its position", () => {
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
* - any other construction (synthesized builtin results, opaque runtime
|
||||
* producers) pins today's table;
|
||||
* - checked conversions from dynamic values (typed JSON.parse, values
|
||||
* from JavaScript) pin today's table: the record-source table keeps
|
||||
* their own key order relative to it, and another table would make
|
||||
* values parsed in today's order keep a source view. Their order is
|
||||
* unknown, so a copy out of such a shape cannot be ordered statically;
|
||||
* from JavaScript) contribute nothing: the record-source table keeps
|
||||
* their own key order relative to the chosen table. A shape only they
|
||||
* build keeps today's table, since another table would make values
|
||||
* parsed in today's order keep a source view; a shape object literals
|
||||
* also build takes the literals' order. Their order is unknown, so a
|
||||
* copy out of such a shape cannot be ordered statically;
|
||||
* - a write to an optional key that may be absent appends it in Node. A
|
||||
* key no construction creates ("late-only") goes after every key a
|
||||
* construction can create; any other moving write goes after every key.
|
||||
@@ -56,8 +58,10 @@ export type KeyOrderSegment =
|
||||
| { kind: "opaque" };
|
||||
|
||||
/** Witness notes the lowering attaches to the IR nodes it builds. Keyed by
|
||||
* node identity, so nodes a speculative lowering discards are never walked,
|
||||
* and a node some later pass rebuilds falls back to pinning. */
|
||||
* node identity, so nodes a speculative lowering discards are never walked.
|
||||
* Literals the IR rewriters rebuild (traverse.ts mapExprChildren) carry
|
||||
* their notes over (follow); a node rebuilt any other way falls back to
|
||||
* pinning. */
|
||||
export class KeyOrderNotes {
|
||||
private readonly literals = new Map<IrExpr, KeyOrderSegment[]>();
|
||||
private readonly copies = new Map<IrExpr, string>();
|
||||
@@ -77,6 +81,15 @@ export class KeyOrderNotes {
|
||||
return node;
|
||||
}
|
||||
|
||||
/** A pass rebuilt `from` as `to` (traverse.ts mapExprChildren): the
|
||||
* rebuilt literal is the same construction, so it keeps the witness. */
|
||||
follow(from: IrExpr, to: IrExpr): void {
|
||||
const segments = this.literals.get(from);
|
||||
if (segments !== undefined) this.literals.set(to, segments);
|
||||
const source = this.copies.get(from);
|
||||
if (source !== undefined) this.copies.set(to, source);
|
||||
}
|
||||
|
||||
observe(type: IrType): void {
|
||||
if (type.kind === "record") this.observed.add(type.shapeId);
|
||||
}
|
||||
@@ -388,8 +401,13 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
// A checked conversion builds values in their source's own order, which
|
||||
// the record-source table reproduces relative to the shape's table. A new
|
||||
// table would make every value parsed in today's order keep a source
|
||||
// view, so such shapes keep today's table.
|
||||
for (const f of facts.values()) if (f.runtime) f.pinned = true;
|
||||
// view, so a shape only checked conversions build keeps today's table.
|
||||
// A shape the program also builds with object literals takes their order:
|
||||
// the literal's values have no other way to enumerate in it, and values
|
||||
// parsed from JSON those literals wrote (a message sent to a child or
|
||||
// another service of the program) arrive in it too. Parsed values in any
|
||||
// other order keep a source view, exactly as before.
|
||||
for (const f of facts.values()) if (f.runtime && f.literals.length === 0) f.pinned = true;
|
||||
|
||||
// Nothing to choose: every witness agrees with today's table and no
|
||||
// optional key is written late.
|
||||
|
||||
@@ -4098,6 +4098,12 @@ const DYN_DISPATCH_METHODS = new Set([
|
||||
"prependOnceListener",
|
||||
"listeners",
|
||||
"listenerCount",
|
||||
// The rest of EventEmitter.prototype, which compiled emitters answer
|
||||
// through the class dispatch (class-dynamic-dispatch.ts).
|
||||
"rawListeners",
|
||||
"eventNames",
|
||||
"setMaxListeners",
|
||||
"getMaxListeners",
|
||||
"write",
|
||||
"end",
|
||||
"destroy",
|
||||
@@ -8079,6 +8085,14 @@ function lowerObjectStaticCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const fnIntegrity = functionIntegrity(
|
||||
lowerer,
|
||||
member,
|
||||
value,
|
||||
call.arguments.nodes[0]!,
|
||||
locOf(call),
|
||||
);
|
||||
if (fnIntegrity) return fnIntegrity;
|
||||
if (member === "preventExtensions" || member === "seal") {
|
||||
const integrity = lowerClassIntegrity(lowerer, call, member, value);
|
||||
if (integrity) return integrity;
|
||||
@@ -8162,6 +8176,8 @@ function lowerObjectStaticCall(
|
||||
)
|
||||
)
|
||||
return value;
|
||||
const fnIntegrity = functionIntegrity(lowerer, "freeze", value, argNode, locOf(call));
|
||||
if (fnIntegrity) return fnIntegrity;
|
||||
const integrity = lowerClassIntegrity(lowerer, call, "freeze", value);
|
||||
if (integrity) return integrity;
|
||||
lowerer.noLowering(
|
||||
@@ -10647,3 +10663,55 @@ function lowerObjectAssignOntoError(lowerer: Lowerer, call: ts.CallExpression):
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Object.freeze(fn)` and the other integrity operations on a function
|
||||
* value: the function's checked-dynamic face holds its own properties (the
|
||||
* ones a program assigns, `name`, `length`, `prototype`) and their
|
||||
* integrity state, which the runtime keeps on its property table. The
|
||||
* mutators answer the function itself; the queries answer from the table.
|
||||
* Null for other values. */
|
||||
function functionIntegrity(
|
||||
lowerer: Lowerer,
|
||||
member: string,
|
||||
value: IrExpr,
|
||||
node: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "func" || !lowerer.dynConvertible(value.type)) return null;
|
||||
const operations = {
|
||||
freeze: "dyn.freeze",
|
||||
seal: "dyn.seal",
|
||||
preventExtensions: "dyn.preventExtensions",
|
||||
isSealed: "dyn.isSealed",
|
||||
isExtensible: "dyn.isExtensible",
|
||||
} as const;
|
||||
if (!(member in operations)) return null;
|
||||
const fn = operations[member as keyof typeof operations];
|
||||
// The first operation on a function creates its property table, whose
|
||||
// `name` comes from the box: give the box the function's JS name, as a
|
||||
// closure flowing into any checked-dynamic slot gets.
|
||||
const name = jsFuncNameOf(node, lowerer);
|
||||
const boxed = (e: IrExpr): IrExpr => {
|
||||
const box = lowerer.coerceToExpected(e, DYN);
|
||||
return box.kind === "dynFrom" && name !== null ? { ...box, fnName: name } : box;
|
||||
};
|
||||
if (fn === "dyn.isSealed" || fn === "dyn.isExtensible") {
|
||||
return { kind: "libCall", fn, args: [boxed(value)], type: BOOL, loc };
|
||||
}
|
||||
const held = lowerer.declareHiddenLocal("%integrityFn", value.type);
|
||||
const ref = (): IrExpr => ({ kind: "varRef", localId: held.id, type: value.type, loc });
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: held.id, init: value, loc },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn, args: [boxed(ref())], type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: ref(),
|
||||
type: value.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -946,7 +946,10 @@ export function registerBuiltinErrorClasses(lowerer: Lowerer): void {
|
||||
throwingSetters: [],
|
||||
staticFields: [],
|
||||
};
|
||||
if (base) base.subclasses.push(info);
|
||||
if (base) {
|
||||
base.subclasses.push(info);
|
||||
lowerer.subclassLinks++;
|
||||
}
|
||||
lowerer.classes.set(irName, info);
|
||||
}
|
||||
}
|
||||
@@ -1026,7 +1029,10 @@ export function registerBuiltinStreamClasses(lowerer: Lowerer): void {
|
||||
throwingSetters: [],
|
||||
staticFields: [],
|
||||
};
|
||||
if (base) base.subclasses.push(info);
|
||||
if (base) {
|
||||
base.subclasses.push(info);
|
||||
lowerer.subclassLinks++;
|
||||
}
|
||||
lowerer.classes.set(irName, info);
|
||||
}
|
||||
}
|
||||
@@ -4471,7 +4477,10 @@ export function collectClassShapeInner(
|
||||
};
|
||||
lowerer.genericClassByDecl.set(decl, info.generic);
|
||||
}
|
||||
if (base) base.subclasses.push(info);
|
||||
if (base) {
|
||||
base.subclasses.push(info);
|
||||
lowerer.subclassLinks++;
|
||||
}
|
||||
lowerer.classes.set(className, info);
|
||||
if (computedBase && !functionLocalClass(decl)) {
|
||||
info.def.baseValueGlobal = `%g.${className}.computedBase`;
|
||||
|
||||
@@ -53,6 +53,8 @@ import {
|
||||
BOOL,
|
||||
canBoxFuncIntoDyn,
|
||||
canConvertToDyn,
|
||||
canDynCheckTo,
|
||||
isDynTypedRefType,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
@@ -69,7 +71,7 @@ import {
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import { STREAM_FORCED_EVENT_NAMES, streamForcedTuple, streamSidesOf } from "./lower-stream.js";
|
||||
import { boolLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { boolLit, dynUndefinedExpr, strLit, varRef } from "../../ir/build.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerAddAbortListener } from "./builtin-calls.js";
|
||||
import { requestGeneralStreamSurface } from "./general-streams.js";
|
||||
@@ -336,9 +338,15 @@ function emitterEvents(lowerer: Lowerer): Map<string, EventSig> {
|
||||
const cbCt = lowerer.typeOf(node.arguments.nodes[1]);
|
||||
const cbT =
|
||||
lowerer.mapTypeOf(cbCt) ?? dynFallbackType(lowerer, node.arguments.nodes[1], cbCt);
|
||||
// A TypeScript listener with no parameters is typed too: it
|
||||
// registers without the adapter, so an untyped emit reaches it
|
||||
// only through the event's fixed ABI. A JavaScript function
|
||||
// without parameters can still read `arguments`.
|
||||
if (
|
||||
cbT?.kind !== "dyn" &&
|
||||
(cbT?.kind !== "func" || cbT.params.some((p) => p.kind !== "dyn"))
|
||||
(cbT?.kind !== "func" ||
|
||||
(cbT.params.length === 0 && !isJsSourceFile(sf)) ||
|
||||
cbT.params.some((p) => p.kind !== "dyn"))
|
||||
) {
|
||||
sigOf(name).typedListener = true;
|
||||
}
|
||||
@@ -931,6 +939,239 @@ function lowerDynListenerCall(
|
||||
};
|
||||
}
|
||||
|
||||
/** The literal events typed code dispatches with a fixed tuple — it emits
|
||||
* them or listens with typed parameters — whose names an untyped receiver
|
||||
* may also use. 'error' and the meta events are excluded: the runtime fixes
|
||||
* their tuples itself (scr_emitter_on_flex / scr_emitter_emit_flex). */
|
||||
function bridgedEvents(lowerer: Lowerer): { name: string; tuple: IrType[] }[] {
|
||||
const table = emitterEvents(lowerer);
|
||||
const events: { name: string; tuple: IrType[] }[] = [];
|
||||
for (const [key, sig] of table) {
|
||||
if (key === "error" || key === ERROR_MONITOR_EVENT || META_EVENTS.has(key)) continue;
|
||||
if (sig.conflict !== null || (!sig.fromEmit && sig.typedListener !== true)) continue;
|
||||
if (flexibleJsEvent(key, sig)) continue;
|
||||
if (computedPatterns(lowerer).some((pattern) => patternMatchesName(pattern, key))) continue;
|
||||
events.push({ name: eventKeyName(key), tuple: sig.tuple });
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
/** `emit(name, ...args)` through an untyped value on a (non-stream)
|
||||
* emitter-rooted receiver, when typed code fixes the tuple of some event.
|
||||
* Listeners registered as checked-dynamic functions receive the arguments
|
||||
* as given; when the name is such an event and a listener with typed
|
||||
* parameters is registered, the arguments also convert to the event's tuple
|
||||
* (a mismatch throws the checked conversion's TypeError). Null when no event
|
||||
* needs this, so the caller keeps the plain untyped emit. */
|
||||
export function lowerDynamicEmitterEmit(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
name: IrExpr,
|
||||
payload: IrExpr[],
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const recvT = receiver.type;
|
||||
if (recvT.kind !== "object") return null;
|
||||
const k = payload.length;
|
||||
const getRecord = (id: string) => lowerer.shapes.get(id);
|
||||
const getUnion = (id: string) => lowerer.unions.get(id);
|
||||
const checkable = (t: IrType): boolean =>
|
||||
t.kind === "dyn" ||
|
||||
isDynTypedRefType(t) ||
|
||||
isUnitType(t) ||
|
||||
(t.kind === "union"
|
||||
? (getUnion(t.unionId)?.arms.every(checkable) ?? false)
|
||||
: canDynCheckTo(t, getRecord, getUnion));
|
||||
const converted = (tuple: IrType[]): IrExpr[] | null => {
|
||||
if (!tuple.every(checkable)) return null;
|
||||
const out: IrExpr[] = [];
|
||||
for (let i = 0; i < tuple.length; i++) {
|
||||
const arg = i < k ? varRef(`a.${i}`, DYN, loc) : dynUndefinedExpr(loc);
|
||||
const value = lowerer.coerceToExpected(arg, tuple[i]!);
|
||||
if (!typeEquals(value.type, tuple[i]!)) return null;
|
||||
out.push(value);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const events = bridgedEvents(lowerer)
|
||||
.map((event) => ({ ...event, args: converted(event.tuple) }))
|
||||
.filter((event) => event.args !== null);
|
||||
if (events.length === 0) return null;
|
||||
const key = `emitter.dynEmit:${typeKey(recvT)}:${k}`;
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%emitter.dynEmit.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const params: IrParam[] = [
|
||||
{ localId: "r.0", name: "r", type: recvT },
|
||||
{ localId: "n.0", name: "n", type: STRING },
|
||||
...payload.map((_, i) => ({ localId: `a.${i}`, name: `a${i}`, type: DYN })),
|
||||
];
|
||||
const r = varRef("r.0", recvT, loc);
|
||||
const n = varRef("n.0", STRING, loc);
|
||||
const dynArgs = payload.map((_, i) => varRef(`a.${i}`, DYN, loc));
|
||||
const body: IrStmt[] = events.map((event): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: n,
|
||||
right: strLit(event.name, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "libCall", fn: "emitter.hasTyped", args: [r, n], type: BOOL, loc },
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "emitter.emitMixed",
|
||||
args: [
|
||||
r,
|
||||
n,
|
||||
{ kind: "numLit", value: k, type: F64, loc },
|
||||
...dynArgs,
|
||||
...event.args!,
|
||||
],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
}));
|
||||
body.push({
|
||||
kind: "return",
|
||||
value: { kind: "libCall", fn: "emitter.emitAny", args: [r, n, ...dynArgs], type: BOOL, loc },
|
||||
loc,
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params,
|
||||
returnType: BOOL,
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [receiver, name, ...payload], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** on/once/prepend*(name, fn) through an untyped value on a (non-stream)
|
||||
* emitter-rooted receiver, when typed code fixes the tuple of some event.
|
||||
* Under such an event's name the function registers with a typed adapter
|
||||
* for typed emits as well as its checked-dynamic thunk for untyped ones;
|
||||
* other names register the thunk alone. Null when no event needs this. */
|
||||
export function lowerDynamicEmitterOn(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
name: IrExpr,
|
||||
cb: IrExpr,
|
||||
once: boolean,
|
||||
prepend: boolean,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const recvT = receiver.type;
|
||||
if (recvT.kind !== "object") return null;
|
||||
const getRecord = (id: string) => lowerer.shapes.get(id);
|
||||
const getUnion = (id: string) => lowerer.unions.get(id);
|
||||
const events = bridgedEvents(lowerer).filter((event) =>
|
||||
event.tuple.every((t) => t.kind === "dyn" || canConvertToDyn(t, getRecord, getUnion)),
|
||||
);
|
||||
if (events.length === 0) return null;
|
||||
const key = `emitter.dynOn:${typeKey(recvT)}`;
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%emitter.dynOn.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const params: IrParam[] = [
|
||||
{ localId: "r.0", name: "r", type: recvT },
|
||||
{ localId: "n.0", name: "n", type: STRING },
|
||||
{ localId: "cb.0", name: "cb", type: DYN },
|
||||
{ localId: "o.0", name: "o", type: BOOL },
|
||||
{ localId: "p.0", name: "p", type: BOOL },
|
||||
];
|
||||
const locals: IrLocal[] = params.map((p) => ({
|
||||
id: p.localId,
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
mutable: false,
|
||||
}));
|
||||
const r = varRef("r.0", recvT, loc);
|
||||
const n = varRef("n.0", STRING, loc);
|
||||
const fn = varRef("cb.0", DYN, loc);
|
||||
const o = varRef("o.0", BOOL, loc);
|
||||
const p = varRef("p.0", BOOL, loc);
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "emitter.checkListener", args: [fn], type: VOID, loc },
|
||||
loc,
|
||||
},
|
||||
];
|
||||
events.forEach((event, i) => {
|
||||
const adapterT: IrType = { kind: "func", params: event.tuple, ret: VOID };
|
||||
const adapter = `a.${i}`;
|
||||
locals.push({ id: adapter, name: "a", type: adapterT, mutable: false });
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: n,
|
||||
right: strLit(event.name, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: adapter,
|
||||
init: { kind: "dynCheck", value: fn, type: adapterT, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "emitter.onDyn",
|
||||
args: [r, n, fn, varRef(adapter, adapterT, loc), o, p],
|
||||
type: recvT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
});
|
||||
body.push({
|
||||
kind: "return",
|
||||
value: { kind: "libCall", fn: "emitter.onAny", args: [r, n, fn, o, p], type: recvT, loc },
|
||||
loc,
|
||||
});
|
||||
lowerer.liftedFns.push({ name: helper, params, returnType: recvT, locals, body, loc });
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
callee: helper,
|
||||
args: [receiver, name, cb, boolLit(once, loc), boolLit(prepend, loc)],
|
||||
type: recvT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A listener argument: lowered, checked void-returning, and its
|
||||
* parameters checked as a typeEquals PREFIX of the event tuple. */
|
||||
function lowerListenerArg(
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { identityPreservingWidening } from "./coercions/identity.js";
|
||||
import { scanDivergences } from "./divergences.js";
|
||||
import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js";
|
||||
import { observeRecordLiteralRebuilds } from "../../ir/traverse.js";
|
||||
import { checkedClassAssertion } from "./class-assertions.js";
|
||||
import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js";
|
||||
import {
|
||||
@@ -210,6 +211,7 @@ import type {
|
||||
IrLocal,
|
||||
IrModule,
|
||||
IrWasmModule,
|
||||
IrWasmUntranslated,
|
||||
IrParam,
|
||||
IrRecordShape,
|
||||
IrStmt,
|
||||
@@ -1210,17 +1212,28 @@ export function lowerToIr(
|
||||
});
|
||||
// A program that uses the WebAssembly API carries every module the build
|
||||
// can see and translate (wasm/translate.ts); the rest fail at run time
|
||||
// with the CompileError for untranslated bytes.
|
||||
// with a CompileError, which names the reason for the modules the build
|
||||
// saw but does not translate.
|
||||
if (result.module !== null && resultLowerer.usesWebAssembly) {
|
||||
const bytesByHash = new Map<string, Uint8Array>();
|
||||
const reports = discoverModules((sha256, bytes) => bytesByHash.set(sha256, bytes));
|
||||
const wasmModules: IrWasmModule[] = [];
|
||||
const wasmUntranslated: IrWasmUntranslated[] = [];
|
||||
for (const report of reports) {
|
||||
const bytes = bytesByHash.get(report.sha256);
|
||||
if (report.status !== "valid" || bytes === undefined) continue;
|
||||
wasmModules.push({ sha256: report.sha256, bytes: encodeBase64(bytes) });
|
||||
if (bytes === undefined) continue;
|
||||
if (report.status === "valid") {
|
||||
wasmModules.push({ sha256: report.sha256, bytes: encodeBase64(bytes) });
|
||||
continue;
|
||||
}
|
||||
const reason =
|
||||
report.status === "unsupported"
|
||||
? `it uses ${report.unsupported.join(", ")}, outside the translated WebAssembly feature set`
|
||||
: `it is not a valid module: ${report.error ?? report.status}`;
|
||||
wasmUntranslated.push({ sha256: report.sha256, size: bytes.length, reason });
|
||||
}
|
||||
result.module.wasmModules = wasmModules;
|
||||
result.module.wasmUntranslated = wasmUntranslated;
|
||||
if (resultLowerer.usesWasmCustomSections) result.module.wasmCustomSections = true;
|
||||
timing("wasm-discovery", { modules: wasmModules.length });
|
||||
}
|
||||
@@ -3045,6 +3058,10 @@ export class Lowerer {
|
||||
// top level, top = the innermost nested function currently lowering).
|
||||
fnStack: FnCtx[] = [];
|
||||
readonly liftedFns: IrFunction[] = [];
|
||||
/** The first lifted function of each name (liftedFnNamed), for the
|
||||
* liftedFns indexed so far. liftedFns only grows. */
|
||||
private readonly liftedFnsByName = new Map<string, IrFunction>();
|
||||
private liftedFnsIndexed = 0;
|
||||
/** Runtime-provided classes used as values (defineClassValueConstruct),
|
||||
* by IR class name. */
|
||||
readonly runtimeClassValues = new Map<string, ClassInfo>();
|
||||
@@ -3116,6 +3133,10 @@ export class Lowerer {
|
||||
* can contain any retained descendant, but importing its declaration
|
||||
* alone does not make every descendant constructible. */
|
||||
private constructedClasses: Set<string> | null = null;
|
||||
/** Classes added to some ClassInfo's subclasses (lower-classes.ts). With
|
||||
* the class and constructed-class counts, it tells caches over the class
|
||||
* graph when the graph changed. */
|
||||
subclassLinks = 0;
|
||||
/** Set while a subclass constructor's super() call notes its base
|
||||
* constructor edge: that edge does not instantiate the base exactly. */
|
||||
private superConstructorEdge = false;
|
||||
@@ -3132,6 +3153,20 @@ export class Lowerer {
|
||||
info.mixinInstance !== undefined
|
||||
);
|
||||
}
|
||||
/** How many classes classCanBeConstructed admits by name, or -1 while
|
||||
* it admits every class. The count only grows. */
|
||||
constructedClassCount(): number {
|
||||
return this.constructedClasses?.size ?? -1;
|
||||
}
|
||||
/** The first lifted function named `name`, the one
|
||||
* `liftedFns.find((fn) => fn.name === name)` returns. */
|
||||
liftedFnNamed(name: string | undefined): IrFunction | undefined {
|
||||
for (; this.liftedFnsIndexed < this.liftedFns.length; this.liftedFnsIndexed++) {
|
||||
const fn = this.liftedFns[this.liftedFnsIndexed]!;
|
||||
if (!this.liftedFnsByName.has(fn.name)) this.liftedFnsByName.set(fn.name, fn);
|
||||
}
|
||||
return name === undefined ? undefined : this.liftedFnsByName.get(name);
|
||||
}
|
||||
/** Coverage remainder mode: the reachability gate inverts (see wantBody)
|
||||
* and no module is built. */
|
||||
readonly remainder: boolean;
|
||||
@@ -7411,6 +7446,10 @@ export class Lowerer {
|
||||
* body; both fire edges through the same hooks and are not units
|
||||
* themselves. */
|
||||
emitReachable(extraRoots?: readonly string[]): { reachable: Set<string>; result: LowerResult } {
|
||||
// Frontend passes below rewrite function bodies through the IR mapper
|
||||
// (caught-value and class dispatch); key-order notes follow the record
|
||||
// literals they rebuild until the witness pass has read them.
|
||||
observeRecordLiteralRebuilds((from, to) => this.keyOrder.follow(from, to));
|
||||
this.deferredModuleInitializers = new DeferredModuleInitializers();
|
||||
const parts = this.splitFiles();
|
||||
this.narrowing.setFiles(parts.map((fp) => fp.sf));
|
||||
@@ -7841,6 +7880,7 @@ export class Lowerer {
|
||||
...this.liftedFns,
|
||||
...this.implicitFns,
|
||||
]);
|
||||
observeRecordLiteralRebuilds(null);
|
||||
for (const finalize of this.shapeOrderHelperFinalizers) finalize();
|
||||
const functions = [
|
||||
...orderedUnits,
|
||||
|
||||
@@ -81,7 +81,9 @@ export class RecordFlowClasses {
|
||||
/** Identifiers by name; symbols resolve only for the names of slots and
|
||||
* functions, so the scan asks the checker about few nodes. */
|
||||
private readonly names = new Map<string, ts.Identifier[]>();
|
||||
private readonly references = new Map<ts.Symbol, ts.Identifier[]>();
|
||||
/** The identifiers of each name grouped by the symbol they resolve to,
|
||||
* built on the first reference query for that name. */
|
||||
private readonly references = new Map<string, Map<ts.Symbol, ts.Identifier[]>>();
|
||||
/** Names written as shorthand properties (`{ p }`): the checker resolves
|
||||
* those to the property, so a binding of that name stays open. */
|
||||
private readonly shorthandNames = new Set<string>();
|
||||
@@ -349,14 +351,20 @@ export class RecordFlowClasses {
|
||||
|
||||
/** The identifiers that name `symbol`, its declaration included. */
|
||||
private referencesOf(symbol: ts.Symbol): readonly ts.Identifier[] {
|
||||
let list = this.references.get(symbol);
|
||||
if (!list) {
|
||||
list = (this.names.get(ts.symbolName(symbol)) ?? []).filter(
|
||||
(node) => this.host.symbolOf(node) === symbol,
|
||||
);
|
||||
this.references.set(symbol, list);
|
||||
const name = ts.symbolName(symbol);
|
||||
let bySymbol = this.references.get(name);
|
||||
if (!bySymbol) {
|
||||
bySymbol = new Map();
|
||||
for (const node of this.names.get(name) ?? []) {
|
||||
const resolved = this.host.symbolOf(node);
|
||||
if (!resolved) continue;
|
||||
const list = bySymbol.get(resolved);
|
||||
if (list) list.push(node);
|
||||
else bySymbol.set(resolved, [node]);
|
||||
}
|
||||
this.references.set(name, bySymbol);
|
||||
}
|
||||
return list;
|
||||
return bySymbol.get(symbol) ?? [];
|
||||
}
|
||||
|
||||
private collectFunctions(): void {
|
||||
|
||||
@@ -2104,6 +2104,8 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
|
||||
glob: ["fsp.globTypes"],
|
||||
// The Uint8Array/Buffer-data form.
|
||||
appendFile: ["fsp.appendFileBytes"],
|
||||
// The options forms.
|
||||
rm: ["fsp.rmOpts", "fsp.rmRetry"],
|
||||
},
|
||||
crypto: {
|
||||
// The composed randomBytes(n).toString(enc) chain keeps its one-libCall
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { nodeFrontendServices } from "./services-node.js";
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function rewrite(source: string): string | null {
|
||||
return nodeFrontendServices().tscBuiltinImports(source, "/pkg/index.js");
|
||||
}
|
||||
|
||||
const helpers = `"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||
result["default"] = mod;
|
||||
return result;
|
||||
};
|
||||
`;
|
||||
|
||||
function expectSameBehavior(source: string): string {
|
||||
const result = rewrite(source);
|
||||
expect(typeof result).toBe("string");
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-tsc-builtin-imports-"));
|
||||
dirs.push(dir);
|
||||
writeFileSync(join(dir, "original.cjs"), source);
|
||||
writeFileSync(join(dir, "rewritten.cjs"), result!);
|
||||
const run = (file: string) =>
|
||||
spawnSync(process.execPath, [join(dir, file)], { encoding: "utf8" });
|
||||
const oracle = run("original.cjs");
|
||||
expect(oracle.status).toBe(0);
|
||||
expect(run("rewritten.cjs").stdout).toBe(oracle.stdout);
|
||||
return result!;
|
||||
}
|
||||
|
||||
test("builtin modules behind tsc's interop helpers become plain requires", () => {
|
||||
const out = expectSameBehavior(`${helpers}const events_1 = __importDefault(require("events"));
|
||||
const path = __importStar(require("node:path"));
|
||||
class Bus extends events_1.default.EventEmitter {}
|
||||
console.log(new Bus() instanceof events_1.default, path.basename("/a/b.txt"), typeof path.join);
|
||||
`);
|
||||
expect(out).toContain('const events_1 = require("events");');
|
||||
expect(out).toContain("class Bus extends events_1.EventEmitter {}");
|
||||
expect(out).toContain('const path = require("node:path");');
|
||||
});
|
||||
|
||||
test("other references, program modules and default reads of a namespace keep their text", () => {
|
||||
// The binding itself as a value.
|
||||
expect(
|
||||
rewrite(
|
||||
`${helpers}const events_1 = __importDefault(require("events"));\nconsole.log(events_1);\n`,
|
||||
),
|
||||
).toBeNull();
|
||||
// A program module (cjs-tsc-interop.ts owns it).
|
||||
expect(
|
||||
rewrite(`${helpers}const m = __importDefault(require("./m"));\nm.default();\n`),
|
||||
).toBeNull();
|
||||
// `ns.default` reads the namespace copy's own default.
|
||||
expect(
|
||||
rewrite(`${helpers}const p = __importStar(require("path"));\nconsole.log(p.default === p);\n`),
|
||||
).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/* tsc wraps every CommonJS import in its interop helpers:
|
||||
*
|
||||
* const events_1 = __importDefault(require("events")); // events_1.default.X
|
||||
* const path = __importStar(require("path")); // path.basename
|
||||
*
|
||||
* For program modules cjs-tsc-interop.ts reads such a binding as a plain
|
||||
* require binding, and the helper declarations (`var __importDefault =
|
||||
* (this && this.__importDefault) || …`) go dead. A builtin module is not a
|
||||
* program module, so its binding kept the helpers live, and their probe of
|
||||
* the module's top-level `this` has no lowering. A builtin carries no
|
||||
* `__esModule`: `events_1.default` is the module and the namespace copy
|
||||
* reads the module's members. When every reference has that form
|
||||
* (`x.default…` for the default helper, `x.<member>` other than `default`
|
||||
* for the namespace helper), the binding becomes the plain `require()` of
|
||||
* the builtin and `x.default` becomes `x`. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import { applyEdits, unparen, type Edit } from "./npm-static-bundled-chunks.js";
|
||||
import { canonicalBuiltinModule } from "./builtin-modules.js";
|
||||
|
||||
/** True when the text may hold such a binding (a cheap probe). */
|
||||
export function mayHoldTscBuiltinImport(source: string): boolean {
|
||||
return /__import(?:Default|Star)\(require\(/.test(source);
|
||||
}
|
||||
|
||||
/** `__importDefault(require("<builtin>"))` / `__importStar(…)`: the helper
|
||||
* and builtin spec, or null. */
|
||||
function builtinInterop(init: ts.Expression): { helper: string; spec: string } | null {
|
||||
const e = unparen(init);
|
||||
if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression) || e.arguments.nodes.length !== 1)
|
||||
return null;
|
||||
const helper = e.expression.text;
|
||||
if (helper !== "__importDefault" && helper !== "__importStar") return null;
|
||||
const req = unparen(e.arguments.nodes[0]!);
|
||||
if (
|
||||
!ts.isCallExpression(req) ||
|
||||
!ts.isIdentifier(req.expression) ||
|
||||
req.expression.text !== "require" ||
|
||||
req.arguments.nodes.length !== 1 ||
|
||||
!ts.isStringLiteral(req.arguments.nodes[0]!)
|
||||
)
|
||||
return null;
|
||||
const spec = (req.arguments.nodes[0] as ts.StringLiteral).text;
|
||||
return canonicalBuiltinModule(spec) === null ? null : { helper, spec };
|
||||
}
|
||||
|
||||
/** The `.default` accesses to remove when every reference to `name` (other
|
||||
* than its declaration) has the helper's form, or null. */
|
||||
function interopReferences(
|
||||
sf: ts.SourceFile,
|
||||
decl: ts.VariableDeclaration,
|
||||
name: string,
|
||||
helper: string,
|
||||
): ts.PropertyAccessExpression[] | null {
|
||||
const defaults: ts.PropertyAccessExpression[] = [];
|
||||
let ok = true;
|
||||
ts.walkPreorder(sf, (n) => {
|
||||
if (!ok) return "stop";
|
||||
if (!ts.isIdentifier(n) || n.text !== name || n === decl.name) return undefined;
|
||||
const parent = n.parent;
|
||||
if (
|
||||
parent === undefined ||
|
||||
!ts.isPropertyAccessExpression(parent) ||
|
||||
parent.expression !== n ||
|
||||
parent.questionDotToken !== undefined
|
||||
) {
|
||||
ok = false;
|
||||
return "stop";
|
||||
}
|
||||
const member = parent.name.text;
|
||||
if (helper === "__importDefault") {
|
||||
if (member !== "default") ok = false;
|
||||
else defaults.push(parent);
|
||||
} else if (member === "default") {
|
||||
ok = false;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return ok ? defaults : null;
|
||||
}
|
||||
|
||||
/** The module text with its builtin interop bindings unwrapped, or null. */
|
||||
export function unwrapTscBuiltinImports(sf: ts.SourceFile): string | null {
|
||||
if (ts.isExternalModule(sf) && !sf.fileName.endsWith(".cjs")) return null;
|
||||
const edits: Edit[] = [];
|
||||
const stmts = sf.statements.nodes;
|
||||
// Builtin modules through the interop helpers.
|
||||
for (const stmt of stmts) {
|
||||
if (!ts.isVariableStatement(stmt)) continue;
|
||||
for (const d of stmt.declarationList.declarations.nodes) {
|
||||
if (!ts.isIdentifier(d.name) || d.initializer === undefined) continue;
|
||||
const interop = builtinInterop(d.initializer);
|
||||
if (interop === null) continue;
|
||||
const defaults = interopReferences(sf, d, d.name.text, interop.helper);
|
||||
if (defaults === null) continue;
|
||||
edits.push({
|
||||
start: d.initializer.getStart(sf),
|
||||
end: d.initializer.getEnd(),
|
||||
text: `require(${JSON.stringify(interop.spec)})`,
|
||||
});
|
||||
for (const access of defaults)
|
||||
edits.push({ start: access.getStart(sf), end: access.getEnd(), text: d.name.text });
|
||||
}
|
||||
}
|
||||
if (edits.length === 0) return null;
|
||||
return applyEdits(sf.text, 0, sf.text.length, edits);
|
||||
}
|
||||
@@ -455,6 +455,9 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
// Polyfill packages that ARE a Node builtin compile as the builtin.
|
||||
const aliased = original === null ? null : services.builtinAliasRequires(original, path);
|
||||
if (aliased !== null) original = aliased;
|
||||
// tsc's interop helpers around builtin modules.
|
||||
const tscHelpers = original === null ? null : services.tscBuiltinImports(original, path);
|
||||
if (tscHelpers !== null) original = tscHelpers;
|
||||
// A module exporting one of its functions reaches its members
|
||||
// through that function.
|
||||
const fnExports = original === null ? null : services.cjsFunctionExports(original, path);
|
||||
@@ -469,6 +472,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
if (exportsVar !== null) original = exportsVar;
|
||||
const respelled =
|
||||
split !== null ||
|
||||
tscHelpers !== null ||
|
||||
esm !== null ||
|
||||
aliased !== null ||
|
||||
fnExports !== null ||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
rewriteBuiltinAliasRequires,
|
||||
} from "./npm-static-builtin-aliases.js";
|
||||
import { mayHoldEnumIife, rewriteEnumIifes } from "./npm-static-enum-iife.js";
|
||||
import { mayHoldTscBuiltinImport, unwrapTscBuiltinImports } from "./npm-static-tsc-helpers.js";
|
||||
import { builtinPackageAliasFor } from "./package-shims.js";
|
||||
import * as syntax from "./npm-static-declaration-syntax.js";
|
||||
import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js";
|
||||
@@ -106,6 +107,13 @@ export class FrontendServices {
|
||||
if (!source.includes("module.exports")) return null;
|
||||
return normalizeCjsFunctionExports(this.parse(path, source, "js"));
|
||||
}
|
||||
/** A package module with tsc's interop bindings of builtin modules
|
||||
* unwrapped to plain requires (null: none). */
|
||||
tscBuiltinImports(source: string, path: string): string | null {
|
||||
this.ensureOpen();
|
||||
if (!mayHoldTscBuiltinImport(source)) return null;
|
||||
return unwrapTscBuiltinImports(this.parse(path, source, "js"));
|
||||
}
|
||||
/** A package module with its literal-valued TypeScript enum IIFEs
|
||||
* respelled as object literals (null: none). */
|
||||
enumIifes(source: string, path: string): string | null {
|
||||
|
||||
@@ -101,6 +101,8 @@ const cases: Record<string, string> = {
|
||||
'/** @typedef {Object} Options\n * @property {string} name\n * @property {number} count\n */\n/** @type {Options} */ const options = { name: "x", count: 2 };',
|
||||
"missing.ts": "const incomplete = ;\nfunction f( {\n",
|
||||
"empty.ts": "",
|
||||
"targets.ts":
|
||||
"let x: number | undefined = 1;\nconst a = [1];\n(x) = 2;\nx! = 3;\na[0]! += 1;\n(a[0])!++;\n--(x!);\nfor ((x) of a) {}\nconst y = (x!) + 1;\n",
|
||||
"lists.ts":
|
||||
"const trailing = [1, 2 /* last */, ];\nconst plain = [1, 2];\nf(1, 2,);\nexport abstract class Flags { protected static readonly value = 1; }\n",
|
||||
};
|
||||
@@ -167,6 +169,38 @@ function ids(
|
||||
return nodes?.map((n) => (n as OracleNode).index);
|
||||
}
|
||||
|
||||
test("the writeTargets option answers write targets without their wrappers", () => {
|
||||
const { bytes, file: plain } = decoded.get("targets.ts")!;
|
||||
const file = new AstFile(bytes, undefined, { writeTargets: true });
|
||||
const statements = (root: AstNode) =>
|
||||
(root.childList("statements") as AstNodeList).nodes.map((statement) =>
|
||||
statement.kind === SyntaxKind.ExpressionStatement ? statement.expression : statement,
|
||||
);
|
||||
const text = (node: AstNode | undefined) => node?.getText();
|
||||
const writes = statements(file.root);
|
||||
const raw = statements(plain.root);
|
||||
// Assignment left sides, update operands and a for-of head.
|
||||
const targets = [writes[2]!.left, writes[3]!.left, writes[4]!.left, writes[5]!.operand];
|
||||
expect(targets.map(text)).toEqual(["x", "x", "a[0]", "a[0]"]);
|
||||
expect(text(writes[6]!.operand)).toBe("x");
|
||||
expect(text(writes[7]!.initializer)).toBe("x");
|
||||
for (const [index, target] of targets.entries())
|
||||
expect(target!.parent, `target ${index}`).toBe(writes[index + 2]);
|
||||
expect(writes[7]!.initializer!.parent).toBe(writes[7]);
|
||||
// The reference keeps its own source span.
|
||||
expect(writes[4]!.left!.getStart()).toBe(raw[4]!.left!.getStart());
|
||||
expect(writes[4]!.left!.end).toBe(raw[4]!.left!.end - 1);
|
||||
// A wrapper outside a write target is kept.
|
||||
const init = (writes[8]!.childNode("declarationList")!.childList("declarations") as AstNodeList)
|
||||
.nodes[0]!.initializer!;
|
||||
expect(text(init.left)).toBe("(x!)");
|
||||
expect(init.left!.kind).toBe(SyntaxKind.ParenthesizedExpression);
|
||||
// Without the option, the file is TypeScript's exact tree.
|
||||
expect(raw[3]!.left!.kind).toBe(SyntaxKind.NonNullExpression);
|
||||
expect(raw[2]!.left!.kind).toBe(SyntaxKind.ParenthesizedExpression);
|
||||
expect(raw[5]!.operand!.kind).toBe(SyntaxKind.NonNullExpression);
|
||||
});
|
||||
|
||||
test("node lists report a trailing comma and modifier lists their flags", () => {
|
||||
const { file } = decoded.get("lists.ts")!;
|
||||
const lists: [SyntaxKind, boolean][] = [];
|
||||
|
||||
@@ -60,6 +60,18 @@ export class AstModifierList<T extends AstNode = AstNode> extends AstNodeList<T>
|
||||
* list.
|
||||
* Parent access is lazy, so resolving a checker handle near the bottom
|
||||
* of a deep tree never recursively constructs its ancestors. */
|
||||
export interface AstFileOptions {
|
||||
/** Answer the reference an assignment, a `++`/`--` update or a for-in/of
|
||||
* head writes without the parentheses and non-null assertions around it.
|
||||
* TypeScript accepts both around a target (`(x) = 1`, `a[i]! += 1`) and
|
||||
* both are erased at run time. With this option the writer's `left`,
|
||||
* `operand` or `initializer` is the reference itself and the reference's
|
||||
* `parent` is the writer, so every reader of a write target sees one
|
||||
* shape. Positions stay the source's: the reference keeps its own span.
|
||||
* Files read for other purposes keep TypeScript's exact tree. */
|
||||
writeTargets?: boolean;
|
||||
}
|
||||
|
||||
export class AstFile {
|
||||
readonly wire: AstWireFile;
|
||||
readonly root: AstNode;
|
||||
@@ -73,10 +85,16 @@ export class AstFile {
|
||||
private lines: number[] | undefined;
|
||||
private childIndices: Uint32Array | undefined;
|
||||
|
||||
/** Whether nodes answer write targets without their wrappers (see
|
||||
* AstFileOptions.writeTargets). */
|
||||
readonly writeTargets: boolean;
|
||||
|
||||
constructor(
|
||||
bytes: Uint8Array,
|
||||
private readonly materialized?: () => void,
|
||||
options: AstFileOptions = {},
|
||||
) {
|
||||
this.writeTargets = options.writeTargets === true;
|
||||
this.wire = new AstWireFile(bytes);
|
||||
this.nodes = new Array<AstNode>(this.wire.nodeCount);
|
||||
this.lists = new Array<AstNode[]>(this.wire.nodeCount);
|
||||
@@ -192,6 +210,73 @@ export class AstFile {
|
||||
/** Concrete native layout shared by every syntax kind. Kind-specific
|
||||
* properties are views over the wire; they never copy identity-bearing
|
||||
* nodes into structural records. */
|
||||
/* Write targets without their wrappers (AstFileOptions.writeTargets). */
|
||||
|
||||
function isTargetWrapper(node: AstNode): boolean {
|
||||
return node.kind === AstKind.ParenthesizedExpression || node.kind === AstKind.NonNullExpression;
|
||||
}
|
||||
|
||||
function unwrapTarget(node: AstNode): AstNode {
|
||||
let target = node;
|
||||
while (isTargetWrapper(target)) {
|
||||
const inner = target.childNode("expression");
|
||||
if (inner === undefined) break;
|
||||
target = inner;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/** Whether `field` of `node` is a write target: an assignment's left side,
|
||||
* a `++`/`--` operand, or a for-in/of head that declares nothing. */
|
||||
function writesTarget(node: AstNode, field: "left" | "operand" | "initializer"): boolean {
|
||||
switch (node.kind) {
|
||||
case AstKind.BinaryExpression: {
|
||||
if (field !== "left") return false;
|
||||
const operator = node.childNode("operatorToken")?.kind;
|
||||
return (
|
||||
operator !== undefined &&
|
||||
operator >= AstKind.FirstAssignment &&
|
||||
operator <= AstKind.LastAssignment
|
||||
);
|
||||
}
|
||||
case AstKind.PrefixUnaryExpression:
|
||||
case AstKind.PostfixUnaryExpression:
|
||||
return (
|
||||
field === "operand" &&
|
||||
(node.operator === AstKind.PlusPlusToken || node.operator === AstKind.MinusMinusToken)
|
||||
);
|
||||
case AstKind.ForInStatement:
|
||||
case AstKind.ForOfStatement:
|
||||
return (
|
||||
field === "initializer" &&
|
||||
node.childNode("initializer")?.kind !== AstKind.VariableDeclarationList
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The parent of `node`, whose syntactic parent `wrapper` is a parenthesis
|
||||
* or a non-null assertion: the writer when the wrappers around `node` are
|
||||
* the whole target of an assignment, update or for-in/of head, and
|
||||
* `wrapper` otherwise. */
|
||||
function writerOf(node: AstNode, wrapper: AstNode): AstNode {
|
||||
let outer = wrapper;
|
||||
for (;;) {
|
||||
const up = outer.parent;
|
||||
if (up === undefined) return wrapper;
|
||||
if (isTargetWrapper(up)) {
|
||||
outer = up;
|
||||
continue;
|
||||
}
|
||||
for (const field of ["left", "operand", "initializer"] as const) {
|
||||
if (writesTarget(up, field) && up.childNode(field) === outer && unwrapTarget(outer) === node)
|
||||
return up;
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
export class AstNode {
|
||||
readonly kind: SyntaxKind;
|
||||
readonly pos: number;
|
||||
@@ -221,7 +306,11 @@ export class AstNode {
|
||||
get parent(): AstNode | undefined {
|
||||
if (!this.parentResolved) {
|
||||
const index = this.file.wire.semanticParent(this.index);
|
||||
this.parentCache = index === 0 || index === this.index ? undefined : this.file.node(index);
|
||||
const parent = index === 0 || index === this.index ? undefined : this.file.node(index);
|
||||
this.parentCache =
|
||||
parent !== undefined && this.file.writeTargets && isTargetWrapper(parent)
|
||||
? writerOf(this, parent)
|
||||
: parent;
|
||||
this.parentResolved = true;
|
||||
}
|
||||
return this.parentCache;
|
||||
@@ -483,6 +572,30 @@ export class AstNode {
|
||||
return token?.kind === SyntaxKind.QuestionToken ? token : undefined;
|
||||
}
|
||||
|
||||
// A write target keeps its parentheses and non-null assertions in the
|
||||
// tree; with AstFileOptions.writeTargets these views return the target
|
||||
// without them.
|
||||
get initializer(): AstNode | undefined {
|
||||
const initializer = this.childNode("initializer");
|
||||
return initializer !== undefined && this.file.writeTargets && writesTarget(this, "initializer")
|
||||
? unwrapTarget(initializer)
|
||||
: initializer;
|
||||
}
|
||||
|
||||
get left(): AstNode | undefined {
|
||||
const left = this.childNode("left");
|
||||
return left !== undefined && this.file.writeTargets && writesTarget(this, "left")
|
||||
? unwrapTarget(left)
|
||||
: left;
|
||||
}
|
||||
|
||||
get operand(): AstNode | undefined {
|
||||
const operand = this.childNode("operand");
|
||||
return operand !== undefined && this.file.writeTargets && writesTarget(this, "operand")
|
||||
? unwrapTarget(operand)
|
||||
: operand;
|
||||
}
|
||||
|
||||
// BEGIN GENERATED CHILD GETTERS
|
||||
// Generated from typescript@7.0.2; run scripts/generate-ts7-ast-schema.mjs.
|
||||
get argument(): AstNode | undefined {
|
||||
@@ -611,18 +724,12 @@ export class AstNode {
|
||||
get indexType(): AstNode | undefined {
|
||||
return this.childNode("indexType");
|
||||
}
|
||||
get initializer(): AstNode | undefined {
|
||||
return this.childNode("initializer");
|
||||
}
|
||||
get jsdocPropertyTags(): AstNode | undefined {
|
||||
return this.childNode("jsdocPropertyTags");
|
||||
}
|
||||
get label(): AstNode | undefined {
|
||||
return this.childNode("label");
|
||||
}
|
||||
get left(): AstNode | undefined {
|
||||
return this.childNode("left");
|
||||
}
|
||||
get literal(): AstNode | undefined {
|
||||
return this.childNode("literal");
|
||||
}
|
||||
@@ -662,9 +769,6 @@ export class AstNode {
|
||||
get openingFragment(): AstNode | undefined {
|
||||
return this.childNode("openingFragment");
|
||||
}
|
||||
get operand(): AstNode | undefined {
|
||||
return this.childNode("operand");
|
||||
}
|
||||
get operatorToken(): AstNode | undefined {
|
||||
return this.childNode("operatorToken");
|
||||
}
|
||||
|
||||
@@ -72,6 +72,33 @@ export function parseAstNodeHandle(handle: string): AstNodeHandle {
|
||||
};
|
||||
}
|
||||
|
||||
/** Node fields as word offsets into a node's NODE_LEN bytes. */
|
||||
const NODE_WORDS = NODE_LEN / 4;
|
||||
const WORD_KIND = NODE_OFFSET_KIND / 4;
|
||||
const WORD_POS = NODE_OFFSET_POS / 4;
|
||||
const WORD_END = NODE_OFFSET_END / 4;
|
||||
const WORD_NEXT = NODE_OFFSET_NEXT / 4;
|
||||
const WORD_PARENT = NODE_OFFSET_PARENT / 4;
|
||||
const WORD_DATA = NODE_OFFSET_DATA / 4;
|
||||
const WORD_FLAGS = NODE_OFFSET_FLAGS / 4;
|
||||
if (
|
||||
!Number.isInteger(NODE_WORDS) ||
|
||||
![WORD_KIND, WORD_POS, WORD_END, WORD_NEXT, WORD_PARENT, WORD_DATA, WORD_FLAGS].every((word) =>
|
||||
Number.isInteger(word),
|
||||
)
|
||||
)
|
||||
throw new AstDecodeError("node fields are not word-aligned");
|
||||
|
||||
/** Kept out of line, so the index check that calls it stays small enough
|
||||
* to inline into every node accessor. */
|
||||
function invalidNodeIndex(index: number): never {
|
||||
throw new AstDecodeError(`invalid node index ${index}`);
|
||||
}
|
||||
|
||||
/** Whether this host stores a Uint32Array's words little-endian, the wire's
|
||||
* byte order. */
|
||||
const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
||||
|
||||
function hexWord(value: number): string {
|
||||
const digits = "0123456789abcdef";
|
||||
let result = "";
|
||||
@@ -93,12 +120,14 @@ export class AstWireFile {
|
||||
private readonly extended: number;
|
||||
private readonly structured: number;
|
||||
private readonly nodes: number;
|
||||
private readonly view: DataView;
|
||||
private readonly stringCache = new Map<number, string>();
|
||||
/** The node table as 32-bit words: a view of the response when it is
|
||||
* word-aligned on a little-endian host, otherwise a decoded copy. */
|
||||
private readonly words: Uint32Array;
|
||||
/** Decoded strings by string word index / 2. */
|
||||
private readonly stringCache: (string | undefined)[];
|
||||
|
||||
constructor(private readonly bytes: Uint8Array) {
|
||||
astBounds(bytes, 0, HEADER_SIZE);
|
||||
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const metadata = astU32(bytes, HEADER_OFFSET_METADATA);
|
||||
if (metadata >>> 24 !== PROTOCOL_VERSION)
|
||||
throw new AstDecodeError(`unsupported protocol version ${metadata >>> 24}`);
|
||||
@@ -122,6 +151,15 @@ export class AstWireFile {
|
||||
}
|
||||
this.nodeCount = (bytes.length - this.nodes) / NODE_LEN;
|
||||
this.stringCount = (this.strings - this.stringOffsets) / 8;
|
||||
const wordCount = this.nodeCount * NODE_WORDS;
|
||||
const start = bytes.byteOffset + this.nodes;
|
||||
if (LITTLE_ENDIAN && start % 4 === 0) {
|
||||
this.words = new Uint32Array(bytes.buffer, start, wordCount);
|
||||
} else {
|
||||
this.words = new Uint32Array(wordCount);
|
||||
for (let i = 0; i < wordCount; i++) this.words[i] = astU32(bytes, this.nodes + i * 4);
|
||||
}
|
||||
this.stringCache = new Array<string | undefined>(this.stringCount).fill(undefined);
|
||||
if (this.nodeCount < 2) throw new AstDecodeError("missing root");
|
||||
if (this.kind(1) === KIND_NODE_LIST || this.parent(1) > 1)
|
||||
throw new AstDecodeError("invalid root node");
|
||||
@@ -142,40 +180,41 @@ export class AstWireFile {
|
||||
}
|
||||
}
|
||||
|
||||
private nodeOffset(index: number): number {
|
||||
if (!Number.isInteger(index) || index < 0 || index >= this.nodeCount)
|
||||
throw new AstDecodeError(`invalid node index ${index}`);
|
||||
return this.nodes + index * NODE_LEN;
|
||||
/** The first word of node `index`. `index >>> 0 === index` accepts exactly
|
||||
* the integers in [0, 2^32), so with the bound it accepts the same indices
|
||||
* as an integer and range check. */
|
||||
private nodeWord(index: number): number {
|
||||
if (index >>> 0 !== index || index >= this.nodeCount) invalidNodeIndex(index);
|
||||
return index * NODE_WORDS;
|
||||
}
|
||||
|
||||
// The constructor validates the complete fixed-width node table, and
|
||||
// nodeOffset validates its index. Read each word in one operation while
|
||||
// preserving little-endian decoding even in an unaligned byte view.
|
||||
// nodeWord validates its index.
|
||||
kind(index: number): number {
|
||||
return this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_KIND, true);
|
||||
return this.words[this.nodeWord(index) + WORD_KIND]!;
|
||||
}
|
||||
pos(index: number): number {
|
||||
return this.view.getInt32(this.nodeOffset(index) + NODE_OFFSET_POS, true);
|
||||
return this.words[this.nodeWord(index) + WORD_POS]! | 0;
|
||||
}
|
||||
end(index: number): number {
|
||||
return this.view.getInt32(this.nodeOffset(index) + NODE_OFFSET_END, true);
|
||||
return this.words[this.nodeWord(index) + WORD_END]! | 0;
|
||||
}
|
||||
flags(index: number): number {
|
||||
return this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_FLAGS, true);
|
||||
return this.words[this.nodeWord(index) + WORD_FLAGS]!;
|
||||
}
|
||||
data(index: number): number {
|
||||
return this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_DATA, true);
|
||||
return this.words[this.nodeWord(index) + WORD_DATA]!;
|
||||
}
|
||||
|
||||
next(index: number): number {
|
||||
const next = this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_NEXT, true);
|
||||
const next = this.words[this.nodeWord(index) + WORD_NEXT]!;
|
||||
if (next !== 0 && (next <= index || next >= this.nodeCount))
|
||||
throw new AstDecodeError("invalid sibling link");
|
||||
return next;
|
||||
}
|
||||
|
||||
parent(index: number): number {
|
||||
const parent = this.view.getUint32(this.nodeOffset(index) + NODE_OFFSET_PARENT, true);
|
||||
const parent = this.words[this.nodeWord(index) + WORD_PARENT]!;
|
||||
if (parent >= this.nodeCount || (index <= 1 ? parent > index : parent >= index))
|
||||
throw new AstDecodeError("invalid parent link");
|
||||
return parent;
|
||||
@@ -188,14 +227,14 @@ export class AstWireFile {
|
||||
}
|
||||
|
||||
string(index: number): string {
|
||||
if (!Number.isInteger(index) || index < 0 || index % 2 !== 0 || index >= this.stringCount * 2)
|
||||
if (index >>> 0 !== index || index % 2 !== 0 || index >= this.stringCount * 2)
|
||||
throw new AstDecodeError(`invalid string index ${index}`);
|
||||
const cached = this.stringCache.get(index);
|
||||
const cached = this.stringCache[index / 2];
|
||||
if (cached !== undefined) return cached;
|
||||
const start = astU32(this.bytes, this.stringOffsets + index * 4);
|
||||
const end = astU32(this.bytes, this.stringOffsets + (index + 1) * 4);
|
||||
const text = decodeAstString(this.bytes, this.strings + start, end - start);
|
||||
this.stringCache.set(index, text);
|
||||
this.stringCache[index / 2] = text;
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -250,7 +289,7 @@ export class AstWireFile {
|
||||
}
|
||||
|
||||
firstChild(index: number): number {
|
||||
this.nodeOffset(index);
|
||||
this.nodeWord(index);
|
||||
return index + 1 < this.nodeCount && this.parent(index + 1) === index ? index + 1 : 0;
|
||||
}
|
||||
|
||||
@@ -360,7 +399,7 @@ export class AstWireFile {
|
||||
const result: number[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const index = reader.uint();
|
||||
this.nodeOffset(index);
|
||||
this.nodeWord(index);
|
||||
if (index === 0 || this.kind(index) === KIND_NODE_LIST)
|
||||
throw new AstDecodeError("structured reference is not a node");
|
||||
result.push(index);
|
||||
|
||||
@@ -298,9 +298,13 @@ export class Ts7SessionProgram {
|
||||
if (source === undefined) {
|
||||
const bytes = this.session.binary("getSourceFile", JSON.stringify(this.query(file)));
|
||||
if (bytes.length !== 0) {
|
||||
const ast = new AstFile(bytes, () => {
|
||||
this.session.timing.materialized();
|
||||
});
|
||||
const ast = new AstFile(
|
||||
bytes,
|
||||
() => {
|
||||
this.session.timing.materialized();
|
||||
},
|
||||
{ writeTargets: true },
|
||||
);
|
||||
this.session.timing.fetched(Math.max(0, ast.wire.nodeCount - 2));
|
||||
source = this.session.cache.set(path, ast.sourceFile, this.snapshot.id, this.project.id);
|
||||
}
|
||||
|
||||
+317
-35
@@ -1,5 +1,5 @@
|
||||
import { optimizationField, type NativeOptimization } from "./backend/optimization.js";
|
||||
import { compilationTiming, type CompilationTiming } from "./timing.js";
|
||||
import { compilationTiming, type CompilationTiming, type TimingClock } from "./timing.js";
|
||||
import { prepareExecutableModule } from "./executable/prepare.js";
|
||||
import {
|
||||
prepareLibrary,
|
||||
@@ -45,9 +45,13 @@ export type {
|
||||
} from "./compile-types.js";
|
||||
import { compilePackedLibrary } from "./backend/library-pack.js";
|
||||
import { InternalCompilerError } from "./errors.js";
|
||||
import { frontendProcessExecArgv } from "./frontend-process-args.js";
|
||||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
clearCcCaches,
|
||||
configuredTargetPlatform,
|
||||
@@ -67,6 +71,9 @@ import { emitLlvmModuleSource, LlvmUnsupportedError } from "./backend/llvm/emitt
|
||||
import { inlineRcOption } from "./backend/llvm/shapes.js";
|
||||
import {
|
||||
emitNativeArtifact,
|
||||
isLlvmFile,
|
||||
llvmInputBytes,
|
||||
type LlvmInput,
|
||||
NativeCodegenError,
|
||||
writeLlvmSource,
|
||||
} from "./backend/native-codegen.js";
|
||||
@@ -114,16 +121,24 @@ import {
|
||||
import { type IrModule, type SrcLoc } from "./ir/ir.js";
|
||||
import { moduleRuntimeFeatures } from "./ir/runtime-features.js";
|
||||
import { serializeModuleParts } from "./ir/serialize.js";
|
||||
import { writeTextParts } from "./text-parts.js";
|
||||
import { readTextParts, writeTextParts } from "./text-parts.js";
|
||||
import { validateModule } from "./ir/validate.js";
|
||||
import { loadProgram } from "./frontend/program-node.js";
|
||||
import { runFrontend, type FrontendFactory } from "./frontend/pipeline.js";
|
||||
import { provenanceSources } from "./frontend/provenance-registry.js";
|
||||
import {
|
||||
type ProvenanceSources,
|
||||
provenanceSources,
|
||||
setProvenanceSources,
|
||||
} from "./frontend/provenance-registry.js";
|
||||
import { clearResolveCaches } from "./frontend/resolve.js";
|
||||
import { loadFfiProfile, type FfiProfile } from "./ffi/ffi-manifest.js";
|
||||
import { executableLinkFeatures } from "./backend/executable-features.js";
|
||||
import { youngNurseryRequested } from "./backend/young-nursery.js";
|
||||
import { FrontendInputTracker, trackedReadFile } from "./frontend/input-tracker.js";
|
||||
import {
|
||||
type FrontendInputSnapshot,
|
||||
FrontendInputTracker,
|
||||
trackedReadFile,
|
||||
} from "./frontend/input-tracker.js";
|
||||
import {
|
||||
libraryFrontendImplementationFingerprint,
|
||||
publishEarlyLibraryCache,
|
||||
@@ -135,9 +150,11 @@ import {
|
||||
type SemanticLibraryCacheHit,
|
||||
} from "./library/library-cache.js";
|
||||
import {
|
||||
hasEarlyExecutableNativeBuild,
|
||||
publishEarlyExecutableCache,
|
||||
publishEarlyExecutableRoute,
|
||||
readEarlyExecutableCache,
|
||||
recordEarlyExecutableNativeBuild,
|
||||
type EarlyExecutableCacheOptions,
|
||||
type EarlyExecutableNativeFeatures,
|
||||
} from "./executable/executable-cache.js";
|
||||
@@ -419,10 +436,16 @@ export async function compile(
|
||||
clearCompileSessionCaches();
|
||||
const frontendInputs = new FrontendInputTracker();
|
||||
const warnings: CompileWarningSink = { warnings: [], sourceTexts: null };
|
||||
const result = await frontendInputs.run(() =>
|
||||
compileTracked(entryPath, opts, frontendInputs, warnings),
|
||||
);
|
||||
return withCompileWarnings(result, warnings);
|
||||
// compileTracked starts it for a build with no earlier build of the same invocation.
|
||||
const frontendProcess = preparesInChildProcess(opts) ? new FrontendProcess() : undefined;
|
||||
try {
|
||||
const result = await frontendInputs.run(() =>
|
||||
compileTracked(entryPath, opts, frontendInputs, warnings, frontendProcess),
|
||||
);
|
||||
return withCompileWarnings(result, warnings);
|
||||
} finally {
|
||||
frontendProcess?.release();
|
||||
}
|
||||
}
|
||||
|
||||
/** Divergence warnings collected while a compile lowers (none when a cache
|
||||
@@ -561,7 +584,7 @@ async function compileExecutableNative(
|
||||
...(features.handleProps ? { handleProps: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.dynBufferNum ? { dynBufferNum: true } : {}),
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
...(features.builtinValues ? { builtinValues: true } : {}),
|
||||
events: features.events,
|
||||
emitter: features.emitter,
|
||||
@@ -613,7 +636,7 @@ interface NativeProgramObject {
|
||||
async function emitNativeProgramObject(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
llvm: string | readonly string[],
|
||||
llvm: LlvmInput,
|
||||
features: EarlyExecutableNativeFeatures,
|
||||
profile: NativeProfileOptions | undefined,
|
||||
): Promise<NativeProgramObject> {
|
||||
@@ -625,10 +648,7 @@ async function emitNativeProgramObject(
|
||||
const linkPath = `${privateSiblingPath(artifactPath, "native-program-object")}.o`;
|
||||
// The validated program-object artifact is a single object by contract.
|
||||
const target = nativeCodegenTarget();
|
||||
const llvmBytes = (typeof llvm === "string" ? [llvm] : llvm).reduce(
|
||||
(bytes, part) => bytes + Buffer.byteLength(part),
|
||||
0,
|
||||
);
|
||||
const llvmBytes = llvmInputBytes(llvm);
|
||||
const partitions =
|
||||
opts.nativeProgramObject === true || target === null ? 1 : nativeProgramPartitions(llvmBytes);
|
||||
// Optimized programs import small runtime functions from the bitcode of
|
||||
@@ -718,7 +738,7 @@ function runtimePackDiagnostic(error: RuntimePackError, entryPath: string): ScrD
|
||||
}
|
||||
|
||||
interface PreparedExecutable {
|
||||
llvmSource: string | readonly string[];
|
||||
llvmSource: LlvmInput;
|
||||
llvmPath: string;
|
||||
irPath: string | undefined;
|
||||
nativeFeatures: EarlyExecutableNativeFeatures;
|
||||
@@ -886,13 +906,247 @@ async function prepareExecutableInput(
|
||||
return { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts };
|
||||
}
|
||||
|
||||
/**
|
||||
* The child process that prepares an executable build's input: frontend, lowering and LLVM emission
|
||||
* (`runFrontendProcess`). Node's heap keeps the pages a build's frontend used for the life of the
|
||||
* process, so running them in a process of their own leaves the native helper the memory they took.
|
||||
*/
|
||||
const frontendProcessEntry = fileURLToPath(new URL("./frontend-process.js", import.meta.url));
|
||||
|
||||
/**
|
||||
* Whether `prepareExecutableInputInChild` may prepare this build: optimized executables, when the
|
||||
* compiler runs from its built files. Development builds split the LLVM text in this process, and
|
||||
* SCRIPTC_FRONTEND_PROCESS=0 keeps the frontend in-process.
|
||||
*
|
||||
* compileTracked starts the process only when no earlier build of the same invocation emitted its
|
||||
* native program object (`hasEarlyExecutableNativeBuild`). A rebuild finds most native partitions
|
||||
* in the helper's cache, so its memory peaks in the frontend, and the parent process would add to
|
||||
* that peak.
|
||||
*/
|
||||
function preparesInChildProcess(opts: CompileRequestOptions): boolean {
|
||||
return (
|
||||
process.env["SCRIPTC_FRONTEND_PROCESS"] !== "0" &&
|
||||
(opts.outputKind ?? "exe") === "exe" &&
|
||||
(opts.optimization ?? "release") !== "dev" &&
|
||||
existsSync(frontendProcessEntry)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The frontend process (frontend-process.ts) of one compile. `start` launches it; it then waits on
|
||||
* its stdin for the paths of a request and its result (`run`), and exits once its stdin closes
|
||||
* without them (`release`).
|
||||
*/
|
||||
class FrontendProcess {
|
||||
private child: ChildProcess | undefined;
|
||||
private exited: Promise<number | null> | undefined;
|
||||
private sent = false;
|
||||
|
||||
/** Whether `start` launched the process. */
|
||||
get started(): boolean {
|
||||
return this.child !== undefined;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.child !== undefined) return;
|
||||
const execArgv = frontendProcessExecArgv(process.execArgv);
|
||||
const child = spawn(process.execPath, [...execArgv, frontendProcessEntry], {
|
||||
stdio: ["pipe", "inherit", "inherit"],
|
||||
});
|
||||
this.exited = new Promise<number | null>((resolveExit) => {
|
||||
child.on("error", () => resolveExit(null));
|
||||
child.on("exit", (code) => resolveExit(code));
|
||||
});
|
||||
// A failed start surfaces as a failed run; the build then prepares its input in-process.
|
||||
child.stdin!.on("error", () => undefined);
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
/** Sends the request at `requestPath`; resolves with the process's exit code. */
|
||||
run(requestPath: string, resultPath: string): Promise<number | null> {
|
||||
if (this.child === undefined || this.exited === undefined || this.sent)
|
||||
return Promise.resolve(null);
|
||||
this.sent = true;
|
||||
this.child.stdin!.end(`${requestPath}\n${resultPath}\n`);
|
||||
return this.exited;
|
||||
}
|
||||
|
||||
/** Lets the process exit if no request was sent. */
|
||||
release(): void {
|
||||
if (this.child === undefined || this.sent) return;
|
||||
this.sent = true;
|
||||
this.child.stdin!.end();
|
||||
// The build is done with it: this process need not wait for it to exit.
|
||||
this.child.unref();
|
||||
(this.child.stdin as { unref?: () => void }).unref?.();
|
||||
}
|
||||
}
|
||||
|
||||
interface FrontendProcessRequest {
|
||||
entryPath: string;
|
||||
opts: CompileRequestOptions;
|
||||
buildPlatform: string;
|
||||
/** The compile's SCRIPTC_TIMING clock, which the frontend process's phases continue. */
|
||||
timing: TimingClock;
|
||||
/** The compile's --provenance-sources mappings, which the frontend process installs. */
|
||||
provenance: ProvenanceSources | null;
|
||||
}
|
||||
|
||||
/** What the frontend process sends back. A thrown error comes back as `failed`. */
|
||||
type FrontendProcessResult =
|
||||
| {
|
||||
kind: "prepared";
|
||||
llvmPath: string;
|
||||
/** A private copy of the LLVM text, which the native helper reads (`nativeInputPath`). */
|
||||
nativeInputPath: string;
|
||||
llvmBytes: number;
|
||||
irPath: string | undefined;
|
||||
nativeFeatures: EarlyExecutableNativeFeatures;
|
||||
sourceTexts: [string, string][];
|
||||
warnings: ScrDiagnostic[];
|
||||
frontend: FrontendInputSnapshot;
|
||||
}
|
||||
| {
|
||||
kind: "diagnostics";
|
||||
diagnostics: ScrDiagnostic[];
|
||||
sourceTexts: [string, string][];
|
||||
warnings: ScrDiagnostic[];
|
||||
frontend: FrontendInputSnapshot;
|
||||
}
|
||||
| { kind: "failed" };
|
||||
|
||||
/**
|
||||
* `prepareExecutableInput` in the frontend process, with the LLVM text passed on as its file. The
|
||||
* process's frontend inputs join `frontendInputs` and its warnings `warningSink`. If it fails, the
|
||||
* input is prepared here, so the build reports the same errors.
|
||||
*/
|
||||
async function prepareExecutableInputInChild(
|
||||
frontendProcess: FrontendProcess,
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
ffi: FfiProfile | null,
|
||||
buildPlatform: string,
|
||||
timing: CompilationTiming,
|
||||
timingClock: TimingClock,
|
||||
frontendInputs: FrontendInputTracker,
|
||||
warningSink?: CompileWarningSink,
|
||||
): Promise<PreparedExecutable | CompileRequestResult> {
|
||||
await mkdir(opts.outDir, { recursive: true });
|
||||
const requestPath = privateSiblingPath(join(opts.outDir, "frontend-request.json"), "frontend");
|
||||
const resultPath = privateSiblingPath(join(opts.outDir, "frontend-result.json"), "frontend");
|
||||
let result: FrontendProcessResult = { kind: "failed" };
|
||||
try {
|
||||
const request: FrontendProcessRequest = {
|
||||
entryPath,
|
||||
opts,
|
||||
buildPlatform,
|
||||
timing: timingClock,
|
||||
provenance: provenanceSources(),
|
||||
};
|
||||
await writeFile(requestPath, JSON.stringify(request), { mode: 0o600 });
|
||||
const exitCode = await frontendProcess.run(requestPath, resultPath);
|
||||
if (exitCode === 0)
|
||||
result = JSON.parse(await readFile(resultPath, "utf8")) as FrontendProcessResult;
|
||||
} catch {
|
||||
result = { kind: "failed" };
|
||||
} finally {
|
||||
await rm(requestPath, { force: true });
|
||||
await rm(resultPath, { force: true });
|
||||
}
|
||||
if (result.kind === "failed")
|
||||
return prepareExecutableInput(entryPath, opts, ffi, buildPlatform, timing, warningSink);
|
||||
timing("frontend-process");
|
||||
for (const probe of result.frontend.probes) frontendInputs.record(probe);
|
||||
if (!result.frontend.stable) frontendInputs.markUnstable();
|
||||
const sourceTexts = new Map(result.sourceTexts);
|
||||
if (warningSink !== undefined && result.warnings.length > 0) {
|
||||
warningSink.warnings = result.warnings;
|
||||
warningSink.sourceTexts = sourceTexts;
|
||||
}
|
||||
if (result.kind === "diagnostics")
|
||||
return { ok: false, diagnostics: result.diagnostics, sourceTexts };
|
||||
return {
|
||||
llvmSource: { file: result.nativeInputPath, bytes: result.llvmBytes },
|
||||
llvmPath: result.llvmPath,
|
||||
irPath: result.irPath,
|
||||
nativeFeatures: result.nativeFeatures,
|
||||
programSplit: null,
|
||||
sourceTexts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The frontend process (frontend-process.ts): prepares the executable input of the request at
|
||||
* `requestPath` as `compileTracked` would in-process, and writes what it prepared, without the LLVM
|
||||
* text, to `resultPath`.
|
||||
*/
|
||||
export async function runFrontendProcess(requestPath: string, resultPath: string): Promise<void> {
|
||||
const request = JSON.parse(await readFile(requestPath, "utf8")) as FrontendProcessRequest;
|
||||
clearCompileSessionCaches();
|
||||
setProvenanceSources(request.provenance);
|
||||
const frontendInputs = new FrontendInputTracker();
|
||||
const warnings: CompileWarningSink = { warnings: [], sourceTexts: null };
|
||||
let result: FrontendProcessResult = { kind: "failed" };
|
||||
try {
|
||||
let ffi: FfiProfile | null = null;
|
||||
if (request.opts.ffiProfilePath !== undefined) {
|
||||
const loaded = loadFfiProfile(resolve(request.opts.ffiProfilePath));
|
||||
if (!loaded.ok) throw new InternalCompilerError("the FFI profile could not be reloaded");
|
||||
ffi = loaded.profile;
|
||||
}
|
||||
const prepared = await frontendInputs.run(() =>
|
||||
prepareExecutableInput(
|
||||
request.entryPath,
|
||||
request.opts,
|
||||
ffi,
|
||||
request.buildPlatform,
|
||||
compilationTiming(request.timing),
|
||||
warnings,
|
||||
),
|
||||
);
|
||||
const frontend = frontendInputs.snapshot();
|
||||
if (!("ok" in prepared)) {
|
||||
// The public .ll file may be rewritten by a concurrent build of another entry with the
|
||||
// same output stem before the native helper reads it (see emitNativeArtifact).
|
||||
if (isLlvmFile(prepared.llvmSource))
|
||||
throw new InternalCompilerError("in-process preparation returned LLVM text as a file");
|
||||
const nativeInputPath = privateSiblingPath(prepared.llvmPath, "frontend-llvm");
|
||||
await writeLlvmSource(nativeInputPath, prepared.llvmSource, 0o600);
|
||||
result = {
|
||||
kind: "prepared",
|
||||
llvmPath: prepared.llvmPath,
|
||||
nativeInputPath,
|
||||
llvmBytes: llvmInputBytes(prepared.llvmSource),
|
||||
irPath: prepared.irPath,
|
||||
nativeFeatures: prepared.nativeFeatures,
|
||||
sourceTexts: [...prepared.sourceTexts],
|
||||
warnings: warnings.warnings,
|
||||
frontend,
|
||||
};
|
||||
} else if (!prepared.ok) {
|
||||
result = {
|
||||
kind: "diagnostics",
|
||||
diagnostics: prepared.diagnostics,
|
||||
sourceTexts: [...prepared.sourceTexts],
|
||||
warnings: warnings.warnings,
|
||||
frontend,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
result = { kind: "failed" };
|
||||
}
|
||||
await writeFile(resultPath, JSON.stringify(result), { mode: 0o600 });
|
||||
}
|
||||
|
||||
async function compileTracked(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
frontendInputs: FrontendInputTracker,
|
||||
warningSink?: CompileWarningSink,
|
||||
frontendProcess?: FrontendProcess,
|
||||
): Promise<CompileRequestResult> {
|
||||
const timing = compilationTiming();
|
||||
const timingStart = Date.now();
|
||||
entryPath = resolve(entryPath);
|
||||
const outputKind = opts.outputKind ?? "exe";
|
||||
if (
|
||||
@@ -1121,18 +1375,12 @@ async function compileTracked(
|
||||
if (outputKind === "exe") {
|
||||
const helperObjectRoute =
|
||||
opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, "llvm");
|
||||
// Package content and native driver discovery are independent inputs to
|
||||
// the same cache key. Start both before waiting so edits do not pay their
|
||||
// filesystem and subprocess latency serially.
|
||||
const [implementation, nativeEnvironment] = await Promise.all([
|
||||
compilerImplementationIdentity(),
|
||||
helperObjectRoute
|
||||
? executableLinkerEnvironmentFingerprint(process.env, nativeCodegenTarget()?.defaultLinker)
|
||||
: executableNativeEnvironmentFingerprint(),
|
||||
]);
|
||||
// Calls into Node record the libnode pack (node-islands/libnode.ts).
|
||||
const libnodeKey = opts.libnode ?? process.env["SCRIPTC_LIBNODE"];
|
||||
earlyCacheOptions = {
|
||||
const routeOptions: Omit<
|
||||
EarlyExecutableCacheOptions,
|
||||
"implementation" | "implementationDependencies" | "nativeEnvironment"
|
||||
> = {
|
||||
entryPath,
|
||||
outDir: opts.outDir,
|
||||
outPath: opts.outPath,
|
||||
@@ -1164,8 +1412,27 @@ async function compileTracked(
|
||||
? resolvePlatformLinker(process.env, nativeCodegenTarget()?.defaultLinker)
|
||||
: (process.env["SCRIPTC_CC"] ?? "clang"),
|
||||
],
|
||||
nativeEnvironment,
|
||||
nodeVersion: process.version,
|
||||
};
|
||||
// The frontend process prepares the first build of an invocation (preparesInChildProcess).
|
||||
// Started here, it loads the compiler while this process computes the rest of the cache key.
|
||||
if (
|
||||
frontendProcess !== undefined &&
|
||||
!(await hasEarlyExecutableNativeBuild(cacheRoot, routeOptions))
|
||||
)
|
||||
frontendProcess.start();
|
||||
// Package content and native driver discovery are independent inputs to
|
||||
// the same cache key. Start both before waiting so edits do not pay their
|
||||
// filesystem and subprocess latency serially.
|
||||
const [implementation, nativeEnvironment] = await Promise.all([
|
||||
compilerImplementationIdentity(),
|
||||
helperObjectRoute
|
||||
? executableLinkerEnvironmentFingerprint(process.env, nativeCodegenTarget()?.defaultLinker)
|
||||
: executableNativeEnvironmentFingerprint(),
|
||||
]);
|
||||
earlyCacheOptions = {
|
||||
...routeOptions,
|
||||
nativeEnvironment,
|
||||
implementation: implementation.digest,
|
||||
implementationDependencies: implementation.dependencies,
|
||||
};
|
||||
@@ -1216,7 +1483,9 @@ async function compileTracked(
|
||||
nativeProgramObject = await emitNativeProgramObject(
|
||||
entryPath,
|
||||
opts,
|
||||
await readFile(earlyHit.llvmPath, "utf8"),
|
||||
// A large program's LLVM exceeds the engine's string limit as one
|
||||
// string; the emitter takes it in parts, as on a cold build.
|
||||
await readTextParts(earlyHit.llvmPath),
|
||||
earlyHit.native,
|
||||
profile,
|
||||
);
|
||||
@@ -1299,14 +1568,20 @@ async function compileTracked(
|
||||
};
|
||||
}
|
||||
timing("executable-cache-miss");
|
||||
const prepared = await prepareExecutableInput(
|
||||
entryPath,
|
||||
opts,
|
||||
ffi,
|
||||
buildPlatform,
|
||||
timing,
|
||||
warningSink,
|
||||
);
|
||||
const prepared =
|
||||
frontendProcess?.started === true
|
||||
? await prepareExecutableInputInChild(
|
||||
frontendProcess,
|
||||
entryPath,
|
||||
opts,
|
||||
ffi,
|
||||
buildPlatform,
|
||||
timing,
|
||||
{ start: timingStart, previous: Date.now() },
|
||||
frontendInputs,
|
||||
warningSink,
|
||||
)
|
||||
: await prepareExecutableInput(entryPath, opts, ffi, buildPlatform, timing, warningSink);
|
||||
if ("ok" in prepared) return prepared;
|
||||
const { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts } = prepared;
|
||||
const backend = "llvm" as const;
|
||||
@@ -1335,6 +1610,9 @@ async function compileTracked(
|
||||
profile,
|
||||
);
|
||||
timing("native-object");
|
||||
await recordEarlyExecutableNativeBuild(cacheRoot, executableCacheOptions).catch(
|
||||
() => undefined,
|
||||
);
|
||||
} catch (err) {
|
||||
if (!(err instanceof NativeCodegenError)) throw err;
|
||||
return {
|
||||
@@ -1395,6 +1673,8 @@ async function compileTracked(
|
||||
nativeProgramObject.partitionPaths,
|
||||
);
|
||||
}
|
||||
// The frontend process's private copy of the LLVM text.
|
||||
if (isLlvmFile(llvmSource)) await rm(llvmSource.file, { force: true });
|
||||
}
|
||||
if (!publishedExecutable) {
|
||||
await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, {
|
||||
@@ -1445,6 +1725,7 @@ function libraryNativeFeatures(mod: IrModule, backend: "llvm"): EarlyLibraryNati
|
||||
copying: features.copying,
|
||||
textDecoderLegacy: features.legacyTextDecoder,
|
||||
dynInvoke: features.dynInvoke,
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
...(features.dynTier ? { dynTier: true } : {}),
|
||||
...(mod.lib?.identity !== undefined ? { buildId: mod.lib.identity.buildId } : {}),
|
||||
};
|
||||
@@ -1539,6 +1820,7 @@ async function compileLibraryNative(
|
||||
copying: features.copying,
|
||||
textDecoderLegacy: features.textDecoderLegacy,
|
||||
dynInvoke: features.dynInvoke,
|
||||
...(features.dynBuffer === true ? { dynBuffer: true } : {}),
|
||||
...(features.dynTier === true ? { dynTier: true } : {}),
|
||||
};
|
||||
if (packTarget !== null) await compilePackedLibrary(archiveOptions, packTarget);
|
||||
|
||||
@@ -240,12 +240,16 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// 'error' has no listener, Node's unhandled-'error' contract.
|
||||
"emitter.emit",
|
||||
"emitter.emitFlex",
|
||||
"emitter.emitAny",
|
||||
"emitter.emitMixed",
|
||||
"emitter.listenersDyn",
|
||||
"emitter.emitError",
|
||||
"emitter.on",
|
||||
"emitter.off",
|
||||
"emitter.checkListener",
|
||||
"emitter.onDyn",
|
||||
"emitter.onFlex",
|
||||
"emitter.onAny",
|
||||
"emitter.offDyn",
|
||||
"emitter.onData",
|
||||
"emitter.onDataDyn",
|
||||
|
||||
@@ -3148,6 +3148,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* scandir failure becomes its rejection. */
|
||||
"fsp.readdirTypes": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
|
||||
"fsp.rm": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
|
||||
/** `fs.promises.rm(path, options)`: fs.rmOptsSync and fs.rmRetrySync
|
||||
* behind a settled promise; their errors become its rejection. */
|
||||
"fsp.rmOpts": { argTypes: [STRING, BOOL, BOOL], result: { kind: "promise", inner: VOID } },
|
||||
"fsp.rmRetry": {
|
||||
argTypes: [STRING, BOOL, BOOL, F64, F64],
|
||||
result: { kind: "promise", inner: VOID },
|
||||
},
|
||||
"fsp.stat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } },
|
||||
"fsp.realpath": { argTypes: [STRING], result: { kind: "promise", inner: STRING } },
|
||||
"fsp.lstat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } },
|
||||
@@ -3765,16 +3772,31 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"emitter.checkListener": { argTypes: [DYN], result: VOID },
|
||||
"emitter.onDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID },
|
||||
"emitter.onFlex": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID },
|
||||
/** on/once/prepend* and emit through an untyped value (the class
|
||||
* dispatch of compiled emitters): onFlex/emitFlex plus runtime typed
|
||||
* adapters under 'error' and the meta events, and Node's unhandled-'error'
|
||||
* contract. emitAny is VARIADIC like emitFlex. */
|
||||
"emitter.onAny": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID },
|
||||
"emitter.offDyn": { argTypes: [null, STRING, DYN], result: VOID },
|
||||
"emitter.removeAll": { argTypes: [null, STRING, BOOL], result: VOID },
|
||||
"emitter.emit": { argTypes: [null, STRING], result: BOOL },
|
||||
"emitter.emitFlex": { argTypes: [null, STRING], result: BOOL },
|
||||
"emitter.emitAny": { argTypes: [null, STRING], result: BOOL },
|
||||
/** An untyped emit of an event with a fixed tuple: (recv, name, k, then
|
||||
* k checked-dynamic arguments as given, then the event's converted
|
||||
* tuple) — VARIADIC like emitter.emit. hasTyped answers whether a
|
||||
* listener of the name needs the converted tuple. */
|
||||
"emitter.emitMixed": { argTypes: [null, STRING, F64], result: BOOL },
|
||||
"emitter.hasTyped": { argTypes: [null, STRING], result: BOOL },
|
||||
"emitter.emitError": { argTypes: [null, STRING, null], result: BOOL },
|
||||
"emitter.count": { argTypes: [null, STRING], result: F64 },
|
||||
"emitter.countFn": { argTypes: [null, STRING, null], result: F64 },
|
||||
"emitter.countDyn": { argTypes: [null, STRING, DYN], result: F64 },
|
||||
"emitter.names": { argTypes: [null], result: VOID },
|
||||
"emitter.listeners": { argTypes: [null, STRING], result: VOID },
|
||||
/** listeners()/rawListeners() through an untyped value: a checked-
|
||||
* dynamic array (throws when a listener has typed parameters). */
|
||||
"emitter.listenersDyn": { argTypes: [null, STRING], result: DYN },
|
||||
/** Stream-'data' registration twins of emitter.on/onDyn (same args):
|
||||
* chosen at registration sites whose receiver is stream-rooted, so the
|
||||
* backend emits DATA thunks — the runtime's 'data' emission carries
|
||||
@@ -4631,8 +4653,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* return value has no lowering (statement position only, frontend-
|
||||
* fenced). rmOptsSync is rmSync with (recursive, force) bools: force
|
||||
* swallows ENOENT, recursive removes trees post-order, a directory
|
||||
* without recursive throws the EISDIR-worded error (divergence 13's
|
||||
* wording note). mkdtempSync appends the six X's and returns the
|
||||
* without recursive throws Node's ERR_FS_EISDIR SystemError. mkdtempSync appends the six X's and returns the
|
||||
* created path (+1). accessSync takes the F_OK/R_OK/W_OK/X_OK bits as
|
||||
* one f64 (the frontend bakes fs.constants.* as literals and completes
|
||||
* an omitted mode to 0). readFdSync/readFdSyncBytes are the
|
||||
@@ -4643,7 +4664,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** rmOptsSync's maxRetries/retryDelay form (path, recursive, force,
|
||||
* maxRetries, retryDelay): Node's linear-backoff retry on
|
||||
* EBUSY/EMFILE/ENFILE/ENOTEMPTY/EPERM — the tmpdir-harness shape
|
||||
* rmSync(p, { maxRetries: 3, recursive: true, force: true }). */
|
||||
* rmSync(p, { maxRetries: 3, recursive: true, force: true }). Throws
|
||||
* ERR_OUT_OF_RANGE for a retryDelay outside int32 >= 0 or a maxRetries
|
||||
* outside uint32, before checking the path. */
|
||||
"fs.rmRetrySync": { argTypes: [STRING, BOOL, BOOL, F64, F64], result: VOID },
|
||||
"fs.mkdtempSync": { argTypes: [STRING], result: STRING },
|
||||
"fs.accessSync": { argTypes: [STRING, F64], result: VOID },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isRefCounted, type IrClassDef, type IrExpr, type IrModule, type IrStmt } from "./ir.js";
|
||||
import { dynamicFieldStores } from "./int32-slots.js";
|
||||
import { dynamicFieldStores, type DynamicFieldStores } from "./int32-slots.js";
|
||||
import { everyExprChild, everyStmtChild } from "./traverse.js";
|
||||
|
||||
/** Whole-program final reference fields.
|
||||
@@ -78,11 +78,15 @@ export function constructorClass(fnName: string): string | null {
|
||||
/** `scalars` extends the analysis from reference-counted fields to fields of
|
||||
* every type, for consumers that read a final field's value once instead of
|
||||
* borrowing it (field-values.ts). */
|
||||
export function analyzeFinalFields(mod: IrModule, scalars = false): FinalFields {
|
||||
export function analyzeFinalFields(
|
||||
mod: IrModule,
|
||||
scalars = false,
|
||||
stores?: DynamicFieldStores,
|
||||
): FinalFields {
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
|
||||
const finals = new FinalFields(classes);
|
||||
if (mod.lib) return finals;
|
||||
const dynamic = dynamicFieldStores(mod);
|
||||
const dynamic = stores ?? dynamicFieldStores(mod);
|
||||
if (dynamic.all) return finals;
|
||||
|
||||
const genericFamilies = new Set<string>();
|
||||
|
||||
@@ -214,6 +214,8 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
|
||||
"contracts.programLifetime[]": "other",
|
||||
"wasmModules[].sha256": "other",
|
||||
"wasmModules[].bytes": "other",
|
||||
"wasmUntranslated[].sha256": "other",
|
||||
"wasmUntranslated[].reason": "other",
|
||||
"threadSlotKeys[]": "other",
|
||||
"nodeIsland.libnode": "other",
|
||||
"nodeIsland.nodeVersion": "other",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IrClassDef, IrExpr, IrFunction, IrModule, IrStmt, IrType } from "./ir.js";
|
||||
import type { IrClassDef, IrExpr, IrFunction, IrGlobal, IrModule, IrStmt, IrType } from "./ir.js";
|
||||
import {
|
||||
analyzeIntegerRanges,
|
||||
INT32_RANGE,
|
||||
@@ -458,11 +458,18 @@ function walkFunction(
|
||||
* whose key is not a literal), otherwise the
|
||||
* literal keys of `dyn.keySet`/`dyn.keyDelete`. A view commits a store back
|
||||
* into the instance's native field slots, which the IR does not show. */
|
||||
export interface DynamicFieldStores {
|
||||
all: boolean;
|
||||
names: Set<string>;
|
||||
}
|
||||
|
||||
/** Computes DynamicFieldStores with one walk of every function. Analyses of
|
||||
* the same module accept the result so the emitter walks the module once. */
|
||||
export function dynamicFieldStores(
|
||||
mod: IrModule,
|
||||
records = new Map((mod.records ?? []).map((r) => [r.id, r])),
|
||||
unions = new Map((mod.unions ?? []).map((u) => [u.id, u])),
|
||||
): { all: boolean; names: Set<string> } {
|
||||
): DynamicFieldStores {
|
||||
const dynTypes = new Map<IrType, boolean>();
|
||||
const dynNamed = new Map<string, boolean>();
|
||||
const dynish = (type: IrType): boolean => {
|
||||
@@ -522,7 +529,11 @@ export function dynamicFieldStores(
|
||||
* can write: the class reaches checked-dynamic code (directly or through
|
||||
* another exposed type), and some dynamic store in the program can name the
|
||||
* field. */
|
||||
export function dynamicallyStoredFamilies(mod: IrModule, slots: Int32Slots): Set<string> {
|
||||
export function dynamicallyStoredFamilies(
|
||||
mod: IrModule,
|
||||
slots: Int32Slots,
|
||||
stores?: DynamicFieldStores,
|
||||
): Set<string> {
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
|
||||
const children = new Map<string, string[]>();
|
||||
for (const cls of classes.values())
|
||||
@@ -588,7 +599,8 @@ export function dynamicallyStoredFamilies(mod: IrModule, slots: Int32Slots): Set
|
||||
// writes every field from the view, which holds the exact snapshot value
|
||||
// of any property no store named. So reaching dynamic code disqualifies
|
||||
// exactly the field names some dynamic store in the program can name.
|
||||
const { all: dynWritesAll, names: dynWritten } = dynamicFieldStores(mod, records, unions);
|
||||
const { all: dynWritesAll, names: dynWritten } =
|
||||
stores ?? dynamicFieldStores(mod, records, unions);
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
@@ -655,20 +667,11 @@ export function functionCallFacts(
|
||||
const runtimePrefixes: string[] = [];
|
||||
for (const cls of classes.values())
|
||||
if (runtimeRooted(cls.name)) runtimePrefixes.push(`%${cls.name}.`);
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
const referenced = exprFunctionReference(e);
|
||||
if (referenced !== undefined) escaped.add(referenced);
|
||||
if (e.kind !== "virtualCall") return;
|
||||
for (const name of subtree(e.className)) virtualTargets.add(`%${name}.${e.method}`);
|
||||
for (let c = classes.get(e.className); c; c = c.base ? classes.get(c.base) : undefined)
|
||||
virtualTargets.add(`%${c.name}.${e.method}`);
|
||||
},
|
||||
});
|
||||
// A class value can reach `new` through its construct thunk; one that
|
||||
// only feeds an instanceof interval check cannot.
|
||||
// only feeds an instanceof interval check cannot. Class references are
|
||||
// checked once every function has contributed its tested-only ones.
|
||||
const testedOnly = new Set<IrExpr>();
|
||||
const classRefs: (IrExpr & { kind: "classRef" })[] = [];
|
||||
for (const fn of mod.functions) {
|
||||
// Direct operands, and unboxed locals bound once to a class reference
|
||||
// and read only as instanceof operands (the lowering's temporary).
|
||||
@@ -681,11 +684,18 @@ export function functionCallFacts(
|
||||
};
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "instanceOfValue") {
|
||||
const referenced = exprFunctionReference(e);
|
||||
if (referenced !== undefined) escaped.add(referenced);
|
||||
if (e.kind === "virtualCall") {
|
||||
for (const name of subtree(e.className)) virtualTargets.add(`%${name}.${e.method}`);
|
||||
for (let c = classes.get(e.className); c; c = c.base ? classes.get(c.base) : undefined)
|
||||
virtualTargets.add(`%${c.name}.${e.method}`);
|
||||
} else if (e.kind === "instanceOfValue") {
|
||||
if (e.classValue.kind === "classRef") testedOnly.add(e.classValue);
|
||||
else if (e.classValue.kind === "varRef") count(tests, e.classValue.localId);
|
||||
} else if (e.kind === "varRef") count(reads, e.localId);
|
||||
else if (e.kind === "assignExpr" || e.kind === "incDec") count(writes, e.localId);
|
||||
else if (e.kind === "classRef") classRefs.push(e);
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" && s.init?.kind === "classRef") bound.set(s.localId, s.init);
|
||||
@@ -698,18 +708,19 @@ export function functionCallFacts(
|
||||
if (!boxed.has(id) && writes.get(id) === 1 && reads.get(id) === tests.get(id))
|
||||
testedOnly.add(ref);
|
||||
}
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "classRef" && !testedOnly.has(e)) escaped.add(`%${e.className}.constructor`);
|
||||
},
|
||||
});
|
||||
for (const e of classRefs) if (!testedOnly.has(e)) escaped.add(`%${e.className}.constructor`);
|
||||
const runtimeVirtual = (name: string): boolean =>
|
||||
runtimePrefixes.some((prefix) => name.startsWith(prefix));
|
||||
return { valueEscaped: escaped, virtualTargets, runtimeVirtual };
|
||||
}
|
||||
|
||||
export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
/** Facts about the same module that a caller has already computed. */
|
||||
export interface Int32SlotInputs {
|
||||
dynamicStores?: DynamicFieldStores;
|
||||
initializerBindings?: ReadonlyMap<string, IrGlobal[]>;
|
||||
}
|
||||
|
||||
export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): Int32Slots {
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
|
||||
const slots = new Int32Slots(classes);
|
||||
if (mod.lib) return slots;
|
||||
@@ -780,7 +791,7 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
}
|
||||
|
||||
/* ── dynamic, reflective and runtime exposure ── */
|
||||
for (const family of dynamicallyStoredFamilies(mod, slots)) {
|
||||
for (const family of dynamicallyStoredFamilies(mod, slots, inputs.dynamicStores)) {
|
||||
if (slots.isPinnedField(family)) continue;
|
||||
dropFamily(family, "dynamic stores can name the field");
|
||||
}
|
||||
@@ -799,56 +810,16 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
const dropAllParams = (fn: IrFunction, why: string): void => {
|
||||
for (const param of fn.params) dropParam(fn.name, param.localId, why);
|
||||
};
|
||||
// Only functions some IR call reaches directly get parameter facts: one
|
||||
// the backend invokes by convention has no call site to prove against.
|
||||
// One walk of every function collects what the steps below need: the
|
||||
// functions IR calls reach directly, calls whose arity differs from their
|
||||
// callee's, the captured bindings' boxes, and which functions read which
|
||||
// slot facts. Facts that depend on earlier steps are applied in walk order
|
||||
// where those steps complete.
|
||||
const called = slots.called;
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "call") called.add(e.callee);
|
||||
else if (e.kind === "new") called.add(`%${e.className}.constructor`);
|
||||
},
|
||||
});
|
||||
for (const fn of mod.functions) {
|
||||
if (fn.returnType.kind === "f64" && !fn.async && !fn.generator) {
|
||||
slots.addReturn(fn.name);
|
||||
slots.assumeNonNegative(`ret:${fn.name}`);
|
||||
}
|
||||
if (
|
||||
fn.async ||
|
||||
fn.generator ||
|
||||
!called.has(fn.name) ||
|
||||
escaped.has(fn.name) ||
|
||||
virtual(fn.name)
|
||||
)
|
||||
continue;
|
||||
for (const param of fn.params)
|
||||
if (param.type.kind === "f64" && !slots.isPinned(paramKey(fn.name, param.localId))) {
|
||||
slots.addParam(fn.name, param.localId);
|
||||
slots.assumeNonNegative(paramKey(fn.name, param.localId));
|
||||
}
|
||||
}
|
||||
const arityMismatches: IrFunction[] = [];
|
||||
const arityChecked = (fn: IrFunction | undefined, args: number, offset: number): void => {
|
||||
if (fn && fn.params.length !== args + offset) dropAllParams(fn, "call arity differs");
|
||||
if (fn && fn.params.length !== args + offset) arityMismatches.push(fn);
|
||||
};
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "call") arityChecked(functions.get(e.callee), e.args.length, 0);
|
||||
else if (e.kind === "new")
|
||||
arityChecked(functions.get(`%${e.className}.constructor`), e.args.length, 1);
|
||||
},
|
||||
});
|
||||
|
||||
// Candidates the fixpoint may drop; rangedParameters retries some.
|
||||
const candidates = new Map<string, string[]>();
|
||||
const retry = new Set<string>();
|
||||
for (const fn of mod.functions) {
|
||||
const ids = fn.params.filter((p) => slots.hasParam(fn.name, p.localId)).map((p) => p.localId);
|
||||
if (ids.length > 0) candidates.set(fn.name, ids);
|
||||
}
|
||||
|
||||
/* ── captured bindings ── */
|
||||
// A captured local lives in one box shared by its declaring function and
|
||||
// every closure that captures it (closure.captures[i] is the lifted
|
||||
// function's captures[i]). The box holds an int32 when every write to it
|
||||
@@ -864,6 +835,16 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
const boxKey = (fnName: string, localId: string): string => `${fnName}\0${localId}`;
|
||||
const boxMembers = new Set<string>();
|
||||
const unstableBoxes = new Set<string>();
|
||||
// Readers: which functions depend on which slot facts. Reads of a boxed
|
||||
// local resolve to its box once the boxes are known.
|
||||
const readers = new Map<string, Set<string>>();
|
||||
const reads = (slot: string, fnName: string): void => {
|
||||
const set = readers.get(slot);
|
||||
if (set) set.add(fnName);
|
||||
else readers.set(slot, new Set([fnName]));
|
||||
};
|
||||
const boxedReads: [IrFunction, Set<string>][] = [];
|
||||
const incrementedFamilies: string[] = [];
|
||||
for (const fn of mod.functions) {
|
||||
for (const local of fn.locals)
|
||||
if (local.boxed) {
|
||||
@@ -873,9 +854,27 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
}
|
||||
for (const capture of fn.classCaptures ?? [])
|
||||
unstableBoxes.add(boxKey(fn.name, capture.localId));
|
||||
const read = new Set<string>();
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "closure") {
|
||||
if (e.kind === "call") {
|
||||
called.add(e.callee);
|
||||
arityChecked(functions.get(e.callee), e.args.length, 0);
|
||||
reads(`ret:${e.callee}`, fn.name);
|
||||
} else if (e.kind === "new") {
|
||||
const constructor = `%${e.className}.constructor`;
|
||||
called.add(constructor);
|
||||
arityChecked(functions.get(constructor), e.args.length, 1);
|
||||
// Constructors return nothing: this only records their callers.
|
||||
reads(`ret:${constructor}`, fn.name);
|
||||
} else if (e.kind === "varRef") read.add(e.localId);
|
||||
else if (e.kind === "fieldGet") {
|
||||
const family = slots.family(e.className, e.field);
|
||||
if (family) reads(`field:${family}`, fn.name);
|
||||
} else if (e.kind === "fieldIncDec") {
|
||||
const family = slots.family(e.className, e.field);
|
||||
if (family) incrementedFamilies.push(family);
|
||||
} else if (e.kind === "closure") {
|
||||
const lifted = functions.get(e.fnName);
|
||||
e.captures.forEach((id, i) => {
|
||||
const inner = lifted?.captures?.[i];
|
||||
@@ -897,7 +896,41 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
unstableBoxes.add(boxKey(fn.name, s.catchLocalId));
|
||||
},
|
||||
});
|
||||
if (read.size > 0) boxedReads.push([fn, read]);
|
||||
}
|
||||
|
||||
// Only functions some IR call reaches directly get parameter facts: one
|
||||
// the backend invokes by convention has no call site to prove against.
|
||||
for (const fn of mod.functions) {
|
||||
if (fn.returnType.kind === "f64" && !fn.async && !fn.generator) {
|
||||
slots.addReturn(fn.name);
|
||||
slots.assumeNonNegative(`ret:${fn.name}`);
|
||||
}
|
||||
if (
|
||||
fn.async ||
|
||||
fn.generator ||
|
||||
!called.has(fn.name) ||
|
||||
escaped.has(fn.name) ||
|
||||
virtual(fn.name)
|
||||
)
|
||||
continue;
|
||||
for (const param of fn.params)
|
||||
if (param.type.kind === "f64" && !slots.isPinned(paramKey(fn.name, param.localId))) {
|
||||
slots.addParam(fn.name, param.localId);
|
||||
slots.assumeNonNegative(paramKey(fn.name, param.localId));
|
||||
}
|
||||
}
|
||||
for (const fn of arityMismatches) dropAllParams(fn, "call arity differs");
|
||||
|
||||
// Candidates the fixpoint may drop; rangedParameters retries some.
|
||||
const candidates = new Map<string, string[]>();
|
||||
const retry = new Set<string>();
|
||||
for (const fn of mod.functions) {
|
||||
const ids = fn.params.filter((p) => slots.hasParam(fn.name, p.localId)).map((p) => p.localId);
|
||||
if (ids.length > 0) candidates.set(fn.name, ids);
|
||||
}
|
||||
|
||||
/* ── captured bindings ── */
|
||||
const groups = new Map<string, string[]>();
|
||||
for (const key of boxMembers) {
|
||||
const root = boxRoot(key);
|
||||
@@ -960,34 +993,15 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
return slots.dropBox(group);
|
||||
};
|
||||
|
||||
/* ── readers: which functions depend on which slot facts ── */
|
||||
const readers = new Map<string, Set<string>>();
|
||||
const reads = (slot: string, fnName: string): void => {
|
||||
const set = readers.get(slot);
|
||||
if (set) set.add(fnName);
|
||||
else readers.set(slot, new Set([fnName]));
|
||||
};
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "fieldGet") {
|
||||
const family = slots.family(e.className, e.field);
|
||||
if (family) reads(`field:${family}`, fn.name);
|
||||
} else if (e.kind === "call") reads(`ret:${e.callee}`, fn.name);
|
||||
// Constructors return nothing: this only records their callers.
|
||||
else if (e.kind === "new") reads(`ret:%${e.className}.constructor`, fn.name);
|
||||
else if (e.kind === "varRef") {
|
||||
const group = slots.boxGroup(fn.name, e.localId);
|
||||
if (group !== null) reads(`box:${group}`, fn.name);
|
||||
} else if (e.kind === "fieldIncDec") {
|
||||
const family = slots.family(e.className, e.field);
|
||||
if (family) dropFamily(family, "++/-- on the field");
|
||||
}
|
||||
},
|
||||
});
|
||||
for (const [fn, read] of boxedReads)
|
||||
for (const localId of read) {
|
||||
const group = slots.boxGroup(fn.name, localId);
|
||||
if (group !== null) reads(`box:${group}`, fn.name);
|
||||
}
|
||||
for (const family of incrementedFamilies) dropFamily(family, "++/-- on the field");
|
||||
|
||||
/* ── the optimistic fixpoint ── */
|
||||
const bindings = findInitializerBindings(mod);
|
||||
const bindings = inputs.initializerBindings ?? findInitializerBindings(mod);
|
||||
const numeric = (fn: IrFunction): IrFunction =>
|
||||
withInitializerBindings(fn, bindings.get(fn.name) ?? []);
|
||||
const queue: string[] = mod.functions.map((fn) => fn.name);
|
||||
|
||||
@@ -1058,6 +1058,10 @@ export interface IrModule {
|
||||
* API (possibly empty). The backend appends each module's translation
|
||||
* and emits the registry the runtime looks modules up in. */
|
||||
wasmModules?: IrWasmModule[];
|
||||
/** The modules the build saw but does not translate, with the reason,
|
||||
* present with wasmModules. The backend emits them in a second table, so
|
||||
* loading one rejects with a CompileError that names the reason. */
|
||||
wasmUntranslated?: IrWasmUntranslated[];
|
||||
/** The program calls WebAssembly.Module.customSections, so translated
|
||||
* modules keep their custom sections (otherwise they carry none, and
|
||||
* debug sections cost nothing). */
|
||||
@@ -1073,6 +1077,16 @@ export interface IrWasmModule {
|
||||
bytes: string;
|
||||
}
|
||||
|
||||
/** A WebAssembly module whose bytes the build saw but does not translate. */
|
||||
export interface IrWasmUntranslated {
|
||||
/** Hex SHA-256 of the bytes. */
|
||||
sha256: string;
|
||||
/** The byte length. */
|
||||
size: number;
|
||||
/** Why the module is not translated, as a clause ("it uses simd, ..."). */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** Verified contract facts, keyed by IR names. */
|
||||
export interface IrContracts {
|
||||
/** Int32 class field families: `${topmost declaring class}\0${field}`. */
|
||||
@@ -3550,11 +3564,13 @@ export type IrExpr =
|
||||
* excluding callable and primitive references;
|
||||
* "instanceof" requires `className`: hierarchy payloads use the vtable
|
||||
* interval, while standalone classes use their exact retain adapter.
|
||||
* Other payloads answer false. `negated` flips the result (the `!==` spelling). */
|
||||
* Other payloads answer false. "dynamic" is a value thrown as unknown
|
||||
* (the payload caughtToDyn returns by reference). `negated` flips the
|
||||
* result (the `!==` spelling). */
|
||||
| {
|
||||
kind: "caughtTest";
|
||||
value: IrExpr;
|
||||
test: "string" | "number" | "boolean" | "object" | "instanceof";
|
||||
test: "string" | "number" | "boolean" | "object" | "instanceof" | "dynamic";
|
||||
className?: string;
|
||||
negated?: boolean;
|
||||
type: IrType;
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
import { everyModuleNode } from "./traverse.js";
|
||||
import { JsIcSites, hasJsIcSite } from "./js-ic.js";
|
||||
|
||||
/** The fixed-width Buffer read/write method names (both "UInt" and "Uint"
|
||||
* spellings) a checked-dynamic method call routes to scr_bytes_dyn.c. */
|
||||
export const DYN_BUFFER_NUM_METHODS: ReadonlySet<string> = new Set(
|
||||
const DYN_BUFFER_NUM_METHODS: ReadonlySet<string> = new Set(
|
||||
["read", "write"].flatMap((rw) => [
|
||||
...["UInt", "Uint"].flatMap((u) =>
|
||||
["8", "16LE", "16BE", "32LE", "32BE"].map((w) => `${rw}${u}${w}`),
|
||||
@@ -31,6 +29,45 @@ export const DYN_BUFFER_NUM_METHODS: ReadonlySet<string> = new Set(
|
||||
]),
|
||||
);
|
||||
|
||||
/** Buffer.prototype members scr_bytes_dyn.c refuses on Buffer receivers. */
|
||||
const DYN_BUFFER_REFUSED_METHODS: readonly string[] = [
|
||||
...["read", "write"].flatMap((rw) => [
|
||||
...["UIntLE", "UIntBE", "UintLE", "UintBE", "IntLE", "IntBE"].map((n) => `${rw}${n}`),
|
||||
...["BigUInt64", "BigUint64", "BigInt64"].flatMap((n) => [`${rw}${n}LE`, `${rw}${n}BE`]),
|
||||
]),
|
||||
...["ascii", "base64", "base64url", "latin1", "hex", "ucs2", "utf8"].flatMap((enc) => [
|
||||
`${enc}Slice`,
|
||||
`${enc}Write`,
|
||||
]),
|
||||
"inspect",
|
||||
];
|
||||
|
||||
/** The scr_bytes_dyn.c entry, one per method family, that a checked-dynamic
|
||||
* call of a Buffer.prototype method name routes to: the fixed-width reads and
|
||||
* writes (both "UInt" and "Uint" spellings), write, fill, toString, the
|
||||
* searches, and the other Buffer-only members. Each runs the method on a
|
||||
* Buffer receiver and gives any other receiver the ordinary dispatch. */
|
||||
export const DYN_BUFFER_METHODS: ReadonlyMap<string, string> = new Map([
|
||||
...[...DYN_BUFFER_NUM_METHODS].map((m): [string, string] => [m, "scr_dyn_invoke_buffer_num"]),
|
||||
["write", "scr_dyn_invoke_buffer_write"],
|
||||
["fill", "scr_dyn_invoke_buffer_fill"],
|
||||
["toString", "scr_dyn_invoke_buffer_to_string"],
|
||||
...["indexOf", "lastIndexOf", "includes"].map((m): [string, string] => [
|
||||
m,
|
||||
"scr_dyn_invoke_buffer_search",
|
||||
]),
|
||||
...[
|
||||
"toJSON",
|
||||
"equals",
|
||||
"compare",
|
||||
"copy",
|
||||
"swap16",
|
||||
"swap32",
|
||||
"swap64",
|
||||
...DYN_BUFFER_REFUSED_METHODS,
|
||||
].map((m): [string, string] => [m, "scr_dyn_invoke_buffer"]),
|
||||
]);
|
||||
|
||||
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// DEP0040 rides the dyn-async unit's warning machinery.
|
||||
"async.awaitDyn",
|
||||
@@ -694,10 +731,10 @@ const RUNTIME_FEATURE_RULES = {
|
||||
handleProps: rule({ fns: ["child.propsInstall"] }),
|
||||
/** for await over checked-dynamic values (scr_dyn_for_await.c). */
|
||||
dynForAwait: rule({ fns: ["dyn.asyncIteratorOpen", "dyn.asyncIteratorWasSync"] }),
|
||||
/** Fixed-width Buffer read/write methods called on checked-dynamic
|
||||
* values (scr_bytes_dyn.c). */
|
||||
dynBufferNum: rule({
|
||||
expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_NUM_METHODS.has(node.method),
|
||||
/** Buffer.prototype methods called on checked-dynamic values
|
||||
* (scr_bytes_dyn.c). */
|
||||
dynBuffer: rule({
|
||||
expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_METHODS.has(node.method),
|
||||
}),
|
||||
/** Builtin functions read as values (scr_builtin_values.c). */
|
||||
builtinValues: rule({ fns: ["buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue"] }),
|
||||
@@ -902,7 +939,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
streamDyn: false,
|
||||
handleProps: false,
|
||||
dynForAwait: false,
|
||||
dynBufferNum: false,
|
||||
dynBuffer: false,
|
||||
builtinValues: false,
|
||||
symbol: false,
|
||||
tls: false,
|
||||
|
||||
@@ -466,6 +466,18 @@ export function everyStmtChild(
|
||||
throw new Error("unhandled IR node");
|
||||
}
|
||||
|
||||
/** Called with each record literal mapExprChildren rebuilds and its
|
||||
* replacement, so a side table keyed by node identity (the lowering's
|
||||
* key-order witness notes) follows the literal into the rewritten tree. */
|
||||
let recordLiteralRebuilt: ((from: IrExpr, to: IrExpr) => void) | null = null;
|
||||
|
||||
/** Installs (or with null removes) the record-literal rebuild observer. */
|
||||
export function observeRecordLiteralRebuilds(
|
||||
observer: ((from: IrExpr, to: IrExpr) => void) | null,
|
||||
): void {
|
||||
recordLiteralRebuilt = observer;
|
||||
}
|
||||
|
||||
/** Copy a node with transformed immediate children; preserve all metadata. */
|
||||
export function mapExprChildren(
|
||||
node: IrExpr,
|
||||
@@ -653,11 +665,14 @@ export function mapExprChildren(
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "virtualCall":
|
||||
return { ...node, args: mapOf(node.args, expr) };
|
||||
case "recordLit":
|
||||
return {
|
||||
case "recordLit": {
|
||||
const rebuilt: IrExpr = {
|
||||
...node,
|
||||
fields: node.fields.map((child) => ({ ...child!, value: expr(child.value) })),
|
||||
};
|
||||
if (recordLiteralRebuilt !== null) recordLiteralRebuilt(node, rebuilt);
|
||||
return rebuilt;
|
||||
}
|
||||
case "recordClone":
|
||||
return {
|
||||
...node,
|
||||
|
||||
@@ -1117,7 +1117,7 @@ function validateFunction(
|
||||
// helpers. They are not source-level types and never return as values.
|
||||
if (fn.returnType.kind === "caught") err("return type is caught", fn.loc);
|
||||
for (const p of fn.params) {
|
||||
if (p.type.kind === "caught" && fn.name !== "%caught.dynamicValue")
|
||||
if (p.type.kind === "caught" && !fn.name.startsWith("%caught."))
|
||||
err(`param "${p.name}" is caught-typed`, fn.loc);
|
||||
if (!locals.has(p.localId)) {
|
||||
err(`param "${p.name}" has no local entry "${p.localId}"`, fn.loc);
|
||||
@@ -4080,6 +4080,8 @@ function validateFunction(
|
||||
const variadic =
|
||||
e.fn === "emitter.emit" ||
|
||||
e.fn === "emitter.emitFlex" ||
|
||||
e.fn === "emitter.emitAny" ||
|
||||
e.fn === "emitter.emitMixed" ||
|
||||
e.fn === "readable.new" ||
|
||||
e.fn === "writable.new" ||
|
||||
e.fn === "duplex.new" ||
|
||||
@@ -5847,6 +5849,7 @@ function validateFunction(
|
||||
e.fn === "emitter.off" ||
|
||||
e.fn === "emitter.onDyn" ||
|
||||
e.fn === "emitter.onFlex" ||
|
||||
e.fn === "emitter.onAny" ||
|
||||
e.fn === "stream.onDyn" ||
|
||||
e.fn === "emitter.offDyn" ||
|
||||
e.fn === "emitter.onData" ||
|
||||
@@ -5901,14 +5904,27 @@ function validateFunction(
|
||||
if (!ok) err(`libCall emitter.listeners must return a func array`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "emitter.emitFlex") {
|
||||
if (e.fn === "emitter.emitFlex" || e.fn === "emitter.emitAny") {
|
||||
if (e.args.slice(2).some((arg) => arg.type.kind !== "dyn")) {
|
||||
err("libCall emitter.emitFlex payloads must be checked-dynamic", e.loc);
|
||||
err(`libCall ${e.fn} payloads must be checked-dynamic`, e.loc);
|
||||
}
|
||||
}
|
||||
if (e.fn === "emitter.emitMixed") {
|
||||
const count = e.args[2];
|
||||
const k = count?.kind === "numLit" ? count.value : -1;
|
||||
if (!Number.isInteger(k) || k < 0 || e.args.length < 3 + k) {
|
||||
err("libCall emitter.emitMixed needs a literal argument count", e.loc);
|
||||
} else if (e.args.slice(3, 3 + k).some((arg) => arg.type.kind !== "dyn")) {
|
||||
err("libCall emitter.emitMixed untyped arguments must be checked-dynamic", e.loc);
|
||||
}
|
||||
}
|
||||
if (
|
||||
e.fn === "emitter.emit" ||
|
||||
e.fn === "emitter.emitFlex" ||
|
||||
e.fn === "emitter.emitAny" ||
|
||||
e.fn === "emitter.emitMixed" ||
|
||||
e.fn === "emitter.hasTyped" ||
|
||||
e.fn === "emitter.listenersDyn" ||
|
||||
e.fn === "emitter.count" ||
|
||||
e.fn === "emitter.getMax" ||
|
||||
e.fn === "emitter.ctor" ||
|
||||
|
||||
@@ -63,6 +63,8 @@ interface EarlyLibraryCacheStamp {
|
||||
export interface EarlyLibraryNativeFeatures {
|
||||
backend: "llvm";
|
||||
dynInvoke: boolean;
|
||||
/** Buffer.prototype methods on checked-dynamic values (scr_bytes_dyn.c). */
|
||||
dynBuffer?: boolean;
|
||||
dynTier?: boolean;
|
||||
regex: boolean;
|
||||
assert: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dirname, resolve } from "node:path";
|
||||
import type { CompileFailure } from "../compile-types.js";
|
||||
import { frontendDisposed } from "../frontend/dispose-hook.js";
|
||||
import {
|
||||
fenceSpeculativeWasiFunctions,
|
||||
moduleWasiUnavailableSurface,
|
||||
@@ -585,6 +586,7 @@ export function prepareLibrary(
|
||||
sourceTexts = fe.sourceTexts();
|
||||
} finally {
|
||||
fe.dispose();
|
||||
frontendDisposed();
|
||||
}
|
||||
const mod = lowered.module!;
|
||||
timing("frontend-dispose");
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "../backend/runtime-pack-native.js";
|
||||
import { mergeNativeProfiles } from "../backend/native-tools.js";
|
||||
import { setDeclarationRoot } from "../frontend/dts-paths.js";
|
||||
import { setFrontendDisposedHook } from "../frontend/dispose-hook.js";
|
||||
import { FrontendServices } from "../frontend/services.js";
|
||||
import { createNativeTs7Api } from "../frontend/ts7/native-api.js";
|
||||
import { resolveProvenanceSourcesWithParser } from "../frontend/provenance-core.js";
|
||||
@@ -21,8 +22,10 @@ import { buildSanitizedRuntime } from "./sanitizer.js";
|
||||
import { runCompilerTask } from "./task.js";
|
||||
|
||||
declare function compilerNativePrivateDirectory(path: string, harden: boolean): boolean;
|
||||
declare function compilerNativeCollectCycles(): void;
|
||||
|
||||
async function main(): Promise<number> {
|
||||
setFrontendDisposedHook(() => compilerNativeCollectCycles());
|
||||
const manifestPath = process.env["SCRIPTC_TOOLCHAIN"] ?? process.execPath + ".json";
|
||||
let compiler: NativeCompiler | null = null;
|
||||
let cache: ReturnType<typeof openNativeCache> = null;
|
||||
|
||||
@@ -81,7 +81,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
|
||||
handleProps: false,
|
||||
dynForAwait: false,
|
||||
eventsOnceSignal: false,
|
||||
dynBufferNum: false,
|
||||
dynBuffer: false,
|
||||
builtinValues: false,
|
||||
warningReplace: false,
|
||||
stdioExtended: false,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { constants } from "node:buffer";
|
||||
import { mkdtempSync, rmSync, truncateSync, writeFileSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { readTextParts, writeTextParts } from "./text-parts.js";
|
||||
|
||||
const scratch = mkdtempSync(join(tmpdir(), "scriptc-text-parts-"));
|
||||
afterAll(() => rmSync(scratch, { recursive: true, force: true }));
|
||||
|
||||
test("parts join to the file's text, each within the part length", async () => {
|
||||
// Multi-byte sequences and surrogate pairs straddle the 1 MiB reads and
|
||||
// the 8-unit parts.
|
||||
const unit = "abéc中\u{1f600}d";
|
||||
const text = unit.repeat(90_000);
|
||||
const path = join(scratch, "mixed.ll");
|
||||
writeFileSync(path, text);
|
||||
const parts = await readTextParts(path, 8);
|
||||
expect(parts.join("")).toBe(text);
|
||||
for (const part of parts) {
|
||||
expect(part.length).toBeLessThanOrEqual(8);
|
||||
const last = part.charCodeAt(part.length - 1);
|
||||
expect(last >= 0xd800 && last <= 0xdbff).toBe(false);
|
||||
}
|
||||
// Each part encodes on its own, so writing them back reproduces the file.
|
||||
const copy = join(scratch, "copy.ll");
|
||||
await writeTextParts(copy, parts);
|
||||
expect(await readFile(copy, "utf8")).toBe(text);
|
||||
});
|
||||
|
||||
test("an empty file has no parts", async () => {
|
||||
const path = join(scratch, "empty.ll");
|
||||
writeFileSync(path, "");
|
||||
expect(await readTextParts(path)).toEqual([]);
|
||||
});
|
||||
|
||||
test("a file past the engine's string limit reads in parts", async () => {
|
||||
// A sparse file: its bytes are NUL characters, valid UTF-8, and cost no
|
||||
// disk space.
|
||||
const path = join(scratch, "huge.ll");
|
||||
const size = constants.MAX_STRING_LENGTH + 1;
|
||||
writeFileSync(path, "");
|
||||
truncateSync(path, size);
|
||||
await expect(readFile(path, "utf8")).rejects.toThrow(RangeError);
|
||||
const parts = await readTextParts(path);
|
||||
expect(parts.length).toBeGreaterThan(1);
|
||||
expect(parts.reduce((length, part) => length + part.length, 0)).toBe(size);
|
||||
}, 120_000);
|
||||
@@ -1,4 +1,10 @@
|
||||
import { createReadStream } from "node:fs";
|
||||
import { open } from "node:fs/promises";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
|
||||
/** The longest part readTextParts returns, far below the engine's string
|
||||
* limit. */
|
||||
const TEXT_PART_LENGTH = 64 * 1024 * 1024;
|
||||
|
||||
/** Writes compiler text artifacts (LLVM, serialized IR) in large batches.
|
||||
* Programs past the engine's string limit arrive as many parts, and writing
|
||||
@@ -35,3 +41,43 @@ export async function writeTextParts(
|
||||
await file.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads a text artifact as parts of at most `partLength` UTF-16 units:
|
||||
* cached LLVM and serialized IR of large programs exceed the engine's
|
||||
* string limit as one string. The decoder carries a multi-byte sequence
|
||||
* split across reads into the next part, and no part ends inside a
|
||||
* surrogate pair, so the parts join to the file's text and each encodes on
|
||||
* its own. */
|
||||
export async function readTextParts(
|
||||
path: string,
|
||||
partLength = TEXT_PART_LENGTH,
|
||||
): Promise<string[]> {
|
||||
const decoder = new StringDecoder("utf8");
|
||||
const parts: string[] = [];
|
||||
let pending: string[] = [];
|
||||
let pendingLength = 0;
|
||||
const flush = (): void => {
|
||||
if (pendingLength === 0) return;
|
||||
parts.push(pending.join(""));
|
||||
pending = [];
|
||||
pendingLength = 0;
|
||||
};
|
||||
const add = (text: string): void => {
|
||||
for (let offset = 0; offset < text.length;) {
|
||||
let take = Math.min(text.length - offset, partLength - pendingLength);
|
||||
// A part never ends inside a surrogate pair: writeTextParts encodes
|
||||
// its batches separately.
|
||||
const last = text.charCodeAt(offset + take - 1);
|
||||
if (take > 1 && last >= 0xd800 && last <= 0xdbff && offset + take < text.length) take--;
|
||||
pending.push(text.slice(offset, offset + take));
|
||||
pendingLength += take;
|
||||
offset += take;
|
||||
if (pendingLength >= partLength - 1) flush();
|
||||
}
|
||||
};
|
||||
for await (const chunk of createReadStream(path, { highWaterMark: 1 << 20 }))
|
||||
add(decoder.write(chunk as Buffer));
|
||||
add(decoder.end());
|
||||
flush();
|
||||
return parts;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
export type CompilationTiming = (phase: string, detail?: Record<string, unknown>) => void;
|
||||
|
||||
/** The start of a timing and its latest phase, as `Date.now()` values. */
|
||||
export interface TimingClock {
|
||||
start: number;
|
||||
previous: number;
|
||||
}
|
||||
|
||||
/** Opt-in phase durations shared by the Node seed and native compiler.
|
||||
* The disabled path does not read the clock or write compiler output. */
|
||||
export function compilationTiming(): CompilationTiming {
|
||||
* The disabled path does not read the clock or write compiler output.
|
||||
* With `clock`, the phases continue a timing that started in another
|
||||
* process, so a frontend process reports them as its parent compile would. */
|
||||
export function compilationTiming(clock?: TimingClock): CompilationTiming {
|
||||
if (process.env["SCRIPTC_TIMING"] !== "1") return () => {};
|
||||
const start = performance.now();
|
||||
let previous = start;
|
||||
const opened = performance.now();
|
||||
const start = clock === undefined ? opened : opened - (Date.now() - clock.start);
|
||||
let previous = clock === undefined ? start : opened - (Date.now() - clock.previous);
|
||||
return (phase, detail = {}) => {
|
||||
const now = performance.now();
|
||||
process.stderr.write(
|
||||
|
||||
@@ -211,7 +211,7 @@ export function signatureHash(sig: string): string {
|
||||
}
|
||||
|
||||
/** UTF-8 bytes of a string. */
|
||||
function utf8Bytes(text: string): number[] {
|
||||
export function utf8Bytes(text: string): number[] {
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const cp = text.codePointAt(i)!;
|
||||
@@ -233,7 +233,7 @@ function utf8Bytes(text: string): number[] {
|
||||
}
|
||||
|
||||
/** An LLVM c"..." string body for bytes. */
|
||||
function cBytes(bytes: Uint8Array, start: number, end: number): string {
|
||||
export function cBytes(bytes: Uint8Array, start: number, end: number): string {
|
||||
let s = "";
|
||||
for (let i = start; i < end; i++) {
|
||||
const b = bytes[i]!;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4635-contract-program-lifetime-layouts.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6495-key-order-rewritten-literals.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6496-key-order-built-and-parsed.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6580-emitter-typed-untyped-listeners.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6581-emitter-untyped-error-and-meta.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6582-emitter-class-expression-escapes.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6583-emitter-untyped-surface.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6584-emitter-class-body-untyped-listeners.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6819-caught-instanceof-dynamic-payloads.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6903-typedarray-of.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6904-typedarray-of-loose.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6907-generic-member-defaulted-callback.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6930-dyn-buffer-methods.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6930-fs-promises-rm-options.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6941-emitter-untyped-zero-arg-listener.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/function-integrity-names.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/function-integrity.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/object-literal-tostring-template.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/prototype-tostring-conversions.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user