mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge main
This commit is contained in:
+6
-1
@@ -5,7 +5,12 @@
|
||||
* checks annotated listener parameters against it (unannotated non-empty
|
||||
* parameter lists have no static types and fence at the registration). */
|
||||
declare module "events" {
|
||||
class EventEmitter {
|
||||
/* Generic like @types/node's class: the type argument is the listener
|
||||
* MAP, a pure typing aid with no runtime layout (Node erases it too), so
|
||||
* `class Sink extends EventEmitter<Map>` adopts the same emitter base.
|
||||
* The map stays unconstrained here — the members below are the untyped
|
||||
* `...args: any[]` surface either way. */
|
||||
class EventEmitter<EventMap = Record<string, any[]>> {
|
||||
constructor();
|
||||
static defaultMaxListeners: number;
|
||||
/* Listeners registered under this symbol observe 'error' emits first
|
||||
|
||||
@@ -210,7 +210,7 @@ declare module "fs/promises" {
|
||||
export function access(path: string | URL, mode?: number): Promise<void>;
|
||||
export function truncate(path: string | URL, len?: number): Promise<void>;
|
||||
export function appendFile(path: string | URL, data: string | Uint8Array): Promise<void>;
|
||||
export function copyFile(src: string | URL, dest: string | URL): Promise<void>;
|
||||
export function copyFile(src: string | URL, dest: string | URL, mode?: number): Promise<void>;
|
||||
export function mkdtemp(prefix: string | URL): Promise<string>;
|
||||
export function rmdir(path: string | URL): Promise<void>;
|
||||
export function rename(oldPath: string | URL, newPath: string | URL): Promise<void>;
|
||||
|
||||
+54
-10
@@ -30,8 +30,10 @@ declare module "node:fs" {
|
||||
export function writeFileSync(
|
||||
path: string | URL,
|
||||
data: string,
|
||||
/* The options-record stance (see http.RequestOptions): flag fences
|
||||
* by name at the call site; undocumented keys drop like Node. */
|
||||
/* The options-record stance (see http.RequestOptions): an unsupported
|
||||
* flag fences by name at the call site; undocumented keys drop like
|
||||
* Node. The lowered flags are "w" (truncate-or-create) and "wx"
|
||||
* (exclusive create). */
|
||||
options?: {
|
||||
mode?: number;
|
||||
encoding?: "utf8" | "utf-8";
|
||||
@@ -157,9 +159,11 @@ declare module "node:fs" {
|
||||
export function lchownSync(path: string | URL, uid: number, gid: number): void;
|
||||
export function fchownSync(fd: number, uid: number, gid: number): void;
|
||||
export function truncateSync(path: string | URL, len?: number): void;
|
||||
/* The 2-argument form only (Node's mode flags have no lowering). The
|
||||
* destination is created or truncated carrying the SOURCE's mode. */
|
||||
export function copyFileSync(src: string | URL, dest: string | URL): void;
|
||||
/* The destination is created or truncated carrying the SOURCE's mode.
|
||||
* `mode` is libuv's copyfile flag bits: COPYFILE_EXCL (1) fails when the
|
||||
* destination exists, COPYFILE_FICLONE (2) requests a reflink and falls
|
||||
* back to a copy, COPYFILE_FICLONE_FORCE (4) requires one. */
|
||||
export function copyFileSync(src: string | URL, dest: string | URL, mode?: number): void;
|
||||
export function rename(
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
@@ -604,11 +608,29 @@ declare module "node:fs" {
|
||||
): void;
|
||||
/* fs.watch — the file-watching slice (scr_watch.c, kqueue EVFILT_VNODE):
|
||||
* "rename"/"change" events on ONE path, watcher.close() to stop. The
|
||||
* listener takes zero parameters or the eventType string (the filename
|
||||
* parameter has no lowering); an open watcher keeps the loop alive. A
|
||||
* path that cannot be opened THROWS Node's fs error synchronously. */
|
||||
* listener takes (), (eventType) or (eventType, filename); an open
|
||||
* watcher keeps the loop alive. A path that cannot be opened THROWS
|
||||
* Node's fs error synchronously. */
|
||||
export type WatchEventType = "rename" | "change";
|
||||
/* (eventType, filename): filename is the changed entry's name (null when
|
||||
* the platform does not provide one). */
|
||||
export type WatchListener<T> = (event: WatchEventType, filename: T | null) => void;
|
||||
export interface FSWatcher {
|
||||
close(): void;
|
||||
on(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this;
|
||||
on(event: "error", listener: (error: Error) => void): this;
|
||||
on(event: "close", listener: () => void): this;
|
||||
once(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this;
|
||||
once(event: "error", listener: (error: Error) => void): this;
|
||||
once(event: "close", listener: () => void): this;
|
||||
addListener(
|
||||
event: "change",
|
||||
listener: (eventType: string, filename: string | Buffer) => void,
|
||||
): this;
|
||||
addListener(event: "error", listener: (error: Error) => void): this;
|
||||
addListener(event: "close", listener: () => void): this;
|
||||
off(event: "change" | "error" | "close", listener: (...args: any[]) => void): this;
|
||||
removeListener(event: "change" | "error" | "close", listener: (...args: any[]) => void): this;
|
||||
}
|
||||
/* The options-record stance (see http.RequestOptions): persistent: true,
|
||||
* recursive: false, and encoding: "utf8" state the lowered behavior and
|
||||
@@ -620,12 +642,34 @@ declare module "node:fs" {
|
||||
encoding?: string;
|
||||
[option: string]: unknown;
|
||||
}
|
||||
export function watch(path: string, listener?: (eventType: string) => void): FSWatcher;
|
||||
export function watch(path: string, listener?: WatchListener<string>): FSWatcher;
|
||||
export function watch(
|
||||
path: string,
|
||||
options: WatchOptions,
|
||||
listener?: (eventType: string) => void,
|
||||
listener?: WatchListener<string>,
|
||||
): FSWatcher;
|
||||
/* fs.watchFile — stat polling (scr_watch_file.c, libuv's uv_fs_poll
|
||||
* shape): the listener runs with (current, previous) Stats when a poll
|
||||
* sees a difference, the first poll only records the baseline, and a
|
||||
* path that cannot be stat'd polls as all-zero Stats. One poller per
|
||||
* resolved path is shared by every listener on it. interval defaults to
|
||||
* 5007 ms, persistent defaults to true; bigint: true (BigIntStats) has
|
||||
* no lowering. The call is a statement: Node's StatWatcher has no
|
||||
* lowering. */
|
||||
export interface StatWatcher {}
|
||||
export function watchFile(
|
||||
filename: string,
|
||||
options: { interval?: number; persistent?: boolean; bigint?: false },
|
||||
listener: (current: Stats, previous: Stats) => void,
|
||||
): StatWatcher;
|
||||
export function watchFile(
|
||||
filename: string,
|
||||
listener: (current: Stats, previous: Stats) => void,
|
||||
): StatWatcher;
|
||||
export function unwatchFile(
|
||||
filename: string,
|
||||
listener?: (current: Stats, previous: Stats) => void,
|
||||
): void;
|
||||
/* fs.promises IS the fs/promises module (Node's rule) — the namespace-
|
||||
* import form `fs.promises.readFile(...)` lowers through the same
|
||||
* fs/promises table as `import { readFile } from "node:fs/promises"`. */
|
||||
|
||||
@@ -257,6 +257,23 @@ declare module "net" {
|
||||
event: "request" | "checkContinue" | "checkExpectation",
|
||||
listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void,
|
||||
): void;
|
||||
/* off / removeListener: the LAST listener standing for the same
|
||||
* function leaves that event's list (Node's EventEmitter order). The
|
||||
* server registries that keep a removable list: error, close,
|
||||
* listening, connection/secureConnection, timeout, and drop. */
|
||||
off(event: "error", listener: (err: Error) => void): void;
|
||||
off(event: "close" | "listening", listener: () => void): void;
|
||||
off(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
|
||||
off(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
off(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
removeListener(event: "error", listener: (err: Error) => void): void;
|
||||
removeListener(event: "close" | "listening", listener: () => void): void;
|
||||
removeListener(
|
||||
event: "connection" | "secureConnection",
|
||||
listener: (socket: Socket) => void,
|
||||
): void;
|
||||
removeListener(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
removeListener(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
}
|
||||
/* The 'drop' payload: the turned-away connection's addresses. */
|
||||
export interface DropArgument {
|
||||
|
||||
+57
-13
@@ -246,12 +246,13 @@ declare var process: {
|
||||
/* "v" plus versions.node: the compatibility target's release. */
|
||||
readonly version: string;
|
||||
/* The build-configuration snapshot Node exposes from its gyp config.
|
||||
* A scriptc binary has no V8 and no gyp, so `variables` answers the
|
||||
* honest capability record (v8_enable_i18n_support: 0 — no ICU; asan
|
||||
* mirrors the sanitizer build flag) and `target_defaults` is absent —
|
||||
* probe code takes its documented fallbacks. Reads of undeclared
|
||||
* variables go through the index signature (string | number | boolean
|
||||
* | undefined) and answer undefined. */
|
||||
* A scriptc binary has no V8 and no gyp, so `variables` answers what is
|
||||
* true of a compiled binary: v8_enable_i18n_support is 1 like Node's
|
||||
* (the read links ICU, which the binary then carries), while the gyp
|
||||
* variables it cannot answer (icu_path, asan, …) read undefined through
|
||||
* the index signature (string | number | boolean | undefined), and
|
||||
* `target_defaults` is absent — probe code takes its documented
|
||||
* fallbacks. */
|
||||
readonly config: {
|
||||
readonly variables: { readonly [name: string]: string | number | boolean | undefined };
|
||||
readonly target_defaults?: { readonly default_configuration: string };
|
||||
@@ -340,6 +341,12 @@ declare var process: {
|
||||
* (chunks stay bytes; decode with TextDecoder). */
|
||||
stdin: {
|
||||
readonly isTTY: boolean;
|
||||
/* The descriptor the stream is built over (Node reports 0 here and
|
||||
* 1/2 on stdout/stderr, whatever the underlying file is). */
|
||||
readonly fd: 0;
|
||||
/* True while a TTY stdin runs in raw mode; Node's own property is
|
||||
* undefined on a non-TTY stdin (the `x || false` idiom). */
|
||||
readonly isRaw: boolean;
|
||||
on(event: "data", listener: (chunk: Uint8Array) => void): void;
|
||||
on(event: "end", listener: () => void): void;
|
||||
on(event: "error", listener: (err: Error) => void): void;
|
||||
@@ -374,6 +381,12 @@ declare var process: {
|
||||
* removal see the same value. */
|
||||
on(event: "SIGINT" | "SIGTERM", listener: () => void): void;
|
||||
on(event: "exit", listener: (code: number) => void): void;
|
||||
/* prependListener — the same registration at the FRONT of the event's
|
||||
* listener list (Node's EventEmitter order), for the registries that
|
||||
* own a prepend entry ('exit' and the signal family). prependOnceListener
|
||||
* has no lowering. */
|
||||
prependListener(event: "SIGINT" | "SIGTERM", listener: () => void): void;
|
||||
prependListener(event: "exit", listener: (code: number) => void): void;
|
||||
/* 'warning' — Node's process-warning channel: listeners receive the
|
||||
* warning Error (as a dyn value); emitWarning and the runtime's
|
||||
* deprecation sites dispatch synchronously (SEMANTICS.md). */
|
||||
@@ -1115,9 +1128,9 @@ declare var ReadableStream: {
|
||||
from(iterable: string): ReadableStream<string>;
|
||||
readonly prototype: ReadableStream<unknown>;
|
||||
};
|
||||
/* The writable side of a TransformStream and its writer. The
|
||||
* WritableStream constructor has no native lowering; writable streams come
|
||||
* from `new TransformStream()`. */
|
||||
/* The writable side of a TransformStream and its writer. A program that
|
||||
* constructs a WritableStream, or a TransformStream with a transformer,
|
||||
* builds its streams with the web stream classes (scr_webstreams.c). */
|
||||
interface WritableStreamDefaultWriter<W = unknown> {
|
||||
readonly closed: Promise<void>;
|
||||
readonly desiredSize: number | null;
|
||||
@@ -1133,14 +1146,25 @@ interface WritableStream<W = unknown> {
|
||||
close(): Promise<void>;
|
||||
getWriter(): WritableStreamDefaultWriter<W>;
|
||||
}
|
||||
/* The identity transform: chunks written to `writable` are read from
|
||||
* `readable`, and a write completes when its chunk is read. */
|
||||
/* Without a transformer, the identity transform: chunks written to
|
||||
* `writable` are read from `readable`, and a write completes when its chunk
|
||||
* is read. */
|
||||
interface TransformStream<I = unknown, O = unknown> {
|
||||
readonly readable: ReadableStream<O>;
|
||||
readonly writable: WritableStream<I>;
|
||||
}
|
||||
interface Transformer<I = unknown, O = unknown> {
|
||||
start?(controller: TransformStreamDefaultController<O>): unknown;
|
||||
transform?(chunk: I, controller: TransformStreamDefaultController<O>): unknown;
|
||||
flush?(controller: TransformStreamDefaultController<O>): unknown;
|
||||
cancel?(reason?: unknown): unknown;
|
||||
}
|
||||
declare var TransformStream: {
|
||||
new <I = unknown, O = I>(): TransformStream<I, O>;
|
||||
new <I = unknown, O = I>(
|
||||
transformer?: Transformer<I, O>,
|
||||
writableStrategy?: QueuingStrategy<I>,
|
||||
readableStrategy?: QueuingStrategy<O>,
|
||||
): TransformStream<I, O>;
|
||||
readonly prototype: TransformStream<unknown, unknown>;
|
||||
};
|
||||
/* The other web stream interfaces. JavaScript sources construct and read
|
||||
@@ -1158,8 +1182,14 @@ interface WritableStreamDefaultController {
|
||||
readonly signal: AbortSignal;
|
||||
error(reason?: unknown): void;
|
||||
}
|
||||
interface UnderlyingSink<W = unknown> {
|
||||
start?(controller: WritableStreamDefaultController): unknown;
|
||||
write?(chunk: W, controller: WritableStreamDefaultController): unknown;
|
||||
close?(): unknown;
|
||||
abort?(reason?: unknown): unknown;
|
||||
}
|
||||
declare var WritableStream: {
|
||||
new <W = unknown>(sink?: object, strategy?: QueuingStrategy<W>): WritableStream<W>;
|
||||
new <W = unknown>(sink?: UnderlyingSink<W>, strategy?: QueuingStrategy<W>): WritableStream<W>;
|
||||
readonly prototype: WritableStream<unknown>;
|
||||
};
|
||||
declare var WritableStreamDefaultWriter: {
|
||||
@@ -1747,6 +1777,20 @@ declare module "tty" {
|
||||
/* The bytes queued behind the last write: the compiled binary's fd
|
||||
* 1/2 writes are synchronous, so it is always 0. */
|
||||
readonly writableLength: number;
|
||||
/* The descriptor behind the stream (1 for stdout, 2 for stderr). */
|
||||
readonly fd: number;
|
||||
/* Node's default for the process streams (65536 for a pipe, a file and a
|
||||
* TTY): a constant, because nothing is ever queued. */
|
||||
readonly writableHighWaterMark: number;
|
||||
/* Always false: a synchronous write never returns false, so no 'drain'
|
||||
* is ever pending. */
|
||||
readonly writableNeedDrain: boolean;
|
||||
/* 'drain' and the other events by name. Node returns the stream. */
|
||||
on(event: string, listener: (...args: any[]) => void): this;
|
||||
once(event: string, listener: (...args: any[]) => void): this;
|
||||
addListener(event: string, listener: (...args: any[]) => void): this;
|
||||
off(event: string, listener: (...args: any[]) => void): this;
|
||||
removeListener(event: string, listener: (...args: any[]) => void): this;
|
||||
getWindowSize(): [number, number];
|
||||
}
|
||||
export function isatty(fd: number): boolean;
|
||||
|
||||
@@ -19,10 +19,17 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
...(features.eventTarget ? { eventTarget: true } : {}),
|
||||
...(features.wasm ? { wasm: true } : {}),
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.recordViewPartial ? { recordViewPartial: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
...(features.fsFlags ? { fsFlags: true } : {}),
|
||||
...(features.strdecDyn ? { strdecDyn: true } : {}),
|
||||
...(features.processExtras ? { processExtras: true } : {}),
|
||||
...(features.streamWriteErrors ? { streamWriteErrors: true } : {}),
|
||||
...(features.stdioWriteSlot ? { stdioWriteSlot: true } : {}),
|
||||
...(features.nodeIsland ? { nodeIsland: true } : {}),
|
||||
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
|
||||
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
|
||||
...(features.primitiveWrapper ? { primitiveWrapper: true } : {}),
|
||||
zlib: features.zlib,
|
||||
...(features.brotli ? { brotli: true } : {}),
|
||||
assert: features.assert,
|
||||
@@ -67,6 +74,7 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
dgramDyn: features.dgramDyn,
|
||||
textCodecDyn: features.textCodecDyn,
|
||||
watch: features.fsWatch,
|
||||
watchFile: features.fsWatchFile,
|
||||
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
|
||||
nodeTest: features.nodeTest,
|
||||
tls: features.tls,
|
||||
@@ -79,5 +87,6 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
x509: features.x509,
|
||||
webcrypto: features.webcrypto,
|
||||
perfLoop: features.perfLoop,
|
||||
...(features.icu ? { intl: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export async function compilePackedLibrary(
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { existsSync, realpathSync, statSync } from "node:fs";
|
||||
import { mkdtemp, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import { copyFile, mkdtemp, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import { delimiter } from "node:path";
|
||||
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
toolchainEnvironmentFingerprint,
|
||||
} from "./toolchain-environment.js";
|
||||
import { type NativeArtifactDependency } from "./native/contracts.js";
|
||||
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import {
|
||||
RuntimePackError,
|
||||
stageRuntimePackArtifacts,
|
||||
type RuntimePackSelection,
|
||||
} from "./runtime-pack.js";
|
||||
import type { NativeLinkPlan } from "./link-plan.js";
|
||||
import { inputsWithoutLld, lldLinkerArgs } from "./lld.js";
|
||||
import { hugeTextLinkerArgs, rewriteHugeTextExecutable } from "./huge-text.js";
|
||||
@@ -225,6 +229,38 @@ async function objectLinkerDependencyPaths(
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** Install the selected runtime pack's RUN-time data files (ICU's
|
||||
* icudt78l.dat) beside the executable that links them. The compiler's link
|
||||
* step is the only moment the executable's directory is known, and a data
|
||||
* file next to the binary is what the runtime looks for first.
|
||||
*
|
||||
* A file already present at the pinned size is left alone: the pack ships
|
||||
* one data set per file name, and re-copying 11.1 MiB on every build would
|
||||
* be pure cost. The copy is atomic (private temporary, then rename) so two
|
||||
* concurrent builds of the same program never observe a partial file. */
|
||||
export async function installRuntimeDataFiles(
|
||||
selection: RuntimePackSelection,
|
||||
outputPath: string,
|
||||
): Promise<void> {
|
||||
if (selection.dataFiles.length === 0) return;
|
||||
const outputDir = dirname(resolve(outputPath));
|
||||
for (const source of selection.dataFiles) {
|
||||
const destination = join(outputDir, basename(source));
|
||||
const sourceSize = (await stat(source)).size;
|
||||
const existing = await stat(destination).catch(() => null);
|
||||
if (existing?.isFile() && existing.size === sourceSize) continue;
|
||||
const temporary = join(
|
||||
outputDir,
|
||||
`.${basename(destination)}.scriptc-${process.pid}-${randomUUID()}`,
|
||||
);
|
||||
await copyFile(source, temporary);
|
||||
await rename(temporary, destination).catch(async () => {
|
||||
await rm(destination, { force: true });
|
||||
await rename(temporary, destination);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function linkNativeExecutable(
|
||||
plan: NativeLinkPlan,
|
||||
options: {
|
||||
@@ -314,6 +350,7 @@ export async function linkNativeExecutable(
|
||||
await rm(plan.outputPath, { force: true });
|
||||
await rename(privateOut, plan.outputPath);
|
||||
});
|
||||
await installRuntimeDataFiles(plan.runtimePack, plan.outputPath);
|
||||
if (plan.target.platform === "darwin" && !plan.darwinDebugSymbols) {
|
||||
await rm(`${plan.outputPath}.dSYM`, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
funcOf,
|
||||
type IrExpr,
|
||||
@@ -260,6 +263,59 @@ test("receiver elision admits only synchronous receiver-free work", () => {
|
||||
expect(callbackIgnoresReceiver(fn)).toBe(false);
|
||||
});
|
||||
|
||||
test("receiver elision admits library work that runs no user code", () => {
|
||||
const str: IrExpr = { kind: "strLit", value: "a", type: STRING, loc };
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const dyn = ref("d", DYN);
|
||||
const ignores = (expr: IrExpr): boolean => {
|
||||
const fn = fixture().functions[0]!;
|
||||
fn.body.unshift({ kind: "exprStmt", expr, loc });
|
||||
return callbackIgnoresReceiver(fn);
|
||||
};
|
||||
const lib = (fn: string, args: IrExpr[]): IrExpr =>
|
||||
({ kind: "libCall", fn, args, type: F64, loc }) as IrExpr;
|
||||
const intrinsic = (name: string, args: IrExpr[]): IrExpr =>
|
||||
({ kind: "intrinsic", name, args, type: VOID, loc }) as IrExpr;
|
||||
// String and number members over primitives; a callback operand may run.
|
||||
expect(ignores(lib("string.lastIndexOfFrom", [str, str, num(0)]))).toBe(true);
|
||||
expect(ignores(lib("num.sameValue", [num(0), num(1)]))).toBe(true);
|
||||
expect(ignores(lib("string.lastIndexOf", [str, closure("predicate")]))).toBe(false);
|
||||
expect(ignores(lib("perf.now", []))).toBe(true);
|
||||
// `new Error(message, options)` with a primitive message and no options.
|
||||
const message: IrExpr = { kind: "dynFrom", value: str, type: DYN, loc };
|
||||
expect(ignores(lib("error.newOptions", [message, message]))).toBe(true);
|
||||
expect(ignores(lib("error.newOptions", [message, dyn]))).toBe(false);
|
||||
// The module binding check only throws; console output may inspect.
|
||||
const initialized: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
expect(ignores(intrinsic("module.tdzCheck", [initialized, str]))).toBe(true);
|
||||
expect(ignores(intrinsic("console.log", [dyn]))).toBe(false);
|
||||
// A typed array built from nothing or a typed source; a dynamic source
|
||||
// may be an iterable.
|
||||
const bytes = (source: IrExpr | null): IrExpr => ({
|
||||
kind: "bytesNew",
|
||||
source,
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
});
|
||||
expect(ignores(bytes(null))).toBe(true);
|
||||
expect(ignores(bytes(dyn))).toBe(false);
|
||||
});
|
||||
|
||||
test("receiver elision admits array for-of loops", () => {
|
||||
const fn = fixture().functions[0]!;
|
||||
const numbers = { kind: "array", elem: F64 } as const;
|
||||
const one: IrExpr = { kind: "numLit", value: 1, type: F64, loc };
|
||||
fn.locals.push({ id: "x", name: "x", type: F64, mutable: false });
|
||||
fn.body.unshift({
|
||||
kind: "forOf",
|
||||
localId: "x",
|
||||
iterable: { kind: "arrayLit", elems: [one], type: numbers, loc },
|
||||
body: [],
|
||||
loc,
|
||||
});
|
||||
expect(callbackIgnoresReceiver(fn)).toBe(true);
|
||||
});
|
||||
|
||||
function callsOf(name: string, callee: string, extra: IrStmt[] = []): IrFunction {
|
||||
return {
|
||||
name,
|
||||
|
||||
@@ -12,17 +12,18 @@ export const CPU_DISPATCH_TIERS: readonly TargetCpu[] = ["x86-64-v3"];
|
||||
const LEVEL = "@sc_cpu_level";
|
||||
|
||||
/** The attribute groups of the x86-64-v3 copies: #0's and #2's (see
|
||||
* emit-module.ts functionAttributes) with the level's `target-cpu`. */
|
||||
* emit-module.ts functionAttributes) with the level's `target-cpu`, and
|
||||
* `scriptc-cpu-copy`, by which the LLVM helper finds the copies. After
|
||||
* optimization, a copy whose code gains nothing from x86-64-v3 takes its
|
||||
* baseline copy's code (native/llvm-codegen/src/cpu-copies.h). */
|
||||
const COPY_GROUPS: ReadonlyMap<string, string> = new Map([
|
||||
["0", "3"],
|
||||
["2", "4"],
|
||||
]);
|
||||
|
||||
export function cpuDispatchAttributes(common: string): string[] {
|
||||
return [
|
||||
`attributes #3 = {${common} "target-cpu"="x86-64-v3" }`,
|
||||
`attributes #4 = { noinline${common} "target-cpu"="x86-64-v3" }`,
|
||||
];
|
||||
const copy = `"target-cpu"="x86-64-v3" "scriptc-cpu-copy"`;
|
||||
return [`attributes #3 = {${common} ${copy} }`, `attributes #4 = { noinline${common} ${copy} }`];
|
||||
}
|
||||
|
||||
/** Bits of CPUID leaf 1's ECX that x86-64-v3 needs: SSE3 (0), SSSE3 (9),
|
||||
|
||||
@@ -57,6 +57,7 @@ import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import { mangleRecordNew, mangleRecordStruct } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import { JS_BITS } from "./jsval-encoding.js";
|
||||
import {
|
||||
arrNewCall,
|
||||
elemAccess,
|
||||
@@ -219,6 +220,21 @@ export interface DynHost extends WalkerHost {
|
||||
classValueSubtypes(className: string): readonly string[];
|
||||
/** emitter.ts's pendingTestLines: the inline pending-exception test. */
|
||||
pendingTestLines(dest: string): string[];
|
||||
/** The closures with a jsval body (IrFunction.jsEntry) on targets with
|
||||
* jsval words, which the box builders give a jsval entry. */
|
||||
jsEntryClosures?(): readonly JsEntryClosure[];
|
||||
}
|
||||
|
||||
/** A closure with a jsval body (IrFunction.jsEntry), for the box builders. */
|
||||
export interface JsEntryClosure {
|
||||
/** The closure's entry symbol (ScrClosure.fn) and signature. */
|
||||
closure: string;
|
||||
params: readonly IrType[];
|
||||
ret: IrType;
|
||||
/** The jsval body's entry symbol and signature. */
|
||||
body: string;
|
||||
bodyParams: readonly IrType[];
|
||||
bodyRet: IrType;
|
||||
}
|
||||
|
||||
const FN_ATTRS = "#0";
|
||||
@@ -247,6 +263,7 @@ export class LlDyn {
|
||||
private readonly toDynFns = new Map<string, string>();
|
||||
private readonly dynFuncThunks = new Map<string, string>();
|
||||
private readonly dynFuncBoxes = new Map<string, string>();
|
||||
private readonly jsEntryThunks = new Map<string, string>();
|
||||
private readonly dynFuncAdapters = new Map<string, string>();
|
||||
private readonly messageListeners = new Map<string, string | null>();
|
||||
private readonly messageReaders = new Map<string, string>();
|
||||
@@ -290,6 +307,8 @@ export class LlDyn {
|
||||
return this.dynFuncThunkHelper(args[0] as IrType & { kind: "func" });
|
||||
case "dyn.dynFuncBoxHelper":
|
||||
return this.dynFuncBoxHelper(args[0] as IrType & { kind: "func" });
|
||||
case "dyn.jsEntryThunk":
|
||||
return this.jsEntryThunk(args[0] as string, args[1] as IrType[], args[2] as IrType);
|
||||
case "dyn.dynFuncAdapterHelper":
|
||||
return this.dynFuncAdapterHelper(args[0] as IrType & { kind: "func" });
|
||||
case "dyn.dynToStrHelper":
|
||||
@@ -1179,8 +1198,11 @@ export class LlDyn {
|
||||
const struct = mangleRecordStruct(sourceId);
|
||||
const base = host.nullableUnions.fieldBase(sourceId);
|
||||
const offsets = target.fields.map((field) => {
|
||||
const index = source.fields.findIndex((f) => f.name === field.name) + base;
|
||||
return `${S} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${index}) to ${S})`;
|
||||
const index = source.fields.findIndex((f) => f.name === field.name);
|
||||
// A partial view's target field the source does not declare has
|
||||
// no slot: all ones (SIZE_MAX) marks it absent (record-views.ts).
|
||||
if (index < 0) return `${S} -1`;
|
||||
return `${S} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${index + base}) to ${S})`;
|
||||
});
|
||||
const offsetTable = `${table}_${k}`;
|
||||
this.defs.push(
|
||||
@@ -5534,6 +5556,101 @@ export class LlDyn {
|
||||
return name;
|
||||
}
|
||||
|
||||
/** `sc_jt_<n>(ptr c, ptr args, size argc) -> ptr`, a function value's
|
||||
* jsval entry (ScrDynJsEntry) into a closure's jsval body `target`
|
||||
* (IrFunction.jsEntry, 64-bit targets): passes each argument word to the
|
||||
* body, a missing one as undefined and a dyn parameter's boxed, and
|
||||
* returns the body's result as an owned word. Like the closure's own
|
||||
* entry, the body consumes its arguments. With an exception pending the
|
||||
* result is undefined. */
|
||||
jsEntryThunk(target: string, params: readonly IrType[], ret: IrType): string {
|
||||
const existing = this.jsEntryThunks.get(target);
|
||||
if (existing) return existing;
|
||||
const name = `sc_jt_${this.jsEntryThunks.size}`;
|
||||
this.jsEntryThunks.set(target, name);
|
||||
this.host.journalAllocated?.(name, "dyn.jsEntryThunk", [target, params, ret]);
|
||||
const host = this.host;
|
||||
const B = new BlockBuilder();
|
||||
const undefinedWord = `inttoptr (i64 ${JS_BITS.undefined} to ptr)`;
|
||||
const args = params.map((p, i) => {
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
B.line(`store ptr ${undefinedWord}, ptr ${slot}`);
|
||||
const has = B.tmp();
|
||||
B.line(`${has} = icmp ult ${host.sizeType} ${i}, %argc`);
|
||||
const lHas = B.newLabel("jt.h");
|
||||
const lj = B.newLabel("jt.j");
|
||||
B.condBr(has, lHas, lj);
|
||||
B.startBlock(lHas);
|
||||
const ap = B.tmp();
|
||||
const av = B.tmp();
|
||||
B.line(`${ap} = getelementptr inbounds ptr, ptr %args, ${host.sizeType} ${i}`);
|
||||
B.line(`${av} = load ptr, ptr ${ap}`);
|
||||
B.line(`store ptr ${av}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const word = B.tmp();
|
||||
B.line(`${word} = load ptr, ptr ${slot}`);
|
||||
const owned = B.tmp();
|
||||
if (p.kind === "jsval") {
|
||||
host.declare(`declare ptr @scr_js_retain(ptr)`);
|
||||
B.line(`${owned} = call ptr @scr_js_retain(ptr ${word})`);
|
||||
} else if (p.kind === "dyn") {
|
||||
host.declare(`declare ptr @scr_js_to_dyn_borrow(ptr)`);
|
||||
B.line(`${owned} = call ptr @scr_js_to_dyn_borrow(ptr ${word})`);
|
||||
} else {
|
||||
throw new InternalCompilerError(`llvm emitter bug: jsval entry parameter ${p.kind}`);
|
||||
}
|
||||
return `ptr ${owned}`;
|
||||
});
|
||||
const callArgs = [`ptr %c`, ...args].join(", ");
|
||||
if (ret.kind === "void") {
|
||||
B.line(`call void @${target}(${callArgs})`);
|
||||
this.pendingBail(B, "jtr", () => {}, `ptr ${undefinedWord}`);
|
||||
B.terminate(`ret ptr ${undefinedWord}`);
|
||||
} else {
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ${this.valTy(ret)} @${target}(${callArgs})`);
|
||||
this.pendingBail(B, "jtr", () => {}, `ptr ${undefinedWord}`);
|
||||
if (ret.kind === "jsval") {
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
} else if (ret.kind === "f64") {
|
||||
const raw = B.tmp();
|
||||
const nan = B.tmp();
|
||||
const bits = B.tmp();
|
||||
const word = B.tmp();
|
||||
B.line(`${raw} = bitcast double ${r} to i64`);
|
||||
B.line(`${nan} = fcmp uno double ${r}, ${r}`);
|
||||
B.line(`${bits} = select i1 ${nan}, i64 ${JS_BITS.canonicalNan}, i64 ${raw}`);
|
||||
B.line(`${word} = inttoptr i64 ${bits} to ptr`);
|
||||
B.terminate(`ret ptr ${word}`);
|
||||
} else if (ret.kind === "bool") {
|
||||
const bits = B.tmp();
|
||||
const word = B.tmp();
|
||||
B.line(`${bits} = select i1 ${r}, i64 ${JS_BITS.true}, i64 ${JS_BITS.false}`);
|
||||
B.line(`${word} = inttoptr i64 ${bits} to ptr`);
|
||||
B.terminate(`ret ptr ${word}`);
|
||||
} else {
|
||||
// dyn, or a typed result boxed as the closure's own entry would.
|
||||
const d = ret.kind === "dyn" ? r : this.toDynExpr(B, ret, r);
|
||||
if (ret.kind !== "dyn" && isRefCounted(ret))
|
||||
B.line(`call void ${releaseSym(host, ret)}(ptr ${r})`);
|
||||
host.declare(`declare ptr @scr_js_from_dyn(ptr)`);
|
||||
const word = B.tmp();
|
||||
B.line(`${word} = call ptr @scr_js_from_dyn(ptr ${d})`);
|
||||
B.terminate(`ret ptr ${word}`);
|
||||
}
|
||||
}
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %c, ptr %args, ${host.sizeType} %argc) ${FN_ATTRS} { ; jsval entry into ${llvmCommentText(target)}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The box builder dynFrom emits for one closure signature. */
|
||||
dynFuncBoxHelper(t: IrType & { kind: "func" }): string {
|
||||
const key = typeKey(t);
|
||||
@@ -5547,11 +5664,43 @@ export class LlDyn {
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_new_func(ptr, ptr, i32, ptr, ptr)`);
|
||||
const sigLit = host.cstr(key);
|
||||
// A closure of this signature with a jsval body gets the function
|
||||
// value's jsval entry, however the value is boxed: the closure's
|
||||
// entry pointer names it.
|
||||
const entries =
|
||||
t.rest === true
|
||||
? []
|
||||
: (host.jsEntryClosures?.() ?? []).filter(
|
||||
(c) =>
|
||||
c.params.length === t.params.length &&
|
||||
c.params.every((p, i) => typeKey(p) === typeKey(t.params[i]!)) &&
|
||||
typeKey(c.ret) === typeKey(t.ret),
|
||||
);
|
||||
const register: string[] = [];
|
||||
if (entries.length > 0) {
|
||||
host.declare(`declare void @scr_dyn_func_set_js(ptr, ptr, ptr)`);
|
||||
register.push(
|
||||
` %fnp = getelementptr inbounds %ScrClosure, ptr %c, i64 0, i32 1`,
|
||||
` %fn = load ptr, ptr %fnp`,
|
||||
);
|
||||
entries.forEach((c, i) => {
|
||||
const jsEntry = this.jsEntryThunk(c.body, c.bodyParams, c.bodyRet);
|
||||
register.push(
|
||||
` %is${i} = icmp eq ptr %fn, @${c.closure}`,
|
||||
` br i1 %is${i}, label %set${i}, label %next${i}`,
|
||||
`set${i}:`,
|
||||
` call void @scr_dyn_func_set_js(ptr %r, ptr @${thunk}, ptr @${jsEntry})`,
|
||||
` ret ptr %r`,
|
||||
`next${i}:`,
|
||||
);
|
||||
});
|
||||
}
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %v, ptr %fname) ${FN_ATTRS} { ; box ${key} into dyn`,
|
||||
`entry:`,
|
||||
` %c = call ptr @scr_closure_retain_v(ptr %v)`,
|
||||
` %r = call ptr @scr_dyn_new_func(ptr %c, ptr @${thunk}, i32 ${t.params.length - (t.restAbi === "typed" ? 1 : 0)}, ptr ${sigLit}, ptr %fname)`,
|
||||
...register,
|
||||
` ret ptr %r`,
|
||||
`}`,
|
||||
``,
|
||||
|
||||
@@ -148,7 +148,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
beginFunctionLoops(emitter, fn, numericFn);
|
||||
emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;
|
||||
emitter.loopCopiesEnabled = true;
|
||||
emitter.loopCopiesEnabled = fn.plainLoops !== true;
|
||||
emitter.speculativeFunction = fn.name;
|
||||
emitter.speculation = null;
|
||||
emitter.speculationResume = null;
|
||||
|
||||
@@ -478,6 +478,10 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
const usesEvents = runtimeFeatures.processEvents;
|
||||
const usesChildProcess = runtimeFeatures.childProcess;
|
||||
const usesFsWatch = runtimeFeatures.fsWatch;
|
||||
// fs.watchFile programs register the poller unit's exit cleanup — the
|
||||
// same one-line gating (native/executable.ts links scr_watch_file.c on
|
||||
// the same predicate).
|
||||
const usesFsWatchFile = runtimeFeatures.fsWatchFile;
|
||||
// Programs that call into Node record the libnode pack and its Node
|
||||
// version before %main (scr_node_island.c).
|
||||
const usesNodeIsland = runtimeFeatures.nodeIsland;
|
||||
@@ -541,6 +545,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
if (workerStdioBox || workerStdinBox)
|
||||
emitter.declare(`declare void @scr_worker_stdio_install(ptr, ptr, ptr)`);
|
||||
if (usesFsWatch) emitter.declare(`declare void @scr_watch_install()`);
|
||||
if (usesFsWatchFile) emitter.declare(`declare void @scr_watch_file_install()`);
|
||||
if (usesNodeIsland) emitter.declare(`declare void @scr_node_island_install(ptr, ptr)`);
|
||||
// Interned now: the C-string constants flush before main assembles.
|
||||
const nodeIslandConfig = usesNodeIsland
|
||||
@@ -1226,6 +1231,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// fs.watch programs fill the loop's watch hooks the same way —
|
||||
// scr_watch.c links only when this line is emitted.
|
||||
...(usesFsWatch ? [` call void @scr_watch_install()`] : []),
|
||||
// fs.watchFile programs register the poller unit's atexit cleanup the
|
||||
// same way — scr_watch_file.c links only when this line is emitted.
|
||||
...(usesFsWatchFile ? [` call void @scr_watch_file_install()`] : []),
|
||||
...(nodeIslandConfig
|
||||
? [
|
||||
` call void @scr_node_island_install(ptr ${nodeIslandConfig[0]}, ptr ${nodeIslandConfig[1]})`,
|
||||
@@ -1377,25 +1385,147 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The out-of-line hashed probe of a number-key map (scr_map_find_f64):
|
||||
* `keyLines` define the normalized key word %kb from the parameter, and
|
||||
* `runtimeKey` is the double passed to the runtime lookup. A map with at
|
||||
* most SCR_MAP_LINEAR_LIMIT entries keeps no bucket table: the absent-key
|
||||
* filter is tested before its entries are scanned. A bucket table is
|
||||
* probed without the filter: the filter keeps two of its 64 bits per key,
|
||||
* so past a few dozen keys it rejects few absent keys, and the probe's
|
||||
* first bucket read answers those. Compact tables tag each 24-bit entry
|
||||
* index (+1) with the hash's top byte; wider tables take the runtime
|
||||
* lookup. */
|
||||
/** The `.loop` lookup helpers only read memory and always return, as
|
||||
* declared: LLVM then merges two lookups of one key (a has() followed by
|
||||
* get()) before it inlines them. */
|
||||
const INLINED_LOOKUP_ATTRS = "alwaysinline memory(read) nounwind willreturn";
|
||||
|
||||
/** The number-key entry lookups (sc_map_entry_f64 and sc_map_entry_f64_int)
|
||||
* and their hashed probes. With `loop` they are the `.loop` flavor, which
|
||||
* lookups inside a loop call (emitNumberMapEntry): each of its functions is
|
||||
* inlined, so a lookup that finds its key in a bucket table makes no call,
|
||||
* and the loop keeps no argument moves or spills around one. */
|
||||
function numberMapEntryHelpers(sz: string, loop: boolean): string[] {
|
||||
const flavor = loop ? ".loop" : "";
|
||||
const attrs = loop ? `${INLINED_LOOKUP_ATTRS} ${FN_ATTRS}` : FN_ATTRS;
|
||||
let mapTmp = 0;
|
||||
const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`);
|
||||
return [
|
||||
`define internal ptr @sc_map_entry_f64${flavor}(ptr %m, double %k) ${attrs} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
...(sz === "i64"
|
||||
? [` %nd32 = load i32, ptr %ndp`, ` %nd = zext i32 %nd32 to i64`]
|
||||
: [` %nd = load i32, ptr %ndp`]),
|
||||
// A non-integral key inside the table is as absent as one outside it.
|
||||
...mapIndex.lines.map((line) => ` ${line}`),
|
||||
` br i1 ${mapIndex.ok}, label %slot, label %slow`,
|
||||
`slot:`,
|
||||
sz === "i64"
|
||||
? ` %i = add i64 ${mapIndex.wide}, 0`
|
||||
: ` %i = trunc i64 ${mapIndex.wide} to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed${flavor}(ptr %m, double %k)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// The same lookup for a key the emitter knows is an integer (`%ki`,
|
||||
// the double `%k`'s exact value): the direct index tests the integer
|
||||
// itself, so the double is converted only by the hashed probe. A
|
||||
// negative key is not below the index's size as an unsigned value.
|
||||
`define internal ptr @sc_map_entry_f64_int${flavor}(ptr %m, i64 %ki, double %k) ${attrs} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
` %nd32 = load i32, ptr %ndp`,
|
||||
` %nd = zext i32 %nd32 to i64`,
|
||||
` %inside = icmp ult i64 %ki, %nd`,
|
||||
` br i1 %inside, label %slot, label %slow`,
|
||||
`slot:`,
|
||||
sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed_int${flavor}(ptr %m, i64 %ki, double %k)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// Hashing (scr_map_find_f64): the SameValueZero key word and its
|
||||
// scr_key_mix hash. sc_map_entry_f64_int's probe takes the key's double
|
||||
// with its integer view: its bits are the key word, never NaN, and +0
|
||||
// for the integer 0. The caller converts the integer: cvtsi2sd writes
|
||||
// only the low half of its register, so in a function entry, where
|
||||
// LLVM cannot clear the register first, it waits for the register's
|
||||
// last value, in a loop of lookups often a value the previous
|
||||
// iteration loaded.
|
||||
...hashedNumberProbe(
|
||||
`sc_map_entry_f64_hashed${flavor}`,
|
||||
loop,
|
||||
"double %k",
|
||||
sz,
|
||||
[
|
||||
` %nan = fcmp uno double %k, %k`,
|
||||
` %zero = fcmp oeq double %k, 0.0`,
|
||||
` %kz = select i1 %zero, double 0.0, double %k`,
|
||||
` %kzb = bitcast double %kz to i64`,
|
||||
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
|
||||
],
|
||||
"%k",
|
||||
),
|
||||
...hashedNumberProbe(
|
||||
`sc_map_entry_f64_hashed_int${flavor}`,
|
||||
loop,
|
||||
"i64 %ki, double %k",
|
||||
sz,
|
||||
[
|
||||
` %kzero = icmp eq i64 %ki, 0`,
|
||||
` %kbits = bitcast double %k to i64`,
|
||||
` %kb = select i1 %kzero, i64 0, i64 %kbits`,
|
||||
],
|
||||
"%k",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/** The hashed probe of a number-key map (scr_map_find_f64), out of line,
|
||||
* or inlined with `loop`: `keyLines` define the normalized key word %kb
|
||||
* from the parameter, and `runtimeKey` is the double passed to the runtime
|
||||
* lookup. A map with at most SCR_MAP_LINEAR_LIMIT entries keeps no bucket
|
||||
* table, and neither does a map with the direct index, whose lookups reach
|
||||
* the probe only for a key outside the index, which is absent. In a small
|
||||
* map the absent-key filter is tested before its entries are scanned.
|
||||
* A bucket table is probed without the filter: the filter keeps two of its
|
||||
* 64 bits per key, so past a few dozen keys it rejects few absent keys, and
|
||||
* the probe's first bucket read answers those. Compact tables tag each
|
||||
* 24-bit entry index (+1) with the hash's top byte; wider tables take the
|
||||
* runtime lookup. */
|
||||
function hashedNumberProbe(
|
||||
name: string,
|
||||
loop: boolean,
|
||||
param: string,
|
||||
sz: string,
|
||||
keyLines: string[],
|
||||
runtimeKey: string,
|
||||
): string[] {
|
||||
return [
|
||||
`define internal ptr @${name}(ptr %m, ${param}) noinline ${FN_ATTRS} {`,
|
||||
`define internal ptr @${name}(ptr %m, ${param}) ${loop ? INLINED_LOOKUP_ATTRS : "noinline"} ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
...keyLines,
|
||||
` %x1s = lshr i64 %kb, 30`,
|
||||
@@ -1415,7 +1545,14 @@ function hashedNumberProbe(
|
||||
` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %ents = load ptr, ptr %eps`,
|
||||
` %linear = icmp eq ${sz} %nb, 0`,
|
||||
` br i1 %linear, label %filter, label %buckets`,
|
||||
` br i1 %linear, label %unbucketed, label %buckets`,
|
||||
// A map without a bucket table either indexes its keys directly, and
|
||||
// then holds none outside the index, or scans its entries.
|
||||
`unbucketed:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
` %nd = load i32, ptr %ndp`,
|
||||
` %indexed = icmp ne i32 %nd, 0`,
|
||||
` br i1 %indexed, label %miss, label %filter`,
|
||||
`filter:`,
|
||||
` %f1s = lshr i64 %h, 44`,
|
||||
` %f1a = and i64 %f1s, 63`,
|
||||
@@ -1582,117 +1719,22 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
}
|
||||
// Number-key Map/Set lookups probe the runtime's direct index inline:
|
||||
// while ndense is nonzero every live key is an integer below it, so an
|
||||
// in-range integral key reads its slot (entry index + 1, 0 = absent) and
|
||||
// any other key is absent. Maps without the index (ndense 0) hash the
|
||||
// key in sc_map_entry_f64_hashed, which scans the entries of a small map
|
||||
// or probes a compact bucket table as scr_map_find_f64 does, and calls
|
||||
// the runtime lookup for a wide table. It stays out of line so that the
|
||||
// direct-index path stays small at every call site. Both only read
|
||||
// memory, so LLVM merges lookups of one key when nothing is stored in
|
||||
// between (a has() followed by get(), for example).
|
||||
if (emitter.decls.has(NUMBER_MAP_ENTRY_DECL)) {
|
||||
const sz = emitter.sizeType;
|
||||
let mapTmp = 0;
|
||||
const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`);
|
||||
// in-range integral key reads its slot (entry index + 1, 0 = absent).
|
||||
// Any other key goes to sc_map_entry_f64_hashed, which probes a compact
|
||||
// bucket table as scr_map_find_f64 does, calls the runtime lookup for a
|
||||
// wide table, and in a map without a bucket table answers absent when the
|
||||
// map has the index (a map with buckets keeps no index, scr_map.h) and
|
||||
// scans the entries otherwise. A lookup in a hashed map so tests the
|
||||
// index once. The probe stays out of line so that the direct-index path
|
||||
// stays small at every call site, except in the `.loop` flavor
|
||||
// (numberMapEntryHelpers). Both only read memory, so LLVM merges lookups
|
||||
// of one key when nothing is stored in between (a has() followed by
|
||||
// get(), for example).
|
||||
if (emitter.decls.has(NUMBER_MAP_ENTRY_DECL))
|
||||
defs.push(
|
||||
`define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
...(sz === "i64"
|
||||
? [` %nd32 = load i32, ptr %ndp`, ` %nd = zext i32 %nd32 to i64`]
|
||||
: [` %nd = load i32, ptr %ndp`]),
|
||||
// A non-integral key inside the table is as absent as one outside it.
|
||||
...mapIndex.lines.map((line) => ` ${line}`),
|
||||
` br i1 ${mapIndex.ok}, label %slot, label %outside`,
|
||||
`slot:`,
|
||||
sz === "i64"
|
||||
? ` %i = add i64 ${mapIndex.wide}, 0`
|
||||
: ` %i = trunc i64 ${mapIndex.wide} to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`outside:`,
|
||||
` %dense.any = icmp ne ${sz} %nd, 0`,
|
||||
` br i1 %dense.any, label %miss, label %slow`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// The same lookup for a key the emitter knows is an integer (`%ki`,
|
||||
// the double `%k`'s exact value): the direct index tests the integer
|
||||
// itself, so the double is converted only by the hashed probe. A
|
||||
// negative key is not below the index's size as an unsigned value.
|
||||
`define internal ptr @sc_map_entry_f64_int(ptr %m, i64 %ki, double %k) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
` %nd32 = load i32, ptr %ndp`,
|
||||
` %nd = zext i32 %nd32 to i64`,
|
||||
` %inside = icmp ult i64 %ki, %nd`,
|
||||
` br i1 %inside, label %slot, label %outside`,
|
||||
`slot:`,
|
||||
sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`outside:`,
|
||||
` %dense.any = icmp ne i64 %nd, 0`,
|
||||
` br i1 %dense.any, label %miss, label %slow`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// Hashing (scr_map_find_f64): the SameValueZero key word and its
|
||||
// scr_key_mix hash. sc_map_entry_f64_int's probe converts the integer
|
||||
// view to the key word, which is never NaN or -0, and so skips the
|
||||
// normalization.
|
||||
...hashedNumberProbe(
|
||||
"sc_map_entry_f64_hashed",
|
||||
"double %k",
|
||||
sz,
|
||||
[
|
||||
` %nan = fcmp uno double %k, %k`,
|
||||
` %zero = fcmp oeq double %k, 0.0`,
|
||||
` %kz = select i1 %zero, double 0.0, double %k`,
|
||||
` %kzb = bitcast double %kz to i64`,
|
||||
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
|
||||
],
|
||||
"%k",
|
||||
),
|
||||
...hashedNumberProbe(
|
||||
"sc_map_entry_f64_hashed_int",
|
||||
"i64 %ki",
|
||||
sz,
|
||||
[` %kf = sitofp i64 %ki to double`, ` %kb = bitcast double %kf to i64`],
|
||||
"%kf",
|
||||
),
|
||||
...numberMapEntryHelpers(emitter.sizeType, false),
|
||||
...numberMapEntryHelpers(emitter.sizeType, true),
|
||||
);
|
||||
}
|
||||
// Map/Set iteration steps read the entries array inline: an index is
|
||||
// live when it is below the entry count and its entry's cached hash is
|
||||
// nonzero (a tombstone's is zero), as scr_map_iter_live decides, or for
|
||||
|
||||
@@ -1147,6 +1147,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
(countedLoop?.guarded || pairedLoop?.guarded) &&
|
||||
!LOOPS.of(emitter).provenCountedGuards.has(s)
|
||||
) {
|
||||
// A function that emits each loop once takes only the copy without
|
||||
// the integer form, which needs no safety test.
|
||||
if (LOOPS.of(emitter).plainLoops) {
|
||||
LOOPS.of(emitter).countedLoopsEnabled = false;
|
||||
emitSlowCopy(emitter, s);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = true;
|
||||
break;
|
||||
}
|
||||
const safe = countedLoop
|
||||
? emitCountedLoopSafety(emitter, countedLoop)
|
||||
: emitPairedLoopSafety(emitter, pairedLoop!);
|
||||
@@ -1278,26 +1286,26 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
}
|
||||
};
|
||||
// A counted loop over typed arrays, or a loop to a typed array's
|
||||
// length, polls between blocks of WORKER_POLL_BLOCK iterations instead
|
||||
// length, polls between blocks of workerPollBlock iterations instead
|
||||
// of in its body: the inner loop over a block then has no exit
|
||||
// besides its own bound, which LLVM can vectorize. A typed
|
||||
// array's length is fixed, so each block can take it as its limit when
|
||||
// the loop does not assign the array's binding.
|
||||
//
|
||||
// Inside another loop of this function, which polls at least once
|
||||
// every WORKER_POLL_BLOCK of its own iterations, a loop needs less:
|
||||
// every workerPollBlock of its own iterations, a loop needs less:
|
||||
// - a loop whose counters' ranges bound its runs, and those of the
|
||||
// loops in its body, to WORKER_POLL_BLOCK^2 needs no poll;
|
||||
// loops in its body, to workerPollBlock^2 needs no poll;
|
||||
// - an innermost counted loop is strip-mined, typed arrays or not,
|
||||
// and does not poll after its last block. A run of at most
|
||||
// WORKER_POLL_BLOCK iterations then never polls, and the countdown
|
||||
// workerPollBlock iterations then never polls, and the countdown
|
||||
// leaves its body. A run of at most WORKER_UNPOLLED_RUN iterations
|
||||
// is one block, polled once as it leaves.
|
||||
const pollBlock = workerPollBlock(emitter);
|
||||
const enclosed =
|
||||
emitter.mod.workers === true &&
|
||||
emitter.jumpTargets.some((target) => target.kind === "loop");
|
||||
const bounded =
|
||||
enclosed && boundedLoopRuns(emitter, s) <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK;
|
||||
const bounded = enclosed && boundedLoopRuns(emitter, s) <= pollBlock * pollBlock;
|
||||
const innermost = enclosed && !containsLoop(s.body);
|
||||
const stripMined =
|
||||
!bounded &&
|
||||
@@ -1305,7 +1313,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
integerSlot !== null &&
|
||||
((countedLoop !== null &&
|
||||
countedLimit !== null &&
|
||||
Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) ||
|
||||
Math.abs(countedLoop.step) * pollBlock <= 2 ** 52) ||
|
||||
(integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) &&
|
||||
emitter.workerLoopPolls(s) &&
|
||||
(loopAccessesTypedArrays(s) || (countedLoop !== null && innermost));
|
||||
@@ -1314,10 +1322,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
// step on each side, has a run whose length its counters fix on entry.
|
||||
// With a small body, a run of at most WORKER_UNPOLLED_RUN steps takes a
|
||||
// plain copy of the loop without a poll, which polls once as it leaves
|
||||
// when it ran more than WORKER_POLL_BLOCK steps, like a strip-mined run;
|
||||
// when it ran more than workerPollBlock steps, like a strip-mined run;
|
||||
// longer runs take the loop as it is emitted otherwise.
|
||||
const shortRuns: { stride: number; left: () => string } | null =
|
||||
!innermost ||
|
||||
LOOPS.of(emitter).plainLoops ||
|
||||
bounded ||
|
||||
capturedLets.length > 0 ||
|
||||
!emitter.workerLoopPolls(s) ||
|
||||
@@ -1491,7 +1500,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
emitPlainLoop(shortEnd, null);
|
||||
// A run longer than one block polls once as it leaves.
|
||||
B.startBlock(shortEnd);
|
||||
B.line(`${pollsAfter} = icmp sgt i64 ${left}, ${shortRuns.stride * WORKER_POLL_BLOCK}`);
|
||||
B.line(`${pollsAfter} = icmp sgt i64 ${left}, ${shortRuns.stride * pollBlock}`);
|
||||
B.condBr(pollsAfter, shortPoll, exit);
|
||||
B.startBlock(shortPoll);
|
||||
emitter.emitWorkerPoll();
|
||||
@@ -2811,8 +2820,15 @@ function emitInt32KindLoop(
|
||||
|
||||
/** At the top of a loop copy's header: lets a speculative loop find its
|
||||
* ordinary copy's header, and keeps its third copy out of the loop. */
|
||||
/** Iterations between cancellation polls of a strip-mined counted loop. */
|
||||
const WORKER_POLL_BLOCK = 64;
|
||||
/** Iterations between cancellation polls of a strip-mined counted loop. A
|
||||
* block holds at least one step of the vectorized loop over bytes, so that
|
||||
* a full block runs vector code: 64 iterations at the baseline (16-byte
|
||||
* vectors, interleaved 4 times), and 128 when the program's code is compiled
|
||||
* for x86-64-v3 or above, whose vectors take 32 bytes. */
|
||||
function workerPollBlock(emitter: LlEmitter): number {
|
||||
const cpu = emitter.targetCpu;
|
||||
return emitter.cpuDispatch || cpu === "x86-64-v3" || cpu === "x86-64-v4" ? 128 : 64;
|
||||
}
|
||||
|
||||
/** The longest run of a strip-mined loop inside another loop that polls
|
||||
* only as it leaves. */
|
||||
@@ -2855,7 +2871,8 @@ function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number {
|
||||
if (stepped && range && exactSteps(range, stepped.step))
|
||||
trips = Math.floor((range.max - range.min) / Math.abs(stepped.step)) + 1;
|
||||
}
|
||||
if (!(trips <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK)) return Infinity;
|
||||
const pollBlock = workerPollBlock(emitter);
|
||||
if (!(trips <= pollBlock * pollBlock)) return Infinity;
|
||||
let nested = 0;
|
||||
const visit = (s: IrStmt): boolean => {
|
||||
if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf") {
|
||||
@@ -2909,17 +2926,17 @@ interface StripMinedCounter {
|
||||
limit: () => string;
|
||||
}
|
||||
|
||||
/** A counted `for` loop of a worker program as blocks of WORKER_POLL_BLOCK
|
||||
/** A counted `for` loop of a worker program as blocks of workerPollBlock
|
||||
* iterations, with the cancellation poll between blocks: the outer header
|
||||
* tests the counter against the loop's limit, each block runs the body up
|
||||
* to the nearer of the limit and WORKER_POLL_BLOCK steps on, and the poll
|
||||
* to the nearer of the limit and workerPollBlock steps on, and the poll
|
||||
* follows the block. It polls once per block, and the inner loop's only
|
||||
* exit besides `break` and `return` is its bound.
|
||||
*
|
||||
* With `runSlot` (for a loop that another loop of the function encloses), a
|
||||
* block that ends at the limit leaves the loop without polling, and a run
|
||||
* of at most WORKER_UNPOLLED_RUN iterations is one block, polled once as
|
||||
* it leaves when it took more than WORKER_POLL_BLOCK iterations. `runSlot`
|
||||
* it leaves when it took more than workerPollBlock iterations. `runSlot`
|
||||
* holds whether the current block is such a run. */
|
||||
function emitStripMinedCountedLoop(
|
||||
emitter: LlEmitter,
|
||||
@@ -2932,6 +2949,7 @@ function emitStripMinedCountedLoop(
|
||||
const B = emitter.B;
|
||||
const { slot, type, signed, step } = counter;
|
||||
const ascending = step > 0;
|
||||
const pollBlock = workerPollBlock(emitter);
|
||||
const compare = !signed ? "ult" : ascending ? "slt" : "sgt";
|
||||
const saturating = `llvm.${signed ? "sadd" : "uadd"}.sat.${type}`;
|
||||
const nearer = `llvm.${!signed ? "umin" : ascending ? "smin" : "smax"}.${type}`;
|
||||
@@ -2956,9 +2974,7 @@ function emitStripMinedCountedLoop(
|
||||
B.startBlock(block);
|
||||
const stepped = B.tmp();
|
||||
const bound = B.tmp();
|
||||
B.line(
|
||||
`${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`,
|
||||
);
|
||||
B.line(`${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * pollBlock})`);
|
||||
B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`);
|
||||
if (runSlot === null) B.line(`store ${type} ${bound}, ptr ${boundSlot}`);
|
||||
else {
|
||||
@@ -2975,7 +2991,7 @@ function emitStripMinedCountedLoop(
|
||||
: `${left} = sub ${wrap} ${type} ${start}, ${limit}`,
|
||||
);
|
||||
B.line(`${whole} = icmp ule ${type} ${left}, ${stride * WORKER_UNPOLLED_RUN}`);
|
||||
B.line(`${long} = icmp ugt ${type} ${left}, ${stride * WORKER_POLL_BLOCK}`);
|
||||
B.line(`${long} = icmp ugt ${type} ${left}, ${stride * pollBlock}`);
|
||||
B.line(`${polled} = and i1 ${whole}, ${long}`);
|
||||
B.line(`${end} = select i1 ${whole}, ${type} ${limit}, ${type} ${bound}`);
|
||||
B.line(`store ${type} ${end}, ptr ${boundSlot}`);
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { CompilationTiming } from "../../timing.js";
|
||||
import { relieveHeapPressure } from "../../heap-pressure.js";
|
||||
import { analyzeFrameEscapes, type FrameEscapes } from "./frame-escapes.js";
|
||||
import { wasmBoundsChecks } from "./wasm-modules.js";
|
||||
import { jsTruthy } from "./jsval.js";
|
||||
import { jsImmediates, jsTruthy } from "./jsval.js";
|
||||
import { JsIcSites } from "../../ir/js-ic.js";
|
||||
import { type ArraySearchKind } from "./array-search.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
@@ -286,6 +286,8 @@ import {
|
||||
cryptoIntThunkFor,
|
||||
type BytesThunkValue,
|
||||
fsRenameThunkFor,
|
||||
watchNameThunkFor,
|
||||
watchErrorThunkFor,
|
||||
streamCbThunkFor,
|
||||
zlibBytesThunkFor,
|
||||
} from "./expr-stream-callbacks.js";
|
||||
@@ -374,7 +376,7 @@ import {
|
||||
type ArenaPlan,
|
||||
} from "./arenas.js";
|
||||
import { children, type Node } from "./region-ir.js";
|
||||
import { LlDyn, recordDynKeepsIdentity, type DynHost } from "./dyn.js";
|
||||
import { LlDyn, recordDynKeepsIdentity, type DynHost, type JsEntryClosure } from "./dyn.js";
|
||||
import { VirtualBorrows } from "./virtual-borrows.js";
|
||||
import { planRecordViews, recordViewFieldPtr } from "./record-views.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -1485,6 +1487,10 @@ export class LlEmitter {
|
||||
private readonly rcHelpersNoinline: boolean;
|
||||
|
||||
readonly fnByName = new Map<string, IrFunction>();
|
||||
/** Whether some closure has a jsval body (IrFunction.jsEntry): the
|
||||
* dynamic tier's dynCall and dynInvoke sites then call a function value's
|
||||
* jsval entry when it has one. */
|
||||
jsEntries = false;
|
||||
/** Empty literals that construct with inline slots (appended-array-literals.ts). */
|
||||
appendedArrayLiterals: ReadonlySet<IrExpr> = new Set();
|
||||
/** Emitted LLVM text of every IR function body, by function name: the
|
||||
@@ -2180,7 +2186,10 @@ export class LlEmitter {
|
||||
this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
|
||||
this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
|
||||
this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []);
|
||||
for (const fn of mod.functions) this.fnByName.set(fn.name, fn);
|
||||
for (const fn of mod.functions) {
|
||||
this.fnByName.set(fn.name, fn);
|
||||
if (fn.jsEntry !== undefined) this.jsEntries = true;
|
||||
}
|
||||
this.appendedArrayLiterals = findAppendedArrayLiterals(mod, this.nodeIndex);
|
||||
for (const entry of mod.ffiImports ?? []) {
|
||||
this.ffiByName.set(entry.name, entry);
|
||||
@@ -2396,6 +2405,7 @@ export class LlEmitter {
|
||||
? undefined
|
||||
: (table, key, value) => this.journal?.entry(table, key, value),
|
||||
compactArrayElem: (elem) => this.compactArrayElem(elem),
|
||||
jsEntryClosures: () => this.jsEntryClosures(),
|
||||
compactRegion: () => this.compactRegion,
|
||||
sizeType: this.sizeType,
|
||||
cycleBufOffset: this.cycleBufOffset,
|
||||
@@ -5064,6 +5074,8 @@ export class LlEmitter {
|
||||
idx,
|
||||
(decl) => this.declare(decl),
|
||||
this.rcMode === undefined,
|
||||
(text) => this.cstr(text),
|
||||
this.shapeHost.recordViews?.partialTargets.has(shapeId) === true,
|
||||
);
|
||||
} else {
|
||||
p = this.B.tmp();
|
||||
@@ -5568,6 +5580,30 @@ export class LlEmitter {
|
||||
* the fiber eagerly to its first suspension and returns the promise);
|
||||
* generator bodies via theirs (which only ALLOCATES the suspended
|
||||
* fiber and returns the generator object). */
|
||||
private jsEntryClosureList: readonly JsEntryClosure[] | null = null;
|
||||
|
||||
/** The closures with a jsval body (IrFunction.jsEntry), on targets with
|
||||
* jsval words. */
|
||||
jsEntryClosures(): readonly JsEntryClosure[] {
|
||||
if (this.jsEntryClosureList !== null) return this.jsEntryClosureList;
|
||||
const list: JsEntryClosure[] = [];
|
||||
if (this.jsEntries && jsImmediates(this))
|
||||
for (const fn of this.fnByName.values()) {
|
||||
const body = fn.jsEntry === undefined ? undefined : this.fnByName.get(fn.jsEntry);
|
||||
if (body === undefined) continue;
|
||||
list.push({
|
||||
closure: this.callTarget(fn.name),
|
||||
params: fn.params.map((p) => p.type),
|
||||
ret: fn.returnType,
|
||||
body: this.callTarget(body.name),
|
||||
bodyParams: body.params.map((p) => p.type),
|
||||
bodyRet: body.returnType,
|
||||
});
|
||||
}
|
||||
this.jsEntryClosureList = list;
|
||||
return list;
|
||||
}
|
||||
|
||||
callTarget(fnName: string): string {
|
||||
const fn = this.fnByName.get(fnName);
|
||||
if (fn?.generator !== undefined) return mangleGenSpawn(fnName);
|
||||
@@ -8161,6 +8197,14 @@ export class LlEmitter {
|
||||
return fsRenameThunkFor(this, cbT);
|
||||
}
|
||||
|
||||
watchNameThunkFor(cbT: IrType & { kind: "func" }): string {
|
||||
return watchNameThunkFor(this, cbT);
|
||||
}
|
||||
|
||||
watchErrorThunkFor(cbT: IrType & { kind: "func" }): string {
|
||||
return watchErrorThunkFor(this, cbT);
|
||||
}
|
||||
|
||||
cryptoBytesThunkFor(
|
||||
cbT: IrType & { kind: "func" },
|
||||
arrayBuffer = false,
|
||||
@@ -8553,6 +8597,7 @@ for (const op of [
|
||||
"dyn.promiseDynAdapterHelper",
|
||||
"dyn.dynFuncThunkHelper",
|
||||
"dyn.dynFuncBoxHelper",
|
||||
"dyn.jsEntryThunk",
|
||||
"dyn.dynFuncAdapterHelper",
|
||||
"dyn.dynToStrHelper",
|
||||
"dyn.dynIterNHelper",
|
||||
|
||||
@@ -1003,6 +1003,16 @@ export function emitCallExpr(
|
||||
} else if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
|
||||
return result;
|
||||
};
|
||||
// The direct entry of a known target brackets the call when the target
|
||||
// may observe the receiver. A signature adapter that only forwards the
|
||||
// receiver to its original carries the original's RECEIVER_FREE bit
|
||||
// (set where the adapter closure is built), so its entry tests the bit,
|
||||
// like the entry through the closure's own pointer.
|
||||
const invokeKnown = (target: string): string => {
|
||||
const readers = host.receiverReaders;
|
||||
if (readers.ignores(known!)) return invoke(target, false);
|
||||
return invoke(target, true, args, readers.forwardsOnly(known!));
|
||||
};
|
||||
const lending =
|
||||
lent && !dynamic
|
||||
? known!.captures !== undefined
|
||||
@@ -1025,7 +1035,7 @@ export function emitCallExpr(
|
||||
(e.callee.kind === "closure" ||
|
||||
(e.callee.kind === "varRef" && host.knownCallbacks.has(e.callee.localId)))
|
||||
) {
|
||||
t = invoke(lending ?? direct, !host.receiverReaders.ignores(known!));
|
||||
t = invokeKnown(lending ?? direct);
|
||||
if (quiet) pending = false;
|
||||
} else {
|
||||
const fnp = B.tmp(),
|
||||
@@ -1072,7 +1082,7 @@ export function emitCallExpr(
|
||||
B.line(`${matches} = icmp eq ptr ${fn}, ${direct}`);
|
||||
B.condBr(matches, fast, fallback);
|
||||
B.startBlock(fast);
|
||||
const specialized = invoke(lending ?? direct, !host.receiverReaders.ignores(known!));
|
||||
const specialized = invokeKnown(lending ?? direct);
|
||||
// `fast` may have split for exception checks; join from the end.
|
||||
const fastEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
|
||||
@@ -27,11 +27,13 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { BYTES_ELEM_NUM, closureIdentityEqual } from "./common.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { elemAccess, vAdapters } from "./shapes.js";
|
||||
import { elemAccess, releaseSym, vAdapters } from "./shapes.js";
|
||||
import { JS_BITS } from "./jsval-encoding.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { emitJsIcLiteralMember, emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { jsImmediates } from "./jsval.js";
|
||||
import { jsIcLiteralKey } from "../../ir/js-ic.js";
|
||||
import { DYN_BUFFER_METHODS } from "../../ir/runtime-features.js";
|
||||
|
||||
@@ -189,6 +191,30 @@ export function emitDynamicExpr(
|
||||
return out;
|
||||
}
|
||||
const args = e.args.map((a) => emitBorrowedInput(host, a));
|
||||
host.declare(`declare ptr @scr_dyn_call(ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
const fast = jsEntrySites(host);
|
||||
if (fast || args.some((a) => a.type.kind === "jsval")) {
|
||||
const name = calleeName();
|
||||
const t = emitJsvalArgumentsCall(
|
||||
host,
|
||||
callee.name,
|
||||
receiver?.name ?? "null",
|
||||
args,
|
||||
fast,
|
||||
(argsPtr) => {
|
||||
const r = B.tmp();
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_dyn_call(ptr ${callee.name}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
return r;
|
||||
},
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
let argsPtr = "null";
|
||||
if (args.length > 0) {
|
||||
const arr = B.slot();
|
||||
@@ -202,7 +228,6 @@ export function emitDynamicExpr(
|
||||
});
|
||||
argsPtr = arr;
|
||||
}
|
||||
host.declare(`declare ptr @scr_dyn_call(ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
const t = B.tmp();
|
||||
const name = calleeName();
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
|
||||
@@ -246,6 +271,39 @@ export function emitDynamicExpr(
|
||||
);
|
||||
}
|
||||
}
|
||||
// Buffer.prototype method names take their scr_bytes_dyn.c entry, which
|
||||
// runs them on Buffer receivers and dispatches any other receiver.
|
||||
// Each call passes its method name's id slot, so only the first call
|
||||
// looks the name up.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
|
||||
const discardedPush =
|
||||
discarded && e.method === "push" && invoke === "scr_dyn_invoke_prepared_id";
|
||||
const fast = !discardedPush && invoke === "scr_dyn_invoke_prepared_id" && jsEntrySites(host);
|
||||
if (fast || args.some((a) => a.type.kind === "jsval")) {
|
||||
const name = discardedPush ? host.cstr(e.calleeName) : dynInvokeCalleeName(host, e);
|
||||
if (discardedPush)
|
||||
host.declare(
|
||||
`declare void @scr_dyn_invoke_discard_id(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`,
|
||||
);
|
||||
else host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
const t = emitJsvalArgumentsCall(host, prepared, recv.name, args, fast, (argsPtr) => {
|
||||
const site = `ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name}`;
|
||||
if (discardedPush) {
|
||||
B.line(`call void @scr_dyn_invoke_discard_id(${site})`);
|
||||
return "null";
|
||||
}
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${invoke}(${site})`);
|
||||
return r;
|
||||
});
|
||||
if (discardedPush) {
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: VOID };
|
||||
}
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
let argsPtr = "null";
|
||||
if (args.length > 0) {
|
||||
const arr = B.slot();
|
||||
@@ -259,11 +317,6 @@ export function emitDynamicExpr(
|
||||
});
|
||||
argsPtr = arr;
|
||||
}
|
||||
// Buffer.prototype method names take their scr_bytes_dyn.c entry, which
|
||||
// runs them on Buffer receivers and dispatches any other receiver.
|
||||
// Each call passes its method name's id slot, so only the first call
|
||||
// looks the name up.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
|
||||
// A statement-position push drops the new length, so its entry
|
||||
// need not box it.
|
||||
if (discarded && e.method === "push" && invoke === "scr_dyn_invoke_prepared_id") {
|
||||
@@ -277,12 +330,7 @@ export function emitDynamicExpr(
|
||||
return { name: "", type: VOID };
|
||||
}
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
let name = host.cstr(e.calleeName);
|
||||
if (e.calleeNameValue !== undefined) {
|
||||
const value = emitBorrowedInput(host, e.calleeNameValue);
|
||||
name = B.tmp();
|
||||
B.line(`${name} = getelementptr inbounds %ScrStr, ptr ${value.name}, i32 1`);
|
||||
}
|
||||
const name = dynInvokeCalleeName(host, e);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
@@ -547,6 +595,19 @@ export function emitDynamicExpr(
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
// A declared-only arm without the literal key: the object has no
|
||||
// such own property, so the read answers undefined.
|
||||
if (literal !== null && !shape.indexValue) {
|
||||
const tag = resultDef?.arms.findIndex((a) => a.kind === "undefinedT") ?? -1;
|
||||
if (tag < 0 || e.type.kind !== "union") {
|
||||
throw new InternalCompilerError(
|
||||
"llvm emitter bug: unionKeyGet missing key without an undefined result arm",
|
||||
);
|
||||
}
|
||||
B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, tag)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
// Index-signature arm (or declared-only shape under a runtime
|
||||
// key): the shared keyed-read chain — a literal key naming no
|
||||
// declared field touches only the overflow map.
|
||||
@@ -1389,3 +1450,135 @@ function messageListenerArg(e: ExprOf<"dynInvoke">): (IrType & { kind: "func" })
|
||||
if (listener?.kind !== "dynFrom" || listener.value.type.kind !== "func") return null;
|
||||
return listener.value.type;
|
||||
}
|
||||
|
||||
/** Whether a dynCall or dynInvoke site of the current function tests its
|
||||
* callee for a jsval entry (emitJsvalArgumentsCall): dynamic-tier code of
|
||||
* a module with closure jsval bodies, on targets with jsval words. */
|
||||
function jsEntrySites(host: LlvmEmitterContext): boolean {
|
||||
return (
|
||||
host.jsEntries &&
|
||||
jsImmediates(host) &&
|
||||
host.fnByName.get(host.currentFunctionName)?.tier === "dynamic"
|
||||
);
|
||||
}
|
||||
|
||||
/** A call of the function value `callee` (null for a site without one)
|
||||
* with `args`, which may include jsval words (ir/jsval.ts). With `fast`,
|
||||
* a callee boxed from a closure with a jsval body (ScrDyn.v.fn.js, +72)
|
||||
* takes the arguments as words, a dyn argument under the cell tag, with
|
||||
* `receiver` bound as `this` around the call. Every other call goes
|
||||
* through `slow`, which takes an array of dyn arguments: the words boxed
|
||||
* for it and released after it. Returns the owned dyn result (null on
|
||||
* the slow path with an exception pending). */
|
||||
function emitJsvalArgumentsCall(
|
||||
host: LlvmEmitterContext,
|
||||
callee: string | null,
|
||||
receiver: string,
|
||||
args: readonly LlValue[],
|
||||
fast: boolean,
|
||||
slow: (argsPtr: string) => string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const n = args.length;
|
||||
const result = B.slot();
|
||||
B.entryAllocas.push(`${result} = alloca ptr`);
|
||||
const lSlow = B.newLabel("jse.slow");
|
||||
const lJoin = B.newLabel("jse.join");
|
||||
if (fast && callee !== null) {
|
||||
let words = "null";
|
||||
if (n > 0) {
|
||||
words = B.slot();
|
||||
B.entryAllocas.push(`${words} = alloca [${n} x ptr]`);
|
||||
args.forEach((a, i) => {
|
||||
let word = a.name;
|
||||
if (a.type.kind !== "jsval") {
|
||||
const bits = B.tmp();
|
||||
const tagged = B.tmp();
|
||||
word = B.tmp();
|
||||
B.line(`${bits} = ptrtoint ptr ${a.name} to i64`);
|
||||
B.line(`${tagged} = or i64 ${bits}, ${JS_BITS.cellTag}`);
|
||||
B.line(`${word} = inttoptr i64 ${tagged} to ptr`);
|
||||
}
|
||||
const p = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds [${n} x ptr], ptr ${words}, i64 0, i64 ${i}`);
|
||||
B.line(`store ptr ${word}, ptr ${p}`);
|
||||
});
|
||||
}
|
||||
const lKind = B.newLabel("jse.kind");
|
||||
const lFn = B.newLabel("jse.fn");
|
||||
const lEntry = B.newLabel("jse.entry");
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = icmp ne ptr ${callee}, null`);
|
||||
B.condBr(present, lKind, lSlow);
|
||||
B.startBlock(lKind);
|
||||
const kindPtr = B.tmp();
|
||||
const kind = B.tmp();
|
||||
const isFn = B.tmp();
|
||||
B.line(`${kindPtr} = getelementptr inbounds i8, ptr ${callee}, i64 8`);
|
||||
B.line(`${kind} = load i32, ptr ${kindPtr}`);
|
||||
B.line(`${isFn} = icmp eq i32 ${kind}, ${DYN_KIND.FUNC}`);
|
||||
B.condBr(isFn, lFn, lSlow);
|
||||
B.startBlock(lFn);
|
||||
const entryPtr = B.tmp();
|
||||
const entry = B.tmp();
|
||||
const hasEntry = B.tmp();
|
||||
B.line(`${entryPtr} = getelementptr inbounds i8, ptr ${callee}, i64 72 ; ->v.fn.js`);
|
||||
B.line(`${entry} = load ptr, ptr ${entryPtr}`);
|
||||
B.line(`${hasEntry} = icmp ne ptr ${entry}, null`);
|
||||
B.condBr(hasEntry, lEntry, lSlow);
|
||||
B.startBlock(lEntry);
|
||||
const cloPtr = B.tmp();
|
||||
const clo = B.tmp();
|
||||
const word = B.tmp();
|
||||
const value = B.tmp();
|
||||
host.declare("declare void @scr_dyn_this_push_dyn(ptr)");
|
||||
host.declare("declare void @scr_dyn_this_pop()");
|
||||
host.declare("declare ptr @scr_js_to_dyn(ptr)");
|
||||
B.line(`${cloPtr} = getelementptr inbounds i8, ptr ${callee}, i64 16 ; ->v.fn.clo`);
|
||||
B.line(`${clo} = load ptr, ptr ${cloPtr}`);
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver})`);
|
||||
B.line(`${word} = call ptr ${entry}(ptr ${clo}, ptr ${words}, ${host.sizeType} ${n})`);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
B.line(`${value} = call ptr @scr_js_to_dyn(ptr ${word})`);
|
||||
B.line(`store ptr ${value}, ptr ${result}`);
|
||||
B.br(lJoin);
|
||||
} else {
|
||||
B.br(lSlow);
|
||||
}
|
||||
B.startBlock(lSlow);
|
||||
let cells = "null";
|
||||
const boxed: string[] = [];
|
||||
if (n > 0) {
|
||||
cells = B.slot();
|
||||
B.entryAllocas.push(`${cells} = alloca [${n} x ptr]`);
|
||||
args.forEach((a, i) => {
|
||||
let cell = a.name;
|
||||
if (a.type.kind === "jsval") {
|
||||
host.declare("declare ptr @scr_js_to_dyn_borrow(ptr)");
|
||||
cell = B.tmp();
|
||||
B.line(`${cell} = call ptr @scr_js_to_dyn_borrow(ptr ${a.name})`);
|
||||
boxed.push(cell);
|
||||
}
|
||||
const p = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds [${n} x ptr], ptr ${cells}, i64 0, i64 ${i}`);
|
||||
B.line(`store ptr ${cell}, ptr ${p}`);
|
||||
});
|
||||
}
|
||||
const r = slow(cells);
|
||||
for (const cell of boxed) B.line(`call void ${releaseSym(host.shapeHost, DYN)}(ptr ${cell})`);
|
||||
B.line(`store ptr ${r}, ptr ${result}`);
|
||||
B.br(lJoin);
|
||||
B.startBlock(lJoin);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = load ptr, ptr ${result}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A dynInvoke's callee spelling for the runtime's error texts. */
|
||||
function dynInvokeCalleeName(host: LlvmEmitterContext, e: ExprOf<"dynInvoke">): string {
|
||||
if (e.calleeNameValue === undefined) return host.cstr(e.calleeName);
|
||||
const value = emitBorrowedInput(host, e.calleeNameValue);
|
||||
const name = host.B.tmp();
|
||||
host.B.line(`${name} = getelementptr inbounds %ScrStr, ptr ${value.name}, i32 1`);
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { isRefCounted, type IrExpr, type IrStrCmpOp, type IrType } from "../../ir/ir.js";
|
||||
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, elemAccess } from "./shapes.js";
|
||||
import { recordViewTestLines } from "./record-views.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import { offsetIndex } from "./expr-bytes.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
@@ -1473,16 +1474,70 @@ export function emitRecordExpr(
|
||||
return host.emitRecordKeyGet(e);
|
||||
case "recordHas": {
|
||||
// Own-property presence: only an undefined-armed union slot can
|
||||
// lack its property, by holding the ABSENT state.
|
||||
// lack its property, by holding the ABSENT state, and a PARTIAL
|
||||
// record view (record-views.ts) can lack a field its viewed record
|
||||
// does not declare.
|
||||
const fieldType = host.recordsById
|
||||
.get(e.shapeId)
|
||||
?.fields.find((f) => f.name === e.field)?.type;
|
||||
const obj = host.emitReadReceiver(e.obj);
|
||||
if (host.shapeHost.recordViews?.partialMissing(e.shapeId, e.field) !== true) {
|
||||
const { ptr, type, inline } = host.recordFieldPtr(obj.name, e.shapeId, e.field);
|
||||
if (type.kind !== "union" || undefinedArmTag(type, host.unionsById) < 0)
|
||||
return { name: "true", type: e.type };
|
||||
const v = host.loadField(ptr, type, inline);
|
||||
const absent = host.fieldAbsentTest(v, type.unionId);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = xor i1 ${absent}, true`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
// A field some partial view of this shape lacks: ask the view first.
|
||||
// Only a present field's slot may be looked up — a partial view's
|
||||
// absent field answers no slot at all (scr_record_view_slot).
|
||||
const position = host.recordsById.get(e.shapeId)!.fields.findIndex((f) => f.name === e.field);
|
||||
host.declare(`declare ptr @scr_record_view_slot(ptr, ${host.sizeType})`);
|
||||
const isView = B.tmp();
|
||||
const rhFrom = B.currentLabel();
|
||||
const view = B.newLabel("rh.view");
|
||||
const done = B.newLabel("rh.done");
|
||||
for (const line of recordViewTestLines(host.sizeType, obj.name, isView))
|
||||
B.line(line.trimStart());
|
||||
// Not a view: every field of the target has its slot.
|
||||
B.condBr(isView, view, done, `, !prof !{!"branch_weights", i32 1, i32 1000}`);
|
||||
B.startBlock(view);
|
||||
const slot = B.tmp();
|
||||
B.line(
|
||||
`${slot} = call ptr @scr_record_view_slot(ptr ${obj.name}, ${host.sizeType} ${position})`,
|
||||
);
|
||||
const hasSlot = B.tmp();
|
||||
B.line(`${hasSlot} = icmp ne ptr ${slot}, null`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const hasView = B.tmp();
|
||||
B.line(`${hasView} = phi i1 [ true, %${rhFrom} ], [ ${hasSlot}, %${view} ]`);
|
||||
if (fieldType === undefined || fieldType.kind !== "union")
|
||||
return { name: hasView, type: e.type };
|
||||
const unionId = fieldType.unionId;
|
||||
if (undefinedArmTag(fieldType, host.unionsById) < 0) return { name: hasView, type: e.type };
|
||||
// The field is also undefined-armed: a present slot still answers by
|
||||
// its ABSENT state, a field the view lacks by the view.
|
||||
const check = B.newLabel("rh.present");
|
||||
const join = B.newLabel("rh.join");
|
||||
const from = B.currentLabel();
|
||||
B.condBr(hasView, check, join);
|
||||
B.startBlock(check);
|
||||
const { ptr, type, inline } = host.recordFieldPtr(obj.name, e.shapeId, e.field);
|
||||
if (type.kind !== "union" || undefinedArmTag(type, host.unionsById) < 0)
|
||||
return { name: "true", type: e.type };
|
||||
const v = host.loadField(ptr, type, inline);
|
||||
const absent = host.fieldAbsentTest(v, type.unionId);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = xor i1 ${absent}, true`);
|
||||
return { name: t, type: e.type };
|
||||
const absent = host.fieldAbsentTest(v, unionId);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = xor i1 ${absent}, true`);
|
||||
// The field load can end in a later block (a view's slot read).
|
||||
const checked = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const answer = B.tmp();
|
||||
B.line(`${answer} = phi i1 [ false, %${from} ], [ ${present}, %${checked} ]`);
|
||||
return { name: answer, type: e.type };
|
||||
}
|
||||
case "fieldAbsent":
|
||||
return { name: host.absentInstanceRef(e.unionId), type: e.type };
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
llFieldType,
|
||||
loadStructField,
|
||||
releaseSym,
|
||||
traceAdapter,
|
||||
traceArg,
|
||||
vAdapters,
|
||||
type ShapeHost,
|
||||
@@ -754,6 +755,50 @@ function symbolKeyLines(
|
||||
};
|
||||
}
|
||||
|
||||
/** The ScrStreamTypedDesc constant (scr_stream.h) of a record type an
|
||||
* object-mode stream holds as the record itself, or null for a type whose
|
||||
* chunks stay boxed: records without declared fields, with an index
|
||||
* signature or accessor slots, tuples, the text codec records (they box as
|
||||
* handles), and records the collector traces (a stream does not trace the
|
||||
* records it holds). The constant's `box` converts the record as a
|
||||
* live-reference conversion to dyn does (streamTypedRefBoxValue). */
|
||||
export function streamTypedDesc(host: LlvmEmitterContext, t: IrType): string | null {
|
||||
if (t.kind !== "record") return null;
|
||||
const shape = host.recordsById.get(t.shapeId);
|
||||
if (
|
||||
!shape ||
|
||||
shape.fields.length === 0 ||
|
||||
shape.indexValue !== undefined ||
|
||||
shape.tuple ||
|
||||
shapeHasAccessorSlots(shape) ||
|
||||
shape.fields.some((field) => field.name === "handleEvent" && field.type.kind === "func") ||
|
||||
textCodecRecordClass(t, (id) => host.recordsById.get(id)) !== null ||
|
||||
traceAdapter(host.shapeHost, t) !== null
|
||||
)
|
||||
return null;
|
||||
const key = typeKey(t);
|
||||
const cacheKey = `std:${key}`;
|
||||
const existing = host.resolveThunks.get(cacheKey);
|
||||
if (existing) return `@${existing}`;
|
||||
const sym = host.contentSymbol("sc_std_", key);
|
||||
host.resolveThunks.set(cacheKey, sym);
|
||||
host.journal?.allocated(sym, "streamTypedDesc", [t]);
|
||||
const B = new BlockBuilder();
|
||||
const boxed = streamTypedRefBoxValue(host, B, t, "%p");
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
const rc = vAdapters(host.shapeHost, t);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr @${sym}_box(ptr %p) ${FN_ATTRS} { ; box a typed stream chunk ${key}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
`@${sym} = internal constant { ptr, ${host.sizeType}, ptr, ptr, ptr } { ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${rc.retain}, ptr ${rc.release}, ptr @${sym}_box }`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return `@${sym}`;
|
||||
}
|
||||
|
||||
export function streamTypedRefBoxValue(
|
||||
host: LlvmEmitterContext,
|
||||
B: BlockBuilder,
|
||||
@@ -2018,6 +2063,9 @@ export function streamFromArrayAdapter(
|
||||
}
|
||||
|
||||
registerReplay("dynPromiseAdapter", (host, args) => dynPromiseAdapter(host, args[0] as IrType));
|
||||
registerReplay("streamTypedDesc", (host, args) =>
|
||||
(streamTypedDesc(host, args[0] as IrType) ?? "").slice(1),
|
||||
);
|
||||
registerReplay("liveDynUnionRefAdapter", (host, args) =>
|
||||
liveDynUnionRefAdapter(host, args[0] as IrType & { kind: "union" }),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { type IrType, isRefCounted, typeKey } from "../../ir/ir.js";
|
||||
import { FN_ATTRS, releaseSym, retainSym, traceArg } from "./shapes.js";
|
||||
import { type IrType, isRefCounted, STRING, typeKey } from "../../ir/ir.js";
|
||||
import { streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import { FN_ATTRS, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { unionPeekLines, unionTagLines } from "./union-repr.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
import { streamTypedDesc } from "./expr-stream-bridges.js";
|
||||
|
||||
export function streamDataAdapter(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -259,6 +261,31 @@ export function streamDoneFnFor(
|
||||
strTag = def!.arms.findIndex((a) => a.kind === "string");
|
||||
}
|
||||
}
|
||||
// data of one record type or nothing (a typed completion,
|
||||
// pinTypedDataCompletion): a record with a typed-chunk descriptor
|
||||
// completes as a typed chunk, and another record boxes by reference
|
||||
// (the conversion a dyn `data` parameter receives).
|
||||
let typedTag = -1;
|
||||
let typedDesc: string | null = null;
|
||||
let liveData = false;
|
||||
if (valueData && dataT?.kind === "union") {
|
||||
const arms = host.unionsById.get(dataT.unionId)?.arms ?? [];
|
||||
const record = arms.findIndex((a) => a.kind === "record");
|
||||
if (arms.length === 2 && record >= 0 && arms.some((a) => a.kind === "undefinedT")) {
|
||||
typedDesc = streamTypedDesc(host, arms[record]!);
|
||||
if (typedDesc !== null) typedTag = record;
|
||||
else {
|
||||
// the records a conversion to dyn keeps by reference (Lowerer's
|
||||
// liveReference)
|
||||
const armT = arms[record]!;
|
||||
const shape = armT.kind === "record" ? host.recordsById.get(armT.shapeId) : undefined;
|
||||
liveData =
|
||||
streamTypedRefEligible(armT) &&
|
||||
shape !== undefined &&
|
||||
(shape.fields.length > 0 || shape.indexValue !== undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
const entry =
|
||||
kind === "c"
|
||||
? "scr_stream_construct_done"
|
||||
@@ -269,18 +296,20 @@ export function streamDoneFnFor(
|
||||
: kind === "d"
|
||||
? "scr_stream_destroy_done"
|
||||
: kind === "t"
|
||||
? valueData
|
||||
? "scr_stream_transform_done_value"
|
||||
: "scr_stream_transform_done"
|
||||
: valueData
|
||||
? "scr_stream_flush_done_value"
|
||||
: "scr_stream_flush_done";
|
||||
? typedDesc !== null
|
||||
? "scr_stream_transform_done_typed"
|
||||
: valueData
|
||||
? "scr_stream_transform_done_value"
|
||||
: "scr_stream_transform_done"
|
||||
: typedDesc !== null
|
||||
? "scr_stream_flush_done_typed"
|
||||
: valueData
|
||||
? "scr_stream_flush_done_value"
|
||||
: "scr_stream_flush_done";
|
||||
const twoSlot = (kind === "t" || kind === "l") && !valueData;
|
||||
host.declare(
|
||||
`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`,
|
||||
`declare void @${entry}(ptr, ptr${typedDesc !== null ? ", ptr, ptr" : twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`,
|
||||
);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
host.declare(`declare ptr @scr_error_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
const params = [
|
||||
@@ -293,15 +322,21 @@ export function streamDoneFnFor(
|
||||
`entry:`,
|
||||
` %eslot = alloca ptr`,
|
||||
...(twoSlot ? [` %bslot = alloca ptr`, ` %sslot = alloca ptr`] : []),
|
||||
...(typedDesc !== null ? [` %tslot = alloca ptr`] : []),
|
||||
` %capsp = getelementptr inbounds %ScrClosure, ptr %self, i64 1`,
|
||||
` %bx = load ptr, ptr %capsp`,
|
||||
` %s = call ptr @scr_box_get_ref(ptr %bx) ; the stream, +1`,
|
||||
// the stream, borrowed: the box holds it, and the closure (alive for
|
||||
// the call) holds the box
|
||||
` %sp = getelementptr inbounds %ScrBox, ptr %bx, i64 0, i32 5`,
|
||||
` %s = load ptr, ptr %sp`,
|
||||
` store ptr null, ptr %eslot`,
|
||||
...(twoSlot ? [` store ptr null, ptr %bslot`, ` store ptr null, ptr %sslot`] : []),
|
||||
...(typedDesc !== null ? [` store ptr null, ptr %tslot`] : []),
|
||||
];
|
||||
// A tag-guarded retained peek out of a union argument into a slot.
|
||||
// A tag-guarded peek out of a union argument into a slot, retained
|
||||
// unless retain is null.
|
||||
let arm = 0;
|
||||
const unwrap = (u: string, tag: number, retain: string, slot: string): void => {
|
||||
const unwrap = (u: string, tag: number, retain: string | null, slot: string): void => {
|
||||
const a = arm++;
|
||||
const union = u === "%e" ? errT : dataT;
|
||||
const nullable = union?.kind === "union" ? host.nullableUnions.get(union.unionId) : null;
|
||||
@@ -314,8 +349,9 @@ export function streamDoneFnFor(
|
||||
` br i1 %hit${a}, label %yes${a}, label %done${a}`,
|
||||
`yes${a}:`,
|
||||
...unionPeekLines(nullable, u, `%pv${a}`),
|
||||
` %rt${a} = call ptr ${retain}(ptr %pv${a})`,
|
||||
` store ptr %rt${a}, ptr ${slot}`,
|
||||
...(retain !== null
|
||||
? [` %rt${a} = call ptr ${retain}(ptr %pv${a})`, ` store ptr %rt${a}, ptr ${slot}`]
|
||||
: [` store ptr %pv${a}, ptr ${slot}`]),
|
||||
` br label %done${a}`,
|
||||
`done${a}:`,
|
||||
);
|
||||
@@ -329,8 +365,21 @@ export function streamDoneFnFor(
|
||||
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
|
||||
unwrap("%d", strTag, "@scr_str_retain_v", "%sslot");
|
||||
}
|
||||
if (typedDesc !== null) unwrap("%d", typedTag, null, "%tslot");
|
||||
d.push(` %ev = load ptr, ptr %eslot`);
|
||||
if (valueData) {
|
||||
if (typedDesc !== null) {
|
||||
d.push(
|
||||
` %tv = load ptr, ptr %tslot`,
|
||||
` call void @${entry}(ptr %s, ptr %ev, ptr %tv, ptr ${typedDesc}) ; moves err; borrows s and the record`,
|
||||
);
|
||||
} else if (liveData) {
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
d.push(
|
||||
` %dv = call ptr @${host.liveDynUnionRefAdapter(dataT as IrType & { kind: "union" })}(ptr %d)`,
|
||||
` call void @${entry}(ptr %s, ptr %ev, ptr %dv) ; moves err; borrows s and the value`,
|
||||
` call void @scr_dyn_release(ptr %dv)`,
|
||||
);
|
||||
} else if (valueData) {
|
||||
const conv = host.dyn.toDynHelper(dataT!);
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
d.push(
|
||||
@@ -347,7 +396,6 @@ export function streamDoneFnFor(
|
||||
} else {
|
||||
d.push(` call void @${entry}(ptr %s, ptr %ev) ; moves err; borrows s`);
|
||||
}
|
||||
d.push(` call void @scr_stream_release_v(ptr %s)`);
|
||||
if (errT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, errT)}(ptr %e)`);
|
||||
if (dataT !== undefined && isRefCounted(dataT))
|
||||
d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`);
|
||||
@@ -840,18 +888,42 @@ export function streamCbThunkFor(
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
// A typed chunk of the declared record type (scr_stream_push_typed)
|
||||
// is the record itself; any other chunk converts.
|
||||
const desc = streamTypedDesc(host, p);
|
||||
if (desc !== null) {
|
||||
host.declare(`declare ptr @scr_stream_chunk_typed(ptr, ptr, ptr)`);
|
||||
d.push(
|
||||
` br label %ctchk${i}`,
|
||||
`ctchk${i}:`,
|
||||
` %ctt${i} = call ptr @scr_stream_chunk_typed(ptr %s, ptr %chunk, ptr ${desc})`,
|
||||
` %cthit${i} = icmp ne ptr %ctt${i}, null`,
|
||||
` br i1 %cthit${i}, label %ctok${i}, label %ctconv${i}`,
|
||||
`ctconv${i}:`,
|
||||
);
|
||||
}
|
||||
d.push(
|
||||
` %cv${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`,
|
||||
` %ct${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %cv${i}, ptr null)`,
|
||||
` call void @scr_dyn_release(ptr %cv${i})`,
|
||||
` %ctbad${i} = call zeroext i1 @scr_exc_pending()`,
|
||||
` br i1 %ctbad${i}, label %ctfail${i}, label %ctok${i}`,
|
||||
` br i1 %ctbad${i}, label %ctfail${i}, label %${desc !== null ? "ctconvok" : "ctok"}${i}`,
|
||||
`ctfail${i}:`,
|
||||
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
|
||||
` ret void`,
|
||||
`ctok${i}:`,
|
||||
);
|
||||
passed.push(`${ty} %ct${i}`);
|
||||
if (desc !== null) {
|
||||
d.push(
|
||||
`ctconvok${i}:`,
|
||||
` br label %ctok${i}`,
|
||||
`ctok${i}:`,
|
||||
` %ctv${i} = phi ptr [ %ctt${i}, %ctchk${i} ], [ %ct${i}, %ctconvok${i} ]`,
|
||||
);
|
||||
passed.push(`ptr %ctv${i}`);
|
||||
} else {
|
||||
d.push(`ctok${i}:`);
|
||||
passed.push(`${ty} %ct${i}`);
|
||||
}
|
||||
} else if (isEncPos && p.kind === "string") {
|
||||
// Node's encoding for decoded (Buffer) chunks is 'buffer'; an
|
||||
// object-mode side passes the write's encoding argument ("" for
|
||||
@@ -868,18 +940,46 @@ export function streamCbThunkFor(
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
// An object-mode write without an encoding argument passes
|
||||
// undefined: a pointer union whose unit arm is undefined holds it as
|
||||
// the null pointer, so that case skips the conversion.
|
||||
const pointer = p.kind === "union" ? host.nullableUnions.get(p.unionId) : null;
|
||||
const undefinedIsNull =
|
||||
pointer !== null &&
|
||||
p.kind === "union" &&
|
||||
host.unionsById.get(p.unionId)?.arms[pointer.unitTag]?.kind === "undefinedT";
|
||||
if (undefinedIsNull) {
|
||||
host.declare(`declare zeroext i1 @scr_stream_chunk_encoding_undefined(ptr)`);
|
||||
d.push(
|
||||
` br label %etchk${i}`,
|
||||
`etchk${i}:`,
|
||||
` %eu${i} = call zeroext i1 @scr_stream_chunk_encoding_undefined(ptr %s)`,
|
||||
` br i1 %eu${i}, label %etok${i}, label %etconv${i}`,
|
||||
`etconv${i}:`,
|
||||
);
|
||||
}
|
||||
d.push(
|
||||
` %ev${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`,
|
||||
` %et${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %ev${i}, ptr null)`,
|
||||
` call void @scr_dyn_release(ptr %ev${i})`,
|
||||
` %etbad${i} = call zeroext i1 @scr_exc_pending()`,
|
||||
` br i1 %etbad${i}, label %etfail${i}, label %etok${i}`,
|
||||
` br i1 %etbad${i}, label %etfail${i}, label %${undefinedIsNull ? "etconvok" : "etok"}${i}`,
|
||||
`etfail${i}:`,
|
||||
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
|
||||
` ret void`,
|
||||
`etok${i}:`,
|
||||
);
|
||||
passed.push(`${ty} %et${i}`);
|
||||
if (undefinedIsNull) {
|
||||
d.push(
|
||||
`etconvok${i}:`,
|
||||
` br label %etok${i}`,
|
||||
`etok${i}:`,
|
||||
` %etv${i} = phi ptr [ null, %etchk${i} ], [ %et${i}, %etconvok${i} ]`,
|
||||
);
|
||||
passed.push(`${ty} %etv${i}`);
|
||||
} else {
|
||||
d.push(`etok${i}:`);
|
||||
passed.push(`${ty} %et${i}`);
|
||||
}
|
||||
} else if (isErrPos) {
|
||||
// destroy's error argument: `Error | null` — wrap type-directedly.
|
||||
// The finished/pipeline callback ("e") may declare `Error | null |
|
||||
@@ -966,6 +1066,161 @@ registerReplay("streamDoneFnFor", (host, args) =>
|
||||
registerReplay("fsRenameThunkFor", (host, args) =>
|
||||
fsRenameThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
/** The fs.watch listener adapter for an (eventType, filename) listener, or
|
||||
* for an (eventType) listener whose parameter is dynamic: the runtime hands
|
||||
* the borrowed C strings, the thunk builds what the listener declared. The
|
||||
* listener owns its +1 arguments (the universal convention
|
||||
* scr_watch_thunk_event follows), so a string the thunk boxes into a dynamic
|
||||
* value is released once the box holds it. */
|
||||
export function watchNameThunkFor(
|
||||
host: LlvmEmitterContext,
|
||||
cbT: IrType & { kind: "func" },
|
||||
): string {
|
||||
const key = `watchname:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = host.contentSymbol("sc_watchname_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "watchNameThunkFor", [cbT]);
|
||||
const S = host.sizeType;
|
||||
host.declare(`declare ${S} @strlen(ptr)`);
|
||||
host.declare(`declare ptr @scr_str_new(ptr, ${S})`);
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb, ptr %event, ptr %name) ${FN_ATTRS} { ; fs.watch listener ${typeKey(cbT)}`,
|
||||
`entry:`,
|
||||
` %evlen = call ${S} @strlen(ptr %event)`,
|
||||
` %evstr = call ptr @scr_str_new(ptr %event, ${S} %evlen)`,
|
||||
];
|
||||
const named = cbT.params.length === 2;
|
||||
if (named) {
|
||||
d.push(
|
||||
` %slot = alloca ptr`,
|
||||
` %has = icmp ne ptr %name, null`,
|
||||
` br i1 %has, label %yes, label %no`,
|
||||
`yes:`,
|
||||
);
|
||||
const nameParam = cbT.params[1]!;
|
||||
if (nameParam.kind === "dyn") {
|
||||
host.declare(`declare ptr @scr_dyn_new_null()`);
|
||||
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
|
||||
d.push(
|
||||
` %nlen = call ${S} @strlen(ptr %name)`,
|
||||
` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`,
|
||||
` %ndyn = call ptr @scr_dyn_new_str(ptr %nstr)`,
|
||||
` call void @scr_str_release(ptr %nstr)`,
|
||||
` store ptr %ndyn, ptr %slot`,
|
||||
` br label %go`,
|
||||
`no:`,
|
||||
` %dn = call ptr @scr_dyn_new_null()`,
|
||||
` store ptr %dn, ptr %slot`,
|
||||
);
|
||||
} else {
|
||||
if (nameParam.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: fs.watch filename param is not a union");
|
||||
const def = host.unionsById.get(nameParam.unionId);
|
||||
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
||||
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
||||
if (strTag < 0 || nullTag < 0)
|
||||
throw new InternalCompilerError("llvm emitter bug: fs.watch filename union lacks its arms");
|
||||
if (host.nullableUnions.has(nameParam.unionId)) {
|
||||
// `string | null` erases: the owned payload IS the union's value.
|
||||
d.push(
|
||||
` %nlen = call ${S} @strlen(ptr %name)`,
|
||||
` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`,
|
||||
` store ptr %nstr, ptr %slot`,
|
||||
` br label %go`,
|
||||
`no:`,
|
||||
` store ptr null, ptr %slot`,
|
||||
);
|
||||
} else {
|
||||
const rc = vAdapters(host.shapeHost, STRING);
|
||||
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
|
||||
d.push(
|
||||
` %nlen = call ${S} @strlen(ptr %name)`,
|
||||
` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`,
|
||||
` %nu = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %nstr, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, STRING)})`,
|
||||
` store ptr %nu, ptr %slot`,
|
||||
` br label %go`,
|
||||
`no:`,
|
||||
` store ptr ${host.unitInstanceRef(nameParam.unionId, nullTag)}, ptr %slot`,
|
||||
);
|
||||
}
|
||||
}
|
||||
d.push(` br label %go`, `go:`);
|
||||
}
|
||||
let ev: string;
|
||||
if (cbT.params[0]?.kind === "dyn") {
|
||||
// The box retains the string; the thunk's own reference goes.
|
||||
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
|
||||
d.push(
|
||||
` %evd = call ptr @scr_dyn_new_str(ptr %evstr)`,
|
||||
` call void @scr_str_release(ptr %evstr)`,
|
||||
);
|
||||
ev = "%evd";
|
||||
} else {
|
||||
ev = "%evstr";
|
||||
}
|
||||
d.push(
|
||||
` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`,
|
||||
` %fn = load ptr, ptr %fnp`,
|
||||
);
|
||||
if (named)
|
||||
d.push(` %arg = load ptr, ptr %slot`, ` call void %fn(ptr %cb, ptr ${ev}, ptr %arg)`);
|
||||
else d.push(` call void %fn(ptr %cb, ptr ${ev})`);
|
||||
d.push(` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
/** The FSWatcher 'error' listener adapter: the runtime hands a borrowed
|
||||
* ScrError, the listener receives the declared dyn value or an Error
|
||||
* object it owns. A zero-parameter listener uses the runtime's
|
||||
* scr_watch_err_thunk0 and never reaches here. */
|
||||
export function watchErrorThunkFor(
|
||||
host: LlvmEmitterContext,
|
||||
cbT: IrType & { kind: "func" },
|
||||
): string {
|
||||
const key = `watcherr:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = host.contentSymbol("sc_watcherr_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "watchErrorThunkFor", [cbT]);
|
||||
const param = cbT.params[0]!;
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb, ptr %err) ${FN_ATTRS} { ; FSWatcher error listener ${typeKey(cbT)}`,
|
||||
`entry:`,
|
||||
];
|
||||
if (param.kind === "dyn") {
|
||||
host.declare(`declare ptr @scr_dyn_from_error(ptr)`);
|
||||
d.push(` %de = call ptr @scr_dyn_from_error(ptr %err)`);
|
||||
} else if (param.kind === "object" && param.className === "%Error") {
|
||||
host.declare(`declare ptr @scr_error_retain(ptr)`);
|
||||
d.push(` %de = call ptr @scr_error_retain(ptr %err)`);
|
||||
} else {
|
||||
throw new InternalCompilerError("llvm emitter bug: FSWatcher error param is not an Error");
|
||||
}
|
||||
d.push(
|
||||
` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`,
|
||||
` %fn = load ptr, ptr %fnp`,
|
||||
` call void %fn(ptr %cb, ptr %de)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
registerReplay("watchNameThunkFor", (host, args) =>
|
||||
watchNameThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("watchErrorThunkFor", (host, args) =>
|
||||
watchErrorThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("cryptoIntThunkFor", (host, args) =>
|
||||
cryptoIntThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export function jsIcMethods(host: Host): boolean {
|
||||
return (
|
||||
jsImmediates(host) &&
|
||||
host.currentTier === "dynamic" &&
|
||||
jsIcMethodSites(host.currentFunctionName)
|
||||
jsIcMethodSites(host.currentFunctionName, host.loopDepth > 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -214,6 +214,9 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
|
||||
export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
// The generic emitter registrations queue the maxListeners leak warning
|
||||
// for the next tick like emitter.on (lib-process.ts).
|
||||
if (e.fn === "emitter.onFlex" || e.fn === "emitter.onAny") host.usesTimers = true;
|
||||
if (host.coroAwait && e.fn === "async.hop") {
|
||||
host.emitCoroAwait(null);
|
||||
return { name: "", type: e.type };
|
||||
@@ -1185,6 +1188,10 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "str":
|
||||
case "regexp":
|
||||
case "intl":
|
||||
// ICU-backed Intl (scr_intl.c): ordinary libCalls whose C symbols the
|
||||
// runtime provides — the same generic emission the engine-free intl
|
||||
// forms use.
|
||||
case "icu":
|
||||
case "sym":
|
||||
case "perf":
|
||||
case "number":
|
||||
@@ -1272,6 +1279,9 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
return host.emitGenericLibCall(e);
|
||||
case "nodeIsland":
|
||||
return host.emitGenericLibCall(e);
|
||||
// Primitive wrapper objects (scr_js_wrapper.c).
|
||||
case "wrapper":
|
||||
return host.emitGenericLibCall(e);
|
||||
case "webstream":
|
||||
// A stream's start, pull and cancel steps run as promise jobs, so
|
||||
// a program with the classes runs its event loop.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { DYN, F64, STRING, type IrExpr } from "../../ir/ir.js";
|
||||
import { DYN, F64, STRING, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, traceArg, vAdapters } from "./shapes.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
|
||||
@@ -173,6 +173,22 @@ function emitObjEntryPairs(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The adapter of an fs.watch 'change' listener, by its declared shape: the
|
||||
* runtime's for no parameters and for a string eventType, and one this
|
||||
* module emits for a dynamic eventType or an (eventType, filename) listener.
|
||||
* Only the runtime's adapters need a declaration. */
|
||||
function watchChangeAdapter(host: LlvmEmitterContext, cbT: IrType & { kind: "func" }): string {
|
||||
const runtime =
|
||||
cbT.params.length === 0
|
||||
? "scr_watch_thunk0"
|
||||
: cbT.params.length === 1 && cbT.params[0]!.kind !== "dyn"
|
||||
? "scr_watch_thunk_event"
|
||||
: null;
|
||||
if (runtime === null) return host.watchNameThunkFor(cbT);
|
||||
host.declare(`declare void @${runtime}(ptr, ptr, ptr)`);
|
||||
return runtime;
|
||||
}
|
||||
|
||||
export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
const alwaysThrowSym = FS_ALWAYS_THROW_SYMS[e.fn];
|
||||
@@ -448,17 +464,19 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
// loop (usesTimers).
|
||||
host.usesTimers = true;
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr, i1 zeroext)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr null, ptr null)`);
|
||||
B.line(`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr null, ptr null, i1 false)`);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
if (e.fn === "fs.watchCb") {
|
||||
// The callback MOVES into the watcher's registry; the adapter is
|
||||
// runtime-provided per listener shape (zero-param, or the eventType
|
||||
// string). May throw (ENOENT) — the standard pending check.
|
||||
// runtime-provided per listener shape (zero-param, the eventType
|
||||
// string, or the compiler's (eventType, filename) adapter). May
|
||||
// throw (ENOENT, or the macOS directory-names refusal) — the
|
||||
// standard pending check.
|
||||
host.usesTimers = true;
|
||||
const cbT = e.args[1]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
@@ -466,15 +484,64 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
host.toHeap(cb);
|
||||
const adapter = cbT.params.length === 0 ? "scr_watch_thunk0" : "scr_watch_thunk_event";
|
||||
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr)`);
|
||||
const adapter = watchChangeAdapter(host, cbT);
|
||||
host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr, i1 zeroext)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr ${cb.name}, ptr @${adapter})`);
|
||||
B.line(
|
||||
`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${cbT.params.length === 2 ? "true" : "false"})`,
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
if (e.fn === "watcher.on") {
|
||||
// (recv, event, flags, listener) — flags: 1 once, 2 prepend. The
|
||||
// listener MOVES in; the adapter follows the event (0 change, 1
|
||||
// error, 2 close — CLOSE takes no adapter, a plain zero-arg call).
|
||||
// Registration can throw (the macOS directory-names refusal).
|
||||
const flagsArg = e.args[2]!;
|
||||
if (flagsArg.kind !== "numLit")
|
||||
throw new InternalCompilerError("llvm emitter bug: watcher.on flags not a literal");
|
||||
const flags = flagsArg.value;
|
||||
const codeArg = e.args[1]!;
|
||||
if (codeArg.kind !== "numLit")
|
||||
throw new InternalCompilerError("llvm emitter bug: watcher.on event not a literal");
|
||||
const code = codeArg.value;
|
||||
const cbT = e.args[3]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: watcher.on listener not a func");
|
||||
const w = host.emitExpr(e.args[0]!);
|
||||
const event = host.emitExpr(e.args[1]!);
|
||||
const cb = host.emitExpr(e.args[3]!);
|
||||
host.toHeap(cb);
|
||||
let adapter = "null";
|
||||
let wantsName = false;
|
||||
if (code === 0) {
|
||||
adapter = watchChangeAdapter(host, cbT);
|
||||
wantsName = cbT.params.length === 2;
|
||||
} else if (code === 1) {
|
||||
adapter = cbT.params.length === 0 ? "scr_watch_err_thunk0" : host.watchErrorThunkFor(cbT);
|
||||
if (cbT.params.length === 0) host.declare(`declare void @scr_watch_err_thunk0(ptr, ptr)`);
|
||||
}
|
||||
host.declare(
|
||||
`declare void @scr_watcher_on(ptr, double, ptr, ptr, i1 zeroext, i1 zeroext, i1 zeroext)`,
|
||||
);
|
||||
B.line(
|
||||
`call void @scr_watcher_on(ptr ${w.name}, double ${event.name}, ptr ${cb.name}, ptr ${adapter === "null" ? "null" : `@${adapter}`}, i1 ${flags & 1 ? "true" : "false"}, i1 ${flags & 2 ? "true" : "false"}, i1 ${wantsName ? "true" : "false"})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "watcher.off") {
|
||||
// Removal is by pointer identity: the listener stays borrowed
|
||||
// (never heap-moved), exactly like process.offExit's.
|
||||
const w = host.emitExpr(e.args[0]!);
|
||||
const event = host.emitExpr(e.args[1]!);
|
||||
const cb = host.emitExpr(e.args[2]!);
|
||||
host.declare(`declare void @scr_watcher_off(ptr, double, ptr)`);
|
||||
B.line(`call void @scr_watcher_off(ptr ${w.name}, double ${event.name}, ptr ${cb.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "watcher.close") {
|
||||
// Idempotent; never throws.
|
||||
const w = host.emitExpr(e.args[0]!);
|
||||
@@ -482,6 +549,45 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
B.line(`call void @scr_watcher_close(ptr ${w.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "fs.watchFile") {
|
||||
// (path, interval, persistent, listener) — the listener MOVES into
|
||||
// the poller's registry and the adapter is runtime-provided per
|
||||
// listener shape (zero, one or two Stats parameters). The poller
|
||||
// re-arms a timer every interval, so this is a timer program. May
|
||||
// throw (a null byte in the path, or an interval that is not a
|
||||
// uint32) — the standard pending check.
|
||||
host.usesTimers = true;
|
||||
const cbT = e.args[3]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: fs.watchFile listener not a func");
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
const interval = host.emitExpr(e.args[1]!);
|
||||
const persistent = host.emitExpr(e.args[2]!);
|
||||
const cb = host.emitExpr(e.args[3]!);
|
||||
host.toHeap(cb);
|
||||
const adapter = `scr_watch_file_thunk${cbT.params.length}`;
|
||||
host.declare(`declare void @${adapter}(ptr, ptr, ptr)`);
|
||||
host.declare(`declare void @scr_fs_watch_file(ptr, double, i1 zeroext, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_fs_watch_file(ptr ${path.name}, double ${interval.name}, i1 ${persistent.name}, ptr ${cb.name}, ptr @${adapter})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "fs.unwatchFile") {
|
||||
// Removal is by closure identity: the listener stays borrowed
|
||||
// (never heap-moved), exactly like watcher.off's; an absent one is
|
||||
// the NULL that removes every listener on the path. A null byte in
|
||||
// the path throws.
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
const cb = e.args[1] === undefined ? null : host.emitExpr(e.args[1]!);
|
||||
host.declare(`declare void @scr_fs_unwatch_file(ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_fs_unwatch_file(ptr ${path.name}, ptr ${cb === null ? "null" : cb.name})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "fs.globTypesSync" || e.fn === "fsp.globTypes") {
|
||||
// Dirent rows from one glob snapshot (scr_glob.c): mode 1 is the
|
||||
// globSync form, 3 the fs/promises form with resolved parent paths.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { F64, type IrType, RUNTIME_ERROR_CLASSES, STRING, typeKey } from "../../ir/ir.js";
|
||||
import { BOOL, F64, type IrType, RUNTIME_ERROR_CLASSES, STRING, typeKey } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { mockTimerSym } from "./lib-shared.js";
|
||||
@@ -679,32 +679,46 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "process.onExit") {
|
||||
if (e.fn === "process.onExit" || e.fn === "process.prependExit") {
|
||||
// Runtime adapters cover both shapes (the code is a plain double);
|
||||
// the registry owns the callback.
|
||||
const cbT = e.args[0]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: process.onExit callback not a func");
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const cb = host.emitExpr(e.args[0]!);
|
||||
const once = host.emitExpr(e.args[1]!);
|
||||
host.toHeap(cb);
|
||||
const adapter = cbT.params.length === 0 ? "scr_exit_thunk0" : "scr_exit_thunk_code";
|
||||
host.declare(`declare void @${adapter}(ptr, double)`);
|
||||
if (e.fn === "process.prependExit") {
|
||||
host.declare(`declare void @scr_process_prepend_exit(ptr, ptr)`);
|
||||
B.line(`call void @scr_process_prepend_exit(ptr ${cb.name}, ptr @${adapter})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const once = host.emitExpr(e.args[1]!);
|
||||
host.declare(`declare void @scr_process_on_exit(ptr, ptr, i1 zeroext)`);
|
||||
B.line(`call void @scr_process_on_exit(ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "process.envGet" || e.fn === "process.columns" || e.fn === "process.rows") {
|
||||
// getenv(3) / ioctl(TIOCGWINSZ): the runtime answers a +1 string or
|
||||
// NULL (a width or a negative sentinel); the union construction is
|
||||
// type-directed HERE — present wraps the value arm, absent yields
|
||||
if (
|
||||
e.fn === "process.envGet" ||
|
||||
e.fn === "process.columns" ||
|
||||
e.fn === "process.rows" ||
|
||||
e.fn === "process.stdinIsRaw"
|
||||
) {
|
||||
// getenv(3) / ioctl(TIOCGWINSZ) / termios: the runtime answers a +1
|
||||
// string or NULL, or a number with a negative sentinel (the raw-mode
|
||||
// flag rides the same double channel, 1/0/-1); the union construction
|
||||
// is type-directed HERE — present wraps the value arm, absent yields
|
||||
// the interned immortal undefined-arm instance.
|
||||
if (e.type.kind !== "union")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
|
||||
const def = host.unionsById.get(e.type.unionId);
|
||||
const undefTag = undefinedArmTag(e.type, host.unionsById);
|
||||
const isEnv = e.fn === "process.envGet";
|
||||
const valTag = def ? def.arms.findIndex((a) => a.kind === (isEnv ? "string" : "f64")) : -1;
|
||||
const isRaw = e.fn === "process.stdinIsRaw";
|
||||
const valTag = def
|
||||
? def.arms.findIndex((a) => a.kind === (isEnv ? "string" : isRaw ? "bool" : "f64"))
|
||||
: -1;
|
||||
if (valTag < 0 || undefTag < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
@@ -719,6 +733,10 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
host.declare(`declare ptr @scr_env_get(ptr)`);
|
||||
B.line(`${raw} = call ptr @scr_env_get(ptr ${args[0]!.name})`);
|
||||
B.line(`${present} = icmp ne ptr ${raw}, null`);
|
||||
} else if (isRaw) {
|
||||
host.declare(`declare double @scr_process_stdin_is_raw()`);
|
||||
B.line(`${raw} = call double @scr_process_stdin_is_raw()`);
|
||||
B.line(`${present} = fcmp oge double ${raw}, ${f64Lit(0)}`);
|
||||
} else {
|
||||
const runtimeFn = e.fn === "process.rows" ? "scr_process_rows" : "scr_process_columns";
|
||||
host.declare(`declare double @${runtimeFn}(double)`);
|
||||
@@ -727,9 +745,18 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
}
|
||||
B.condBr(present, lp, la);
|
||||
B.startBlock(lp);
|
||||
B.line(
|
||||
`store ptr ${host.unionNewOwned(e.type.unionId, valTag, { name: raw, type: isEnv ? STRING : F64 })}, ptr ${slot}`,
|
||||
);
|
||||
if (isRaw) {
|
||||
// The raw flag: 1/0 becomes the bool arm's i1.
|
||||
const flag = B.tmp();
|
||||
B.line(`${flag} = fcmp one double ${raw}, ${f64Lit(0)}`);
|
||||
B.line(
|
||||
`store ptr ${host.unionNewOwned(e.type.unionId, valTag, { name: flag, type: BOOL })}, ptr ${slot}`,
|
||||
);
|
||||
} else {
|
||||
B.line(
|
||||
`store ptr ${host.unionNewOwned(e.type.unionId, valTag, { name: raw, type: isEnv ? STRING : F64 })}, ptr ${slot}`,
|
||||
);
|
||||
}
|
||||
B.br(lj);
|
||||
B.startBlock(la);
|
||||
B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, undefTag)}, ptr ${slot}`);
|
||||
@@ -811,10 +838,19 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const kind = host.emitExpr(e.args[0]!);
|
||||
const code = host.emitExpr(e.args[1]!);
|
||||
const msg = host.emitExpr(e.args[2]!);
|
||||
host.declare(`declare void @scr_throw_node_coded(double, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_throw_node_coded(double ${kind.name}, ptr ${code.name}, ptr ${msg.name})`,
|
||||
);
|
||||
if (e.args.length > 3) {
|
||||
// The fourth argument: the error's own `url` property.
|
||||
const url = host.emitExpr(e.args[3]!);
|
||||
host.declare(`declare void @scr_throw_node_coded_url(double, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_throw_node_coded_url(double ${kind.name}, ptr ${code.name}, ptr ${msg.name}, ptr ${url.name})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare void @scr_throw_node_coded(double, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_throw_node_coded(double ${kind.name}, ptr ${code.name}, ptr ${msg.name})`,
|
||||
);
|
||||
}
|
||||
const ty = host.llType(e.type);
|
||||
if (ty === "void") {
|
||||
host.emitAlwaysPendingUnwind();
|
||||
@@ -867,6 +903,10 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: emitter.on listener not a func");
|
||||
// A registration past the emitter's maxListeners queues the leak
|
||||
// warning for the next tick (scr_events_emitter.c), which the loop
|
||||
// delivers before the process exits.
|
||||
host.usesTimers = true;
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const { fn: adapterFn, shim } = host.emitterFixedAdapter(cbT);
|
||||
// The wrapper's capture box owns its OWN +1 of the listener; the
|
||||
@@ -897,6 +937,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const adT = e.args[3]!.type;
|
||||
if (adT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: emitter.onDyn adapter not a func");
|
||||
host.usesTimers = true; // the leak warning's tick (emitter.on)
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const { fn: adapterFn, shim } = host.emitterFixedAdapter(adT);
|
||||
host.toHeap(args[3]!); // the frame's +1 moves into the wrapper's box
|
||||
@@ -946,6 +987,34 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "emitter.emitErrorValue") {
|
||||
// (recv, ...payload as dyn): emit('error') with a non-Error payload.
|
||||
// The unhandled case renders the payload with util.inspect
|
||||
// (scr_insp_dyn), which this call links.
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const n = args.length - 1;
|
||||
let payload = "null";
|
||||
if (n > 0) {
|
||||
const arr = B.slot();
|
||||
B.entryAllocas.push(`${arr} = alloca [${n} x ptr]`);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const slot = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds [${n} x ptr], ptr ${arr}, i64 0, i64 ${i}`);
|
||||
B.line(`store ptr ${args[i + 1]!.name}, ptr ${slot}`);
|
||||
}
|
||||
payload = arr;
|
||||
}
|
||||
host.declare(`declare ptr @scr_insp_dyn(ptr, double, double)`);
|
||||
host.declare(
|
||||
`declare zeroext i1 @scr_emitter_emit_error_value(ptr, ptr, ${host.sizeType}, ptr)`,
|
||||
);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call i1 @scr_emitter_emit_error_value(ptr ${args[0]!.name}, ptr ${payload}, ${host.sizeType} ${n}, ptr @scr_insp_dyn)`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "emitter.emitFlex" || e.fn === "emitter.emitAny") {
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const n = args.length - 2;
|
||||
@@ -1046,6 +1115,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
// emitter.on/onDyn, but the DATA adapter (the three-slot payload ABI
|
||||
// — scr_stream_emit_data) behind the arity-3 fixed shim.
|
||||
const isDyn = e.fn === "emitter.onDataDyn";
|
||||
host.usesTimers = true; // the leak warning's tick (emitter.on)
|
||||
const cbT = e.args[isDyn ? 3 : 2]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`);
|
||||
|
||||
@@ -67,6 +67,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw",
|
||||
"intl.segmenterNew": "scr_intl_segmenter_new",
|
||||
"intl.segmenterRefusing": "scr_intl_segmenter_refusing",
|
||||
"icu.segmenterWord": "scr_intl_segmenter_word_new",
|
||||
"icu.numFormatDefault": "scr_intl_num_format_default",
|
||||
"icu.i18nSupport": "scr_intl_i18n_support",
|
||||
"icu.dateLocaleString": "scr_intl_date_locale_string",
|
||||
"icu.dateLocaleTimeString": "scr_intl_date_locale_time_string",
|
||||
"weakMap.is": "scr_weak_map_is",
|
||||
"weakSet.is": "scr_weak_set_is",
|
||||
"weakMap.new": "scr_weak_map_new",
|
||||
@@ -261,6 +266,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"os.userName": "scr_os_user_name",
|
||||
"os.userShell": "scr_os_user_shell",
|
||||
"os.userHomedir": "scr_os_user_homedir",
|
||||
"os.userUid": "scr_os_user_uid",
|
||||
"os.userGid": "scr_os_user_gid",
|
||||
"os.tmpdir": "scr_os_tmpdir",
|
||||
"process.argv": "scr_process_argv",
|
||||
"process.execArgv": "scr_process_exec_argv",
|
||||
@@ -304,9 +311,19 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"navigator.languages": "scr_navigator_languages",
|
||||
"navigator.platform": "scr_navigator_platform",
|
||||
"process.builtinModule": "scr_process_builtin_module",
|
||||
"process.builtinObject": "scr_process_builtin_object",
|
||||
"process.builtinUnsupported": "scr_process_builtin_unsupported",
|
||||
"module.namespaceValue": "scr_module_namespace_value",
|
||||
"module.namespaceDyn": "scr_module_namespace_dyn_value",
|
||||
"wrapper.new": "scr_js_wrapper_new",
|
||||
"wrapper.toObject": "scr_js_wrapper_to_object",
|
||||
"wrapper.instanceOf": "scr_js_wrapper_instance_of",
|
||||
"wrapper.objectTag": "scr_js_wrapper_object_tag",
|
||||
"wrapper.typeIs": "scr_js_wrapper_type_is",
|
||||
"wrapper.guard": "scr_js_wrapper_guard",
|
||||
"wrapper.guardList": "scr_js_wrapper_guard_list",
|
||||
"wrapper.isStringMethod": "scr_js_wrapper_is_string_method",
|
||||
"wrapper.thisString": "scr_js_wrapper_this_string",
|
||||
"fs.callbackValue": "scr_fs_callback_value",
|
||||
"fs.callbackCall": "scr_fs_callback_call",
|
||||
"process.hrtimeValue": "scr_process_hrtime_value",
|
||||
@@ -379,9 +396,14 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"process.envPairs": "scr_env_pairs",
|
||||
"process.stdoutWrite": "scr_process_stdout_write",
|
||||
"process.stdio": "scr_process_stdio",
|
||||
"process.stdioSlot": "scr_process_stdio",
|
||||
"process.stdioWriteDyn": "scr_process_stdio_write_dyn",
|
||||
"process.stderrWrite": "scr_process_stderr_write",
|
||||
"process.isTTY": "scr_process_is_tty",
|
||||
"process.isTTYValue": "scr_process_is_tty_value",
|
||||
// A promise that never settles (events.once of an event a synchronous
|
||||
// stream never emits); a fresh pending promise is its owned result.
|
||||
"events.pendingForever": "scr_promise_new",
|
||||
"date.now": "scr_date_now",
|
||||
"date.newNow": "scr_date_now",
|
||||
"date.newMs": "scr_date_new_ms",
|
||||
@@ -441,10 +463,17 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fsp.globError": "scr_fsp_glob_error",
|
||||
"fs.realpathSync": "scr_fs_realpath",
|
||||
"fs.realpathValue": "scr_fs_realpath_value",
|
||||
"dyn.validateString": "scr_value_string",
|
||||
"dyn.validateStrings": "scr_value_strings",
|
||||
"path.resolveValue": "scr_path_resolve_value",
|
||||
"path.parseValue": "scr_path_parse_value",
|
||||
"path.formatValue": "scr_path_format_value",
|
||||
"path.toNamespacedValue": "scr_path_to_namespaced_value",
|
||||
"fs.realpathNativeSync": "scr_fs_realpath_promise",
|
||||
"fs.unlinkSync": "scr_fs_unlink",
|
||||
"fs.chmodSync": "scr_fs_chmod",
|
||||
"fs.copyFileSync": "scr_fs_copyfile",
|
||||
"fs.copyFileFlagsSync": "scr_fs_copyfile_flags",
|
||||
"fs.renameSync": "scr_fs_rename",
|
||||
"fs.accessSync": "scr_fs_access",
|
||||
// node:assert (scr_assert.c): all args borrowed; failures throw the
|
||||
@@ -593,6 +622,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// string_decoder (scr_bytes.c): stateless helpers over (enc, pending,
|
||||
// chunk) — never throw.
|
||||
"strdec.write": "scr_strdec_write",
|
||||
"strdec.writeDyn": "scr_strdec_write_dyn",
|
||||
"strdec.nextDyn": "scr_strdec_next_dyn",
|
||||
"strdec.next": "scr_strdec_next",
|
||||
"strdec.end": "scr_strdec_end",
|
||||
"strdec.encoding": "scr_strdec_encoding",
|
||||
@@ -868,6 +899,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// identity); registration is special-cased above (ownership moves).
|
||||
"process.offSignal": "scr_signal_off",
|
||||
"process.offExit": "scr_process_off_exit",
|
||||
// Front-of-list registrations (process.prependListener): the signal and
|
||||
// uncaught-exception registries. prependExit is special-cased below with
|
||||
// the exit adapters, like onExit.
|
||||
"process.prependSignal": "scr_signal_prepend",
|
||||
"process.prependUncaughtException": "scr_process_prepend_uncaught_exception",
|
||||
// child_process (scr_child.c): spawnSync/exec forms are synchronous
|
||||
// (execSync/execCapture throw — the may-throw check); handle getters
|
||||
// are pure reads. Registration forms are special-cased (moves + per-
|
||||
@@ -908,6 +944,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fsp.readFileBytes": "scr_fsp_read_file_bytes",
|
||||
"fsp.writeFile": "scr_fsp_write_file",
|
||||
"fsp.writeFileMode": "scr_fsp_write_file_mode",
|
||||
"fsp.writeFileFlags": "scr_fsp_write_file_flags",
|
||||
"fsp.copyFileFlags": "scr_fsp_copyfile_flags",
|
||||
"fsp.mkdir": "scr_fsp_mkdir",
|
||||
"fsp.mkdirMode": "scr_fsp_mkdir_mode",
|
||||
"fsp.mkdirRecursive": "scr_fsp_mkdir_recursive",
|
||||
@@ -1113,6 +1151,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.keyDelete": "scr_dyn_key_delete",
|
||||
"dyn.keyDeleteComputed": "scr_dyn_key_delete_computed",
|
||||
"dyn.reflectDeleteProperty": "scr_dyn_reflect_delete_property",
|
||||
"dyn.reflectHasTarget": "scr_dyn_reflect_has_target",
|
||||
"dyn.hasKeyComputed": "scr_dyn_has_key_computed",
|
||||
"dyn.hasOwnComputed": "scr_dyn_has_own_computed",
|
||||
"dyn.propertyIsEnumerableComputed": "scr_dyn_property_is_enumerable_computed",
|
||||
@@ -1262,6 +1301,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"crypto.x509Verify": "scr_crypto_x509_verify",
|
||||
"crypto.x509CheckPrivateKey": "scr_crypto_x509_check_private_key",
|
||||
"fs.writeFileModeSync": "scr_fs_write_file_mode",
|
||||
"fs.writeFileFlagsSync": "scr_fs_write_file_flags",
|
||||
"fs.chownSync": "scr_fs_chown",
|
||||
"fs.lchownSync": "scr_fs_lchown",
|
||||
"fs.fchownSync": "scr_fs_fchown",
|
||||
@@ -1450,6 +1490,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"net.serverListening": "scr_net_server_listening",
|
||||
"net.sockSetRef": "scr_net_sock_set_ref",
|
||||
"net.serverSetRef": "scr_net_server_set_ref",
|
||||
"net.serverOff": "scr_net_server_off",
|
||||
"net.sockWrite": "scr_net_sock_write_str",
|
||||
"net.sockWriteBytes": "scr_net_sock_write_bytes",
|
||||
"net.sockEnd": "scr_net_sock_end",
|
||||
@@ -1896,6 +1937,11 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"worker.receiveMessageOnPort",
|
||||
"worker.broadcastChannel",
|
||||
"process.stdio",
|
||||
// The typed write slot and the run-time-typed chunk write keep the loop
|
||||
// live for the write callbacks they queue: both reach the same stream
|
||||
// object as process.stdio and call back from a nextTick.
|
||||
"process.stdioSlot",
|
||||
"process.stdioWriteDyn",
|
||||
"fetch.start",
|
||||
"fetch.input",
|
||||
"fetch.function",
|
||||
@@ -1981,6 +2027,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"writable.writeU",
|
||||
"writable.writeDyn",
|
||||
"writable.writeEnc",
|
||||
"writable.writeEcb",
|
||||
"writable.end",
|
||||
"writable.uncork",
|
||||
"stream.destroy",
|
||||
|
||||
@@ -3,6 +3,8 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { pipelinePumpedRuns } from "../../ir/builtin-signatures.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { streamTypedDesc } from "./expr-stream-bridges.js";
|
||||
|
||||
const STREAM_CANONICAL_CBS: Record<string, ("r" | "w" | "f" | "d" | "t" | "l")[]> = {
|
||||
readable: ["r", "d"],
|
||||
@@ -14,6 +16,10 @@ const STREAM_CANONICAL_CBS: Record<string, ("r" | "w" | "f" | "d" | "t" | "l")[]
|
||||
|
||||
export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
if (e.fn === "readable.pushDyn" || e.fn === "writable.writeDyn") {
|
||||
const typed = emitTypedChunkCall(host, e);
|
||||
if (typed !== null) return typed;
|
||||
}
|
||||
if (e.fn === "stream.addAbortSignal" || e.fn === "stream.addAbortSignalFn") {
|
||||
// stream.addAbortSignal: [stream, signal] — the constructors' signal
|
||||
// option chained over the fresh stream (a falsy signal is ignored);
|
||||
@@ -455,6 +461,44 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
host.emitPendingCheck();
|
||||
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "writable.writeEcb") {
|
||||
// (recv, mode, chunk, encoding, cb): mode 0 bytes, 1 string, 2 dyn. The
|
||||
// callback MOVES into an error-first wrapper (scr_stream_ecb_wrap); the
|
||||
// adapter turns the runtime's ScrError (NULL = success) into the
|
||||
// callback's Error | null argument, as fs.rename callbacks do.
|
||||
const modeArg = e.args[1]!;
|
||||
if (modeArg.kind !== "numLit")
|
||||
throw new InternalCompilerError("llvm emitter bug: writable.writeEcb mode not a literal");
|
||||
const cbT = e.args[4]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: writable.writeEcb callback not a func");
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.toHeap(args[4]!);
|
||||
const adapter = host.fsRenameThunkFor(cbT);
|
||||
host.declare(`declare ptr @scr_stream_ecb_wrap(ptr, ptr)`);
|
||||
const wrapped = B.tmp();
|
||||
B.line(`${wrapped} = call ptr @scr_stream_ecb_wrap(ptr ${args[4]!.name}, ptr @${adapter})`);
|
||||
const entry =
|
||||
modeArg.value === 0
|
||||
? "scr_stream_write"
|
||||
: modeArg.value === 1
|
||||
? "scr_stream_write_str"
|
||||
: null;
|
||||
const t = B.tmp();
|
||||
if (entry !== null) {
|
||||
host.declare(`declare zeroext i1 @${entry}(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @${entry}(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${wrapped})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${args[3]!.name}, ptr ${wrapped})`,
|
||||
);
|
||||
}
|
||||
host.emitPendingCheck();
|
||||
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "writable.end") {
|
||||
// (recv, flags[, chunk][, encoding][, cb]) — flags: 1 bytes chunk, 2
|
||||
// string chunk, 8 dyn chunk (write first, Node's end(chunk)
|
||||
@@ -834,6 +878,34 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
return host.emitGenericLibCall(e);
|
||||
}
|
||||
|
||||
/** push(value) or write(value) of a record that converts to dyn by
|
||||
* reference: the stream receives the record as a typed chunk
|
||||
* (scr_stream_push_typed / scr_stream_write_typed), boxed only for a
|
||||
* consumer that reads it as a dyn value. Null for another chunk. */
|
||||
function emitTypedChunkCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null {
|
||||
const chunk = e.args[1];
|
||||
if (chunk === undefined || chunk.kind !== "dynFrom" || !chunk.liveRef) return null;
|
||||
const desc = streamTypedDesc(host, chunk.value.type);
|
||||
if (desc === null) return null;
|
||||
const B = host.B;
|
||||
const recv = host.emitExpr(e.args[0]!);
|
||||
const rec = emitBorrowedInput(host, chunk.value);
|
||||
const t = B.tmp();
|
||||
if (e.fn === "readable.pushDyn") {
|
||||
host.declare(`declare zeroext i1 @scr_stream_push_typed(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @scr_stream_push_typed(ptr ${recv.name}, ptr ${rec.name}, ptr ${desc})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare zeroext i1 @scr_stream_write_typed(ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @scr_stream_write_typed(ptr ${recv.name}, ptr ${rec.name}, ptr ${desc}, ptr null)`,
|
||||
);
|
||||
}
|
||||
host.emitPendingCheck();
|
||||
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
|
||||
}
|
||||
|
||||
/** The encoding argument a write passes when the call has none: undefined
|
||||
* (a null pointer), or the null value when a callback took its place. */
|
||||
function writeEncNull(host: LlvmEmitterContext, which: "null" | "undef"): string {
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
* local arrays they rely on. */
|
||||
import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js";
|
||||
import type { IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { findInitializedByteLoopBindings } from "./byte-windows.js";
|
||||
import type { DenseArrayLoop } from "./dense-array-loops.js";
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
import { defineModuleState } from "./emitter-state.js";
|
||||
|
||||
/** The most IR nodes a function may have for its in-loop number-key
|
||||
* lookups to inline the hashed probe (emitNumberMapEntry). Each inlined
|
||||
* probe adds code to its function, and LLVM's passes over a large function
|
||||
* take longer for it; a small function's loop of lookups, such as a k-mer
|
||||
* count, gains the most from it. */
|
||||
const MAX_INLINED_LOOKUP_NODES = 200;
|
||||
|
||||
/** Sets the loop state for `fn` at the start of its emission; `numericFn`
|
||||
* is `fn` with its initializer bindings (emitFunction). */
|
||||
export function beginFunctionLoops(
|
||||
@@ -19,10 +27,11 @@ export function beginFunctionLoops(
|
||||
loops.unaliasedArrays = findUnaliasedLocalArrays(fn);
|
||||
loops.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
loops.byteWindowsEnabled = loops.countedLoopsEnabled;
|
||||
loops.bytesLoopVersionsEnabled = loops.countedLoopsEnabled;
|
||||
loops.plainLoops = fn.plainLoops === true;
|
||||
loops.bytesLoopVersionsEnabled = loops.countedLoopsEnabled && !loops.plainLoops;
|
||||
loops.bytesLoopEntries = null;
|
||||
loops.bytesLoopFunction = fn;
|
||||
loops.denseLoopsEnabled = loops.countedLoopsEnabled;
|
||||
loops.denseLoopsEnabled = loops.countedLoopsEnabled && !loops.plainLoops;
|
||||
loops.denseLoop = null;
|
||||
loops.denseLoopEntries =
|
||||
loops.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array")
|
||||
@@ -32,6 +41,15 @@ export function beginFunctionLoops(
|
||||
loops.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
: new Map();
|
||||
loops.inlinedLookups = fewNodes(fn.body, MAX_INLINED_LOOKUP_NODES);
|
||||
}
|
||||
|
||||
/** Whether `body` has at most `max` statements and expressions. */
|
||||
function fewNodes(body: IrStmt[], max: number): boolean {
|
||||
let count = 0;
|
||||
const visit = (): boolean => ++count <= max;
|
||||
everyStmtList(body, { expr: visit, stmt: visit });
|
||||
return count <= max;
|
||||
}
|
||||
|
||||
/** The state LOOPS holds on each emitter. */
|
||||
@@ -48,8 +66,11 @@ export interface LoopState {
|
||||
byteWindowsEnabled: boolean;
|
||||
byteWindowEntries: ReturnType<typeof findInitializedByteLoopBindings>;
|
||||
/** Typed-array loop versioning (bytes-loop-versions.ts) for the current
|
||||
* function: off in debug and suspending functions and inside a copy. */
|
||||
* function: off in debug and suspending functions, in a function that
|
||||
* emits each loop once and inside a copy. */
|
||||
bytesLoopVersionsEnabled: boolean;
|
||||
/** The current function emits each loop once (IrFunction.plainLoops). */
|
||||
plainLoops: boolean;
|
||||
/** Loops being versioned around the statement being emitted: a loop is
|
||||
* never versioned inside its own copies, and versions nest boundedly. */
|
||||
readonly bytesLoopVersioning: Set<IrStmt>;
|
||||
@@ -58,6 +79,9 @@ export interface LoopState {
|
||||
/** Counted loops whose integer-form safety test an enclosing version
|
||||
* guard already passed. */
|
||||
readonly provenCountedGuards: Set<IrStmt>;
|
||||
/** Number-key lookups in this function's loops inline the hashed probe:
|
||||
* the function has at most MAX_INLINED_LOOKUP_NODES nodes. */
|
||||
inlinedLookups: boolean;
|
||||
}
|
||||
|
||||
export const LOOPS = defineModuleState<LoopState>(() => ({
|
||||
@@ -69,8 +93,10 @@ export const LOOPS = defineModuleState<LoopState>(() => ({
|
||||
byteWindowsEnabled: true,
|
||||
byteWindowEntries: new Map(),
|
||||
bytesLoopVersionsEnabled: false,
|
||||
plainLoops: false,
|
||||
bytesLoopVersioning: new Set<IrStmt>(),
|
||||
bytesLoopEntries: null,
|
||||
bytesLoopFunction: null,
|
||||
provenCountedGuards: new Set<IrStmt>(),
|
||||
inlinedLookups: false,
|
||||
}));
|
||||
|
||||
@@ -117,8 +117,9 @@ test("string keys and reference values are retained inline, boolean slots compar
|
||||
expect(text).toContain("call ptr @scr_map_iter_val_ref(");
|
||||
});
|
||||
|
||||
/** work(map, k): map.has(k) on a number-keyed map. */
|
||||
function lookupFixture(key: IrExpr = ref("k", F64)): IrModule {
|
||||
/** work(map, k): map.has(k) on a number-keyed map, or with `loop`, the
|
||||
* condition of `while (map.has(k)) {}`, after `filler` statements `k;`. */
|
||||
function lookupFixture(key: IrExpr = ref("k", F64), loop = false, filler = 0): IrModule {
|
||||
const map: IrType = { kind: "map", key: F64, value: F64 };
|
||||
const params = [
|
||||
{ localId: "map", name: "map", type: map },
|
||||
@@ -138,7 +139,17 @@ function lookupFixture(key: IrExpr = ref("k", F64)): IrModule {
|
||||
returnType: BOOL,
|
||||
loc,
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: true })),
|
||||
body: [{ kind: "return", value: has, loc }],
|
||||
body: loop
|
||||
? [
|
||||
...Array.from({ length: filler }, (): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: ref("k", F64),
|
||||
loc,
|
||||
})),
|
||||
{ kind: "while", cond: has, body: [], loc },
|
||||
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
|
||||
]
|
||||
: [{ kind: "return", value: has, loc }],
|
||||
};
|
||||
return {
|
||||
irVersion: 17,
|
||||
@@ -159,6 +170,10 @@ test("hashed number-key lookups probe inline and call the runtime only for wide
|
||||
module,
|
||||
)![0];
|
||||
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)");
|
||||
// A key outside the direct index goes to the probe, whose small-map
|
||||
// branch tells a map with the index (field 16) from one without it.
|
||||
expect(helper).toMatch(/br i1 %ix\d+, label %slot, label %slow\n/);
|
||||
expect(helper).not.toContain("dense.any");
|
||||
const hashed =
|
||||
/^define internal ptr @sc_map_entry_f64_hashed\(ptr %m, double %k\) noinline [^]*?^}/m.exec(
|
||||
module,
|
||||
@@ -171,7 +186,10 @@ test("hashed number-key lookups probe inline and call the runtime only for wide
|
||||
// Filter (field 10) before the small-map scan of the entry count (field
|
||||
// 8) only, and compact buckets (fields 13 and 14) tagged with the hash's
|
||||
// top byte.
|
||||
expect(hashed).toContain("br i1 %linear, label %filter, label %buckets");
|
||||
expect(hashed).toContain("br i1 %linear, label %unbucketed, label %buckets");
|
||||
expect(hashed).toMatch(
|
||||
/^unbucketed:\n[^]*i32 0, i32 16\n[^]*br i1 %indexed, label %miss, label %filter\n/m,
|
||||
);
|
||||
expect(hashed).toContain("getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10");
|
||||
expect(hashed).toContain(`load i32, ptr %nep`);
|
||||
expect(hashed).toContain(`load i32, ptr %nbp`);
|
||||
@@ -194,22 +212,27 @@ test("a key with an integer view tests the direct index on the integer", () => {
|
||||
integral.module,
|
||||
)![0];
|
||||
// An unsigned compare also rejects negative keys; the hashed probe takes
|
||||
// the integer.
|
||||
// the integer and its double.
|
||||
expect(helper).toContain("icmp ult i64 %ki, %nd");
|
||||
expect(helper).toContain(`getelementptr inbounds i32, ptr %dense, ${sz} %i`);
|
||||
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)");
|
||||
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki, double %k)");
|
||||
expect(helper).toContain("br i1 %inside, label %slot, label %slow");
|
||||
expect(helper).not.toContain("fptosi");
|
||||
// Its key word is the integer's double, which needs no NaN or -0
|
||||
// normalization; the runtime lookup gets that double.
|
||||
// Its key word is the bits of the integer's double, which needs no NaN
|
||||
// normalization and is +0 for the integer 0; the runtime lookup gets
|
||||
// that double. The probe converts nothing itself (cvtsi2sd would wait
|
||||
// for its register's last value).
|
||||
const hashed =
|
||||
/^define internal ptr @sc_map_entry_f64_hashed_int\(ptr %m, i64 %ki\) noinline [^]*?^}/m.exec(
|
||||
/^define internal ptr @sc_map_entry_f64_hashed_int\(ptr %m, i64 %ki, double %k\) noinline [^]*?^}/m.exec(
|
||||
integral.module,
|
||||
)![0];
|
||||
expect(hashed).toContain("sitofp i64 %ki to double");
|
||||
expect(hashed).toContain("bitcast double %k to i64");
|
||||
expect(hashed).toContain("select i1 %kzero, i64 0, i64 %kbits");
|
||||
expect(hashed).not.toContain("sitofp");
|
||||
expect(hashed).not.toContain("fcmp");
|
||||
expect(hashed).toContain("mul i64 %x1, -4658895280553007687");
|
||||
expect(hashed).toContain(
|
||||
"call ptr @scr_map_entry_f64(ptr %m, double %kf) memory(read) nounwind willreturn",
|
||||
"call ptr @scr_map_entry_f64(ptr %m, double %k) memory(read) nounwind willreturn",
|
||||
);
|
||||
// A fractional key and a key without a proof keep the double lookup.
|
||||
for (const key of [{ kind: "numLit", value: 0.5, type: F64, loc } as IrExpr, ref("k", F64)]) {
|
||||
@@ -220,6 +243,36 @@ test("a key with an integer view tests the direct index on the integer", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("a lookup inside a loop calls the flavor that inlines the hashed probe", () => {
|
||||
for (const key of [ref("k", F64), { kind: "numLit", value: 12, type: F64, loc } as IrExpr]) {
|
||||
const { work, module } = emitted(lookupFixture(key, true), 64);
|
||||
const name = key.kind === "numLit" ? "sc_map_entry_f64_int" : "sc_map_entry_f64";
|
||||
expect(work).toContain(`call ptr @${name}.loop(`);
|
||||
expect(work).not.toContain(`call ptr @${name}(`);
|
||||
// They only read memory and always return, so a has() and get() of one
|
||||
// key merge before they are inlined.
|
||||
const helper = new RegExp(
|
||||
`^define internal ptr @${name}\\.loop\\([^)]*\\) alwaysinline memory\\(read\\) nounwind willreturn [^]*?^}`,
|
||||
"m",
|
||||
).exec(module)![0];
|
||||
const hashed =
|
||||
key.kind === "numLit" ? "sc_map_entry_f64_hashed_int" : "sc_map_entry_f64_hashed";
|
||||
expect(helper).toContain(`call ptr @${hashed}.loop(`);
|
||||
expect(module).toMatch(
|
||||
new RegExp(
|
||||
`^define internal ptr @${hashed}\\.loop\\([^)]*\\) alwaysinline memory\\(read\\) nounwind willreturn `,
|
||||
"m",
|
||||
),
|
||||
);
|
||||
// A lookup outside a loop keeps the out-of-line probe, and so does a
|
||||
// lookup in a loop of a function of more than 200 nodes.
|
||||
expect(emitted(lookupFixture(key), 64).work).toContain(`call ptr @${name}(`);
|
||||
const large = emitted(lookupFixture(key, true, 100), 64).work;
|
||||
expect(large).toContain(`call ptr @${name}(`);
|
||||
expect(large).not.toContain(".loop(");
|
||||
}
|
||||
});
|
||||
|
||||
test("a key view converted in a branch is not reused by a later lookup of the same constant", () => {
|
||||
// work(map, k, taken): if (taken) return map.has((k = 2 ** 40)); return map.has(2 ** 40).
|
||||
const wide = 2 ** 40;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { LOOPS } from "./loop-state.js";
|
||||
import { mapKeyAccess } from "./shapes.js";
|
||||
|
||||
export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)";
|
||||
@@ -55,19 +56,22 @@ export function noteNumberMapKey(host: LlvmEmitterContext, key: LlValue, expr: I
|
||||
|
||||
/** The entry for `key`, or null when absent. Valid until the next mutation.
|
||||
* A key with an integer view (noteNumberMapKey) tests the direct index on
|
||||
* that integer instead of converting the double back. */
|
||||
* that integer instead of converting the double back. A lookup inside a
|
||||
* loop of a small function (LOOPS.inlinedLookups) calls the helpers' `.loop`
|
||||
* flavor, which inlines the hashed probe. */
|
||||
export function emitNumberMapEntry(host: LlvmEmitterContext, map: string, key: string): string {
|
||||
host.declare(NUMBER_MAP_ENTRY_DECL);
|
||||
const entry = host.B.tmp();
|
||||
const flavor = host.loopDepth > 0 && LOOPS.of(host).inlinedLookups ? ".loop" : "";
|
||||
const integer = host.numberMapKeyIntegers.get(key);
|
||||
if (integer) {
|
||||
const wide = widenInteger(host, integer);
|
||||
host.B.line(
|
||||
`${entry} = call ptr @sc_map_entry_f64_int(ptr ${map}, i64 ${wide}, double ${key})`,
|
||||
`${entry} = call ptr @sc_map_entry_f64_int${flavor}(ptr ${map}, i64 ${wide}, double ${key})`,
|
||||
);
|
||||
return entry;
|
||||
}
|
||||
host.B.line(`${entry} = call ptr @sc_map_entry_f64(ptr ${map}, double ${key})`);
|
||||
host.B.line(`${entry} = call ptr @sc_map_entry_f64${flavor}(ptr ${map}, double ${key})`);
|
||||
return entry;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
|
||||
async function llvmFor(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-receiver-brackets-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
return emitLlvmModule(deserializeModule(await readFile(output, "utf8")));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function body(ir: string, name: string): string {
|
||||
const match = new RegExp(`^define [^\\n]*@${name}\\(.*?^}`, "ms").exec(ir);
|
||||
if (!match) throw new Error(`no function ${name}`);
|
||||
return match[0];
|
||||
}
|
||||
|
||||
test("a known signature adapter's direct entry brackets the receiver only when its original reads it", async () => {
|
||||
const ir = await llvmFor(`
|
||||
class Node {
|
||||
kids: Node[] = [];
|
||||
constructor(readonly kind: number) {}
|
||||
}
|
||||
function walk(root: Node, cb: (node: Node, depth: number) => void | "skip" | "stop"): void {
|
||||
const stack: Node[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
const verdict = cb(node, stack.length);
|
||||
if (verdict === "stop") return;
|
||||
if (verdict === "skip") continue;
|
||||
for (const kid of node.kids) stack.push(kid);
|
||||
}
|
||||
}
|
||||
class Counter {
|
||||
count = 0;
|
||||
run(root: Node): number {
|
||||
walk(root, (node) => {
|
||||
if (node.kind === 3) this.count++;
|
||||
});
|
||||
return this.count;
|
||||
}
|
||||
}
|
||||
const root = new Node(0);
|
||||
root.kids.push(new Node(3));
|
||||
console.log(new Counter().run(root));
|
||||
`);
|
||||
const walk = body(ir, "sc_bf_walk");
|
||||
const blocks = walk.split(/^(?=[\w.]+:$)/m);
|
||||
const direct = blocks.find((block) => block.startsWith("callback.direct"));
|
||||
// The adapter forwards the receiver to the arrow function, which never
|
||||
// reads it: the direct entry tests the closure's RECEIVER_FREE bit, and
|
||||
// every receiver push is behind such a test.
|
||||
expect(direct).toMatch(/ = and i32 %\S+, 16\n/);
|
||||
const pushes = blocks.filter((block) => block.includes("@scr_dyn_this_push_dyn"));
|
||||
expect(pushes.length).toBeGreaterThan(0);
|
||||
for (const block of pushes) expect(block).toMatch(/^receiver\.push\d+:/);
|
||||
});
|
||||
@@ -3,8 +3,11 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import type { IrModule } from "../../ir/ir.js";
|
||||
import { DYN } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import { computeTraced } from "../cycle-analysis.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import { planRecordViews } from "./record-views.js";
|
||||
|
||||
/** The views planned for `source`'s module, as target field names → source
|
||||
@@ -128,3 +131,98 @@ ${conversions}
|
||||
Object.freeze(wide);`);
|
||||
expect(views).toEqual(new Map());
|
||||
});
|
||||
|
||||
/** `source`'s module shaped the way the frontend lowers a partial
|
||||
* assertion (C2): the checked cast of the source record literal carries
|
||||
* `partialView` over a `dynFrom{liveRef}` of that literal. */
|
||||
async function partialModule(
|
||||
source: string,
|
||||
sourceFields: string,
|
||||
targetFields: string,
|
||||
): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-record-views-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
||||
const mod: IrModule = deserializeModule(await readFile(outPath, "utf8"));
|
||||
const idOf = (fields: string): string =>
|
||||
(mod.records ?? []).find((shape) => shape.fields.map((f) => f.name).join(",") === fields)!.id;
|
||||
const exprs: IrExpr[] = [];
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
exprs.push(e);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const lit = exprs.find(
|
||||
(e) =>
|
||||
e.kind === "recordLit" && e.type.kind === "record" && e.type.shapeId === idOf(sourceFields),
|
||||
);
|
||||
const check = exprs.find(
|
||||
(e) =>
|
||||
e.kind === "dynCheck" && e.type.kind === "record" && e.type.shapeId === idOf(targetFields),
|
||||
);
|
||||
if (!lit || !check || check.kind !== "dynCheck") throw new Error("shapes not found");
|
||||
check.partialView = true;
|
||||
check.value = { kind: "dynFrom", liveRef: true, value: lit, type: DYN, loc: check.loc };
|
||||
return mod;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("a partial assertion plans a partial view with an absent-field sentinel", async () => {
|
||||
const mod = await partialModule(
|
||||
`
|
||||
type Wide = { a: number; b: string };
|
||||
type Narrow = { a: number; z: boolean };
|
||||
const wide: Wide = { a: 1, b: "x" };
|
||||
const o: unknown = wide;
|
||||
const v = o as Narrow;
|
||||
console.log(v.a);
|
||||
`,
|
||||
"a,b",
|
||||
"a,z",
|
||||
);
|
||||
const idOf = (fields: string): string =>
|
||||
(mod.records ?? []).find((shape) => shape.fields.map((f) => f.name).join(",") === fields)!.id;
|
||||
const target = idOf("a,z");
|
||||
const source = idOf("a,b");
|
||||
const views = planRecordViews(mod, { traced: () => false, excluded: () => false });
|
||||
expect(views.targets.has(target)).toBe(true);
|
||||
expect(views.sources(target)).toEqual([source]);
|
||||
expect(views.partial(target, source)).toBe(true);
|
||||
expect(views.partialTargets.has(target)).toBe(true);
|
||||
expect(views.partialMissing(target, "z")).toBe(true);
|
||||
expect(views.partialMissing(target, "a")).toBe(false);
|
||||
// The offset table marks the target field the source does not declare
|
||||
// with the all-ones sentinel.
|
||||
const llvm = emitLlvmModule(mod);
|
||||
expect(llvm).toMatch(/@sc_rv_\d+_0 = internal constant \[2 x i64\] \[i64 ptrtoint .*, i64 -1\]/);
|
||||
});
|
||||
|
||||
test("a full source keeps an ordinary view, not a partial one", async () => {
|
||||
const mod = await partialModule(
|
||||
`
|
||||
type Wide = { a: number; b: string };
|
||||
type Narrow = { a: number };
|
||||
const wide: Wide = { a: 1, b: "x" };
|
||||
const o: unknown = wide;
|
||||
const v = o as Narrow;
|
||||
console.log(v.a);
|
||||
`,
|
||||
"a,b",
|
||||
"a",
|
||||
);
|
||||
const idOf = (fields: string): string =>
|
||||
(mod.records ?? []).find((shape) => shape.fields.map((f) => f.name).join(",") === fields)!.id;
|
||||
const views = planRecordViews(mod, { traced: () => false, excluded: () => false });
|
||||
expect(views.sources(idOf("a"))).toEqual([idOf("a,b")]);
|
||||
expect(views.partial(idOf("a"), idOf("a,b"))).toBe(false);
|
||||
expect(views.partialTargets.size).toBe(0);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,15 @@
|
||||
* checked conversion can produce (the closure of dynCheck targets), and
|
||||
* some other record type whose values reach untyped code as live capsules
|
||||
* (the closure of live dynFrom types) has all of B's fields. A program
|
||||
* without checked conversions has none, so its code is unchanged. */
|
||||
* without checked conversions has none, so its code is unchanged.
|
||||
*
|
||||
* A PARTIAL view (`dynCheck.partialView`) pairs a record literal's shape A
|
||||
* with the wider record B it was asserted to, though A lacks some of B's
|
||||
* fields. The view is a B value whose record-source entry names A, with
|
||||
* the sentinel SIZE_MAX for every B field A does not declare: a slot lookup
|
||||
* answers NULL there (scr_record_view_slot), so a presence test answers false
|
||||
* and an address lookup stops the program. The frontend's checked reads of
|
||||
* those fields throw their TypeError first. */
|
||||
import {
|
||||
isDirentShape,
|
||||
KEY_ORDER_FIELD,
|
||||
@@ -34,9 +42,28 @@ export interface RecordViews {
|
||||
readonly targets: ReadonlySet<string>;
|
||||
/** The record shapes whose capsules convert to a view of `target`. */
|
||||
sources(target: string): readonly string[];
|
||||
/** Whether `target` may be a PARTIAL view of `source`: some of its
|
||||
* fields are absent, because `source` does not declare them (a partial
|
||||
* record literal asserted to a wider type). */
|
||||
partial(target: string, source: string): boolean;
|
||||
/** Shape ids a partial record assertion may produce a view of. */
|
||||
readonly partialTargets: ReadonlySet<string>;
|
||||
/** Whether some partial source of view target `target` lacks `field`. */
|
||||
partialMissing(target: string, field: string): boolean;
|
||||
}
|
||||
|
||||
export const NO_RECORD_VIEWS: RecordViews = { targets: new Set(), sources: () => [] };
|
||||
/** The trap of an internal reader of a field a partial view's viewed record
|
||||
* lacks (a width copy, say): a source-level read of the field threw first. */
|
||||
export const PARTIAL_VIEW_ABSENT =
|
||||
"scriptc: a value asserted as a wider record has no property at this field (copy it explicitly)\n";
|
||||
|
||||
export const NO_RECORD_VIEWS: RecordViews = {
|
||||
targets: new Set(),
|
||||
sources: () => [],
|
||||
partial: () => false,
|
||||
partialTargets: new Set(),
|
||||
partialMissing: () => false,
|
||||
};
|
||||
|
||||
interface ViewOptions {
|
||||
readonly traced: (shapeId: string) => boolean;
|
||||
@@ -156,16 +183,41 @@ export function planRecordViews(mod: IrModule, options: ViewOptions): RecordView
|
||||
};
|
||||
const checked: IrType[] = [];
|
||||
const boxed: IrType[] = [];
|
||||
// (target B, source A) of a partial record assertion's checked cast.
|
||||
const partialPairs: { target: string; source: string }[] = [];
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
if (e.kind === "dynCheck") checked.push(e.type);
|
||||
else if (e.kind === "dynFrom" && e.liveRef === true) boxed.push(e.value.type);
|
||||
if (e.kind === "dynCheck") {
|
||||
checked.push(e.type);
|
||||
if (
|
||||
e.partialView === true &&
|
||||
e.type.kind === "record" &&
|
||||
e.value.kind === "dynFrom" &&
|
||||
e.value.value.type.kind === "record"
|
||||
)
|
||||
partialPairs.push({
|
||||
target: e.type.shapeId,
|
||||
source: e.value.value.type.shapeId,
|
||||
});
|
||||
} else if (e.kind === "dynFrom" && e.liveRef === true) boxed.push(e.value.type);
|
||||
else if (e.kind === "libCall" && /^(transform|passthrough)\.(new|init)$/.test(e.fn)) {
|
||||
// A transform or flush callback's typed completion data
|
||||
// (pinTypedDataCompletion) reaches untyped consumers as a live
|
||||
// capsule: scr_stream_push_typed boxes it for them.
|
||||
for (const arg of e.args) {
|
||||
if (arg.type.kind !== "func") continue;
|
||||
for (const param of arg.type.params)
|
||||
if (param.kind === "func" && param.params[1] !== undefined)
|
||||
boxed.push(param.params[1]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (checked.length === 0 || boxed.length === 0) return NO_RECORD_VIEWS;
|
||||
if ((checked.length === 0 || boxed.length === 0) && partialPairs.length === 0)
|
||||
return NO_RECORD_VIEWS;
|
||||
const reachable = closure(checked);
|
||||
const live = closure(boxed);
|
||||
if (reachable.size === 0) return NO_RECORD_VIEWS;
|
||||
@@ -183,6 +235,24 @@ export function planRecordViews(mod: IrModule, options: ViewOptions): RecordView
|
||||
}
|
||||
if (found.length > 0) sources.set(id, found);
|
||||
}
|
||||
// Partial pairs join the plan before the cycle analysis, which owns their
|
||||
// view's capsule reference like any other. A pair whose source has every
|
||||
// target field is an ordinary full view and is already planned above.
|
||||
const partials = new Map<string, Set<string>>();
|
||||
for (const pair of partialPairs) {
|
||||
const target = shapes.get(pair.target);
|
||||
const source = shapes.get(pair.source);
|
||||
if (!target || !source || !plainShape(target) || !plainShape(source)) continue;
|
||||
if (target.freezable === true || source.freezable === true) continue;
|
||||
if (options.traced(pair.target) || options.excluded(pair.target)) continue;
|
||||
if (covers(source, target)) continue;
|
||||
const list = sources.get(pair.target) ?? [];
|
||||
if (!list.includes(pair.source)) list.push(pair.source);
|
||||
sources.set(pair.target, list);
|
||||
const set = partials.get(pair.target);
|
||||
if (set) set.add(pair.source);
|
||||
else partials.set(pair.target, new Set([pair.source]));
|
||||
}
|
||||
// A view's record-source entry owns the source's capsule, an edge the
|
||||
// cycle analysis does not see. A view stays off every cycle only when its
|
||||
// source cannot hold a value of the target type, such as the view itself,
|
||||
@@ -200,11 +270,34 @@ export function planRecordViews(mod: IrModule, options: ViewOptions): RecordView
|
||||
changed = true;
|
||||
if (kept.length > 0) sources.set(id, kept);
|
||||
else sources.delete(id);
|
||||
const partial = partials.get(id);
|
||||
if (!partial) continue;
|
||||
for (const source of [...partial]) if (!kept.includes(source)) partial.delete(source);
|
||||
if (partial.size === 0) partials.delete(id);
|
||||
}
|
||||
}
|
||||
if (sources.size === 0) return NO_RECORD_VIEWS;
|
||||
const targets = new Set(sources.keys());
|
||||
return { targets, sources: (target) => sources.get(target) ?? [] };
|
||||
// The target fields some partial source lacks: their presence test is a
|
||||
// runtime view probe, and their slot lookup traps.
|
||||
const missing = new Map<string, Set<string>>();
|
||||
for (const [id, list] of partials) {
|
||||
const absent = new Set<string>();
|
||||
const target = shapes.get(id);
|
||||
for (const source of list) {
|
||||
const declared = new Set((shapes.get(source)?.fields ?? []).map((field) => field.name));
|
||||
for (const field of target?.fields ?? [])
|
||||
if (!declared.has(field.name)) absent.add(field.name);
|
||||
}
|
||||
missing.set(id, absent);
|
||||
}
|
||||
return {
|
||||
targets,
|
||||
sources: (target) => sources.get(target) ?? [],
|
||||
partial: (target, source) => partials.get(target)?.has(source) === true,
|
||||
partialTargets: new Set(partials.keys()),
|
||||
partialMissing: (target, field) => missing.get(target)?.has(field) === true,
|
||||
};
|
||||
}
|
||||
|
||||
/** Lines setting `dest` (i1) to whether the record `obj` of a view target
|
||||
@@ -229,7 +322,12 @@ export function recordViewTestLines(sizeType: "i32" | "i64", obj: string, dest:
|
||||
* `obj` of view-target shape `struct`: the slot itself, or the viewed
|
||||
* record's slot when `obj` is a view. Optimized builds test the mark
|
||||
* inline and look the slot up out of line; unoptimized builds, which give
|
||||
* every value a frame slot, make the address one call. */
|
||||
* every value a frame slot, make the address one call.
|
||||
*
|
||||
* `partial` names a shape a PARTIAL view may present: its viewed record can
|
||||
* lack the field, and the slot lookup answers NULL there (scr_record_view_slot),
|
||||
* so the address stops with `scr_trap` and `cstr(PARTIAL_VIEW_ABSENT)`. The
|
||||
* message is the program's own constant; the runtime carries none. */
|
||||
export function recordViewFieldPtr(
|
||||
B: BlockBuilder,
|
||||
sizeType: "i32" | "i64",
|
||||
@@ -239,13 +337,25 @@ export function recordViewFieldPtr(
|
||||
position: number,
|
||||
declare: (decl: string) => void,
|
||||
outOfLine: boolean,
|
||||
cstr: (text: string) => string,
|
||||
partial = false,
|
||||
): string {
|
||||
if (outOfLine) {
|
||||
declare(`declare ptr @scr_record_view_field(ptr, ${sizeType}, ${sizeType})`);
|
||||
const field = "scr_record_view_field";
|
||||
declare(`declare ptr @${field}(ptr, ${sizeType}, ${sizeType})`);
|
||||
const ptr = B.tmp();
|
||||
B.line(
|
||||
`${ptr} = call ptr @scr_record_view_field(ptr ${obj}, ${sizeType} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${slotIndex}) to ${sizeType}), ${sizeType} ${position})`,
|
||||
`${ptr} = call ptr @${field}(ptr ${obj}, ${sizeType} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${slotIndex}) to ${sizeType}), ${sizeType} ${position})`,
|
||||
);
|
||||
if (!partial) return ptr;
|
||||
const absent = B.tmp();
|
||||
B.line(`${absent} = icmp eq ptr ${ptr}, null`);
|
||||
const trap = B.newLabel("rv.absent");
|
||||
const join = B.newLabel("rv.present");
|
||||
B.condBr(absent, trap, join, `, !prof !{!"branch_weights", i32 1, i32 1000}`);
|
||||
B.startBlock(trap);
|
||||
absentTrapLines(B, cstr, declare);
|
||||
B.startBlock(join);
|
||||
return ptr;
|
||||
}
|
||||
const own = B.tmp();
|
||||
@@ -261,9 +371,34 @@ export function recordViewFieldPtr(
|
||||
declare(`declare ptr @scr_record_view_slot(ptr, ${sizeType})`);
|
||||
const viewed = B.tmp();
|
||||
B.line(`${viewed} = call ptr @scr_record_view_slot(ptr ${obj}, ${sizeType} ${position})`);
|
||||
let viewFrom = view;
|
||||
if (partial) {
|
||||
const absent = B.tmp();
|
||||
B.line(`${absent} = icmp eq ptr ${viewed}, null`);
|
||||
const trap = B.newLabel("rv.absent");
|
||||
const present = B.newLabel("rv.present");
|
||||
B.condBr(absent, trap, present);
|
||||
B.startBlock(trap);
|
||||
absentTrapLines(B, cstr, declare);
|
||||
viewFrom = present;
|
||||
B.startBlock(present);
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const ptr = B.tmp();
|
||||
B.line(`${ptr} = phi ptr [ ${own}, %${from} ], [ ${viewed}, %${view} ]`);
|
||||
B.line(`${ptr} = phi ptr [ ${own}, %${from} ], [ ${viewed}, %${viewFrom} ]`);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/** The block of an address lookup of a field a partial view lacks: the
|
||||
* program stops with its own message (PARTIAL_VIEW_ABSENT) and does not
|
||||
* return. */
|
||||
export function absentTrapLines(
|
||||
B: BlockBuilder,
|
||||
cstr: (text: string) => string,
|
||||
declare: (decl: string) => void,
|
||||
): void {
|
||||
declare(`declare void @scr_trap(ptr)`);
|
||||
B.line(`call void @scr_trap(ptr ${cstr(PARTIAL_VIEW_ABSENT)})`);
|
||||
B.terminate("unreachable");
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"json.parse",
|
||||
"process.stderrWrite",
|
||||
"process.stdoutWrite",
|
||||
"process.stdioWriteDyn",
|
||||
"process.envGet",
|
||||
"process.envPairs",
|
||||
"regex.groupEquals",
|
||||
|
||||
@@ -28,6 +28,10 @@ function declarationHost(
|
||||
declare(decl) {
|
||||
declarations.push(decl);
|
||||
},
|
||||
cstr(text) {
|
||||
declarations.push(`@cstr ${text}`);
|
||||
return "@sc_cstr";
|
||||
},
|
||||
needOom() {},
|
||||
mapOps: (operands) => (operands.every((op) => op === "null") ? "null" : "@sc_map_ops"),
|
||||
sizeType: pointerBits === 32 ? "i32" : "i64",
|
||||
|
||||
@@ -73,7 +73,7 @@ import {
|
||||
disposeFieldLines,
|
||||
plainRefType,
|
||||
} from "./arenas.js";
|
||||
import { recordViewTestLines, type RecordViews } from "./record-views.js";
|
||||
import { PARTIAL_VIEW_ABSENT, recordViewTestLines, type RecordViews } from "./record-views.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
@@ -109,6 +109,9 @@ export interface ShapeHost {
|
||||
/** Byte offset from an object pointer back to ScrCycHdr.buf. */
|
||||
readonly cycleBufOffset: number;
|
||||
declare(decl: string): void;
|
||||
/** The module constant for a string (LlEmitter.cstr), for a trap message
|
||||
* an emitted accessor passes to the runtime. */
|
||||
cstr(text: string): string;
|
||||
/** Inline the small-object allocator's fast paths (llvm/alloc.ts). */
|
||||
readonly inlineAlloc?: boolean;
|
||||
/** Record and class fields that store a scalar union's tag and bits
|
||||
@@ -2451,15 +2454,29 @@ export function emitRecordShapes(
|
||||
const inline = host.inlineUnions.record(field.type);
|
||||
const fieldTy = llFieldType(field.type, inline);
|
||||
if (host.recordViews?.targets.has(shape.id) === true) {
|
||||
// A view's fields live in the viewed record (record-views.ts).
|
||||
// A view's fields live in the viewed record (record-views.ts); a
|
||||
// shape a partial view may present answers NULL for a field the
|
||||
// viewed record lacks, so the copy stops with the program's own
|
||||
// message (a source-level read threw first).
|
||||
const partial = host.recordViews.partialTargets.has(shape.id);
|
||||
host.declare(`declare ptr @scr_record_view_slot(ptr, ${host.sizeType})`);
|
||||
const fromView = partial
|
||||
? [
|
||||
` %sn${i} = icmp eq ptr %sw${i}, null`,
|
||||
` br i1 %sn${i}, label %sv${i}.absent, label %sv${i}.join`,
|
||||
`sv${i}.absent:`,
|
||||
` call void @scr_trap(ptr ${host.cstr(PARTIAL_VIEW_ABSENT)})`,
|
||||
` unreachable`,
|
||||
]
|
||||
: [` br label %sv${i}.join`];
|
||||
if (partial) host.declare(`declare void @scr_trap(ptr)`);
|
||||
clone.push(
|
||||
` %so${i} = getelementptr inbounds %${struct}, ptr %src, i64 0, i32 ${index}`,
|
||||
...recordViewTestLines(host.sizeType, "%src", `%sr${i}v`),
|
||||
` br i1 %sr${i}v, label %sv${i}.view, label %sv${i}.join`,
|
||||
`sv${i}.view:`,
|
||||
` %sw${i} = call ptr @scr_record_view_slot(ptr %src, ${host.sizeType} ${i})`,
|
||||
` br label %sv${i}.join`,
|
||||
...fromView,
|
||||
`sv${i}.join:`,
|
||||
` %sp${i} = phi ptr [ %so${i}, %${i === 0 ? "entry" : `sv${i - 1}.join`} ], [ %sw${i}, %sv${i}.view ]`,
|
||||
` %sv${i} = load ${fieldTy}, ptr %sp${i}`,
|
||||
|
||||
@@ -234,6 +234,17 @@ function powerOfTwo(e: IrExpr): boolean {
|
||||
/** Typed-array element kinds whose elements are integers. */
|
||||
const INTEGER_ELEMENTS = new Set(["u8", "u8c", "i8", "u16", "i16", "u32", "i32"]);
|
||||
|
||||
/** The values of each integer element kind. */
|
||||
const ELEMENT_RANGES: Record<string, { min: number; max: number }> = {
|
||||
u8: { min: 0, max: 255 },
|
||||
u8c: { min: 0, max: 255 },
|
||||
i8: { min: -128, max: 127 },
|
||||
u16: { min: 0, max: 65535 },
|
||||
i16: { min: -32768, max: 32767 },
|
||||
u32: { min: 0, max: 2 ** 32 - 1 },
|
||||
i32: { min: -(2 ** 31), max: 2 ** 31 - 1 },
|
||||
};
|
||||
|
||||
/** A typed-array element read of an integer kind with no effect: an
|
||||
* out-of-range read gives NaN, which fails its check. Float elements would
|
||||
* only bail. */
|
||||
@@ -904,6 +915,17 @@ export function emitSafeIntegerCheck(host: LlvmEmitterContext, value: string): [
|
||||
return [integer, ok];
|
||||
}
|
||||
|
||||
/** The condition that a bounded value's exponent is above `limit`. A
|
||||
* value test it replaces can fail only past that limit; testing the
|
||||
* exponent alone, which is usually the same in every iteration of a loop,
|
||||
* keeps the value out of the test. Past the limit the write bails to the
|
||||
* double copy, also for a value the replaced test would have passed. */
|
||||
function exponentAbove(host: LlvmEmitterContext, bound: MaskBound, limit: number): string {
|
||||
const above = host.B.tmp();
|
||||
host.B.line(`${above} = icmp sgt i64 ${bound.exponent}, ${limit}`);
|
||||
return above;
|
||||
}
|
||||
|
||||
/** A condition that is true when an i64 lies outside the safe range. */
|
||||
function unsafeInteger(host: LlvmEmitterContext, value: string): string {
|
||||
const B = host.B;
|
||||
@@ -932,6 +954,29 @@ function outsideRange(host: LlvmEmitterContext, value: string, low: number, high
|
||||
return outside;
|
||||
}
|
||||
|
||||
/** The range of an operand once its speculative evaluation continued: a
|
||||
* literal's value, the range analysis' proof (well inside the safe
|
||||
* integers), or for an integer element read, which bails on the NaN of an
|
||||
* out-of-range index, the element kind's values. */
|
||||
function operandRange(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr,
|
||||
): { min: number; max: number } | undefined {
|
||||
const range = staticRange(host, e);
|
||||
if (range || !isSpeculativeByteRead(e) || e.kind !== "bytesIntrinsic") return range;
|
||||
return e.receiver.type.kind === "bytes" ? ELEMENT_RANGES[e.receiver.type.elem] : undefined;
|
||||
}
|
||||
|
||||
/** An i64 the speculative evaluation has bounded to [0, 2^(exponent +
|
||||
* shift)): the floor-mod of a value by 2^exponent, shifted left by `shift`.
|
||||
* `exponent` is an i64 already checked to be in [0, 52]; in a loop it is
|
||||
* usually the same value on every iteration, so a test on it alone is
|
||||
* loop-invariant. */
|
||||
interface MaskBound {
|
||||
exponent: string;
|
||||
shift: number;
|
||||
}
|
||||
|
||||
/** A literal's value or the range analysis' proof for an expression whose
|
||||
* range lies well inside the safe integers. */
|
||||
function staticRange(
|
||||
@@ -962,6 +1007,7 @@ export function emitSpeculativeInteger(
|
||||
bail: (condition: string) => void,
|
||||
reads: ReadonlyMap<IrExpr, LlValue> = new Map(),
|
||||
integral = false,
|
||||
bounds = new Map<string, MaskBound>(),
|
||||
): string {
|
||||
const B = host.B;
|
||||
// A value the range analysis already proves takes the ordinary integer
|
||||
@@ -1000,12 +1046,13 @@ export function emitSpeculativeInteger(
|
||||
// bits, also for a negative value: in [0, 2^n), and never -0.
|
||||
const mod = floorModPowerOfTwo(e);
|
||||
if (mod) {
|
||||
const value = emitSpeculativeInteger(host, mod.value, bail, reads, integral);
|
||||
const value = emitSpeculativeInteger(host, mod.value, bail, reads, integral, bounds);
|
||||
const n = exponentOf(host, mod.power, bail, reads, integral);
|
||||
const mask = B.tmp(),
|
||||
low = B.tmp();
|
||||
B.line(`${mask} = sub i64 ${shiftOne(host, n)}, 1`);
|
||||
B.line(`${low} = and i64 ${value}, ${mask}`);
|
||||
bounds.set(low, { exponent: n, shift: 0 });
|
||||
return low;
|
||||
}
|
||||
// A product with 2^n shifts: it is safe iff the other factor's
|
||||
@@ -1013,8 +1060,19 @@ export function emitSpeculativeInteger(
|
||||
// integer and a positive power is +0.
|
||||
if (e.op === "*" && (powerOfTwo(e.right) || powerOfTwo(e.left))) {
|
||||
const [factor, power] = powerOfTwo(e.right) ? [e.left, e.right] : [e.right, e.left];
|
||||
const value = emitSpeculativeInteger(host, factor, bail, reads, integral);
|
||||
const value = emitSpeculativeInteger(host, factor, bail, reads, integral, bounds);
|
||||
const n = exponentOf(host, power, bail, reads, integral);
|
||||
// A factor below 2^(m + s) times 2^k is below 2^(m + s + k), so
|
||||
// safe when m + s + k <= 53.
|
||||
const bound = bounds.get(value);
|
||||
const k = power.kind === "numLit" ? exactPowerOfTwo(power.value) : null;
|
||||
if (bound && k !== null) {
|
||||
bail(exponentAbove(host, bound, 53 - bound.shift - k));
|
||||
const product = B.tmp();
|
||||
B.line(`${product} = shl nuw nsw i64 ${value}, ${n}`);
|
||||
bounds.set(product, { exponent: bound.exponent, shift: bound.shift + k });
|
||||
return product;
|
||||
}
|
||||
const limit = B.tmp(),
|
||||
magnitude = B.tmp(),
|
||||
outside = B.tmp(),
|
||||
@@ -1027,16 +1085,24 @@ export function emitSpeculativeInteger(
|
||||
B.line(`${product} = shl nsw i64 ${value}, ${n}`);
|
||||
return product;
|
||||
}
|
||||
const a = emitSpeculativeInteger(host, e.left, bail, reads, integral);
|
||||
const b = emitSpeculativeInteger(host, e.right, bail, reads, integral);
|
||||
const a = emitSpeculativeInteger(host, e.left, bail, reads, integral, bounds);
|
||||
const b = emitSpeculativeInteger(host, e.right, bail, reads, integral, bounds);
|
||||
const result = B.tmp();
|
||||
if (e.op === "+" || e.op === "-") {
|
||||
// With one operand's range known, the other is checked against the
|
||||
// narrowed bounds before the operation: the check then depends on
|
||||
// no new value, and LLVM keeps the operation itself simple.
|
||||
const right = staticRange(host, e.right);
|
||||
const left = e.op === "+" ? staticRange(host, e.left) : undefined;
|
||||
if (right) {
|
||||
const right = operandRange(host, e.right);
|
||||
const left = e.op === "+" ? operandRange(host, e.left) : undefined;
|
||||
// A bounded operand, in [0, 2^(m + s)), plus one in [lo, hi] with
|
||||
// |lo|, hi <= 2^52 is safe when m + s <= 52.
|
||||
const bound = (value: string): MaskBound | undefined =>
|
||||
e.op === "+" ? bounds.get(value) : undefined;
|
||||
const aBound = right ? bound(a) : undefined;
|
||||
const bBound = !right && left ? bound(b) : undefined;
|
||||
if (aBound) bail(exponentAbove(host, aBound, 52 - aBound.shift));
|
||||
else if (bBound) bail(exponentAbove(host, bBound, 52 - bBound.shift));
|
||||
else if (right) {
|
||||
// a + b with b in [lo, hi] is safe iff a in [-SAFE - lo, SAFE - hi];
|
||||
// a - b iff a in [-SAFE + hi, SAFE + lo].
|
||||
const low = e.op === "+" ? -SAFE - right.min : -SAFE + right.max;
|
||||
|
||||
@@ -72,9 +72,9 @@ function recursiveFunctions(edges: ReadonlyMap<string, readonly string[]>): Set<
|
||||
* `call` from emitted code that can lower it as an `invoke`: it is not
|
||||
* async or a generator, not a constructor, not a method a virtual call can
|
||||
* reach or one a vtable holds (an overridden method), not a closure
|
||||
* target, not the entry or a library export, not named by any node other
|
||||
* than a `call`'s callee, and not called from an async or generator body
|
||||
* (coroutine bodies keep plain calls).
|
||||
* target or a closure's jsval body, not the entry or a library export,
|
||||
* not named by any node other than a `call`'s callee, and not called from
|
||||
* an async or generator body (coroutine bodies keep plain calls).
|
||||
*
|
||||
* A native unwind costs about 0.05 µs a raise and 10 to 25 ns a frame on
|
||||
* x86-64 (scr_unwind.c's cached plans) and about 2 µs a raise and 0.1 µs a
|
||||
@@ -103,6 +103,9 @@ export function unwindAbiFunctions(
|
||||
blocked.add("%error.name.read");
|
||||
blocked.add("%error.message.read");
|
||||
for (const entry of mod.lib?.exports ?? []) blocked.add(entry.fnName);
|
||||
// A closure's jsval body is called through the function value's jsval
|
||||
// entry (IrFunction.jsEntry), which runtime calls reach.
|
||||
for (const fn of mod.functions) if (fn.jsEntry !== undefined) blocked.add(fn.jsEntry);
|
||||
// Methods a virtual call can reach, and the overridden methods of class
|
||||
// hierarchies (whose vtables list them), are called through pointers; the
|
||||
// others are called directly, like functions. A virtual call reaches its listed
|
||||
|
||||
@@ -293,6 +293,8 @@ export class LlWalkers {
|
||||
index - base,
|
||||
(decl) => this.host.declare(decl),
|
||||
this.host.rcMode === undefined,
|
||||
(text) => this.host.cstr(text),
|
||||
this.host.recordViews?.partialTargets.has(shapeId) === true,
|
||||
);
|
||||
} else {
|
||||
p = B.tmp();
|
||||
|
||||
@@ -563,6 +563,9 @@ function helperProfileMatch(match: NativeProfileMatch): Record<string, number> {
|
||||
missing: match.missing,
|
||||
recovered: match.recovered,
|
||||
unknown_partitions: match.unknownPartitions,
|
||||
...(match.annotatedCounts === undefined || match.mismatchedCounts === undefined
|
||||
? {}
|
||||
: { annotated_counts: match.annotatedCounts, mismatched_counts: match.mismatchedCounts }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,28 @@ export interface NativeLinkFeatures {
|
||||
/** The program assigns process.emitWarning: links the
|
||||
* SCR_WARNING_REPLACE variant of the dyn-async unit. */
|
||||
warningReplace?: boolean;
|
||||
/** The program asserts a record literal to a wider record type (a partial
|
||||
* record view): links the SCR_RECORD_VIEW_PARTIAL variant of scr_json.c.
|
||||
* Absent for every program without one, so their objects stay identical. */
|
||||
recordViewPartial?: boolean;
|
||||
/** The program uses process.stdin.setEncoding, string 'data' listeners
|
||||
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
|
||||
stdioExtended?: boolean;
|
||||
/** writeFile's `flag` disposition or copyFile's mode argument:
|
||||
* scr_fs_flags.c and the SCR_FS_FLAGS build of scr_fs_cp.c. */
|
||||
fsFlags?: boolean;
|
||||
/** string_decoder's checked-dynamic chunk forms: scr_bytes_strdec_dyn.c. */
|
||||
strdecDyn?: boolean;
|
||||
/** process.prependListener, stdin.isRaw or server.off: the
|
||||
* SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and the
|
||||
* SCR_NET_SERVER_OFF build of scr_net.c. */
|
||||
processExtras?: boolean;
|
||||
/** A write completion callback takes the error argument: links the
|
||||
* SCR_STREAM_WRITE_ERRORS build of the stream unit. */
|
||||
streamWriteErrors?: boolean;
|
||||
/** The program reaches the process output streams' write slot: links the
|
||||
* SCR_STDIO_WRITE_SLOT build of the events unit. */
|
||||
stdioWriteSlot?: boolean;
|
||||
/** Calls into Node.js (@scriptc/node): scr_node_island.c and the
|
||||
* SCR_NODE_ISLANDS variant of the event loop unit. */
|
||||
nodeIsland?: boolean;
|
||||
@@ -43,6 +62,9 @@ export interface NativeLinkFeatures {
|
||||
/** A module namespace boxed for typed code links
|
||||
* scr_module_namespace_dyn.c; absent (false) otherwise. */
|
||||
moduleNamespaceDyn?: boolean;
|
||||
/** Primitive wrapper objects link scr_js_wrapper.c; absent (false)
|
||||
* otherwise. */
|
||||
primitiveWrapper?: boolean;
|
||||
zlib: boolean;
|
||||
/** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */
|
||||
brotli?: boolean;
|
||||
@@ -120,6 +142,8 @@ export interface NativeLinkFeatures {
|
||||
* scr_text_codec_dyn.c. */
|
||||
textCodecDyn: boolean;
|
||||
watch: boolean;
|
||||
/** fs.watchFile/fs.unwatchFile: scr_watch_file.c. */
|
||||
watchFile: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
tls: boolean;
|
||||
@@ -144,6 +168,10 @@ export interface NativeLinkFeatures {
|
||||
x509: boolean;
|
||||
/** perf_hooks event-loop metrics: scr_perf_loop.c. */
|
||||
perfLoop: boolean;
|
||||
/** Intl over ICU (scr_intl.c plus the ICU archive and its data
|
||||
* file): the `icu.*` libCalls of the first slice. Absent (false) for
|
||||
* programs without them, so their link features stay unchanged. */
|
||||
intl?: boolean;
|
||||
tlsCa: boolean;
|
||||
/** crypto.subtle importKey/sign/verify/digest and CryptoKey values:
|
||||
* scr_webcrypto.c. */
|
||||
|
||||
@@ -34,6 +34,7 @@ export async function createNativeLinkInfo(options: {
|
||||
features: pack.features,
|
||||
runtime: pack.selectedRuntimeArtifacts,
|
||||
archives: pack.selectedArchiveArtifacts,
|
||||
data: pack.selectedDataArtifacts,
|
||||
systemLibraries: pack.systemLibraries,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -145,6 +145,20 @@ test("the helper's profile match is read from its output", () => {
|
||||
'{"ok":true,"profile":{"annotated":1,"mismatched":0,"missing":0,"recovered":-1,"unknown_partitions":0}}\n',
|
||||
),
|
||||
).toBeNull();
|
||||
// With the profile's counters on the annotated and the mismatched functions.
|
||||
expect(
|
||||
parseNativeProfileMatch(
|
||||
'{"ok":true,"profile":{"annotated":120,"mismatched":7,"missing":40,"recovered":0,"unknown_partitions":0,"annotated_counts":9000,"mismatched_counts":1000}}\n',
|
||||
),
|
||||
).toEqual({
|
||||
annotated: 120,
|
||||
mismatched: 7,
|
||||
missing: 40,
|
||||
recovered: 0,
|
||||
unknownPartitions: 0,
|
||||
annotatedCounts: 9000,
|
||||
mismatchedCounts: 1000,
|
||||
});
|
||||
// A helper that predates the report prints nothing.
|
||||
expect(parseNativeProfileMatch("")).toBeNull();
|
||||
expect(parseNativeProfileMatch('{"ok":true}\n')).toBeNull();
|
||||
|
||||
@@ -111,13 +111,16 @@ export function nativeProfileHelperArgs(profile: NativeProfileOptions | undefine
|
||||
* profile has no record of (new, or never run while it was collected),
|
||||
* mismatched functions that the block-count file annotated (never more
|
||||
* than the mismatched ones), and partitions reused from the cache without
|
||||
* their counts. */
|
||||
* their counts. Helpers that weigh the match also report the profile's
|
||||
* counters on the annotated and on the mismatched functions. */
|
||||
export interface NativeProfileMatch {
|
||||
annotated: number;
|
||||
mismatched: number;
|
||||
missing: number;
|
||||
recovered: number;
|
||||
unknownPartitions: number;
|
||||
annotatedCounts?: number;
|
||||
mismatchedCounts?: number;
|
||||
}
|
||||
|
||||
/** The profile match in a successful helper emit's output, or null when the
|
||||
@@ -150,7 +153,18 @@ export function parseNativeProfileMatch(stdout: string): NativeProfileMatch | nu
|
||||
unknownPartitions === null
|
||||
)
|
||||
continue;
|
||||
return { annotated, mismatched, missing, recovered, unknownPartitions };
|
||||
const annotatedCounts = count("annotated_counts");
|
||||
const mismatchedCounts = count("mismatched_counts");
|
||||
return {
|
||||
annotated,
|
||||
mismatched,
|
||||
missing,
|
||||
recovered,
|
||||
unknownPartitions,
|
||||
...(annotatedCounts === null || mismatchedCounts === null
|
||||
? {}
|
||||
: { annotatedCounts, mismatchedCounts }),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,17 @@ export interface CcOptions {
|
||||
/** The program uses node:zlib's Brotli forms (with zlib): compiles
|
||||
* scr_brotli.c and links the vendored brotli archive. */
|
||||
brotli?: boolean;
|
||||
/** The program uses an Intl form that only ICU can answer (the `icu.*`
|
||||
* libCalls: default-locale number/date formats, word segmentation):
|
||||
* compiles scr_intl.c and links the ICU archive, and the
|
||||
* executable gets icudt78l.dat next to it. Every engine-free form keeps
|
||||
* linking none of it. */
|
||||
intl?: boolean;
|
||||
/** With `intl`: the installed runtime pack's ICU archive and the include
|
||||
* root of the ICU headers it ships. The C-source path then links that
|
||||
* archive and compiles scr_intl.c against those headers, instead of
|
||||
* building ICU from its fetched release with Zig's C++ driver. */
|
||||
icuPack?: { archive: string; include: string };
|
||||
/** The program uses node:assert (index.ts detects assert.* libCalls on
|
||||
* the IR): compiles scr_assert.c — the zlib gating precedent, so
|
||||
* assert-free binaries keep their exact size class. scr_regex.c calls
|
||||
@@ -159,8 +170,26 @@ export interface CcOptions {
|
||||
wasm?: boolean;
|
||||
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
|
||||
warningReplace?: boolean;
|
||||
/** SCR_RECORD_VIEW_PARTIAL: the scr_json.c build whose view slot answers
|
||||
* NULL for a field a partial record view does not have. */
|
||||
recordViewPartial?: boolean;
|
||||
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
|
||||
stdioExtended?: boolean;
|
||||
/** SCR_FS_FLAGS: writeFile's `flag` option and copyFile's mode argument
|
||||
* (scr_fs_flags.c and that build of scr_fs_cp.c). */
|
||||
fsFlags?: boolean;
|
||||
/** string_decoder's checked-dynamic chunk forms: compiles
|
||||
* scr_bytes_strdec_dyn.c. */
|
||||
strdecDyn?: boolean;
|
||||
/** SCR_PROCESS_EXTRAS / SCR_NET_SERVER_OFF: process.prependListener,
|
||||
* stdin.isRaw and server.off. */
|
||||
processExtras?: boolean;
|
||||
/** SCR_STREAM_WRITE_ERRORS: the stream unit publishes the error each
|
||||
* write completion callback receives (error-taking write callbacks). */
|
||||
streamWriteErrors?: boolean;
|
||||
/** SCR_STDIO_WRITE_SLOT: the events unit carries the typed write slot
|
||||
* (the write method as a value, its replacement, run-time-typed chunks). */
|
||||
stdioWriteSlot?: boolean;
|
||||
/** Calls into Node.js (@scriptc/node): compiles scr_node_island.c and
|
||||
* the event loop with SCR_NODE_ISLANDS (its helper slot). */
|
||||
nodeIsland?: boolean;
|
||||
@@ -170,6 +199,8 @@ export interface CcOptions {
|
||||
/** A module namespace boxed for typed code: compiles
|
||||
* scr_module_namespace_dyn.c. */
|
||||
moduleNamespaceDyn?: boolean;
|
||||
/** Primitive wrapper objects: compiles scr_js_wrapper.c. */
|
||||
primitiveWrapper?: boolean;
|
||||
/** The program uses the process-events surface (signal/exit listeners,
|
||||
* stdin events, for-await over stdin — moduleUsesProcessEvents on the
|
||||
* IR): compiles scr_events.c into the binary. Event-free binaries keep
|
||||
@@ -258,6 +289,11 @@ export interface CcOptions {
|
||||
* scr_watch.c into the binary — the net gating precedent, so watch-free
|
||||
* binaries keep their exact link line. */
|
||||
watch?: boolean;
|
||||
/** The program uses fs.watchFile or fs.unwatchFile (moduleUsesFsWatchFile
|
||||
* on the IR): compiles scr_watch_file.c into the binary — its own gated
|
||||
* unit under the same precedent, so polling-free binaries keep their
|
||||
* exact link line. */
|
||||
watchFile?: boolean;
|
||||
/** The executable manifest has a format-5 foreign callback descriptor:
|
||||
* compiles the MPSC queue/self-pipe unit. Other FFI and non-FFI binaries
|
||||
* keep their existing runtime size class. */
|
||||
|
||||
@@ -84,6 +84,9 @@ import {
|
||||
vendorBuildCacheRoot,
|
||||
tlsArchivePath,
|
||||
brotliArchivePath,
|
||||
ensureIcuSources,
|
||||
icuArchivePath,
|
||||
ensureIcuArchive,
|
||||
lreObjectPaths,
|
||||
zlibObjectPaths,
|
||||
stageVendorInputs,
|
||||
@@ -124,6 +127,10 @@ interface RuntimeUnitContext {
|
||||
zlibObjects: readonly string[];
|
||||
tlsArchive: string | null;
|
||||
brotliArchive: string | null;
|
||||
icuArchive: string | null;
|
||||
/** The include roots of the ICU headers scr_intl.c compiles against: the
|
||||
* runtime pack's, or the fetched release's common/ and i18n/. */
|
||||
icuIncludeDirs: readonly string[];
|
||||
}
|
||||
|
||||
/** An optional runtime input: runtime sources compiled when `when` holds,
|
||||
@@ -153,6 +160,11 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
),
|
||||
unit((u) => u.opts.copying, "scr_copying.c"),
|
||||
unit((u) => u.opts.fileHandle, "scr_file_handle.c"),
|
||||
// writeFile's `flag` option and the fs/promises twins; the copyFile mode
|
||||
// argument is the SCR_FS_FLAGS build of scr_fs_cp.c.
|
||||
unit((u) => u.opts.fsFlags === true, "scr_fs_flags.c"),
|
||||
// string_decoder's checked-dynamic chunk forms, beside their typed twins.
|
||||
unit((u) => u.opts.strdecDyn === true, "scr_bytes_strdec_dyn.c"),
|
||||
// BigIntStats member reads (`statSync(p, { bigint: true })` integers and
|
||||
// `*Ns`/`*Ms` members). Gated on the reads themselves (the bigstats
|
||||
// feature), not on bigint: every bigint value or wasm conversion turns
|
||||
@@ -174,9 +186,10 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
// SCR_MUSL flag and this target-selected TU travel together.
|
||||
unit((u) => isMuslTarget(u.driver), "scr_musl.c"),
|
||||
// scr_glob.c (path.matchesGlob) compiles its segments through the
|
||||
// regex unit and scr_idna.c (node:url's IDNA) normalizes through it,
|
||||
// so both ride the same switch; section GC drops them from binaries
|
||||
// that never use them.
|
||||
// regex unit, scr_idna.c (node:url's IDNA) normalizes through it and
|
||||
// scr_path_win32_relative.c lowercases through its case mapping, so all
|
||||
// three ride the same switch; section GC drops them from binaries that
|
||||
// never use them.
|
||||
unitArgs((u) =>
|
||||
u.regex
|
||||
? [
|
||||
@@ -185,6 +198,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
u.rt(join(u.rtDir, "scr_regex.c")),
|
||||
u.rt(join(u.rtDir, "scr_glob.c")),
|
||||
u.rt(join(u.rtDir, "scr_idna.c")),
|
||||
u.rt(join(u.rtDir, "scr_path_win32_relative.c")),
|
||||
...u.lreObjects,
|
||||
]
|
||||
: [],
|
||||
@@ -212,6 +226,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.moduleNamespace === true, "scr_module_namespace.c"),
|
||||
// A module namespace as the frozen null-prototype object typed code sees.
|
||||
unit((u) => u.opts.moduleNamespaceDyn === true, "scr_module_namespace_dyn.c"),
|
||||
// Primitive wrapper objects (new String/Number/Boolean, Object(primitive)).
|
||||
unit((u) => u.opts.primitiveWrapper === true, "scr_js_wrapper.c"),
|
||||
unit(
|
||||
(u) =>
|
||||
u.opts.dynAsync ||
|
||||
@@ -253,6 +269,20 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
]
|
||||
: [],
|
||||
),
|
||||
// Intl over ICU (scr_intl.c): ICU's C API only, at the default locale,
|
||||
// reading its data file from beside the executable. The ICU headers come
|
||||
// from the installed runtime pack beside its archive, or, when ICU is
|
||||
// built from its fetched release, from that source tree (common/unicode
|
||||
// for the core API, i18n/unicode for udat.h and unumberformatter.h).
|
||||
unitArgs((u) =>
|
||||
u.icuIncludeDirs.length === 0 || u.icuArchive === null
|
||||
? []
|
||||
: [
|
||||
...u.icuIncludeDirs.flatMap((dir) => ["-I", dir]),
|
||||
u.rt(join(u.rtDir, "scr_intl.c")),
|
||||
u.icuArchive,
|
||||
],
|
||||
),
|
||||
unit((u) => u.opts.events, "scr_events.c", "scr_readline.c"),
|
||||
// readline's terminal mode: rl.terminal sets the regex unit it
|
||||
// measures with.
|
||||
@@ -344,6 +374,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.dgram && u.opts.dgramDyn, "scr_dgram_dyn.c"),
|
||||
unit((u) => u.opts.textCodecDyn, "scr_text_codec_dyn.c"),
|
||||
unit((u) => u.opts.watch, "scr_watch.c"),
|
||||
// fs.watchFile's stat poller: its own gated unit (no loop hook — the
|
||||
// re-armed timers are the liveness), linked only by programs that poll.
|
||||
unit((u) => u.opts.watchFile, "scr_watch_file.c"),
|
||||
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
|
||||
unit((u) => u.opts.nodeTest, "scr_test.c"),
|
||||
unit((u) => u.opts.webcrypto, "scr_webcrypto.c"),
|
||||
@@ -389,6 +422,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
"scr_node_island_handle.c",
|
||||
"scr_node_island_export.c",
|
||||
"scr_node_island_boot.c",
|
||||
"scr_node_island_gc.c",
|
||||
),
|
||||
// Readable.fromWeb reads a web ReadableStream through its reader.
|
||||
unit((u) => u.nativeFetch && u.opts.stream, "scr_stream_from_web.c"),
|
||||
@@ -416,13 +450,30 @@ const RUNTIME_DEFINES: readonly {
|
||||
// programs with dynamic-tier code select it.
|
||||
{ define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true },
|
||||
{ define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace },
|
||||
// scr_json.c's view slot answers NULL for a field a partial record view
|
||||
// does not have; only a program with a partial assertion selects it.
|
||||
{ define: "SCR_RECORD_VIEW_PARTIAL", when: (opts) => opts.recordViewPartial },
|
||||
{ define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended },
|
||||
// The wave's prepend registrations and stdin.isRaw (scr_events.c,
|
||||
// scr_async_dyn.c), server.off (scr_net.c) and copyFile's mode form
|
||||
// (scr_fs_cp.c). The source build compiles these units from source, so the
|
||||
// defines must name the same builds runtime-pack-matrix.mjs selects.
|
||||
{ define: "SCR_PROCESS_EXTRAS", when: (opts) => opts.processExtras },
|
||||
{ define: "SCR_NET_SERVER_OFF", when: (opts) => opts.processExtras },
|
||||
{ define: "SCR_FS_FLAGS", when: (opts) => opts.fsFlags },
|
||||
// scr_stream.c's write-callback error publication, which the compiler's
|
||||
// error-first write adapter reads.
|
||||
{ define: "SCR_STREAM_WRITE_ERRORS", when: (opts) => opts.streamWriteErrors },
|
||||
// scr_events.c's typed process output write slot.
|
||||
{ define: "SCR_STDIO_WRITE_SLOT", when: (opts) => opts.stdioWriteSlot },
|
||||
// scr_async.c's native promise reactions, which the web streams unit uses,
|
||||
// scr_json.c's and scr_dyn_builtin_class.c's instanceof over its classes,
|
||||
// and scr_fetch.c's bodies of its streams.
|
||||
{ define: "SCR_NATIVE_REACTIONS", when: (opts) => opts.webStreams },
|
||||
{ define: "SCR_HANDLE_CLASSES", when: (opts) => opts.webStreams },
|
||||
{ define: "SCR_YIN", when: (opts) => opts.youngNursery },
|
||||
// scr_lib.c's path stat and zero Stats for the fs.watchFile poll.
|
||||
{ define: "SCR_FS_WATCH_FILE", when: (opts) => opts.watchFile },
|
||||
{ define: "SCR_NODE_ISLANDS", when: (opts) => opts.nodeIsland },
|
||||
];
|
||||
|
||||
@@ -455,6 +506,7 @@ export async function compileCInternal(
|
||||
const pki = opts.pki ?? false;
|
||||
const mbedtls = tls || cipher || ecdh || dh || pki;
|
||||
const brotli = (opts.zlib ?? false) && opts.brotli === true;
|
||||
const icu = opts.intl ?? false;
|
||||
const driver = resolveCc();
|
||||
const darwinDebugSymbols = needsDarwinDebugSymbols(
|
||||
targetPlatform(driver),
|
||||
@@ -697,6 +749,24 @@ export async function compileCInternal(
|
||||
let brotliArchive = brotli
|
||||
? brotliArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
|
||||
: null;
|
||||
// The installed runtime pack's ICU archive is the one this path would
|
||||
// build (uninstrumented either way), so a pack that ships it and its
|
||||
// headers spares the Zig C++ build and the source download.
|
||||
const icuPack = icu ? (opts.icuPack ?? null) : null;
|
||||
let icuArchive = icu
|
||||
? (icuPack?.archive ?? icuArchivePath(driver, vendorBuildIdentity, vendorCacheRoot))
|
||||
: null;
|
||||
// scr_intl.c's ICU headers. They are known before the runtime-object flags
|
||||
// are: those flags compile every runtime unit, so without them a cached
|
||||
// scr_intl.o would take whatever ICU headers the host has.
|
||||
const icuSource =
|
||||
icu && icuPack === null ? join(await ensureIcuSources(vendorCacheRoot), "source") : null;
|
||||
const icuIncludeDirs: readonly string[] =
|
||||
icuPack !== null
|
||||
? [icuPack.include]
|
||||
: icuSource !== null
|
||||
? [join(icuSource, "common"), join(icuSource, "i18n")]
|
||||
: [];
|
||||
|
||||
// ours would collide; see scr_regex.c) — the standalone objects are for
|
||||
// static builds only.
|
||||
@@ -751,6 +821,18 @@ export async function compileCInternal(
|
||||
: await stageVendorInputs(materialize, join(stageRoot, "brotli"));
|
||||
brotliArchive = paths[0]!;
|
||||
}
|
||||
if (icu && icuPack === null) {
|
||||
const cachedArchive = icuArchivePath(driver, vendorBuildIdentity, materializeCacheRoot);
|
||||
protectCachedArtifact(cacheWarmPaths, cachedArchive);
|
||||
const materialize = async (): Promise<string[]> => [
|
||||
await ensureIcuArchive(driver, vendorBuildIdentity, materializeCacheRoot),
|
||||
];
|
||||
const paths =
|
||||
stageRoot === undefined
|
||||
? await materialize()
|
||||
: await stageVendorInputs(materialize, join(stageRoot, "icu"));
|
||||
icuArchive = paths[0]!;
|
||||
}
|
||||
if (regex) {
|
||||
for (const object of lreObjectPaths(
|
||||
sanitize,
|
||||
@@ -838,6 +920,8 @@ export async function compileCInternal(
|
||||
zlibObjects,
|
||||
tlsArchive,
|
||||
brotliArchive,
|
||||
icuArchive,
|
||||
icuIncludeDirs,
|
||||
}),
|
||||
// A .ll program TU (the LLVM backend) deliberately carries no target
|
||||
// triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET
|
||||
@@ -886,6 +970,7 @@ export async function compileCInternal(
|
||||
...(regex ? ["-I", vendorEngineDir()] : []),
|
||||
...(zlibObjects.length > 0 ? ["-I", vendorZlibDir()] : []),
|
||||
...(tlsArchive !== null ? ["-I", join(vendorTlsDir(), "include")] : []),
|
||||
...icuIncludeDirs.flatMap((dir) => ["-I", dir]),
|
||||
];
|
||||
// Keep sanitizer runtime objects optimized in both postures. Large dev
|
||||
// programs still compile at O0, avoiding an expensive LLVM optimization
|
||||
|
||||
@@ -104,7 +104,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
// Zig's musl sysroot does not provide arc4random_buf. Keep the fallback
|
||||
// inside the archive so library embedders need no extra system library.
|
||||
...(isMuslTarget(driver) ? ["scr_musl.c"] : []),
|
||||
...(regex ? ["scr_regex.c", "scr_glob.c"] : []),
|
||||
...(regex ? ["scr_regex.c", "scr_glob.c", "scr_path_win32_relative.c"] : []),
|
||||
...(opts.assert || regex || opts.symbol ? ["scr_assert.c"] : []),
|
||||
...(opts.inspect ? ["scr_inspect.c", "scr_console_native.c"] : []),
|
||||
...(opts.symbol ? ["scr_symbol.c"] : []),
|
||||
|
||||
@@ -3,7 +3,13 @@ import { createRequire } from "node:module";
|
||||
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
||||
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { BROTLI_VERSION, MBEDTLS_VERSION, QJS_COMMIT, ZLIB_VERSION } from "../vendor-inputs.js";
|
||||
import {
|
||||
BROTLI_VERSION,
|
||||
ICU_VERSION,
|
||||
MBEDTLS_VERSION,
|
||||
QJS_COMMIT,
|
||||
ZLIB_VERSION,
|
||||
} from "../vendor-inputs.js";
|
||||
import { stableTestMemo } from "./session.js";
|
||||
|
||||
const implementationPath = fileURLToPath(import.meta.url);
|
||||
@@ -139,6 +145,7 @@ async function runtimeFingerprintFresh(rtDir: string): Promise<string> {
|
||||
.update(MBEDTLS_VERSION)
|
||||
.update(ZLIB_VERSION)
|
||||
.update(BROTLI_VERSION)
|
||||
.update(ICU_VERSION)
|
||||
.update("\0backend-recipe\0");
|
||||
for (const path of NATIVE_RECIPE_IMPLEMENTATION_PATHS) {
|
||||
h.update(basename(path))
|
||||
@@ -159,19 +166,29 @@ async function runtimeFingerprintFresh(rtDir: string): Promise<string> {
|
||||
return h.digest("hex");
|
||||
}
|
||||
|
||||
interface RuntimeFingerprintGroup {
|
||||
label: string;
|
||||
dir: string;
|
||||
}
|
||||
|
||||
async function runtimeFingerprintInputGroups(
|
||||
rtDir: string,
|
||||
): Promise<{ label: string; dir: string; names: string[] }[]> {
|
||||
return Promise.all(
|
||||
[
|
||||
{ label: "runtime", dir: rtDir },
|
||||
{ label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
|
||||
{ label: "quickjs-ng", dir: join(rtDir, "..", "vendor", "quickjs-ng") },
|
||||
{ label: "mbedtls", dir: join(rtDir, "..", "vendor", "mbedtls") },
|
||||
{ label: "zlib", dir: join(rtDir, "..", "vendor", "zlib") },
|
||||
{ label: "brotli", dir: join(rtDir, "..", "vendor", "brotli", "c") },
|
||||
{ label: "curl", dir: join(rtDir, "..", "vendor", "curl") },
|
||||
].map(async (group) => {
|
||||
(
|
||||
[
|
||||
{ label: "runtime", dir: rtDir },
|
||||
{ label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
|
||||
{ label: "quickjs-ng", dir: join(rtDir, "..", "vendor", "quickjs-ng") },
|
||||
{ label: "mbedtls", dir: join(rtDir, "..", "vendor", "mbedtls") },
|
||||
{ label: "zlib", dir: join(rtDir, "..", "vendor", "zlib") },
|
||||
{ label: "brotli", dir: join(rtDir, "..", "vendor", "brotli", "c") },
|
||||
// ICU is fetched, not vendored: its pinned version and sha256 key the
|
||||
// archive's cache path (vendor-archives.ts icuArchivePath), so it is
|
||||
// not part of the runtime tree's fingerprint.
|
||||
{ label: "curl", dir: join(rtDir, "..", "vendor", "curl") },
|
||||
] satisfies RuntimeFingerprintGroup[]
|
||||
).map(async (group) => {
|
||||
const names = (await nativeSourceFiles(group.dir, true))
|
||||
.map((path) => relative(group.dir, path))
|
||||
.sort();
|
||||
|
||||
@@ -9,6 +9,9 @@ export const {
|
||||
vendorBrotliDir,
|
||||
brotliArchivePath,
|
||||
ensureBrotliArchive,
|
||||
ensureIcuSources,
|
||||
icuArchivePath,
|
||||
ensureIcuArchive,
|
||||
vendorBuildCacheRoot,
|
||||
vendorCacheTargetFlavor,
|
||||
vendorCacheBuildIdentity,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 47 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v47" as const;
|
||||
export const RUNTIME_ABI_VERSION = 48 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v48" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -34,7 +34,7 @@ interface RuntimePackUnit {
|
||||
}
|
||||
|
||||
interface RuntimePackArchive extends RuntimePackArtifact {
|
||||
id: "quickjs" | "libregexp" | "zlib" | "brotli" | "mbedtls";
|
||||
id: "quickjs" | "libregexp" | "zlib" | "brotli" | "mbedtls" | "icu";
|
||||
predicate: RuntimePredicate;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,15 @@ export interface RuntimePackManifest {
|
||||
};
|
||||
flavors: Partial<Record<RuntimePackFlavor, RuntimePackFlavorManifest>>;
|
||||
archives: RuntimePackArchive[];
|
||||
/** Files the linked executable needs at RUN time rather than at link time,
|
||||
* installed beside it on every build. ICU's data file (icudt78l.dat, 11.1
|
||||
* MiB) is the only one; Intl programs copy it, every other program links
|
||||
* neither ICU nor its data. */
|
||||
data?: RuntimePackArtifact[];
|
||||
/** C headers under one include root (`<root>/unicode/*.h`): the ICU C API
|
||||
* headers scr_intl.c includes, which a sanitized build compiles that unit
|
||||
* against (native/sanitizer.ts). Only a pack that ships ICU lists them. */
|
||||
headers?: RuntimePackArtifact[];
|
||||
system_libraries: { name: string; predicate: RuntimePredicate }[];
|
||||
licenses: { path: string; license: string }[];
|
||||
}
|
||||
@@ -100,11 +109,19 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
builtinValues: boolean;
|
||||
computedRequire: boolean;
|
||||
warningReplace: boolean;
|
||||
recordViewPartial: boolean;
|
||||
stdioExtended: boolean;
|
||||
/** The program's write completion callbacks take the error argument:
|
||||
* the SCR_STREAM_WRITE_ERRORS build of the stream unit. */
|
||||
streamWriteErrors: boolean;
|
||||
/** The program reaches the process output streams' write slot: the
|
||||
* SCR_STDIO_WRITE_SLOT build of the events unit. */
|
||||
stdioWriteSlot: boolean;
|
||||
nodeIsland: boolean;
|
||||
brotli: boolean;
|
||||
moduleNamespace: boolean;
|
||||
moduleNamespaceDyn: boolean;
|
||||
primitiveWrapper: boolean;
|
||||
nativeFetch: boolean;
|
||||
abortSignalOnly: boolean;
|
||||
netEffective: boolean;
|
||||
@@ -245,6 +262,10 @@ export function parseRuntimePackManifest(value: unknown): RuntimePackManifest {
|
||||
validArtifact(raw) && typeof archive?.id === "string" && validPredicate(archive.predicate)
|
||||
);
|
||||
}) ||
|
||||
(manifest.data !== undefined &&
|
||||
(!Array.isArray(manifest.data) || !manifest.data.every(validArtifact))) ||
|
||||
(manifest.headers !== undefined &&
|
||||
(!Array.isArray(manifest.headers) || !manifest.headers.every(validArtifact))) ||
|
||||
!Array.isArray(manifest.system_libraries) ||
|
||||
!manifest.system_libraries.every((raw) => {
|
||||
const library = object(raw);
|
||||
@@ -311,11 +332,15 @@ export function effectiveRuntimeFeatures(
|
||||
builtinValues: features.builtinValues === true,
|
||||
computedRequire: features.computedRequire === true,
|
||||
warningReplace: features.warningReplace === true,
|
||||
recordViewPartial: features.recordViewPartial === true,
|
||||
stdioExtended: features.stdioExtended === true,
|
||||
streamWriteErrors: features.streamWriteErrors === true,
|
||||
stdioWriteSlot: features.stdioWriteSlot === true,
|
||||
nodeIsland: features.nodeIsland === true,
|
||||
brotli: features.brotli === true,
|
||||
moduleNamespace: features.moduleNamespace === true,
|
||||
moduleNamespaceDyn: features.moduleNamespaceDyn === true,
|
||||
primitiveWrapper: features.primitiveWrapper === true,
|
||||
nativeFetch,
|
||||
// The fetch unit has AbortController and AbortSignal; scr_abort.c is
|
||||
// the same implementation without the fetch transport.
|
||||
@@ -395,6 +420,8 @@ export interface RuntimePackArtifacts {
|
||||
/** Selected unit variants; each may carry its bitcode artifact. */
|
||||
runtime: (RuntimePackArtifact & { bitcode?: RuntimePackArtifact })[];
|
||||
archives: RuntimePackArtifact[];
|
||||
/** Files to install beside the executable (ICU's data file for Intl). */
|
||||
data: RuntimePackArtifact[];
|
||||
systemLibraries: string[];
|
||||
}
|
||||
|
||||
@@ -447,12 +474,26 @@ export function selectRuntimePackArtifacts(
|
||||
`runtime pack has no ${key} flavor; reinstall the matching runtime package`,
|
||||
"invalid",
|
||||
);
|
||||
// Intl needs the ICU objects only the host-flavored packs carry (ICU is
|
||||
// C++, so a pack builds it with Zig's C++ driver for its own glibc target);
|
||||
// say so instead of linking a program whose scr_intl.c references would go
|
||||
// undefined.
|
||||
if (
|
||||
features.intl === true &&
|
||||
!selectedFlavor.runtime_units.some((unit) => unit.source === "scr_intl.c")
|
||||
)
|
||||
throw new RuntimePackError(
|
||||
`the ${manifest.target.name} runtime pack has no ICU objects, which Intl needs; reinstall a runtime package that supports Intl`,
|
||||
"unsupported",
|
||||
);
|
||||
return {
|
||||
features,
|
||||
runtime: selectedRuntimeUnits(selectedFlavor, features).map(({ variant }) => variant),
|
||||
archives: manifest.archives.filter((archive) =>
|
||||
evaluateRuntimePredicate(archive.predicate, features),
|
||||
),
|
||||
// ICU's data file is 11.1 MiB: only a program that links ICU installs it.
|
||||
data: features.intl === true ? (manifest.data ?? []) : [],
|
||||
systemLibraries: manifest.system_libraries
|
||||
.filter((entry) => evaluateRuntimePredicate(entry.predicate, features))
|
||||
.map((entry) => entry.name),
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
@@ -148,10 +152,12 @@ async function packFixture() {
|
||||
store,
|
||||
);
|
||||
let stages = 0;
|
||||
const stage = (selection: NativeRuntimeSelection) => {
|
||||
const directory = join(root, `stage-${stages++}`);
|
||||
const stage = (selection: NativeRuntimeSelection, with_: RuntimePackStore = store) => {
|
||||
const directory = join(tmpdir(), `scriptc-runtime-pack-stage-${process.pid}-${stages++}`);
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
mkdirSync(directory);
|
||||
return stageNativeRuntimeSelection(selection, directory, store);
|
||||
dirs.push(directory);
|
||||
return stageNativeRuntimeSelection(selection, directory, with_);
|
||||
};
|
||||
return { root, object, manifestText, entries, select, stage };
|
||||
}
|
||||
@@ -241,3 +247,180 @@ test.skipIf(process.platform === "win32")(
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
},
|
||||
);
|
||||
|
||||
/** A store that keeps nothing: a build with an empty cache. */
|
||||
function emptyStore(): RuntimePackStore {
|
||||
const entries = new Map<string, string>();
|
||||
return {
|
||||
read: (key) => entries.get(key) ?? null,
|
||||
write: (key, text) => void entries.set(key, text),
|
||||
};
|
||||
}
|
||||
|
||||
const packRecord = (root: string) => join(root, "runtime-pack.verified.json");
|
||||
const hashes = () => vi.mocked(createHash).mock.calls.length;
|
||||
|
||||
test("a build with an empty cache trusts the record beside the pack", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
let start = hashes();
|
||||
f.stage(selection);
|
||||
const firstStage = hashes() - start;
|
||||
const record = JSON.parse(readFileSync(packRecord(f.root), "utf8"));
|
||||
expect(record.schema).toBe(1);
|
||||
expect(record.artifacts.map((artifact: { path: string }) => artifact.path)).toEqual([
|
||||
"artifacts/base.o",
|
||||
]);
|
||||
start = hashes();
|
||||
const staged = f.stage(selection, emptyStore());
|
||||
// Only the store key is hashed.
|
||||
expect(hashes() - start).toBe(firstStage - 1);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
});
|
||||
|
||||
test.each(["replaced", "rewritten in place", "resized"])(
|
||||
"the record beside the pack does not trust an artifact that was %s",
|
||||
async (change) => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
const before = statSync(f.object);
|
||||
const changed = change === "replaced" ? f.object + ".new" : f.object;
|
||||
writeFileSync(changed, "x".repeat(before.size + (change === "resized" ? 1 : 0)));
|
||||
utimesSync(changed, before.atime, before.mtime);
|
||||
if (changed !== f.object) renameSync(changed, f.object);
|
||||
expect(() => f.stage(selection, emptyStore())).toThrow(
|
||||
"runtime pack artifact hash mismatch: artifacts/base.o",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("an artifact whose mtime changed is hashed again and recorded again", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
const before = statSync(f.object);
|
||||
utimesSync(f.object, before.atime, new Date(before.mtimeMs + 5000));
|
||||
const start = hashes();
|
||||
f.stage(selection, emptyStore());
|
||||
// The store key and the artifact.
|
||||
expect(hashes() - start).toBe(2);
|
||||
const [entry] = JSON.parse(readFileSync(packRecord(f.root), "utf8")).artifacts;
|
||||
expect(entry.mtime).toBe(statSync(f.object).mtimeMs);
|
||||
const again = hashes();
|
||||
f.stage(selection, emptyStore());
|
||||
expect(hashes() - again).toBe(1);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["truncated", () => "{"],
|
||||
["null", () => "null"],
|
||||
["another schema", (text: string) => text.replace('"schema":1', '"schema":2')],
|
||||
[
|
||||
"an entry with a string ctime",
|
||||
(text: string) => text.replace(/"ctime":([\d.]+)/, '"ctime":"$1"'),
|
||||
],
|
||||
["an entry without an inode", (text: string) => text.replace(/"ino":[\d.]+,/, "")],
|
||||
[
|
||||
"another digest",
|
||||
(text: string) => text.replace(/"sha256":"[0-9a-f]+"/, `"sha256":"${"0".repeat(64)}"`),
|
||||
],
|
||||
])("a record beside the pack that is %s is not trusted", async (_name, rewrite) => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
const text = readFileSync(packRecord(f.root), "utf8");
|
||||
const rewritten = rewrite(text);
|
||||
expect(rewritten).not.toBe(text);
|
||||
writeFileSync(packRecord(f.root), rewritten);
|
||||
const start = hashes();
|
||||
const staged = f.stage(selection, emptyStore());
|
||||
expect(hashes() - start).toBe(2);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
// The record is written again, whole.
|
||||
const [entry] = JSON.parse(readFileSync(packRecord(f.root), "utf8")).artifacts;
|
||||
expect(entry.ino).toBe(statSync(f.object).ino);
|
||||
});
|
||||
|
||||
test("an unreadable record beside the pack is not trusted, and staging still succeeds", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
// A directory where the record would be: it can be neither read nor replaced.
|
||||
mkdirSync(packRecord(f.root));
|
||||
const start = hashes();
|
||||
f.stage(selection, emptyStore());
|
||||
f.stage(selection, emptyStore());
|
||||
expect(hashes() - start).toBe(4);
|
||||
expect(readdirSync(f.root).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
||||
});
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"a pack directory that cannot be written keeps the record in the cache only",
|
||||
async (context) => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
chmodSync(f.root, 0o555);
|
||||
try {
|
||||
// A process that may write any directory cannot run this case.
|
||||
try {
|
||||
writeFileSync(join(f.root, "probe"), "");
|
||||
rmSync(join(f.root, "probe"));
|
||||
context.skip();
|
||||
} catch {
|
||||
/* The directory is read-only for this process. */
|
||||
}
|
||||
const store = emptyStore();
|
||||
let start = hashes();
|
||||
f.stage(selection, store);
|
||||
expect(hashes() - start).toBe(2);
|
||||
expect(existsSync(packRecord(f.root))).toBe(false);
|
||||
expect(readdirSync(f.root).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
||||
// The cache's record still answers the next build with that cache.
|
||||
start = hashes();
|
||||
f.stage(selection, store);
|
||||
expect(hashes() - start).toBe(1);
|
||||
} finally {
|
||||
chmodSync(f.root, 0o755);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("writing the record beside the pack keeps the entries another build wrote", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
const other = {
|
||||
path: "artifacts/other.o",
|
||||
sha256: "1".repeat(64),
|
||||
dev: 1,
|
||||
ino: 2,
|
||||
size: 3,
|
||||
mtime: 4,
|
||||
ctime: 5,
|
||||
};
|
||||
writeFileSync(packRecord(f.root), JSON.stringify({ schema: 1, artifacts: [other] }));
|
||||
f.stage(selection, emptyStore());
|
||||
const record = JSON.parse(readFileSync(packRecord(f.root), "utf8"));
|
||||
expect(record.artifacts.map((artifact: { path: string }) => artifact.path).sort()).toEqual([
|
||||
"artifacts/base.o",
|
||||
"artifacts/other.o",
|
||||
]);
|
||||
expect(
|
||||
record.artifacts.find((artifact: { path: string }) => artifact.path === other.path),
|
||||
).toEqual(other);
|
||||
expect(readdirSync(f.root).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
||||
});
|
||||
|
||||
test("a cache that already trusts the artifacts writes the record beside the pack", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
rmSync(packRecord(f.root));
|
||||
const start = hashes();
|
||||
f.stage(selection);
|
||||
// The cache's record answers; only the store key is hashed.
|
||||
expect(hashes() - start).toBe(1);
|
||||
expect(existsSync(packRecord(f.root))).toBe(true);
|
||||
const empty = hashes();
|
||||
f.stage(selection, emptyStore());
|
||||
expect(hashes() - empty).toBe(1);
|
||||
});
|
||||
|
||||
@@ -2,22 +2,27 @@
|
||||
* selected artifact is verified and copied from the same read into private
|
||||
* storage. With a store, a pack's selections and the identities of the
|
||||
* artifacts it verified are kept between builds: an artifact whose file
|
||||
* identity is unchanged since it was hashed is not hashed again. */
|
||||
* identity is unchanged since it was hashed is not hashed again. The
|
||||
* identities are also kept in a record beside the pack when its directory
|
||||
* can be written, which builds with an empty store read too. */
|
||||
import type { NativeOptimization } from "./optimization.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
||||
import {
|
||||
closeSync,
|
||||
constants,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
fstatSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeSync,
|
||||
type Stats,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
import {
|
||||
@@ -94,7 +99,9 @@ function storeKey(text: string): string {
|
||||
/** A stored selection: what selecting from the pack's files, with these
|
||||
* identities, returned. */
|
||||
interface StoredSelection {
|
||||
schema: 1;
|
||||
/** 2 adds the selection's data artifacts: a stored selection from before
|
||||
* them would make an Intl build stage none of them, so it is discarded. */
|
||||
schema: 2;
|
||||
licenses: RuntimePackManifest["licenses"];
|
||||
selected: RuntimePackArtifacts;
|
||||
}
|
||||
@@ -172,7 +179,7 @@ export function selectNativeRuntimePack(
|
||||
sameIdentity(fileIdentity(packagePath), identities[0]!) &&
|
||||
sameIdentity(fileIdentity(manifestPath), identities[1]!)
|
||||
) {
|
||||
const entry: StoredSelection = { schema: 1, licenses: manifest.licenses, selected };
|
||||
const entry: StoredSelection = { schema: 2, licenses: manifest.licenses, selected };
|
||||
store.write(key, JSON.stringify(entry));
|
||||
}
|
||||
} catch {
|
||||
@@ -201,13 +208,14 @@ function readStoredSelection(text: string | null): StoredSelection | null {
|
||||
if (value === null || typeof value !== "object") return null;
|
||||
const entry = value as Partial<StoredSelection>;
|
||||
if (
|
||||
entry.schema !== 1 ||
|
||||
entry.schema !== 2 ||
|
||||
!Array.isArray(entry.licenses) ||
|
||||
entry.selected === undefined ||
|
||||
entry.selected === null ||
|
||||
typeof entry.selected !== "object" ||
|
||||
!Array.isArray(entry.selected.runtime) ||
|
||||
!Array.isArray(entry.selected.archives) ||
|
||||
!Array.isArray(entry.selected.data) ||
|
||||
!Array.isArray(entry.selected.systemLibraries)
|
||||
)
|
||||
return null;
|
||||
@@ -239,21 +247,53 @@ function readVerified(
|
||||
try {
|
||||
const text = store?.read(key) ?? null;
|
||||
if (text === null) return verified;
|
||||
const stored = JSON.parse(text) as StoredVerification;
|
||||
if (stored.schema === 1)
|
||||
for (const artifact of stored.artifacts) verified.set(artifact.path, artifact);
|
||||
for (const artifact of storedArtifacts(text)) verified.set(artifact.path, artifact);
|
||||
} catch {
|
||||
/* A malformed entry verifies everything again. */
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
|
||||
/** The well-formed entries of a stored verification; none when the text is
|
||||
* not a stored verification. */
|
||||
function storedArtifacts(text: string): VerifiedArtifact[] {
|
||||
const stored = JSON.parse(text) as Partial<StoredVerification> | null;
|
||||
if (stored === null || stored.schema !== 1 || !Array.isArray(stored.artifacts)) return [];
|
||||
const number = (value: unknown): boolean => typeof value === "number" && Number.isFinite(value);
|
||||
return stored.artifacts.filter(
|
||||
(artifact): artifact is VerifiedArtifact =>
|
||||
artifact !== null &&
|
||||
typeof artifact === "object" &&
|
||||
typeof artifact.path === "string" &&
|
||||
typeof artifact.sha256 === "string" &&
|
||||
number(artifact.dev) &&
|
||||
number(artifact.ino) &&
|
||||
number(artifact.size) &&
|
||||
number(artifact.mtime) &&
|
||||
number(artifact.ctime),
|
||||
);
|
||||
}
|
||||
|
||||
/** `known` records `artifact`'s digest and size for a file with `identity`. */
|
||||
function verifiedAs(
|
||||
known: VerifiedArtifact | undefined,
|
||||
artifact: { sha256: string; size: number },
|
||||
identity: FileIdentity,
|
||||
): boolean {
|
||||
return (
|
||||
known !== undefined &&
|
||||
known.sha256 === artifact.sha256 &&
|
||||
known.size === artifact.size &&
|
||||
sameIdentity(known, identity)
|
||||
);
|
||||
}
|
||||
|
||||
function stageArtifact(
|
||||
root: string,
|
||||
stage: string,
|
||||
artifact: RuntimePackArtifact,
|
||||
verified: Map<string, VerifiedArtifact>,
|
||||
newlyVerified: VerifiedArtifact[],
|
||||
records: readonly Map<string, VerifiedArtifact>[],
|
||||
verified: VerifiedArtifact[],
|
||||
): string {
|
||||
const source = join(root, artifact.path);
|
||||
const destination = join(stage, artifact.path);
|
||||
@@ -276,26 +316,17 @@ function stageArtifact(
|
||||
}
|
||||
// The bytes are those last hashed when the file kept the identity it had
|
||||
// then for the whole read.
|
||||
const known = verified.get(artifact.path);
|
||||
const unchanged = sameIdentity(before, after);
|
||||
const trusted =
|
||||
known !== undefined &&
|
||||
known.sha256 === artifact.sha256 &&
|
||||
known.size === artifact.size &&
|
||||
unchanged &&
|
||||
known.dev === before.dev &&
|
||||
known.ino === before.ino &&
|
||||
known.size === before.size &&
|
||||
known.mtime === before.mtime &&
|
||||
known.ctime === before.ctime;
|
||||
unchanged && records.some((record) => verifiedAs(record.get(artifact.path), artifact, before));
|
||||
if (
|
||||
bytes.length !== artifact.size ||
|
||||
(!trusted && createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
|
||||
) {
|
||||
throw new RuntimePackError(`runtime pack artifact hash mismatch: ${artifact.path}`, "invalid");
|
||||
}
|
||||
if (!trusted && unchanged)
|
||||
newlyVerified.push({
|
||||
if (unchanged)
|
||||
verified.push({
|
||||
path: artifact.path,
|
||||
sha256: artifact.sha256,
|
||||
dev: before.dev,
|
||||
@@ -353,10 +384,12 @@ export function stageNativeRuntimeSelection(
|
||||
const packagePath = join(root, "package.json");
|
||||
const manifestPath = join(root, "runtime-pack.json");
|
||||
const key = storeKey(JSON.stringify({ schema: 1, verified: root }));
|
||||
const verified = readVerified(store, key);
|
||||
const newlyVerified: VerifiedArtifact[] = [];
|
||||
const cacheRecord = readVerified(store, key);
|
||||
const packRecordPath = join(root, PACK_VERIFIED_FILE);
|
||||
const packRecord = readPackVerified(packRecordPath);
|
||||
const verified: VerifiedArtifact[] = [];
|
||||
const stage = (artifact: RuntimePackArtifact): string =>
|
||||
stageArtifact(root, stageRoot, artifact, verified, newlyVerified);
|
||||
stageArtifact(root, stageRoot, artifact, [cacheRecord, packRecord], verified);
|
||||
const runtimeObjects = selected.runtime.map(stage);
|
||||
const archives = selected.archives.map(stage);
|
||||
for (const license of licenses) {
|
||||
@@ -372,14 +405,112 @@ export function stageNativeRuntimeSelection(
|
||||
) {
|
||||
throw new RuntimePackError("runtime pack changed while staging verified artifacts", "invalid");
|
||||
}
|
||||
if (store !== undefined && newlyVerified.length > 0) {
|
||||
for (const artifact of newlyVerified) verified.set(artifact.path, artifact);
|
||||
const entry: StoredVerification = { schema: 1, artifacts: [...verified.values()] };
|
||||
// Each record learns the artifacts verified here that it does not list
|
||||
// with their current identity.
|
||||
const missing = (record: Map<string, VerifiedArtifact>): VerifiedArtifact[] =>
|
||||
verified.filter((artifact) => !verifiedAs(record.get(artifact.path), artifact, artifact));
|
||||
const cacheMissing = missing(cacheRecord);
|
||||
if (store !== undefined && cacheMissing.length > 0) {
|
||||
for (const artifact of cacheMissing) cacheRecord.set(artifact.path, artifact);
|
||||
const entry: StoredVerification = { schema: 1, artifacts: [...cacheRecord.values()] };
|
||||
try {
|
||||
store.write(key, JSON.stringify(entry));
|
||||
} catch {
|
||||
/* A failed write only means hashing again next time. */
|
||||
}
|
||||
}
|
||||
const packMissing = missing(packRecord);
|
||||
if (packMissing.length > 0) writePackVerified(packRecordPath, packMissing);
|
||||
return { runtimeObjects, archives, systemLibraries: selected.systemLibraries };
|
||||
}
|
||||
|
||||
/** Install the selected RUN-time data artifacts (ICU's icudt78l.dat) beside an
|
||||
* executable this host linked. The in-process linker does the same from its
|
||||
* own selection (backend/linker.ts); this host writes executables
|
||||
* synchronously and has no link plan, so it installs them here.
|
||||
*
|
||||
* A file already present at the pinned size is left alone (11.1 MiB of pure
|
||||
* cost otherwise), and the copy is atomic — a private temporary, then a
|
||||
* rename — so two concurrent builds of one program never read a partial
|
||||
* file. */
|
||||
export function installNativeRuntimeData(
|
||||
selection: NativeRuntimeSelection,
|
||||
outputPath: string,
|
||||
): void {
|
||||
const data = selection.selected.data ?? [];
|
||||
if (data.length === 0) return;
|
||||
const outputDir = dirname(resolve(outputPath));
|
||||
for (const artifact of data) {
|
||||
const destination = join(outputDir, basename(artifact.path));
|
||||
const existing = statSync(destination, { throwIfNoEntry: false });
|
||||
if (existing?.isFile() === true && existing.size === artifact.size) continue;
|
||||
const temporary = join(
|
||||
outputDir,
|
||||
`.${basename(destination)}.scriptc-${process.pid}-${randomUUID()}`,
|
||||
);
|
||||
copyFileSync(join(selection.root, artifact.path), temporary);
|
||||
try {
|
||||
renameSync(temporary, destination);
|
||||
} catch {
|
||||
rmSync(destination, { force: true });
|
||||
renameSync(temporary, destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The verification record kept beside the runtime pack, so a build with an
|
||||
* empty cache does not hash the pack's artifacts again. Like the cache's
|
||||
* record, it trusts an artifact only while its file keeps the identity
|
||||
* (device, inode, size, mtime, ctime) the record lists. */
|
||||
export const PACK_VERIFIED_FILE = "runtime-pack.verified.json";
|
||||
|
||||
/** The pack record's well-formed entries; none when it is missing,
|
||||
* unreadable or malformed. */
|
||||
function readPackVerified(path: string): Map<string, VerifiedArtifact> {
|
||||
const verified = new Map<string, VerifiedArtifact>();
|
||||
try {
|
||||
for (const artifact of storedArtifacts(readFileSync(path, "utf8")))
|
||||
verified.set(artifact.path, artifact);
|
||||
} catch {
|
||||
/* The artifacts are hashed again. */
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
|
||||
/** Adds `artifacts` to the pack record: the record as it is now, with these
|
||||
* entries replacing the ones for the same paths, written to a new file and
|
||||
* renamed over the record, so a reader sees a whole record. Of two builds
|
||||
* writing at once, the last rename wins; the entries the other added are
|
||||
* hashed again by a later build. When the pack directory cannot be written,
|
||||
* the cache's record is the only one. */
|
||||
function writePackVerified(path: string, artifacts: readonly VerifiedArtifact[]): void {
|
||||
const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
||||
try {
|
||||
const record = readPackVerified(path);
|
||||
for (const artifact of artifacts) record.set(artifact.path, artifact);
|
||||
const entry: StoredVerification = { schema: 1, artifacts: [...record.values()] };
|
||||
const bytes = Buffer.from(JSON.stringify(entry));
|
||||
const fd = openSync(
|
||||
temporary,
|
||||
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
|
||||
0o644,
|
||||
);
|
||||
try {
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
const written = writeSync(fd, bytes, offset, bytes.length - offset);
|
||||
if (written === 0) throw new Error("the pack record was not written");
|
||||
offset += written;
|
||||
}
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
renameSync(temporary, path);
|
||||
} catch {
|
||||
try {
|
||||
rmSync(temporary, { force: true });
|
||||
} catch {
|
||||
/* Cleanup is best effort. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" },
|
||||
runtime_abi: { version: 48, marker: "scr_runtime_abi_v48" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
@@ -120,6 +120,7 @@ const features = {
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
|
||||
@@ -62,6 +62,7 @@ const BASE: NativeLinkFeatures = {
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
@@ -135,7 +136,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" },
|
||||
runtime_abi: { version: 48, marker: "scr_runtime_abi_v48" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -40,12 +40,19 @@ export interface RuntimePackSelection {
|
||||
features: RuntimeFeatureSet;
|
||||
runtimeObjects: string[];
|
||||
archives: string[];
|
||||
/** Files the executable loads at run time, installed beside it. */
|
||||
dataFiles: string[];
|
||||
/** The ICU headers an Intl selection ships (manifest `headers`), all under
|
||||
* `<include root>/unicode/`, verified like the other artifacts. */
|
||||
headerFiles: string[];
|
||||
systemLibraries: string[];
|
||||
dependencyPaths: string[];
|
||||
/** Exact installed inputs observed while the selected artifacts were verified. */
|
||||
sourceDependencies: NativeArtifactDependency[];
|
||||
selectedRuntimeArtifacts: RuntimePackArtifact[];
|
||||
selectedArchiveArtifacts: RuntimePackArtifact[];
|
||||
selectedDataArtifacts: RuntimePackArtifact[];
|
||||
selectedHeaderArtifacts: RuntimePackArtifact[];
|
||||
}
|
||||
|
||||
async function verifyArtifact(root: string, artifact: RuntimePackArtifact): Promise<string> {
|
||||
@@ -209,18 +216,27 @@ export async function loadRuntimePack(options: {
|
||||
const features = selected.features;
|
||||
const selectedVariants = selected.runtime;
|
||||
const selectedArchives = selected.archives;
|
||||
const selectedArtifactPaths = [...selectedVariants, ...selectedArchives].map((artifact) =>
|
||||
join(root, artifact.path),
|
||||
);
|
||||
const selectedData = selected.data;
|
||||
// The C-source path compiles scr_intl.c against these headers and links
|
||||
// the pack's ICU archive (index.ts runtimePackIcuInputs).
|
||||
const selectedHeaders = features.intl === true ? (manifest.headers ?? []) : [];
|
||||
const selectedArtifactPaths = [
|
||||
...selectedVariants,
|
||||
...selectedArchives,
|
||||
...selectedData,
|
||||
...selectedHeaders,
|
||||
].map((artifact) => join(root, artifact.path));
|
||||
let artifactDependencies: NativeArtifactDependency[];
|
||||
try {
|
||||
artifactDependencies = await snapshotDependencies(selectedArtifactPaths);
|
||||
} catch {
|
||||
throw new RuntimePackError("runtime pack artifact set changed during selection", "invalid");
|
||||
}
|
||||
const [runtimeObjects, archives] = await Promise.all([
|
||||
const [runtimeObjects, archives, dataFiles, headerFiles] = await Promise.all([
|
||||
Promise.all(selectedVariants.map((artifact) => verifyArtifact(root, artifact))),
|
||||
Promise.all(selectedArchives.map((artifact) => verifyArtifact(root, artifact))),
|
||||
Promise.all(selectedData.map((artifact) => verifyArtifact(root, artifact))),
|
||||
Promise.all(selectedHeaders.map((artifact) => verifyArtifact(root, artifact))),
|
||||
]);
|
||||
await Promise.all(manifest.licenses.map((license) => readFile(join(root, license.path)))).catch(
|
||||
() => {
|
||||
@@ -242,10 +258,21 @@ export async function loadRuntimePack(options: {
|
||||
features,
|
||||
runtimeObjects,
|
||||
archives,
|
||||
dataFiles,
|
||||
headerFiles,
|
||||
systemLibraries: selected.systemLibraries,
|
||||
dependencyPaths: [packagePath, manifestPath, ...runtimeObjects, ...archives],
|
||||
dependencyPaths: [
|
||||
packagePath,
|
||||
manifestPath,
|
||||
...runtimeObjects,
|
||||
...archives,
|
||||
...dataFiles,
|
||||
...headerFiles,
|
||||
],
|
||||
sourceDependencies,
|
||||
selectedRuntimeArtifacts: selectedVariants,
|
||||
selectedArchiveArtifacts: selectedArchives,
|
||||
selectedDataArtifacts: selectedData,
|
||||
selectedHeaderArtifacts: selectedHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { availableParallelism, tmpdir } from "node:os";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -11,9 +11,32 @@ import {
|
||||
validCachedFile,
|
||||
} from "./build-cache.js";
|
||||
import type { CcDriver } from "./native/driver.js";
|
||||
import { NativeCodegenError } from "./native-codegen-core.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/** One C++ translation unit of the ICU archive: the source, the archive member
|
||||
* name it compiles to, and the flags of its group. */
|
||||
interface IcuUnit {
|
||||
path: string;
|
||||
member: string;
|
||||
flags: string[];
|
||||
define?: string;
|
||||
}
|
||||
|
||||
/** Zig's library directory, which carries the libc++/libc++abi sources the
|
||||
* ICU archive merges: `ZIG_LIB_DIR` when the caller pins one, else the pinned
|
||||
* toolchain's own answer. */
|
||||
async function zigLibDir(): Promise<string> {
|
||||
const pinned = process.env["ZIG_LIB_DIR"];
|
||||
if (pinned !== undefined && pinned !== "") return pinned;
|
||||
const { stdout } = await execFileAsync("zig", ["env"]);
|
||||
const match = /\.lib_dir\s*=\s*"([^"]+)"/.exec(stdout);
|
||||
if (match === null)
|
||||
throw new Error("Intl needs Zig 0.16.0's C++ toolchain: `zig env` named no lib_dir");
|
||||
return match[1]!;
|
||||
}
|
||||
|
||||
/** The section flags the libregexp/libunicode objects build with (the
|
||||
* runtime pack's executable_section_elimination flags). */
|
||||
const LRE_SECTION_FLAGS = ["-ffunction-sections", "-fdata-sections"];
|
||||
@@ -24,13 +47,33 @@ function vendorObjectName(source: string): string {
|
||||
return source.replaceAll("/", "-").replace(/\.c$/, ".o");
|
||||
}
|
||||
|
||||
/** The sha256 of a buffer, as lowercase hex. */
|
||||
function sha256(bytes: Uint8Array): string {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
/** Whether `path` is a non-empty file. */
|
||||
async function presentFile(path: string): Promise<boolean> {
|
||||
try {
|
||||
return (await stat(path)).size > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
import {
|
||||
QJS_COMMIT,
|
||||
MBEDTLS_VERSION,
|
||||
ZLIB_VERSION,
|
||||
BROTLI_VERSION,
|
||||
ICU_VERSION,
|
||||
ICU_SOURCE_URL,
|
||||
ICU_SOURCE_SHA256,
|
||||
ICU_SOURCE_SIZE,
|
||||
ICU_CXX_TARGET,
|
||||
LRE_SOURCES,
|
||||
ZLIB_SOURCES,
|
||||
icuSourceList,
|
||||
zlibInputs,
|
||||
} from "./vendor-inputs.js";
|
||||
export {
|
||||
@@ -38,6 +81,7 @@ export {
|
||||
MBEDTLS_VERSION,
|
||||
ZLIB_VERSION,
|
||||
BROTLI_VERSION,
|
||||
ICU_VERSION,
|
||||
LRE_SOURCES,
|
||||
ZLIB_SOURCES,
|
||||
} from "./vendor-inputs.js";
|
||||
@@ -532,9 +576,271 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
return archive;
|
||||
}
|
||||
|
||||
/** The fetched ICU release root (`icu/`, holding `source/` and LICENSE),
|
||||
* downloaded once per cache root and reused after that. ICU is not
|
||||
* vendored: the release's own source tarball is pinned by version and
|
||||
* sha256 in vendor-inputs.ts, verified on every use, and extracted by the
|
||||
* system `tar` as the libnode build does. An existing extracted tree is
|
||||
* enough, so an offline build works from a pre-populated cache. */
|
||||
const icuSourcesByCacheRoot = new Map<string, Promise<string>>();
|
||||
function ensureIcuSources(cacheRoot: string = vendorBuildCacheRoot()): Promise<string> {
|
||||
const pending = icuSourcesByCacheRoot.get(cacheRoot);
|
||||
if (pending !== undefined) return pending;
|
||||
const pendingFetch = (async (): Promise<string> => {
|
||||
const tree = join(cacheRoot, `icu4c-${ICU_VERSION}-source`);
|
||||
if (await presentFile(join(tree, "source", "common", "sources.txt"))) return tree;
|
||||
const tarball = join(cacheRoot, `icu4c-${ICU_VERSION}-sources.tgz`);
|
||||
let archive: Buffer;
|
||||
try {
|
||||
archive = await readFile(tarball);
|
||||
if (archive.length !== ICU_SOURCE_SIZE || sha256(archive) !== ICU_SOURCE_SHA256)
|
||||
throw new Error("cached ICU source tarball does not match its pin");
|
||||
} catch {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(ICU_SOURCE_URL);
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`could not download ICU's sources (${ICU_SOURCE_URL}); ` +
|
||||
`populate ${tarball} or allow network access`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`could not download ICU's sources (${ICU_SOURCE_URL}): ` +
|
||||
`${response.status} ${response.statusText}`,
|
||||
);
|
||||
archive = Buffer.from(await response.arrayBuffer());
|
||||
if (archive.length !== ICU_SOURCE_SIZE || sha256(archive) !== ICU_SOURCE_SHA256)
|
||||
throw new Error(
|
||||
`ICU source tarball from ${ICU_SOURCE_URL} is ${archive.length} bytes with sha256 ` +
|
||||
`${sha256(archive)}, expected ${ICU_SOURCE_SIZE} bytes with sha256 ` +
|
||||
`${ICU_SOURCE_SHA256}`,
|
||||
);
|
||||
await mkdir(cacheRoot, { recursive: true });
|
||||
await writeFile(tarball, archive);
|
||||
}
|
||||
await mkdir(cacheRoot, { recursive: true });
|
||||
const stage = await mkdtemp(join(cacheRoot, ".icu-sources-"));
|
||||
try {
|
||||
await execFileAsync("tar", ["-xzf", tarball, "-C", stage]);
|
||||
if (!(await presentFile(join(stage, "icu", "source", "common", "sources.txt"))))
|
||||
throw new Error(`ICU source tarball ${ICU_SOURCE_URL} does not hold icu/source`);
|
||||
await rm(tree, { recursive: true, force: true });
|
||||
await rename(join(stage, "icu"), tree);
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
return tree;
|
||||
})();
|
||||
icuSourcesByCacheRoot.set(cacheRoot, pendingFetch);
|
||||
pendingFetch.catch(() => icuSourcesByCacheRoot.delete(cacheRoot));
|
||||
return pendingFetch;
|
||||
}
|
||||
|
||||
/** The archive is the same for sanitized and plain builds: it is never
|
||||
* instrumented (see ensureIcuArchive), so neither the path nor the build
|
||||
* carries a sanitizer flavor. */
|
||||
function icuArchivePath(
|
||||
driver: CcDriver,
|
||||
buildIdentity: string,
|
||||
cacheRoot: string = vendorBuildCacheRoot(),
|
||||
): string {
|
||||
const flavor = `plain-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
|
||||
return join(cacheRoot, `icu-${ICU_VERSION}-${flavor}`, "libicu.a");
|
||||
}
|
||||
|
||||
/** The ICU archive for one flavor, compiled lazily on the first
|
||||
* Intl-using build (~470 C++ TUs, parallelized) and cached under
|
||||
* <build-cache>/vendor/icu-<version>-<flavor>-<target>-<toolchain>/
|
||||
* libicu.a with the mbedTLS archive's atomic-rename publish.
|
||||
*
|
||||
* Three things make this archive different from every other vendored one:
|
||||
*
|
||||
* - It is C++, so the pack's C driver cannot compile it: the TUs (and the
|
||||
* standard library they use) build with Zig's C++ driver for the glibc
|
||||
* target the runtime packs pin. That target is the point. In C++ mode,
|
||||
* glibc 2.38+ headers redirect strtol/strtoul to the C23 __isoc23_*
|
||||
* names, and a host libstdc++ binds those names too, so a host-compiled
|
||||
* C++ archive would raise the glibc floor above the packs'. Zig's
|
||||
* bundled libc++ and libc++abi compile from the toolchain's own sources
|
||||
* for the target and bind the target's names only.
|
||||
* - ICU requires RTTI (`typeid` in unifiedcache.h and ustrenum.cpp,
|
||||
* `dynamic_cast` in uniset_props.cpp), so the design note's
|
||||
* `-fno-rtti` is not available; `-fno-exceptions` is, and ICU, libc++,
|
||||
* and libc++abi all build so. libc++abi's Itanium personality routine
|
||||
* (cxa_personality.cpp) is the one unit that needs exceptions, so it is
|
||||
* left out: nothing can throw into it.
|
||||
* - Both the standard library and the C++ runtime it needs are merged INTO
|
||||
* the archive, so the final link stays C-only: a plain `clang`/`zig` C
|
||||
* link with the runtime pack's `-lm` resolves every undefined symbol.
|
||||
* Members are prefixed by source (`cxx-libcxx-`, `cxx-libcxxabi-`) so
|
||||
* their names stay unique.
|
||||
*
|
||||
* ICU builds with symbol renaming on (version suffixes `_78`), which is
|
||||
* what lets the zero-cost gate assert that only Intl programs link ICU
|
||||
* symbols.
|
||||
*
|
||||
* Sanitized builds link this same uninstrumented archive. AddressSanitizer
|
||||
* cannot instrument it: Zig's bundled libc++ compiles with
|
||||
* `-fsanitize=address` only against a `sanitizer/asan_interface.h` its
|
||||
* tree does not ship, and compiling ~600 C++ TUs again for the sanitizer
|
||||
* lane would add minutes to the first Intl program of every sanitized
|
||||
* build. The runtime's own ICU code (scr_intl.c) still compiles with the
|
||||
* sanitizer, and AddressSanitizer's interceptors cover ICU's allocations,
|
||||
* so a sanitized Intl program reports heap errors from ICU data as it
|
||||
* does everywhere else — the ICU code itself is simply not instrumented. */
|
||||
async function ensureIcuArchive(
|
||||
driver: CcDriver,
|
||||
buildIdentity: string,
|
||||
cacheRoot: string = vendorBuildCacheRoot(),
|
||||
): Promise<string> {
|
||||
// The archive is host-targeted ELF for the packs' glibc floor; a cross
|
||||
// target would need a Zig C++ link recipe this slice has not validated.
|
||||
// Both refusals are compile diagnostics (SC3002), not internal errors:
|
||||
// an Intl program on any other host or target is a documented "not
|
||||
// here yet", the same answer the missing runtime pack gives.
|
||||
if (driver.target !== null)
|
||||
throw new NativeCodegenError(
|
||||
"SC3002",
|
||||
`Intl needs ICU, which scriptc builds for the host's linux-x64 gnu target only: ` +
|
||||
`the ${driver.target} target is not supported yet`,
|
||||
);
|
||||
if (process.platform !== "linux" || process.arch !== "x64")
|
||||
throw new NativeCodegenError(
|
||||
"SC3002",
|
||||
"Intl needs ICU, which this compiler builds for linux-x64 only: " +
|
||||
`the ${process.platform}-${process.arch} host is not supported yet`,
|
||||
);
|
||||
const flavor = `plain-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
|
||||
const archive = icuArchivePath(driver, buildIdentity, cacheRoot);
|
||||
if (await validVendorArtifact(archive)) return archive;
|
||||
|
||||
await mkdir(cacheRoot, { recursive: true });
|
||||
const buildDir = await mkdtemp(join(tmpdir(), `scriptc-vendor-icu-${flavor}-`));
|
||||
// The tarball extracts to `icu/source/...` with ICU's LICENSE beside it.
|
||||
const vendor = join(await ensureIcuSources(cacheRoot), "source");
|
||||
const libDir = await zigLibDir();
|
||||
const cxx = ["zig", "c++", "-target", ICU_CXX_TARGET];
|
||||
const optimize = ["-Os"];
|
||||
const commonFlags = [
|
||||
"-std=c++17",
|
||||
...optimize,
|
||||
"-fno-exceptions",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
];
|
||||
const icuFlags = [
|
||||
...commonFlags,
|
||||
"-DU_STATIC_IMPLEMENTATION",
|
||||
"-DU_ATTRIBUTE_DEPRECATED=",
|
||||
"-DNDEBUG",
|
||||
"-Wno-deprecated-declarations",
|
||||
"-I",
|
||||
join(vendor, "common"),
|
||||
"-I",
|
||||
join(vendor, "i18n"),
|
||||
];
|
||||
// libc++ 20 sources need C++20 (`constinit`, ranges) and libc++'s
|
||||
// charconv reads a header from `libcxx/libc`.
|
||||
const cxxLibraryFlags = [
|
||||
"-std=c++23",
|
||||
...optimize,
|
||||
"-fno-exceptions",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-I",
|
||||
join(libDir, "libcxx", "include"),
|
||||
"-I",
|
||||
join(libDir, "libcxx", "src"),
|
||||
"-I",
|
||||
join(libDir, "libcxx", "libc"),
|
||||
"-I",
|
||||
join(libDir, "libcxxabi", "include"),
|
||||
"-D_LIBCPP_BUILDING_LIBRARY",
|
||||
"-D_LIBCXXABI_BUILDING_LIBRARY",
|
||||
"-DLIBCXX_BUILDING_LIBCXXABI",
|
||||
"-Wno-unknown-warning-option",
|
||||
];
|
||||
const cxxLibraryUnits = async (
|
||||
subdirectory: string,
|
||||
memberPrefix: string,
|
||||
skip: ReadonlySet<string>,
|
||||
): Promise<IcuUnit[]> => {
|
||||
const sources = (await readdir(join(libDir, subdirectory, "src")))
|
||||
.filter((name) => name.endsWith(".cpp") && !skip.has(name))
|
||||
.sort();
|
||||
return sources.map((source) => ({
|
||||
path: join(libDir, subdirectory, "src", source),
|
||||
member: `${memberPrefix}${source}`,
|
||||
flags: cxxLibraryFlags,
|
||||
}));
|
||||
};
|
||||
try {
|
||||
const units: IcuUnit[] = [
|
||||
...icuSourceList(await readFile(join(vendor, "common", "sources.txt"), "utf8")).map(
|
||||
(source) => ({
|
||||
path: join(vendor, "common", source),
|
||||
member: `common-${source}`,
|
||||
flags: icuFlags,
|
||||
define: "U_COMMON_IMPLEMENTATION",
|
||||
}),
|
||||
),
|
||||
{
|
||||
path: join(vendor, "stubdata", "stubdata.cpp"),
|
||||
member: "stubdata-stubdata.cpp",
|
||||
flags: icuFlags,
|
||||
define: "U_COMMON_IMPLEMENTATION",
|
||||
},
|
||||
...icuSourceList(await readFile(join(vendor, "i18n", "sources.txt"), "utf8")).map(
|
||||
(source) => ({
|
||||
path: join(vendor, "i18n", source),
|
||||
member: `i18n-${source}`,
|
||||
flags: icuFlags,
|
||||
define: "U_I18N_IMPLEMENTATION",
|
||||
}),
|
||||
),
|
||||
...(await cxxLibraryUnits("libcxx", "cxx-libcxx-", new Set())),
|
||||
...(await cxxLibraryUnits("libcxxabi", "cxx-libcxxabi-", new Set(["cxa_personality.cpp"]))),
|
||||
];
|
||||
const width = availableParallelism();
|
||||
for (let i = 0; i < units.length; i += width) {
|
||||
await Promise.all(
|
||||
units.slice(i, i + width).map(async (unit) => {
|
||||
const output = join(buildDir, unit.member.replace(/\.cpp$/, ".o"));
|
||||
await execFileAsync(cxx[0]!, [
|
||||
...cxx.slice(1),
|
||||
...unit.flags,
|
||||
...(unit.define === undefined ? [] : [`-D${unit.define}`]),
|
||||
"-g0",
|
||||
"-c",
|
||||
unit.path,
|
||||
"-o",
|
||||
output,
|
||||
]);
|
||||
return output;
|
||||
}),
|
||||
);
|
||||
}
|
||||
await execFileAsync("ar", [
|
||||
"rcs",
|
||||
join(buildDir, "libicu.a"),
|
||||
...units.map((unit) => join(buildDir, unit.member.replace(/\.cpp$/, ".o"))),
|
||||
]);
|
||||
await publishVendorArtifact(join(buildDir, "libicu.a"), archive);
|
||||
} finally {
|
||||
await rm(buildDir, { recursive: true, force: true });
|
||||
}
|
||||
return archive;
|
||||
}
|
||||
|
||||
return {
|
||||
vendorEngineDir,
|
||||
vendorTlsDir,
|
||||
ensureIcuSources,
|
||||
icuArchivePath,
|
||||
ensureIcuArchive,
|
||||
vendorBrotliDir,
|
||||
brotliArchivePath,
|
||||
ensureBrotliArchive,
|
||||
|
||||
@@ -2,7 +2,21 @@ import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { expect, test } from "vitest";
|
||||
import { ZLIB_SOURCES, zlibInputs } from "./vendor-inputs.js";
|
||||
import {
|
||||
ZLIB_SOURCES,
|
||||
ICU_VERSION,
|
||||
ICU_SOURCE_URL,
|
||||
ICU_SOURCE_SHA256,
|
||||
ICU_SOURCE_SIZE,
|
||||
ICU_DATA_URL,
|
||||
ICU_DATA_FILE,
|
||||
ICU_DATA_SHA256,
|
||||
ICU_DATA_SIZE,
|
||||
ICU_DATA_ZIP_SHA256,
|
||||
ICU_DATA_ZIP_SIZE,
|
||||
icuSourceList,
|
||||
zlibInputs,
|
||||
} from "./vendor-inputs.js";
|
||||
|
||||
const repoRoot = fileURLToPath(new URL("../../../..", import.meta.url));
|
||||
const zlibDir = join(repoRoot, "packages/runtime/vendor/zlib");
|
||||
@@ -78,3 +92,39 @@ test("zlib inputs follow Node's deps/zlib/zlib.gyp", () => {
|
||||
expect(wasm.sources).not.toContain("contrib/optimizations/inflate.c");
|
||||
expect(wasm.defines).toEqual(["HAVE_HIDDEN", "CPU_NO_SIMD"]);
|
||||
});
|
||||
|
||||
test("the pinned ICU is the release the oracle Node bundles", () => {
|
||||
// Node's own Intl answers from the ICU it was built with, so the fetched
|
||||
// release must be that version exactly: the differential corpus compares
|
||||
// scriptc's ICU output against Node's, byte for byte.
|
||||
expect(process.versions.icu).toBe(ICU_VERSION);
|
||||
});
|
||||
|
||||
test("the compiler's ICU pins match the runtime packs'", async () => {
|
||||
const pack = (await import(
|
||||
pathToFileURL(join(repoRoot, "packages/runtime-pack-common/scripts/icu-inputs.mjs")).href
|
||||
)) as Record<string, string | number>;
|
||||
expect(pack["ICU_VERSION"]).toBe(ICU_VERSION);
|
||||
expect(pack["ICU_SOURCE_URL"]).toBe(ICU_SOURCE_URL);
|
||||
expect(pack["ICU_SOURCE_SHA256"]).toBe(ICU_SOURCE_SHA256);
|
||||
expect(pack["ICU_SOURCE_SIZE"]).toBe(ICU_SOURCE_SIZE);
|
||||
expect(pack["ICU_DATA_URL"]).toBe(ICU_DATA_URL);
|
||||
expect(pack["ICU_DATA_FILE"]).toBe(ICU_DATA_FILE);
|
||||
expect(pack["ICU_DATA_SHA256"]).toBe(ICU_DATA_SHA256);
|
||||
expect(pack["ICU_DATA_SIZE"]).toBe(ICU_DATA_SIZE);
|
||||
expect(pack["ICU_DATA_ZIP_SHA256"]).toBe(ICU_DATA_ZIP_SHA256);
|
||||
expect(pack["ICU_DATA_ZIP_SIZE"]).toBe(ICU_DATA_ZIP_SIZE);
|
||||
// Both pins name the official release assets for the pinned version.
|
||||
expect(ICU_SOURCE_URL).toBe(
|
||||
`https://github.com/unicode-org/icu/releases/download/release-${ICU_VERSION}/icu4c-${ICU_VERSION}-sources.tgz`,
|
||||
);
|
||||
expect(ICU_DATA_URL).toBe(
|
||||
`https://github.com/unicode-org/icu/releases/download/release-${ICU_VERSION}/icu4c-${ICU_VERSION}-data-bin-l.zip`,
|
||||
);
|
||||
expect(ICU_SOURCE_SHA256).toMatch(/^[0-9a-f]{64}$/);
|
||||
expect(ICU_DATA_SHA256).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test("ICU source lists parse to the units a build compiles", () => {
|
||||
expect(icuSourceList("# a comment\n\n b.cpp \nc.cpp\n")).toEqual(["b.cpp", "c.cpp"]);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,49 @@ export const MBEDTLS_VERSION = "3.6.7";
|
||||
export const ZLIB_VERSION = "1.3.2.1-motley-285e94b";
|
||||
/** The brotli release Node v24.22.0 bundles (packages/runtime/vendor/brotli). */
|
||||
export const BROTLI_VERSION = "1.2.0";
|
||||
/** ICU4C release 78.3 — the exact version Node v24.22.0 bundles
|
||||
* (`process.versions.icu` there), from the `release-78.3` tag. ICU is not
|
||||
* vendored: the build fetches the release's own source tarball and data zip
|
||||
* from unicode-org/icu's GitHub release, pinning each by version and sha256,
|
||||
* and caches the download. The runtime packs ship its data file icudt78l.dat
|
||||
* (33,107,232 B) without the categories no Intl form reads (11,653,984 B),
|
||||
* beside the executable; see packages/runtime-pack-common/scripts/
|
||||
* icu-data.mjs. */
|
||||
export const ICU_VERSION = "78.3";
|
||||
const ICU_RELEASE_URL = `https://github.com/unicode-org/icu/releases/download/release-${ICU_VERSION}`;
|
||||
/** The release's source tarball (icu, not icu4j), the only build input that
|
||||
* is not vendored. */
|
||||
export const ICU_SOURCE_URL = `${ICU_RELEASE_URL}/icu4c-${ICU_VERSION}-sources.tgz`;
|
||||
export const ICU_SOURCE_SHA256 = "3a2e7a47604ba702f345878308e6fefeca612ee895cf4a5f222e7955fabfe0c0";
|
||||
export const ICU_SOURCE_SIZE = 27_977_255;
|
||||
/** The release's prebuilt little-endian data zip. 969 source files are
|
||||
* reviewable; 33 MiB of compiled CLDR tables are not, so the data is fetched
|
||||
* too, pinned the same way. */
|
||||
export const ICU_DATA_URL = `${ICU_RELEASE_URL}/icu4c-${ICU_VERSION}-data-bin-l.zip`;
|
||||
export const ICU_DATA_ZIP_SHA256 =
|
||||
"982619632b78887f1895b063e96e8c3cc7f99283337c8abbd05aa71635de613c";
|
||||
export const ICU_DATA_ZIP_SIZE = 12_836_659;
|
||||
export const ICU_DATA_FILE = "icudt78l.dat";
|
||||
export const ICU_DATA_SHA256 = "d5cf2a40dccbe471781ec7af85693bff542ff12f0b670c9630c4e72d60714b8b";
|
||||
export const ICU_DATA_SIZE = 33_107_232;
|
||||
/** The glibc target the ICU archive compiles for: the floor the linux-x64-gnu
|
||||
* runtime pack builds against (packages/runtime-pack-common/scripts/
|
||||
* glibc-toolchain.mjs GLIBC_RUNTIME_FLOOR), spelled for Zig's C++ driver.
|
||||
* ICU is the one C++ archive, and C++ compiled against a newer host glibc
|
||||
* binds the C23 strtol names (__isoc23_*) that floor forbids. */
|
||||
export const ICU_CXX_TARGET = "x86_64-linux-gnu.2.34";
|
||||
/** The translation units one of ICU's own source lists names, in its order.
|
||||
* The fetched tarball carries the lists (`source/common/sources.txt`,
|
||||
* `source/i18n/sources.txt`) verbatim, so the units a build compiles are
|
||||
* exactly the units ICU's own autoconf build compiles for this release.
|
||||
* Entries are bare file names. The pack build reads the same files
|
||||
* (packages/runtime-pack-common/scripts/icu-data.mjs). */
|
||||
export function icuSourceList(text: string): string[] {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
||||
}
|
||||
/** Exact translation-unit membership shared by cache builds and external
|
||||
* source-pack recipes. */
|
||||
export const LRE_SOURCES = ["libregexp.c", "libunicode.c"] as const;
|
||||
|
||||
@@ -63,6 +63,7 @@ export async function warmNativeCaches(
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
|
||||
@@ -155,6 +155,12 @@ const typedInterfaceUnsupported = "typed source has no compiler bridge for this
|
||||
const metadataExclusion =
|
||||
"WebIDL brand metadata is observable reflection, not an executable compatibility operation";
|
||||
|
||||
/** ReadableStream methods a typed program reaches through the web stream
|
||||
* classes (scr_webstreams.c): on a stream it constructs with them, and on
|
||||
* the native stream through its web view (scr_fetch.c, sf_stream_web_view).
|
||||
* A program that calls one links the classes. */
|
||||
export const READABLE_STREAM_WEB_VIEW_CALLS = ["pipeThrough", "pipeTo", "tee"] as const;
|
||||
|
||||
function buildFetchCompatProfile() {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
@@ -233,7 +239,7 @@ function buildFetchCompatProfile() {
|
||||
],
|
||||
responseCalls: ["json", "text", "bytes", "clone"],
|
||||
readableStreamReads: ["locked"],
|
||||
readableStreamCalls: ["cancel", "getReader"],
|
||||
readableStreamCalls: ["cancel", "getReader", ...READABLE_STREAM_WEB_VIEW_CALLS],
|
||||
},
|
||||
operations: [
|
||||
{
|
||||
@@ -447,6 +453,13 @@ function buildFetchCompatProfile() {
|
||||
facets: ["webidl-conversion", "identity", "state-machine", "error-shape"],
|
||||
evidence: [generated("stream-traces"), fixture("static-stream")],
|
||||
},
|
||||
...READABLE_STREAM_WEB_VIEW_CALLS.map((member): FetchCompatOperation => ({
|
||||
id: `stdlib.readable-stream.${member}`,
|
||||
name: `ReadableStream.${member}`,
|
||||
kind: "method",
|
||||
facets: ["identity", "promise-settlement", "state-machine", "error-shape"],
|
||||
evidence: [fixture("static-web-streams")],
|
||||
})),
|
||||
{
|
||||
id: "stdlib.readable-stream-default-reader.closed",
|
||||
name: "ReadableStreamDefaultReader.closed",
|
||||
@@ -757,25 +770,15 @@ function buildFetchCompatProfile() {
|
||||
"getReader",
|
||||
"prototype",
|
||||
),
|
||||
...["pipeThrough", "values"].map((member) =>
|
||||
unsupportedEntry(
|
||||
`stdlib.readable-stream.${member}`,
|
||||
"ReadableStream",
|
||||
member,
|
||||
"prototype",
|
||||
member === "values"
|
||||
? "direct for-await consumption compiles statically; storing or driving the first-class iterator handle is unsupported"
|
||||
: "the wider Web Streams graph is outside the native readable-stream slice",
|
||||
),
|
||||
...READABLE_STREAM_WEB_VIEW_CALLS.map((member) =>
|
||||
staticEntry(`stdlib.readable-stream.${member}`, "ReadableStream", member, "prototype"),
|
||||
),
|
||||
...["pipeTo", "tee"].map((member) =>
|
||||
unsupportedEntry(
|
||||
`stdlib.readable-stream.${member}`,
|
||||
"ReadableStream",
|
||||
member,
|
||||
"prototype",
|
||||
"this operation is outside the native readable-stream implementation",
|
||||
),
|
||||
unsupportedEntry(
|
||||
"stdlib.readable-stream.values",
|
||||
"ReadableStream",
|
||||
"values",
|
||||
"prototype",
|
||||
"direct for-await consumption compiles statically; storing or driving the first-class iterator handle is unsupported",
|
||||
),
|
||||
unsupportedEntry(
|
||||
"stdlib.readable-stream.symbol.asyncIterator",
|
||||
@@ -954,7 +957,8 @@ export interface StaticFetchMembers {
|
||||
readableStreamReads: ReadonlySet<string>;
|
||||
readableStreamCalls: ReadonlySet<string>;
|
||||
/** ReadableStream prototype members the native stream does not
|
||||
* implement, symbol-keyed ones as `[Symbol.name]`. */
|
||||
* implement itself, symbol-keyed ones as `[Symbol.name]`: the unsupported
|
||||
* ones, and the calls it answers through its web view. */
|
||||
readableStreamUnsupported: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
@@ -979,8 +983,8 @@ export function staticFetchMembers(): StaticFetchMembers {
|
||||
),
|
||||
readableStreamReads: new Set(profile.members.readableStreamReads),
|
||||
readableStreamCalls: new Set(profile.members.readableStreamCalls),
|
||||
readableStreamUnsupported: new Set(
|
||||
profile.inventory.entries
|
||||
readableStreamUnsupported: new Set([
|
||||
...profile.inventory.entries
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.owner === "ReadableStream" &&
|
||||
@@ -988,7 +992,8 @@ export function staticFetchMembers(): StaticFetchMembers {
|
||||
entry.status === "unsupported",
|
||||
)
|
||||
.map((entry) => entry.member),
|
||||
),
|
||||
...READABLE_STREAM_WEB_VIEW_CALLS,
|
||||
]),
|
||||
};
|
||||
return staticFetchMemberSets;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ function coverageNotes(): string[] {
|
||||
"Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.",
|
||||
`The engine-free fetch projection targets Node ${fetchTarget.node} with bundled Undici ${fetchTarget.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`,
|
||||
"The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.",
|
||||
"Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
|
||||
"Process-level diagnostic codes are not surface entries: SC0001-SC0007 are preflight gates (SC0005-SC0007 are the ECMAScript early errors), SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
|
||||
"Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.",
|
||||
"No scheduling metadata is published; entry ids are the stable diff keys across releases.",
|
||||
];
|
||||
|
||||
@@ -19,6 +19,9 @@ export const DIAGNOSTIC_CODES = {
|
||||
SC0002: "project configuration incompatible with scriptc",
|
||||
SC0003: "malformed imported JSON module",
|
||||
SC0004: "TypeScript checker panic",
|
||||
SC0005: "duplicate lexical declaration in a block or switch statement list",
|
||||
SC0006: "lexical declaration colliding with a var in the same block",
|
||||
SC0007: "declaration in a single-statement position",
|
||||
SC1010: "package imports outside the supported set",
|
||||
SC1012: "default exports/imports (residual edges)",
|
||||
SC1013: "namespace imports (residual edges)",
|
||||
@@ -40,6 +43,7 @@ export const DIAGNOSTIC_CODES = {
|
||||
SC1071: "generators",
|
||||
SC1080: "'this' outside a class method",
|
||||
SC1090: "this syntax (generic construct fence)",
|
||||
SC1091: "write to an undeclared name in a JavaScript source",
|
||||
SC1100: "operations on 'unknown' values",
|
||||
SC1101: "converting typed values to 'unknown'",
|
||||
SC1110: "comptime evaluation failure",
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
* Code bands:
|
||||
* SC0xxx preflight gates: TypeScript errors passed through (SC0001),
|
||||
* project-config incompatibilities (SC0002), malformed imported
|
||||
* JSON modules (SC0003), and upstream tsgo checker panics
|
||||
* surfaced as source-anchored diagnostics (SC0004)
|
||||
* JSON modules (SC0003), upstream tsgo checker panics surfaced
|
||||
* as source-anchored diagnostics (SC0004), and the ECMAScript
|
||||
* early errors the checker does not model: a duplicated or
|
||||
* `var`-colliding lexical declaration in a block or switch
|
||||
* statement list (SC0005/SC0006) and a declaration where the
|
||||
* grammar admits only a Statement (SC0007)
|
||||
* SC1xxx supported-TypeScript-not-yet: valid TS outside the current subset
|
||||
* SC2xxx scriptc type rules (types we cannot compile yet)
|
||||
* SC3xxx backend/target coverage: the program is valid, but the selected
|
||||
@@ -366,6 +370,24 @@ export function sharedModuleDuplicatedDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8020 — the program replaces a process output stream's write method
|
||||
* while a Node island exists: the island's `process.stdout` is its own
|
||||
* object, so the replacement and the island's writes cannot see each other.
|
||||
* H2 shares the native stream object with the islands and removes this. */
|
||||
export function stdioPatchAcrossIslandsDiag(
|
||||
stream: "stdout" | "stderr" | null,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
const what =
|
||||
stream === null ? "process.stdout.write or process.stderr.write" : `process.${stream}.write`;
|
||||
return {
|
||||
code: "SC8020",
|
||||
message: `${what} is replaced while a Node island exists: the island's process output stream would not see this patch, and island writes would bypass it`,
|
||||
loc,
|
||||
hint: "the island's process.stdout would not see this patch",
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8022 — island code extends a class of a module the program compiles.
|
||||
* A native instance cannot take an island subclass's overrides, so the
|
||||
* module is not owned by the program: a package runs in Node for both
|
||||
@@ -687,6 +709,11 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
|
||||
// remains is `this` outside any method (top level, plain functions).
|
||||
SC1080: { feature: "'this' outside a class method", milestone: "later" },
|
||||
SC1090: { feature: "this syntax", milestone: "later" }, // generic fallback; message names the construct
|
||||
SC1091: {
|
||||
feature: "writing an undeclared name in a JavaScript source",
|
||||
milestone: "later",
|
||||
hint: "a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there)",
|
||||
},
|
||||
SC1100: {
|
||||
feature: "operations on 'unknown' values",
|
||||
milestone: "later",
|
||||
|
||||
@@ -60,6 +60,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
@@ -220,7 +221,7 @@ test("early executable cache misses on source and resolution changes", async ()
|
||||
expect(await readEarlyExecutableCache(f.root, f.options)).toBeNull();
|
||||
});
|
||||
|
||||
test("early executable keys isolate compile modes, paths, implementation, checker, function layouts, and FFI manifests", async () => {
|
||||
test("early executable keys isolate compile modes, paths, implementation, checker, function layouts, shape inference, and FFI manifests", async () => {
|
||||
const f = await fixture();
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(f.source));
|
||||
@@ -245,6 +246,8 @@ test("early executable keys isolate compile modes, paths, implementation, checke
|
||||
{ ...f.options, profile: "profile-use:digest" },
|
||||
{ ...f.options, functionLayout: "layout-digest" },
|
||||
{ ...f.options, checker: "tsc-ts-executable-identity" },
|
||||
{ ...f.options, shapeOverlay: true },
|
||||
{ ...f.options, jsInference: true },
|
||||
{ ...f.options, npmStatic: "auto" },
|
||||
{ ...f.options, npmStatic: ["commander"] },
|
||||
{ ...f.options, outPath: join(f.options.outDir, "other") },
|
||||
|
||||
@@ -64,10 +64,19 @@ export interface EarlyExecutableNativeFeatures {
|
||||
eventTarget?: boolean;
|
||||
wasm?: boolean;
|
||||
warningReplace?: boolean;
|
||||
recordViewPartial?: boolean;
|
||||
stdioExtended?: boolean;
|
||||
fsFlags?: boolean;
|
||||
strdecDyn?: boolean;
|
||||
processExtras?: boolean;
|
||||
/** Error-taking write completion callbacks: SCR_STREAM_WRITE_ERRORS. */
|
||||
streamWriteErrors?: boolean;
|
||||
/** The process output write slot: SCR_STDIO_WRITE_SLOT. */
|
||||
stdioWriteSlot?: boolean;
|
||||
nodeIsland?: boolean;
|
||||
moduleNamespace?: boolean;
|
||||
moduleNamespaceDyn?: boolean;
|
||||
primitiveWrapper?: boolean;
|
||||
zlib: boolean;
|
||||
brotli?: boolean;
|
||||
assert: boolean;
|
||||
@@ -111,6 +120,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
dgramDyn: boolean;
|
||||
textCodecDyn: boolean;
|
||||
watch: boolean;
|
||||
watchFile: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
tls: boolean;
|
||||
@@ -123,6 +133,8 @@ export interface EarlyExecutableNativeFeatures {
|
||||
perfLoop: boolean;
|
||||
tlsCa: boolean;
|
||||
webcrypto: boolean;
|
||||
/** Intl over ICU (scr_intl.c, the ICU archive and its data file). */
|
||||
intl?: boolean;
|
||||
}
|
||||
|
||||
interface EarlyExecutableCacheStamp {
|
||||
@@ -198,6 +210,9 @@ export interface EarlyExecutableCacheOptions {
|
||||
/** Omitted unless shape inference's overlay compiles the packages
|
||||
* (frontend/shape-inference). */
|
||||
shapeOverlay?: true;
|
||||
/** Omitted unless shape inference types the program's own JavaScript
|
||||
* (SCRIPTC_INFER_JS=1, frontend/js-inference.ts). */
|
||||
jsInference?: true;
|
||||
/** Raw manifest identity: path and bytes. Native archives remain under the
|
||||
* stricter native cache's independent dependency validation. */
|
||||
ffiProfile: { path: string; bytes: Uint8Array } | null;
|
||||
@@ -299,6 +314,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"dgramDyn",
|
||||
"textCodecDyn",
|
||||
"watch",
|
||||
"watchFile",
|
||||
"foreignFfi",
|
||||
"nodeTest",
|
||||
"tls",
|
||||
@@ -355,6 +371,8 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
|
||||
...(options.youngNursery ? ["young-nursery"] : []),
|
||||
...(options.nodeIslands !== undefined ? [`node-islands:${options.nodeIslands}`] : []),
|
||||
...(options.heapCensus ? ["heap-census"] : []),
|
||||
...(options.shapeOverlay ? ["shape-overlay"] : []),
|
||||
...(options.jsInference ? ["js-inference"] : []),
|
||||
options.npmStatic === null
|
||||
? "<npm-static-off>"
|
||||
: options.npmStatic === "auto"
|
||||
@@ -406,6 +424,8 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
|
||||
if (options.youngNursery) hash.update("young-nursery\0");
|
||||
if (options.nodeIslands !== undefined) hash.update(`node-islands:${options.nodeIslands}\0`);
|
||||
if (options.heapCensus) hash.update("heap-census\0");
|
||||
if (options.shapeOverlay) hash.update("shape-overlay\0");
|
||||
if (options.jsInference) hash.update("js-inference\0");
|
||||
hash
|
||||
.update(
|
||||
options.npmStatic === null
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
npmStaticRefusalDiag,
|
||||
iceDiag,
|
||||
isCheckerPanic,
|
||||
stdioPatchAcrossIslandsDiag,
|
||||
type ScrDiagnostic,
|
||||
} from "../diagnostics/diagnostic.js";
|
||||
import type { IrModule, SrcLoc } from "../ir/ir.js";
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
} from "../backend/target-diagnostics.js";
|
||||
import type { LowerResult, LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { FrontendFactory } from "../frontend/pipeline.js";
|
||||
import type { UnitStore } from "../frontend/lowering/unit-reuse.js";
|
||||
import type { CompilationTiming } from "../timing.js";
|
||||
import { readProjectConfig } from "../project-config.js";
|
||||
import { nodeIslandPolicy } from "../node-islands/policy.js";
|
||||
@@ -42,7 +44,9 @@ export interface PreparedExecutableModule {
|
||||
warnings: ScrDiagnostic[];
|
||||
}
|
||||
|
||||
/** Release the parser/checker before native object generation begins. */
|
||||
/** Release the parser/checker before native object generation begins.
|
||||
* `unitStore` keeps the lowered units of this build for the next one
|
||||
* (unit-reuse.ts). */
|
||||
export function prepareExecutableModule(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
@@ -52,6 +56,7 @@ export function prepareExecutableModule(
|
||||
timing: CompilationTiming = () => {},
|
||||
lowerIslands: ReadonlyMap<string, IslandPackage> = new Map(),
|
||||
retryFences = true,
|
||||
unitStore: UnitStore | null = null,
|
||||
): PreparedExecutableModule | CompileFailure {
|
||||
// The island decision reads the detector's scan unless islands are off.
|
||||
const islandScan =
|
||||
@@ -115,6 +120,7 @@ export function prepareExecutableModule(
|
||||
timing,
|
||||
grown,
|
||||
retryFences,
|
||||
unitStore,
|
||||
);
|
||||
}
|
||||
return fail(fe.preflight);
|
||||
@@ -127,6 +133,7 @@ export function prepareExecutableModule(
|
||||
targetPlatform: buildPlatform,
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
...(unitStore !== null ? { unitStore } : {}),
|
||||
},
|
||||
(module) => validateModule(module).length === 0,
|
||||
);
|
||||
@@ -165,6 +172,7 @@ export function prepareExecutableModule(
|
||||
timing,
|
||||
grown,
|
||||
retryFences,
|
||||
unitStore,
|
||||
);
|
||||
}
|
||||
// A failed program whose compiled packages need the runtime or a
|
||||
@@ -195,6 +203,14 @@ export function prepareExecutableModule(
|
||||
return fail(lowered.diagnostics);
|
||||
}
|
||||
|
||||
// A program that replaces a process output stream's write method has
|
||||
// nowhere to route an island's writes: the island's process.stdout is
|
||||
// its own object. Until islands share the native stream object (H2),
|
||||
// refuse the combination instead of silently missing the patch.
|
||||
if (islandDecision !== null && lowered.stdioPatches !== undefined) {
|
||||
return fail(lowered.stdioPatches.map((p) => stdioPatchAcrossIslandsDiag(p.stream, p.loc)));
|
||||
}
|
||||
|
||||
const validation = validateModule(lowered.module);
|
||||
timing("ir-validate");
|
||||
if (validation.length > 0) {
|
||||
@@ -267,6 +283,7 @@ export function prepareExecutableModule(
|
||||
timing,
|
||||
islands,
|
||||
false,
|
||||
unitStore,
|
||||
);
|
||||
if (islanded.ok) return islanded;
|
||||
firstError ??= islanded.diagnostics[0] ?? null;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { checkPreflight, loadProgram } from "./program-node.js";
|
||||
|
||||
/** The preflight diagnostics of a CommonJS script (sloppy unless it says
|
||||
* "use strict"). */
|
||||
function diagnostics(source: string) {
|
||||
const directory = mkdtempSync(join(tmpdir(), "scriptc-early-errors-"));
|
||||
writeFileSync(join(directory, "main.cjs"), source);
|
||||
const load = loadProgram(join(directory, "main.cjs"));
|
||||
try {
|
||||
return checkPreflight(load).map((diagnostic) => diagnostic.code);
|
||||
} finally {
|
||||
load.dispose();
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test.for([
|
||||
// A function body's top-level function declarations are var-scoped, so
|
||||
// duplicates are legal in strict code too.
|
||||
'function outer() { "use strict"; function d() {} function d() {} }\nouter();',
|
||||
"class C { static { function s() {} function s() {} } }\nnew C();",
|
||||
// Object literal methods and accessors are sloppy unless they say otherwise.
|
||||
"var o = { m() { if (true) function f() {} } };\no.m();",
|
||||
"var o = { get g() { { function h() {} function h() {} } return 1; } };\no.g;",
|
||||
// A function's own directive makes its body strict, not the code declaring it.
|
||||
'if (true) function k() { "use strict"; }',
|
||||
'{ function z() { "use strict"; } function z() {} }',
|
||||
])("Node accepts: %s", (source) => {
|
||||
expect(diagnostics(source)).toEqual([]);
|
||||
});
|
||||
|
||||
test.for([
|
||||
["function outer() { let d; function d() {} }\nouter();", "SC0006"],
|
||||
['(() => { "use strict"; if (true) function f() {} })();', "SC0007"],
|
||||
["class C { m() { if (true) function f() {} } }\nnew C().m();", "SC0007"],
|
||||
['var o = { m() { "use strict"; { function h() {} function h() {} } } };\no.m();', "SC0005"],
|
||||
])("Node refuses: %s", ([source, code]) => {
|
||||
expect(diagnostics(source!)).toContain(code);
|
||||
});
|
||||
@@ -0,0 +1,384 @@
|
||||
/* ECMAScript early errors the TypeScript checker does not model.
|
||||
*
|
||||
* Static semantics reject a program before it runs, and until this pass
|
||||
* existed scriptc borrowed that rejection from whatever tsc happened to
|
||||
* report (`Let x` declarations at an odd position, a redeclared block name
|
||||
* TypeScript refuses). Test262's negative-parse suites show where the
|
||||
* borrowing stops: 1,198 programs the spec makes SyntaxErrors compile — the
|
||||
* constructs below are the ones the corpus covers, in descending variant
|
||||
* count (plan §7.7 D-4 option (c)).
|
||||
*
|
||||
* One code per construct, so C1's checker relaxation stays safe: relaxing a
|
||||
* tsc code that doubles as an early error removes the ONLY rejection some
|
||||
* parse-negative test had, which is why these codes live beside
|
||||
* JS_RELAXED_TSC_CODES in tsc-codes.ts and never depend on it.
|
||||
*
|
||||
* Scope: JavaScript sources only. A TypeScript source is type-checked, and
|
||||
* its checker diagnostics (TS2451 and friends) are the gate; re-reporting
|
||||
* the same collision under a second code would only duplicate them.
|
||||
*
|
||||
* The pass runs at preflight, before any lowering, and reports through
|
||||
* SC0xxx — the preflight band — so a rejected program fails the same way a
|
||||
* tsc error does. */
|
||||
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { SrcLoc } from "../ir/ir.js";
|
||||
import { isJsSourceFileName } from "./tsc-codes.js";
|
||||
|
||||
/** The early-error codes. SC0005 and SC0006 are the Block/CaseBlock static
|
||||
* semantics (a statement list may not declare one lexical name twice, and a
|
||||
* lexical name may not be `var`-declared in the same list); SC0007 is a
|
||||
* declaration where the grammar admits only a Statement. */
|
||||
export const EARLY_ERROR_CODES = ["SC0005", "SC0006", "SC0007"] as const;
|
||||
|
||||
const NO_DUPLICATE_LEXICAL = "SC0005";
|
||||
const LEXICAL_VAR_COLLISION = "SC0006";
|
||||
const NOT_A_STATEMENT = "SC0007";
|
||||
|
||||
/** Whether the code around a node is strict — the one input this pass needs
|
||||
* from outside: Node's own module classification (program.ts's
|
||||
* isNodeEsmFile7: fixed extensions, package type, then syntax markers). The
|
||||
* checker's external-module bit cannot answer it, because
|
||||
* moduleDetection=force gives every file module scope, CommonJS included. */
|
||||
export type IsStrictCode = (sf: ts.SourceFile, node: ts.Node) => boolean;
|
||||
|
||||
export function earlyErrorDiagnostics7(
|
||||
files: readonly ts.SourceFile[],
|
||||
isModuleCode: (sf: ts.SourceFile) => boolean,
|
||||
): ScrDiagnostic[] {
|
||||
const diags: ScrDiagnostic[] = [];
|
||||
const isStrict: IsStrictCode = (sf, node) => isStrictCode(sf, node, isModuleCode);
|
||||
for (const sf of files) {
|
||||
if (!isJsSourceFileName(sf.fileName)) continue;
|
||||
// Iterative walk: a deeply nested program must not overflow the stack
|
||||
// before the lowering's own nesting fence runs.
|
||||
const stack: ts.Node[] = [sf];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
checkStatementListScope(sf, node, diags, isStrict);
|
||||
checkStatementSlot(node, diags, isStrict);
|
||||
ts.forEachChild(node, (child) => {
|
||||
stack.push(child);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
return diags;
|
||||
}
|
||||
|
||||
function locOf(node: ts.Node): SrcLoc {
|
||||
const sf = ts.getSourceFileOfNode(node);
|
||||
return { file: sf.fileName, start: node.getStart(sf), end: node.end };
|
||||
}
|
||||
|
||||
function report(diags: ScrDiagnostic[], code: EarlyErrorCode, node: ts.Node, message: string) {
|
||||
diags.push({ code, message, loc: locOf(node) });
|
||||
}
|
||||
|
||||
type EarlyErrorCode = (typeof EARLY_ERROR_CODES)[number];
|
||||
|
||||
/* ── SC0005 / SC0006: Block and CaseBlock static semantics ───────────────
|
||||
*
|
||||
* Block : { StatementList }
|
||||
* CaseBlock : { CaseClauses_opt DefaultClause CaseClauses_opt }
|
||||
*
|
||||
* * It is a Syntax Error if the LexicallyDeclaredNames of StatementList
|
||||
* contains any duplicate entries.
|
||||
* * It is a Syntax Error if any element of the LexicallyDeclaredNames of
|
||||
* StatementList also occurs in the VarDeclaredNames of StatementList.
|
||||
*
|
||||
* A case block is ONE lexical scope across its clauses, and VarDeclaredNames
|
||||
* reach into nested blocks (a `var` anywhere below the block belongs to it)
|
||||
* but never into a function or class body. The duplicate rule's one carve-out
|
||||
* is Annex B's B.3.3.1: two plain function declarations of the same name in
|
||||
* the same block are legal in non-strict code.
|
||||
*
|
||||
* A function body and a class static block have the same two rules over
|
||||
* their top-level declarations (FunctionStatementList and
|
||||
* ClassStaticBlockStatementList), but there a function declaration is
|
||||
* var-scoped: it is one of the VarDeclaredNames, so two of them are legal in
|
||||
* strict code too, while `let f; function f() {}` still collides. */
|
||||
|
||||
function checkStatementListScope(
|
||||
sf: ts.SourceFile,
|
||||
node: ts.Node,
|
||||
diags: ScrDiagnostic[],
|
||||
isStrict: IsStrictCode,
|
||||
): void {
|
||||
let statements: readonly ts.Statement[] | null = null;
|
||||
if (ts.isBlock(node)) statements = node.statements.nodes;
|
||||
else if (ts.isCaseBlock(node)) {
|
||||
statements = node.clauses.nodes.flatMap((clause) => clause.statements.nodes);
|
||||
}
|
||||
if (statements === null) return;
|
||||
if (statements.length < 2) return;
|
||||
const functionBody = isFunctionBody(node);
|
||||
|
||||
const lexical = new Map<string, ts.Node[]>();
|
||||
for (const stmt of statements) {
|
||||
for (const [name, decl] of lexicalDeclarationsOf(stmt, functionBody)) {
|
||||
const list = lexical.get(name);
|
||||
if (list === undefined) lexical.set(name, [decl]);
|
||||
else list.push(decl);
|
||||
}
|
||||
}
|
||||
if (lexical.size === 0) return;
|
||||
|
||||
const varNames = new Set<string>();
|
||||
for (const stmt of statements) {
|
||||
collectVarDeclaredNames(stmt, varNames);
|
||||
if (functionBody && ts.isFunctionDeclaration(stmt) && stmt.name !== undefined)
|
||||
varNames.add(stmt.name.text);
|
||||
}
|
||||
|
||||
for (const [name, decls] of lexical) {
|
||||
if (varNames.has(name)) {
|
||||
report(
|
||||
diags,
|
||||
LEXICAL_VAR_COLLISION,
|
||||
decls[0]!,
|
||||
functionBody
|
||||
? `the lexical declaration '${name}' collides with a 'var' or function declaration of the same name in this function body`
|
||||
: `the lexical declaration '${name}' collides with a 'var' declaration of the same name in this block`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (decls.length < 2) continue;
|
||||
// Annex B B.3.3.1: duplicate plain function declarations are the web's
|
||||
// legacy, and only non-strict code keeps them.
|
||||
if (decls.every((d) => ts.isFunctionDeclaration(d) && !isEnhancedFunction(d))) {
|
||||
if (!isStrict(sf, decls[0]!)) continue;
|
||||
}
|
||||
report(
|
||||
diags,
|
||||
NO_DUPLICATE_LEXICAL,
|
||||
decls[1]!,
|
||||
`the declaration '${name}' is already declared in this block's statement list`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a block is the body of a function or a class static block, whose
|
||||
* top-level function declarations are var-scoped. */
|
||||
function isFunctionBody(node: ts.Node): boolean {
|
||||
const parent = node.parent;
|
||||
if (parent === undefined || !ts.isBlock(node)) return false;
|
||||
if (ts.isClassStaticBlockDeclaration(parent)) return true;
|
||||
return ts.isFunctionLike(parent) && (parent as { body?: ts.Node }).body === node;
|
||||
}
|
||||
|
||||
/** The names a DIRECT statement of a statement list declares lexically
|
||||
* (let/const/using, class, and every function declaration form outside a
|
||||
* function body), paired with the node to point the diagnostic at. */
|
||||
function lexicalDeclarationsOf(
|
||||
stmt: ts.Statement,
|
||||
functionBody: boolean,
|
||||
): Array<[string, ts.Node]> {
|
||||
const names: Array<[string, ts.Node]> = [];
|
||||
if (ts.isVariableStatement(stmt)) {
|
||||
if ((stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0) return names;
|
||||
for (const decl of stmt.declarationList.declarations.nodes)
|
||||
collectBindingNames(decl.name, names);
|
||||
return names;
|
||||
}
|
||||
if (ts.isClassDeclaration(stmt) || (ts.isFunctionDeclaration(stmt) && !functionBody)) {
|
||||
const name = stmt.name;
|
||||
if (name !== undefined && ts.isIdentifier(name)) names.push([name.text, stmt]);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function collectBindingNames(name: ts.BindingName, into: Array<[string, ts.Node]>): void {
|
||||
if (ts.isIdentifier(name)) {
|
||||
into.push([name.text, name]);
|
||||
return;
|
||||
}
|
||||
for (const element of name.elements.nodes) {
|
||||
if (ts.isOmittedExpression(element)) continue;
|
||||
if (ts.isBindingElement(element) && element.name !== undefined)
|
||||
collectBindingNames(element.name, into);
|
||||
}
|
||||
}
|
||||
|
||||
/** VarDeclaredNames of a statement: every `var`-declared name below it,
|
||||
* stopping at function and class boundaries (their declarations are not
|
||||
* var-scoped here). */
|
||||
function collectVarDeclaredNames(root: ts.Node, into: Set<string>): void {
|
||||
const stack: ts.Node[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node))
|
||||
continue;
|
||||
if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.BlockScoped) === 0) {
|
||||
const names: Array<[string, ts.Node]> = [];
|
||||
for (const decl of node.declarations.nodes) collectBindingNames(decl.name, names);
|
||||
for (const [name] of names) into.add(name);
|
||||
}
|
||||
ts.forEachChild(node, (child) => {
|
||||
stack.push(child);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** `async function` and `function*` are Annex B's allowance never — only the
|
||||
* plain form is. */
|
||||
function isEnhancedFunction(decl: ts.FunctionDeclaration): boolean {
|
||||
if (decl.asteriskToken !== undefined) return true;
|
||||
return (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Async) !== 0;
|
||||
}
|
||||
|
||||
/* ── SC0007: a declaration where the grammar admits only a Statement ─────
|
||||
*
|
||||
* Every `Statement` slot below admits Statement productions only, and only
|
||||
* Annex B widens three of them to a plain FunctionDeclaration in non-strict
|
||||
* code: the body of an `if`/`else` and a labelled statement. Async functions,
|
||||
* generators, classes and lexical declarations are never admitted, and a
|
||||
* LABELLED function is never admitted as an if/else body (`IsLabelledFunction`
|
||||
* is an explicit early error there, so `if (x) l: function f(){}` fails even
|
||||
* in sloppy code). */
|
||||
type StatementSlot = "if" | "loop" | "with" | "label";
|
||||
|
||||
function checkStatementSlot(node: ts.Node, diags: ScrDiagnostic[], isStrict: IsStrictCode): void {
|
||||
if (ts.isIfStatement(node)) {
|
||||
checkStatementPosition(node.thenStatement, "if", diags, isStrict);
|
||||
if (node.elseStatement !== undefined)
|
||||
checkStatementPosition(node.elseStatement, "if", diags, isStrict);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
ts.isWhileStatement(node) ||
|
||||
ts.isDoStatement(node) ||
|
||||
ts.isForStatement(node) ||
|
||||
ts.isForInStatement(node) ||
|
||||
ts.isForOfStatement(node)
|
||||
) {
|
||||
checkStatementPosition(node.statement, "loop", diags, isStrict);
|
||||
return;
|
||||
}
|
||||
if (ts.isWithStatement(node)) {
|
||||
checkStatementPosition(node.statement, "with", diags, isStrict);
|
||||
return;
|
||||
}
|
||||
if (ts.isLabeledStatement(node)) checkStatementPosition(node.statement, "label", diags, isStrict);
|
||||
}
|
||||
|
||||
function checkStatementPosition(
|
||||
stmt: ts.Statement,
|
||||
slot: StatementSlot,
|
||||
diags: ScrDiagnostic[],
|
||||
isStrict: IsStrictCode,
|
||||
): void {
|
||||
const sf = ts.getSourceFileOfNode(stmt);
|
||||
// A labelled function is an early error as an if-clause, loop, or 'with'
|
||||
// body in BOTH modes (Node agrees); only a plain labelled STATEMENT is
|
||||
// Annex B's, and that is the `slot === "label"` case, whose terminal
|
||||
// function the function branches below handle.
|
||||
const labelTerminal =
|
||||
slot === "label" || !ts.isLabeledStatement(stmt) ? null : labelledFunctionOf(stmt);
|
||||
if (labelTerminal !== null) {
|
||||
report(
|
||||
diags,
|
||||
NOT_A_STATEMENT,
|
||||
labelTerminal,
|
||||
"a labelled function declaration cannot be the body of a loop, a 'with' statement, or an 'if' clause",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (ts.isVariableStatement(stmt)) {
|
||||
if ((stmt.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0)
|
||||
report(
|
||||
diags,
|
||||
NOT_A_STATEMENT,
|
||||
stmt,
|
||||
"a lexical declaration cannot appear in a single-statement position",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (ts.isClassDeclaration(stmt)) {
|
||||
report(
|
||||
diags,
|
||||
NOT_A_STATEMENT,
|
||||
stmt,
|
||||
"a class declaration cannot appear in a single-statement position",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!ts.isFunctionDeclaration(stmt)) return;
|
||||
if (isEnhancedFunction(stmt)) {
|
||||
report(
|
||||
diags,
|
||||
NOT_A_STATEMENT,
|
||||
stmt,
|
||||
"an async function or generator declaration can only appear at the top level or inside a block",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// A plain function declaration: Annex B admits it as an if/else body and as
|
||||
// a labelled statement, in non-strict code only, and never as a loop's or a
|
||||
// 'with' statement's body.
|
||||
if (slot === "loop" || slot === "with") {
|
||||
report(
|
||||
diags,
|
||||
NOT_A_STATEMENT,
|
||||
stmt,
|
||||
"a function declaration can only appear at the top level, inside a block, or as the body of an if statement",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isStrict(sf, stmt))
|
||||
report(
|
||||
diags,
|
||||
NOT_A_STATEMENT,
|
||||
stmt,
|
||||
"strict mode code may not declare a function in a single-statement position",
|
||||
);
|
||||
}
|
||||
|
||||
/** The function declaration at the end of a `label:` chain, when `stmt` is a
|
||||
* labelled statement around one. */
|
||||
function labelledFunctionOf(stmt: ts.Statement): ts.FunctionDeclaration | null {
|
||||
let cur: ts.Statement = stmt;
|
||||
while (ts.isLabeledStatement(cur)) cur = cur.statement;
|
||||
return ts.isFunctionDeclaration(cur) ? cur : null;
|
||||
}
|
||||
|
||||
/* ── strictness of the code around a node ───────────────────────────────
|
||||
*
|
||||
* A directive prologue's "use strict" applies to the function (or script) it
|
||||
* opens, an arrow function's block body included, and every part of a class is strict by
|
||||
* construction. Object literal methods and accessors are ordinary functions:
|
||||
* strict only by their own prologue or their surroundings. Module sources are
|
||||
* strict whether they say so or not, and Node's classification — not the
|
||||
* checker's forced external-module scope — decides which sources are modules.
|
||||
*
|
||||
* The walk starts at the node's parent: a function declaration's own "use
|
||||
* strict" makes its body strict, not the code that declares it. */
|
||||
function isStrictCode(
|
||||
sf: ts.SourceFile,
|
||||
node: ts.Node,
|
||||
isModuleCode: (sf: ts.SourceFile) => boolean,
|
||||
): boolean {
|
||||
if (isModuleCode(sf)) return true;
|
||||
for (let n: ts.Node | undefined = node.parent; n !== undefined; n = n.parent) {
|
||||
if (ts.isSourceFile(n)) return hasUseStrictPrologue(n.statements.nodes);
|
||||
if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) return true;
|
||||
if (ts.isFunctionLike(n)) {
|
||||
const body = n.body;
|
||||
if (body !== undefined && ts.isBlock(body) && hasUseStrictPrologue(body.statements.nodes))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasUseStrictPrologue(statements: readonly ts.Statement[]): boolean {
|
||||
for (const stmt of statements) {
|
||||
if (!ts.isExpressionStatement(stmt)) return false;
|
||||
const expr = stmt.expression;
|
||||
if (!ts.isStringLiteral(expr)) return false;
|
||||
if (expr.text === "use strict") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -94,3 +94,23 @@ test.for([
|
||||
invalid,
|
||||
);
|
||||
});
|
||||
|
||||
test("JavaScript async generators may delegate over a runtime async-iterator shape", () => {
|
||||
expect(
|
||||
diagnostics(
|
||||
`export async function* delegated() { yield* { [Symbol.asyncIterator]: 42 }; }
|
||||
export async function* looped() { for await (const value of { [Symbol.asyncIterator]() { return "text"; } }) console.dir(value); }`,
|
||||
'import { delegated, looped } from "./library.js"; delegated(); looped();',
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test("TypeScript keeps the async-iterator demand", () => {
|
||||
expect(
|
||||
diagnostics(
|
||||
"export async function* delegated() { yield* { [Symbol.asyncIterator]: 42 }; }",
|
||||
'import { delegated } from "./library.js"; delegated();',
|
||||
"ts",
|
||||
).some((diagnostic) => diagnostic.message.includes("Symbol.asyncIterator")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* Shape inference for the program's own JavaScript (SCRIPTC_INFER_JS=1).
|
||||
*
|
||||
* Shape inference (shape-inference/) proves value sets with a closed-world
|
||||
* points-to analysis and writes them as JSDoc into overlay copies of the
|
||||
* files, which the checker then types. By default it runs only for builds
|
||||
* with --npm-static packages, behind SCRIPTC_SHAPE_OVERLAY=1. This option
|
||||
* runs it for the program's own JavaScript files, with or without
|
||||
* npm-static packages, and leaves the packages' files as they are unless
|
||||
* SCRIPTC_SHAPE_OVERLAY=1 is set too (SCRIPTC_SHAPE_OVERLAY=0 turns it
|
||||
* off). The overlay's fallback applies unchanged: a refusal, or a deferred
|
||||
* refusal the build without the overlay lacks, drops the annotations it
|
||||
* depends on, and the program compiles as it does without the overlay where
|
||||
* it must. */
|
||||
|
||||
/** Whether the option is on. */
|
||||
export function jsInferenceEnabled(): boolean {
|
||||
return process.env["SCRIPTC_INFER_JS"] === "1";
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import { isGenericCallableMemberType } from "../type-mapper.js";
|
||||
|
||||
/** True when nothing in `sym`'s DECLARING FILE ever writes it after the
|
||||
* initializer: assignments (plain and compound, destructuring targets
|
||||
* included), ++/--, and for-of/for-in expression targets all count.
|
||||
* included), ++/--, for-of/for-in expression targets, a `var` in a
|
||||
* for-of/for-in head, and redeclarations that initialize all count.
|
||||
* Sound file-locally for module-scope bindings because ESM import
|
||||
* bindings are read-only — no other file can write one. Cached per
|
||||
* symbol (the scan walks the whole file once). */
|
||||
@@ -31,6 +32,7 @@ export function fileWritesBinding(
|
||||
file: ts.SourceFile,
|
||||
resolve: (ident: ts.Identifier) => ts.Symbol | null | undefined,
|
||||
): boolean {
|
||||
if (declarationsWriteBinding(sym)) return true;
|
||||
let written = false;
|
||||
// Text pre-check keeps the file walk cheap: only same-named
|
||||
// identifiers pay a symbol resolution.
|
||||
@@ -78,6 +80,77 @@ export function fileWritesBinding(
|
||||
return written;
|
||||
}
|
||||
|
||||
/** True when `sym`'s own declarations write it after it holds a value:
|
||||
* a `var` in a for-in or for-of head is written on every iteration, a
|
||||
* `var` with an initializer inside a loop body is written each time the
|
||||
* body runs, and a binding declared more than once (`var x = 1; ... var x
|
||||
* = 2;`, a parameter or function declaration that a `var` redeclares) is
|
||||
* written by each initializing declaration that runs. Only value
|
||||
* declarations count, and of functions only implementations: overload
|
||||
* signatures have no body. A redeclaration without an initializer is
|
||||
* counted too, which only over-approximates. */
|
||||
function declarationsWriteBinding(sym: ts.Symbol): boolean {
|
||||
let values = 0;
|
||||
let initializing = 0;
|
||||
for (const d of sym.declarations) {
|
||||
let root: ts.Node = d;
|
||||
while (
|
||||
root.parent !== undefined &&
|
||||
(ts.isBindingElement(root) ||
|
||||
ts.isObjectBindingPattern(root) ||
|
||||
ts.isArrayBindingPattern(root))
|
||||
)
|
||||
root = root.parent;
|
||||
if (ts.isVariableDeclaration(root)) {
|
||||
const list = root.parent;
|
||||
if (
|
||||
ts.isVariableDeclarationList(list) &&
|
||||
(list.flags & ts.NodeFlags.BlockScoped) === 0 &&
|
||||
(ts.isForInStatement(list.parent) || ts.isForOfStatement(list.parent))
|
||||
)
|
||||
return true;
|
||||
values++;
|
||||
if (root.initializer !== undefined) {
|
||||
if (insideLoopBody(root)) return true;
|
||||
initializing++;
|
||||
}
|
||||
} else if (
|
||||
ts.isParameter(root) ||
|
||||
(ts.isFunctionDeclaration(d) && d.body !== undefined) ||
|
||||
ts.isClassDeclaration(d)
|
||||
) {
|
||||
values++;
|
||||
initializing++;
|
||||
}
|
||||
}
|
||||
return values > 1 && initializing > 0;
|
||||
}
|
||||
|
||||
/** Whether a declaration sits in the body of a loop of its own function,
|
||||
* where it runs once per iteration. */
|
||||
function insideLoopBody(node: ts.Node): boolean {
|
||||
let child = node;
|
||||
for (let parent = node.parent; parent !== undefined; parent = parent.parent) {
|
||||
if (
|
||||
ts.isFunctionLike(parent) ||
|
||||
ts.isClassStaticBlockDeclaration(parent) ||
|
||||
ts.isSourceFile(parent)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
(ts.isForStatement(parent) ||
|
||||
ts.isForInStatement(parent) ||
|
||||
ts.isForOfStatement(parent) ||
|
||||
ts.isWhileStatement(parent) ||
|
||||
ts.isDoStatement(parent)) &&
|
||||
parent.statement === child
|
||||
)
|
||||
return true;
|
||||
child = parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Strips the value-preserving wrappers off an expression: parens,
|
||||
* non-null assertions, `as`/`satisfies`/angle-bracket casts. What
|
||||
* remains is the expression that actually evaluates. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { lowerReadlineCursorCall } from "./builtins/readline.js";
|
||||
import { lowerFsWatchFileCall } from "./builtins/filesystem.js";
|
||||
import { lowerMonitorEventLoopDelayCall } from "./builtins/performance.js";
|
||||
import {
|
||||
lowerEnvironmentData,
|
||||
@@ -134,6 +135,13 @@ export function lowerBuiltinCall(
|
||||
if (streamServed) return streamServed;
|
||||
const fsTs = lowerer.lowerFsToUnixTimestampCall(expr, bi, loc);
|
||||
if (fsTs) return fsTs;
|
||||
// fs.watchFile/fs.unwatchFile have no table entry (their checked-dynamic
|
||||
// form is the fs.watchFileChk ladder below), so the typed polling forms
|
||||
// dispatch here; anything else falls through to the ladder and the fence.
|
||||
if (bi.module === "fs" && (bi.member === "watchFile" || bi.member === "unwatchFile")) {
|
||||
const polling = lowerFsWatchFileCall(lowerer, expr, loc, bi.member === "unwatchFile");
|
||||
if (polling) return polling;
|
||||
}
|
||||
const fsLadder = lowerer.lowerFsLadderCall(expr, bi, loc);
|
||||
if (fsLadder) return fsLadder;
|
||||
const cryptoServed = lowerer.lowerCryptoModuleCall(expr, bi, loc);
|
||||
|
||||
@@ -31,7 +31,9 @@ export function lowerChildMethodCall(
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
// A `?.` in the chain re-dispatches here with the receiver bound to the
|
||||
// chain (optional-chains.ts), so only an UNHANDLED token declines.
|
||||
if (lowerer.chainBlocked(call, access)) return null;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "child") return null;
|
||||
if (!isChildSurfaceMember(lowerer, access)) return null;
|
||||
const name = access.name.text;
|
||||
|
||||
@@ -477,11 +477,11 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S
|
||||
} else if (m.value.kind === ts.SyntaxKind.FalseKeyword) {
|
||||
detached = boolLit(false, loc);
|
||||
} else {
|
||||
lowerer.noLowering(
|
||||
"spawn with a non-literal detached option",
|
||||
m.value,
|
||||
"detached must be a boolean literal",
|
||||
);
|
||||
// A runtime boolean — the portable daemon idiom `detached:
|
||||
// process.platform !== "win32"` — reaches the same
|
||||
// POSIX_SPAWN_SETSID flag through scr_spawn_opts's bool slot.
|
||||
detached = lowerer.lowerExprExpecting(m.value, BOOL);
|
||||
plain = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
|
||||
]);
|
||||
|
||||
export const DATE_METHOD_HINT =
|
||||
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering";
|
||||
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), toLocaleString(), toLocaleTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale and options arguments, toLocaleDateString() and the Intl.DateTimeFormat surface have no lowering";
|
||||
|
||||
/** Date statics and methods on Date objects (a TimeClip'd millisecond
|
||||
* time value with identity): getters, text forms and setters.
|
||||
@@ -208,6 +208,19 @@ export function dateValueMethod(name: string, value: IrExpr, loc: SrcLoc): IrExp
|
||||
if (textFn !== undefined) {
|
||||
return { kind: "libCall", fn: textFn, args: [value], type: STRING, loc };
|
||||
}
|
||||
// The default locale's forms. The spec makes them DateTimeFormat(locale,
|
||||
// options).format(this); with neither argument that is ICU's default
|
||||
// locale over the host time zone, which scr_intl.c formats over the
|
||||
// ICU release exactly as V8 does.
|
||||
if (name === "toLocaleString" || name === "toLocaleTimeString") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: name === "toLocaleString" ? "icu.dateLocaleString" : "icu.dateLocaleTimeString",
|
||||
args: [value],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1049,8 +1049,14 @@ export function lowerFsWriteOptionsCall(
|
||||
const operation = promiseWriteOptions ? "fs.promises.writeFile" : "writeFileSync";
|
||||
const plainFn: IrLibFn = promiseWriteOptions ? "fsp.writeFile" : "fs.writeFileSync";
|
||||
const modeFn: IrLibFn = promiseWriteOptions ? "fsp.writeFileMode" : "fs.writeFileModeSync";
|
||||
const flagsFn: IrLibFn = promiseWriteOptions ? "fsp.writeFileFlags" : "fs.writeFileFlagsSync";
|
||||
const resultType: IrType = promiseWriteOptions ? { kind: "promise", inner: VOID } : VOID;
|
||||
type WriteOptionValue = { kind: "effect" | "mode"; value: IrExpr };
|
||||
/** `flag: "w"` is the plain truncate-or-create write; `flag: "wx"` is
|
||||
* open(2)'s O_EXCL (EEXIST when the path exists). Both are literals, so
|
||||
* the bit is known here and carries no evaluation of its own. */
|
||||
let exclusive = false;
|
||||
let sawFlag = false;
|
||||
// The runtime needs only `mode`, but every source option value is an
|
||||
// ordinary JS expression. Stage path/data and then evaluate the option
|
||||
// values in object-literal order before issuing the write; otherwise a
|
||||
@@ -1079,19 +1085,36 @@ export function lowerFsWriteOptionsCall(
|
||||
});
|
||||
mode = varRef(modeLocal.id, modeLocal.type, loc);
|
||||
}
|
||||
const result: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: mode ? modeFn : plainFn,
|
||||
args: mode
|
||||
? [
|
||||
const result: IrExpr = sawFlag
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: flagsFn,
|
||||
args: [
|
||||
varRef(pathLocal.id, pathLocal.type, loc),
|
||||
varRef(dataLocal.id, dataLocal.type, loc),
|
||||
mode,
|
||||
]
|
||||
: [varRef(pathLocal.id, pathLocal.type, loc), varRef(dataLocal.id, dataLocal.type, loc)],
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
// Node's default creation mode when only the flag is given.
|
||||
mode ?? { kind: "numLit", value: 0o666, type: F64, loc },
|
||||
{ kind: "numLit", value: exclusive ? 1 : 0, type: F64, loc },
|
||||
],
|
||||
type: resultType,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: mode ? modeFn : plainFn,
|
||||
args: mode
|
||||
? [
|
||||
varRef(pathLocal.id, pathLocal.type, loc),
|
||||
varRef(dataLocal.id, dataLocal.type, loc),
|
||||
mode,
|
||||
]
|
||||
: [
|
||||
varRef(pathLocal.id, pathLocal.type, loc),
|
||||
varRef(dataLocal.id, dataLocal.type, loc),
|
||||
],
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
return { kind: "seqExpr", stmts, result, type: resultType, loc };
|
||||
};
|
||||
// The bare-encoding spelling — writeFileSync(p, data, "utf-8") — is
|
||||
@@ -1134,13 +1157,21 @@ export function lowerFsWriteOptionsCall(
|
||||
}
|
||||
optionValues.push({ kind: "effect", value: lowerer.lowerExprExpecting(m.value, STRING) });
|
||||
} else if (m.name === "flag") {
|
||||
// Documented, behavior-changing (open(2)'s disposition — 'a'
|
||||
// IS appendFileSync), no lowering: fence by name.
|
||||
lowerer.noLowering(
|
||||
`${operation} with the flag option`,
|
||||
p,
|
||||
"the write truncates-or-creates (Node's default 'w'); other flags have no lowering",
|
||||
);
|
||||
// Node's disposition flag changes whether an existing file is
|
||||
// truncated: "w" is the default write, "wx" the exclusive create.
|
||||
// Every other flag (append "a"/"as", synchronous "ws", ...) writes
|
||||
// different bytes and keeps the fence by name.
|
||||
const t = lowerer.typeOf(m.value);
|
||||
if (!t.isStringLiteralType() || (t.value !== "w" && t.value !== "wx")) {
|
||||
lowerer.noLowering(
|
||||
`${operation} with the flag option`,
|
||||
p,
|
||||
'the lowered flags are "w" (the default truncate-or-create) and "wx" (O_EXCL: EEXIST when the path exists); other flags write different bytes',
|
||||
);
|
||||
}
|
||||
exclusive = t.value === "wx";
|
||||
sawFlag = true;
|
||||
optionValues.push({ kind: "effect", value: lowerer.lowerExprExpecting(m.value, STRING) });
|
||||
} else {
|
||||
// The options-record stance: documented keys with no lowering
|
||||
// fence by name; undocumented keys drop like Node.
|
||||
@@ -1150,7 +1181,7 @@ export function lowerFsWriteOptionsCall(
|
||||
m.name,
|
||||
operation,
|
||||
FS_WRITE_FILE_DOCUMENTED_OPTIONS,
|
||||
'the supported options are { mode: <number>, encoding: "utf8" }',
|
||||
'the supported options are { mode: <number>, encoding: "utf8", flag: "w" | "wx" }',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1159,7 +1190,7 @@ export function lowerFsWriteOptionsCall(
|
||||
lowerer.noLowering(
|
||||
`${operation} with 3 arguments`,
|
||||
optsNode,
|
||||
'the supported options are { mode: <number>, encoding: "utf8" } over string data',
|
||||
'the supported options are { mode: <number>, encoding: "utf8", flag: "w" | "wx" } over string data',
|
||||
);
|
||||
}
|
||||
return finishWrite(optionValues);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
builtinModuleFnOf,
|
||||
FS_READDIR_DOCUMENTED_OPTIONS,
|
||||
FS_WATCH_DOCUMENTED_OPTIONS,
|
||||
FS_WATCH_FILE_DOCUMENTED_OPTIONS,
|
||||
fenceOrDropOptionKey,
|
||||
} from "../surfaces.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js";
|
||||
@@ -1127,13 +1128,37 @@ export function lowerStatsMethodCall(
|
||||
);
|
||||
}
|
||||
|
||||
/** The watch-listener shapes: () , (eventType: string), and
|
||||
* (eventType: string, filename: string | null) — the filename parameter
|
||||
* must admit null (Node passes null when the platform names nothing), or
|
||||
* be the checked dynamic. Returns the parameter count, or null when the
|
||||
* shape has no lowering. */
|
||||
function watchListenerArity(lowerer: Lowerer, cb: IrExpr): 0 | 1 | 2 | null {
|
||||
if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 2)
|
||||
return null;
|
||||
const [event, name] = cb.type.params;
|
||||
if (event !== undefined && event.kind !== "string" && event.kind !== "dyn") return null;
|
||||
if (name !== undefined && name.kind !== "dyn") {
|
||||
const def = name.kind === "union" ? lowerer.unions.get(name.unionId) : undefined;
|
||||
if (
|
||||
!def ||
|
||||
!def.arms.some((a) => a.kind === "nullT") ||
|
||||
!def.arms.some((a) => a.kind === "string") ||
|
||||
!def.arms.every((a) => a.kind === "nullT" || a.kind === "undefinedT" || a.kind === "string")
|
||||
)
|
||||
return null;
|
||||
}
|
||||
return cb.type.params.length as 0 | 1 | 2;
|
||||
}
|
||||
|
||||
/** `fs.watch(path[, options][, listener])` → the fs.watch libCall
|
||||
* (scr_watch.c — kqueue EVFILT_VNODE on the opened path; an unopenable
|
||||
* path THROWS Node's fs error synchronously, the polling-fallback catch
|
||||
* shape). The listener fires with "rename"/"change" and takes zero
|
||||
* parameters or the eventType string — the filename parameter has no
|
||||
* lowering (kqueue watches the inode, not the directory entry; you
|
||||
* watched one path). The options record follows the options-record
|
||||
* parameters, the eventType string, or (eventType, filename: string |
|
||||
* null) — the filename is the entry name on a directory watch and the
|
||||
* watched file's basename otherwise (Linux inotify names both; a macOS
|
||||
* directory watch has no name source until F3). The options record follows the options-record
|
||||
* stance: persistent: true, recursive: false, and encoding: "utf8"
|
||||
* state the lowered behavior and are accepted; persistent: false
|
||||
* (a watcher that does NOT hold the loop), recursive: true (kqueue
|
||||
@@ -1204,7 +1229,7 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc:
|
||||
lowerer.noLowering(
|
||||
"fs.watch with a non-utf8 encoding",
|
||||
prop,
|
||||
"the encoding applies to the filename argument, which has no lowering — utf8 (the default) is accepted",
|
||||
"the filename argument is a string — utf8 (the default) is the accepted encoding",
|
||||
);
|
||||
}
|
||||
if (key === "signal") {
|
||||
@@ -1236,19 +1261,11 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc:
|
||||
: null;
|
||||
if (listenerArg !== null) {
|
||||
const cb = lowerer.lowerExpr(listenerArg);
|
||||
if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) {
|
||||
if (watchListenerArity(lowerer, cb) === null) {
|
||||
lowerer.noLowering(
|
||||
"fs.watch with this listener shape",
|
||||
listenerArg,
|
||||
"the listener takes () or (eventType: string) — the filename parameter has no lowering (you watched one path)",
|
||||
);
|
||||
}
|
||||
const param = cb.type.params[0];
|
||||
if (param !== undefined && param.kind !== "string") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
listenerArg,
|
||||
`watch listeners whose parameter is not the eventType string (got '${lowerer.fmt(param)}')`,
|
||||
"the listener takes (), (eventType: string), or (eventType: string, filename: string | null)",
|
||||
);
|
||||
}
|
||||
args.push(cb);
|
||||
@@ -1262,10 +1279,134 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc:
|
||||
};
|
||||
}
|
||||
|
||||
/** Method calls on FSWatcher receivers: close() — idempotent, statement
|
||||
* position (Node returns void there too). Everything else @types/node
|
||||
* declares (ref/unref, the EventEmitter surface) fences member-
|
||||
* qualified. Null for non-watcher receivers. */
|
||||
/** `fs.watchFile(path[, options], listener)` and
|
||||
* `fs.unwatchFile(path[, listener])` → the polling libCalls
|
||||
* (scr_watch_file.c: libuv's uv_fs_poll shape — one poller per RESOLVED
|
||||
* path, shared by every listener on it, re-armed every `interval` ms,
|
||||
* 5007 by default, persistent by default). The listener takes
|
||||
* (current: Stats, previous: Stats) — zero, one or both — and runs when a
|
||||
* poll sees a difference; the first poll only records the baseline, and a
|
||||
* file that cannot be stat'd polls as all-zero Stats. The options record
|
||||
* follows the options-record stance: interval, persistent: true/false and
|
||||
* bigint: false are accepted; bigint: true fences (BigIntStats); other
|
||||
* documented keys fence by name; undocumented keys drop like Node.
|
||||
* Statement position only — Node returns a StatWatcher, which has no
|
||||
* lowering. Returns null for any untyped shape so the checked-dynamic
|
||||
* ladder (fs.watchFileChk) keeps its current IR. */
|
||||
export function lowerFsWatchFileCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
unwatch: boolean,
|
||||
): IrExpr | null {
|
||||
const nodes = expr.arguments.nodes;
|
||||
if (nodes.length < 1 || nodes.length > 3 || nodes.some(ts.isSpreadElement)) return null;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(nodes[0]!))?.kind !== "string") return null;
|
||||
const path = lowerer.lowerExprExpecting(nodes[0]!, STRING);
|
||||
if (unwatch) {
|
||||
if (nodes.length > 2) return null;
|
||||
const args: IrExpr[] = [path];
|
||||
if (nodes.length === 2) {
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(nodes[1]!))?.kind !== "func") return null;
|
||||
args.push(lowerer.lowerExpr(nodes[1]!));
|
||||
}
|
||||
return { kind: "libCall", fn: "fs.unwatchFile", args, type: VOID, loc };
|
||||
}
|
||||
const hasOptions = nodes.length >= 2 && ts.isObjectLiteralExpression(nodes[1]!);
|
||||
if (nodes.length === 3 && !hasOptions) return null;
|
||||
if (nodes.length === 2 && hasOptions) return null; /* no listener */
|
||||
const listenerArg = nodes[nodes.length - 1]!;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(listenerArg))?.kind !== "func") return null;
|
||||
const parent = expr.parent;
|
||||
if (!ts.isExpressionStatement(parent) && !(ts.isArrowFunction(parent) && parent.body === expr)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"using the returned StatWatcher (register the fs.watchFile call as its own statement)",
|
||||
);
|
||||
}
|
||||
let interval: IrExpr | null = null;
|
||||
let persistent: IrExpr | null = null;
|
||||
if (hasOptions) {
|
||||
for (const prop of (nodes[1] as ts.ObjectLiteralExpression).properties.nodes) {
|
||||
if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) {
|
||||
lowerer.noLowering(
|
||||
"fs.watchFile options with computed keys or spreads",
|
||||
prop,
|
||||
"each option must be a plain `name: value` entry with a literal key",
|
||||
);
|
||||
}
|
||||
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
|
||||
lowerer.noLowering(
|
||||
"fs.watchFile options with computed keys",
|
||||
prop,
|
||||
"each option must be a plain `name: value` entry with a literal key",
|
||||
);
|
||||
}
|
||||
const key = prop.name.text;
|
||||
const value = ts.isPropertyAssignment(prop)
|
||||
? prop.initializer
|
||||
: ts.isShorthandPropertyAssignment(prop)
|
||||
? prop.name
|
||||
: null;
|
||||
if (key === "interval" && value !== null) {
|
||||
interval = lowerer.lowerExprExpecting(value, F64);
|
||||
continue;
|
||||
}
|
||||
if (key === "persistent" && value !== null) {
|
||||
persistent = lowerer.lowerExprExpecting(value, BOOL);
|
||||
continue;
|
||||
}
|
||||
if (key === "bigint") {
|
||||
// The fallback's option record declares `bigint: false`, so the
|
||||
// false literal (or `false` as a value) states the lowered Stats
|
||||
// shape; BigIntStats has no lowering.
|
||||
if (value !== null && value.kind === ts.SyntaxKind.FalseKeyword) continue;
|
||||
lowerer.noLowering(
|
||||
"fs.watchFile with bigint: true",
|
||||
prop,
|
||||
"the listener receives Stats (double integers); BigIntStats has no lowering",
|
||||
);
|
||||
}
|
||||
fenceOrDropOptionKey(
|
||||
lowerer,
|
||||
prop,
|
||||
key,
|
||||
"fs.watchFile",
|
||||
FS_WATCH_FILE_DOCUMENTED_OPTIONS,
|
||||
"interval, persistent: true/false, and bigint: false are the accepted options",
|
||||
);
|
||||
// An undocumented key, dropped like Node drops it.
|
||||
}
|
||||
}
|
||||
const cb = lowerer.lowerExpr(listenerArg);
|
||||
if (
|
||||
cb.type.kind !== "func" ||
|
||||
cb.type.ret.kind !== "void" ||
|
||||
cb.type.params.length > 2 ||
|
||||
!cb.type.params.every((p) => p.kind === "stats")
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
"fs.watchFile with this listener shape",
|
||||
listenerArg,
|
||||
"the listener takes (current: Stats, previous: Stats)",
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "fs.watchFile",
|
||||
args: [path, interval ?? numLit(5007, loc), persistent ?? boolLit(true, loc), cb],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Method calls on FSWatcher receivers: close() (idempotent, statement
|
||||
* position — Node returns void there too) and the EventEmitter surface for
|
||||
* 'change', 'error' and 'close' (on/once/addListener/prependListener/
|
||||
* prependOnceListener/off/removeListener, registered as their own
|
||||
* statement). Everything else @types/node declares (ref/unref, other
|
||||
* events) fences member-qualified. Null for non-watcher receivers. */
|
||||
export function lowerWatcherMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -1280,10 +1421,82 @@ export function lowerWatcherMethodCall(
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
return { kind: "libCall", fn: "watcher.close", args: [receiver], type: VOID, loc };
|
||||
}
|
||||
// The EventEmitter surface for the three events the watcher emits:
|
||||
// "change" (the listener shapes fs.watch takes), "error", and "close".
|
||||
const ON = ["on", "once", "addListener", "prependListener", "prependOnceListener"];
|
||||
if (
|
||||
(ON.includes(name) || name === "off" || name === "removeListener") &&
|
||||
call.arguments.nodes.length === 2
|
||||
) {
|
||||
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
|
||||
const event = evT.isStringLiteralType() ? evT.value : null;
|
||||
const code = event === "change" ? 0 : event === "error" ? 1 : event === "close" ? 2 : -1;
|
||||
if (code < 0) {
|
||||
lowerer.noLowering(
|
||||
`FSWatcher.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
|
||||
call.arguments.nodes[0]!,
|
||||
'"change", "error", and "close" are the supported FSWatcher events (as literals)',
|
||||
);
|
||||
}
|
||||
const parent = call.parent;
|
||||
if (
|
||||
!ts.isExpressionStatement(parent) &&
|
||||
!(ts.isArrowFunction(parent) && parent.body === call)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`using the result of FSWatcher.${name} (register it as its own statement)`,
|
||||
);
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const cb = lowerer.lowerExpr(call.arguments.nodes[1]!);
|
||||
if (name === "off" || name === "removeListener") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "watcher.off",
|
||||
args: [receiver, numLit(code, loc), cb],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const ok =
|
||||
code === 0
|
||||
? watchListenerArity(lowerer, cb) !== null
|
||||
: code === 1
|
||||
? cb.type.kind === "func" &&
|
||||
cb.type.ret.kind === "void" &&
|
||||
cb.type.params.length <= 1 &&
|
||||
(cb.type.params[0] === undefined ||
|
||||
cb.type.params[0].kind === "dyn" ||
|
||||
(cb.type.params[0].kind === "object" && cb.type.params[0].className === "%Error"))
|
||||
: cb.type.kind === "func" && cb.type.ret.kind === "void" && cb.type.params.length === 0;
|
||||
if (!ok) {
|
||||
lowerer.noLowering(
|
||||
`FSWatcher.${name}("${event}", ...) with this listener shape`,
|
||||
call.arguments.nodes[1]!,
|
||||
code === 0
|
||||
? "the listener takes (), (eventType: string), or (eventType: string, filename: string | null)"
|
||||
: code === 1
|
||||
? "the listener takes () or (error: Error)"
|
||||
: "the listener takes ()",
|
||||
);
|
||||
}
|
||||
const flags =
|
||||
(name === "once" || name === "prependOnceListener" ? 1 : 0) |
|
||||
(name.startsWith("prepend") ? 2 : 0);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "watcher.on",
|
||||
args: [receiver, numLit(code, loc), numLit(flags, loc), cb],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`FSWatcher.${name}`,
|
||||
call,
|
||||
"close() is the supported FSWatcher member",
|
||||
'close(), and on/once/addListener/prependListener/prependOnceListener/off/removeListener for "change", "error", and "close" are the supported FSWatcher members',
|
||||
lowerer.checker.getSymbolAtLocation(access.name),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,20 @@ import { lowerUtilTypeCall } from "./util-types.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { jsCheckedBuiltinCall } from "../lower-builtin-values.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { type BuiltinModuleFn } from "../surfaces.js";
|
||||
import { type ParamShape } from "../call-signatures.js";
|
||||
import { BYTES_U8, DYN, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js";
|
||||
import {
|
||||
BYTES_U8,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrLibFn,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
arrayOf,
|
||||
} from "../../../ir/ir.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { lowerProcessLoadEnvFile } from "./environment.js";
|
||||
import {
|
||||
@@ -72,6 +82,10 @@ export function lowerBuiltinModuleCall(
|
||||
);
|
||||
if (fallible) return fallible;
|
||||
checkFsUrlPaths(lowerer, expr, bi);
|
||||
{
|
||||
const checked = jsCheckedBuiltinCall(lowerer, expr, fn, `${bi.module}.${bi.member}`, loc);
|
||||
if (checked) return checked;
|
||||
}
|
||||
if (bi.module === "util/types") {
|
||||
return lowerUtilTypeCall(lowerer, expr, bi, loc, name);
|
||||
}
|
||||
@@ -331,6 +345,31 @@ export function lowerBuiltinModuleCall(
|
||||
: { kind: "numLit", value: 0, type: F64, loc };
|
||||
return { kind: "libCall", fn: fn.fn, args: [path, second], type: fn.result, loc };
|
||||
}
|
||||
// copyFileSync(src, dest, mode?) and copyFile(src, dest, mode?): the
|
||||
// mode is libuv's copyfile flag bits (COPYFILE_EXCL = 1, COPYFILE_FICLONE
|
||||
// = 2, COPYFILE_FICLONE_FORCE = 4 — fs.constants.* reads bake to
|
||||
// literals). The omitted form is the plain copy and keeps its libCall.
|
||||
if (
|
||||
(bi.module === "fs" && bi.member === "copyFileSync") ||
|
||||
(bi.module === "fs/promises" && bi.member === "copyFile")
|
||||
) {
|
||||
const count = expr.arguments.nodes.length;
|
||||
if (count < 2 || count > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(`${bi.member} with ${count} arguments`, expr);
|
||||
}
|
||||
const src = lowerFsPath(lowerer, expr.arguments.nodes[0]!);
|
||||
const dest = lowerFsPath(lowerer, expr.arguments.nodes[1]!);
|
||||
const modeNode = expr.arguments.nodes[2];
|
||||
if (!modeNode) return { kind: "libCall", fn: fn.fn, args: [src, dest], type: fn.result, loc };
|
||||
const flags: IrLibFn = bi.module === "fs" ? "fs.copyFileFlagsSync" : "fsp.copyFileFlags";
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: flags,
|
||||
args: [src, dest, lowerer.lowerExprExpecting(modeNode, F64)],
|
||||
type: fn.result,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
bi.module === "fs/promises" &&
|
||||
bi.member === "appendFile" &&
|
||||
|
||||
@@ -467,10 +467,40 @@ export function lowerCreateRequireCall(
|
||||
}
|
||||
}
|
||||
|
||||
function staticString(node: ts.Expression | undefined): string | null {
|
||||
if (node === undefined) return null;
|
||||
/** The statically-known text of a module specifier expression. Module
|
||||
* resolution is a build-time artifact, so only a value the compiler already
|
||||
* knows can name a module: a string literal (or substitution-free
|
||||
* template), a parenthesized or cast form of one, a `+` concatenation of
|
||||
* those, or a `const` binding whose initializer folds to one
|
||||
* (`const FS = "node:" + "fs"; import(FS)`). Null when the text is only
|
||||
* known at run time. */
|
||||
export function staticString(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression | undefined,
|
||||
depth = 0,
|
||||
): string | null {
|
||||
if (node === undefined || depth > 8) return null;
|
||||
const value = stripTypeCasts(node);
|
||||
return ts.isStringLiteralLike(value) ? value.text : null;
|
||||
if (ts.isStringLiteralLike(value)) return value.text;
|
||||
if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
||||
const left = staticString(lowerer, value.left, depth + 1);
|
||||
if (left === null) return null;
|
||||
const right = staticString(lowerer, value.right, depth + 1);
|
||||
return right === null ? null : left + right;
|
||||
}
|
||||
if (ts.isIdentifier(value)) {
|
||||
const decl = lowerer.checker.getSymbolAtLocation(value)?.valueDeclaration;
|
||||
if (
|
||||
decl !== undefined &&
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.initializer !== undefined &&
|
||||
decl.initializer !== value &&
|
||||
ts.isVariableDeclarationList(decl.parent) &&
|
||||
(decl.parent.flags & ts.NodeFlags.Const) !== 0
|
||||
)
|
||||
return staticString(lowerer, decl.initializer, depth + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function runtimeResolveThrow(error: RuntimeResolveError, type: IrType, loc: SrcLoc): IrExpr {
|
||||
@@ -503,7 +533,7 @@ export function lowerImportMetaResolveCall(
|
||||
'the lowered form is import.meta.resolve("<static specifier>") using the containing module as its parent',
|
||||
);
|
||||
}
|
||||
const specifier = staticString(call.arguments.nodes[0]);
|
||||
const specifier = staticString(lowerer, call.arguments.nodes[0]);
|
||||
if (specifier === null) {
|
||||
lowerer.noLowering(
|
||||
"import.meta.resolve with a runtime-computed specifier",
|
||||
@@ -618,7 +648,7 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio
|
||||
call,
|
||||
);
|
||||
}
|
||||
const specifier = staticString(call.arguments.nodes[0]);
|
||||
const specifier = staticString(lowerer, call.arguments.nodes[0]);
|
||||
// A computed request (or options) through a createRequire loader in
|
||||
// JavaScript source resolves at run time.
|
||||
if (
|
||||
|
||||
@@ -17,8 +17,9 @@ import {
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
/** `os.userInfo()` — the passwd-entry snapshot, Node's uv_os_get_passwd
|
||||
* behind the call site's own mapped record shape: username = pw_name,
|
||||
* uid/gid = getuid/getgid, shell = pw_shell (as the `string | null`
|
||||
* for the effective uid behind the call site's own mapped record shape:
|
||||
* username = pw_name, uid/gid = pw_uid/pw_gid (the user's primary group,
|
||||
* not the process gid; -1 on Windows), shell = pw_shell (as the `string | null`
|
||||
* union @types/node declares — POSIX always answers the string arm;
|
||||
* null is Node's Windows answer), homedir = pw_dir (the PASSWD home,
|
||||
* NOT os.homedir's $HOME-first cascade — Node's own split). The record
|
||||
@@ -56,12 +57,12 @@ export function lowerOsUserInfoCall(
|
||||
} else if (f.name === "uid" && f.type.kind === "f64") {
|
||||
fields.push({
|
||||
name: f.name,
|
||||
value: { kind: "libCall", fn: "process.getuid", args: [], type: F64, loc },
|
||||
value: { kind: "libCall", fn: "os.userUid", args: [], type: F64, loc },
|
||||
});
|
||||
} else if (f.name === "gid" && f.type.kind === "f64") {
|
||||
fields.push({
|
||||
name: f.name,
|
||||
value: { kind: "libCall", fn: "process.getgid", args: [], type: F64, loc },
|
||||
value: { kind: "libCall", fn: "os.userGid", args: [], type: F64, loc },
|
||||
});
|
||||
} else if (f.name === "homedir" && f.type.kind === "string") {
|
||||
fields.push({
|
||||
|
||||
@@ -34,6 +34,7 @@ import { builtinFunctionValue } from "../lower-exprs.js";
|
||||
import { canonicalBuiltinModule } from "../../builtin-modules.js";
|
||||
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
|
||||
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";
|
||||
import { isCheckerPanic } from "../../../diagnostics/diagnostic.js";
|
||||
|
||||
/** Whether the process has an IPC channel, Node's test for defining
|
||||
* process.send and process.disconnect: a channel this fork child opened
|
||||
@@ -173,6 +174,70 @@ export function programReadsExitCode(lowerer: Lowerer): boolean {
|
||||
return found;
|
||||
}
|
||||
|
||||
/** A write-method replacement on a process output stream, its source site.
|
||||
* `stream` is null when the receiver is a first-class alias, whose type does
|
||||
* not say which of the two output streams it holds. */
|
||||
export interface StdioPatchSite {
|
||||
stream: "stdout" | "stderr" | null;
|
||||
loc: SrcLoc;
|
||||
}
|
||||
|
||||
/** Whether any program file replaces a process output stream's write
|
||||
* method: `process.stdout.write = …` / `process.stderr.write = …`, or the
|
||||
* same assignment through a first-class alias of the stream (a value whose
|
||||
* type maps to the process stream). Such a program routes its typed
|
||||
* stdout/stderr writes through the stream object, so the replacement sees
|
||||
* them with their arguments, as in Node. Cached per program; the first
|
||||
* site found is recorded for the executable planner's SC8020 check. */
|
||||
export function programPatchesStdio(lowerer: Lowerer): boolean {
|
||||
if (lowerer.patchesStdio !== null) return lowerer.patchesStdio;
|
||||
let found = false;
|
||||
// undefined: not an output stream; null: a first-class alias of one.
|
||||
const streamOf = (recv: ts.Expression): "stdout" | "stderr" | null | undefined => {
|
||||
if (ts.isPropertyAccessExpression(recv)) {
|
||||
const direct = lowerer.stdlibGlobalMember(recv, "process");
|
||||
if (direct === "stdout" || direct === "stderr") return direct;
|
||||
}
|
||||
// An arbitrary receiver's mapping is a query, never a commit: a checker
|
||||
// panic (the type has no readable form) is not a patch site. The type
|
||||
// cannot say WHICH output stream an alias holds.
|
||||
try {
|
||||
return lowerer.mapTypeOf(lowerer.typeOf(recv))?.kind === "procStream" ? null : undefined;
|
||||
} catch (e) {
|
||||
if (!isCheckerPanic(e)) throw e;
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const walk = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(node.left) &&
|
||||
node.left.name.text === "write"
|
||||
) {
|
||||
const recv = node.left.expression;
|
||||
const direct = ts.isPropertyAccessExpression(recv)
|
||||
? lowerer.stdlibGlobalMember(recv, "process")
|
||||
: null;
|
||||
const stream = direct === "stdout" || direct === "stderr" ? direct : streamOf(recv);
|
||||
if (stream !== undefined) {
|
||||
found = true;
|
||||
lowerer.stdioPatchSites.push({ stream, loc: locOf(node) });
|
||||
return;
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, walk);
|
||||
};
|
||||
for (const sf of lowerer.program.getImplementationSourceFiles()) {
|
||||
if (sf.isDeclarationFile) continue;
|
||||
walk(sf);
|
||||
if (found) break;
|
||||
}
|
||||
lowerer.patchesStdio = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The pinned Node release the runtime reports as process.versions.node
|
||||
* (packages/runtime/src/scr_lib.c SCR_NODE_COMPAT_VERSION, kept equal to
|
||||
* internal/compatibility/node-v24.json by test/node-pin.test.ts). */
|
||||
@@ -247,12 +312,13 @@ export function lowerProcStreamMethodCall(
|
||||
}
|
||||
|
||||
/** Standard streams expose optional terminal properties: pipes have no
|
||||
* isTTY, columns, or rows. The declarations do not prove their presence. */
|
||||
* isTTY, columns, or rows, and only a TTY stdin has a raw-mode flag. The
|
||||
* declarations do not prove their presence. */
|
||||
export function isOptionalProcessStreamProperty(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
): boolean {
|
||||
if (!["isTTY", "columns", "rows"].includes(expr.name.text)) return false;
|
||||
if (!["isTTY", "columns", "rows", "isRaw"].includes(expr.name.text)) return false;
|
||||
let recv = expr.expression;
|
||||
while (ts.isParenthesizedExpression(recv) || ts.isAsExpression(recv) || ts.isTypeAssertion(recv))
|
||||
recv = recv.expression;
|
||||
@@ -261,7 +327,7 @@ export function isOptionalProcessStreamProperty(
|
||||
if (
|
||||
stream === "stdout" ||
|
||||
stream === "stderr" ||
|
||||
(stream === "stdin" && expr.name.text === "isTTY")
|
||||
(stream === "stdin" && (expr.name.text === "isTTY" || expr.name.text === "isRaw"))
|
||||
)
|
||||
return true;
|
||||
}
|
||||
@@ -341,6 +407,26 @@ export function lowerProcessStreamProperty(
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
// The write method as a VALUE in TypeScript (`const original =
|
||||
// process.stdout.write`, `process.stdout.write.bind(process.stdout)`):
|
||||
// the stream object's current write slot, a checked-dynamic function
|
||||
// (the native method until a program replaces it).
|
||||
if (member === "write" && (direct === "stdout" || direct === "stderr")) {
|
||||
const loc = locOf(expr);
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "process.stdioSlot",
|
||||
args: [numLit(direct === "stdout" ? 1 : 2, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
key: strLit("write", locOf(expr.name)),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `if (process.stdin.setRawMode)`: Node's stdin has the method only
|
||||
// when it is a TTY. The runtime's stream object answers the same
|
||||
// presence (the method or undefined); calls keep their direct lowering.
|
||||
@@ -357,7 +443,11 @@ export function lowerProcessStreamProperty(
|
||||
member !== "isTTY" &&
|
||||
member !== "columns" &&
|
||||
member !== "rows" &&
|
||||
member !== "writableLength"
|
||||
member !== "writableLength" &&
|
||||
member !== "fd" &&
|
||||
member !== "isRaw" &&
|
||||
member !== "writableHighWaterMark" &&
|
||||
member !== "writableNeedDrain"
|
||||
)
|
||||
return null;
|
||||
let recv: ts.Expression = expr.expression;
|
||||
@@ -391,19 +481,31 @@ export function lowerProcessStreamProperty(
|
||||
// nothing is ever queued and Node's number is always 0. The other
|
||||
// members stay keyed reads — the runtime stream answers their live
|
||||
// values (isatty columns/rows, stdin readableLength).
|
||||
if (member === "writableLength") {
|
||||
if (
|
||||
member === "writableLength" ||
|
||||
member === "writableHighWaterMark" ||
|
||||
member === "writableNeedDrain"
|
||||
) {
|
||||
// The receiver still EVALUATES: a call or property read that hands
|
||||
// back the stream runs its own side effects — and throws where Node
|
||||
// throws — before the constant answers, exactly as the read of the
|
||||
// member would. A pure receiver (the alias above) folds away.
|
||||
const answer: IrExpr = { kind: "numLit", value: 0, type: F64, loc: locOf(expr) };
|
||||
const answer: IrExpr =
|
||||
member === "writableNeedDrain"
|
||||
? { kind: "boolLit", value: false, type: BOOL, loc: locOf(expr) }
|
||||
: {
|
||||
kind: "numLit",
|
||||
value: member === "writableLength" ? 0 : 65536,
|
||||
type: F64,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
const effect = lowerer.lowerExpr(expr.expression);
|
||||
if (isSafeToDiscard(effect)) return answer;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: effect, loc: locOf(expr.expression) }],
|
||||
result: answer,
|
||||
type: F64,
|
||||
type: answer.type,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
@@ -435,6 +537,16 @@ export function lowerProcessStreamProperty(
|
||||
// true for the same reason). Backpressure probes read 0 and stay
|
||||
// correct; the write itself never defers.
|
||||
if (member === "writableLength") return { kind: "numLit", value: 0, type: F64, loc };
|
||||
// Node's default high-water mark for the process streams, and the
|
||||
// drain flag a synchronous write never sets.
|
||||
if (member === "writableHighWaterMark") {
|
||||
if (stream === "stdin") return null;
|
||||
return { kind: "numLit", value: 65536, type: F64, loc };
|
||||
}
|
||||
if (member === "writableNeedDrain") {
|
||||
if (stream === "stdin") return null;
|
||||
return { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
}
|
||||
if (member === "isTTY") {
|
||||
const type = lowerer.withUndefinedArm(BOOL);
|
||||
return {
|
||||
@@ -449,7 +561,29 @@ export function lowerProcessStreamProperty(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (stream === "stdin") return null;
|
||||
if (member === "fd") {
|
||||
// The process streams are built over fd 0/1/2 (Node answers those on
|
||||
// a TTY, a pipe, and a redirected file alike).
|
||||
return {
|
||||
kind: "numLit",
|
||||
value: stream === "stdin" ? 0 : stream === "stdout" ? 1 : 2,
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (stream === "stdin") {
|
||||
if (member !== "isRaw") return null;
|
||||
// Node answers the raw-mode flag of a TTY stdin and undefined on
|
||||
// anything else (a pipe-backed stdin); the runtime sentinel carries
|
||||
// both (scr_process_stdin_is_raw).
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "process.stdinIsRaw",
|
||||
args: [],
|
||||
type: lowerer.withUndefinedArm(BOOL),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const want = lowerer.withUndefinedArm(F64);
|
||||
return {
|
||||
kind: "libCall",
|
||||
@@ -569,9 +703,10 @@ export function lowerProcessProperty(
|
||||
// The capability-probe members that honestly DON'T EXIST in a
|
||||
// compiled binary — each reads undefined (their declared types carry
|
||||
// the undefined arm), so feature probes take their documented
|
||||
// fallbacks: no gyp build config (process.config.variables.* — no ICU,
|
||||
// no QUIC — and process.config.target_defaults), no feature flags
|
||||
// (process.features.* — no inspector, not a debug build).
|
||||
// fallbacks: no gyp build config (process.config.variables.* — no QUIC
|
||||
// — and process.config.target_defaults; v8_enable_i18n_support is the
|
||||
// exception below, answered from the binary's linked ICU), no feature
|
||||
// flags (process.features.* — no inspector, not a debug build).
|
||||
if (!expr.questionDotToken && ts.isPropertyAccessExpression(expr.expression)) {
|
||||
const container = lowerer.stdlibGlobalMember(expr.expression, "process");
|
||||
if (container === "features") {
|
||||
@@ -594,6 +729,13 @@ export function lowerProcessProperty(
|
||||
expr.expression.name.text === "variables" &&
|
||||
lowerer.stdlibGlobalMember(expr.expression.expression, "process") === "config"
|
||||
) {
|
||||
// The one variable a compiled binary can answer truthfully:
|
||||
// v8_enable_i18n_support. Node bundles ICU and answers the number 1;
|
||||
// this read selects the `icu` runtime feature (scr_intl.c), so a
|
||||
// binary that reports ICU is a binary that carries it.
|
||||
if (expr.name.text === "v8_enable_i18n_support") {
|
||||
return { kind: "libCall", fn: "icu.i18nSupport", args: [], type: F64, loc: locOf(expr) };
|
||||
}
|
||||
return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc: locOf(expr) };
|
||||
}
|
||||
}
|
||||
@@ -920,6 +1062,58 @@ export function lowerProcessMethodCall(
|
||||
}
|
||||
return { kind: "libCall", fn: "process.stdinSetRawMode", args: [mode], type: VOID, loc };
|
||||
}
|
||||
// process.stdout/stderr.on("drain", fn) and its spellings: Node emits
|
||||
// 'drain' only after a write() returned false, and these writes are
|
||||
// synchronous (they never return false), so the listener can never run.
|
||||
// Registering it is therefore observably a no-op: the receiver and the
|
||||
// listener are evaluated for their effects and nothing is retained.
|
||||
// listenerCount/listeners/eventNames on the process streams stay
|
||||
// refused, so the missing registration cannot be observed.
|
||||
if (
|
||||
[
|
||||
"on",
|
||||
"once",
|
||||
"off",
|
||||
"addListener",
|
||||
"removeListener",
|
||||
"prependListener",
|
||||
"prependOnceListener",
|
||||
].includes(access.name.text) &&
|
||||
ts.isPropertyAccessExpression(access.expression) &&
|
||||
call.arguments.nodes.length === 2 &&
|
||||
!call.arguments.nodes.some(ts.isSpreadElement)
|
||||
) {
|
||||
const stream = lowerer.stdlibGlobalMember(access.expression, "process");
|
||||
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
|
||||
if (
|
||||
(stream === "stdout" || stream === "stderr") &&
|
||||
evT.isStringLiteralType() &&
|
||||
evT.value === "drain"
|
||||
) {
|
||||
const loc = locOf(call);
|
||||
const parent = call.parent;
|
||||
const discarded =
|
||||
ts.isExpressionStatement(parent) || (ts.isArrowFunction(parent) && parent.body === call);
|
||||
if (!discarded) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`using the result of process.${stream}.${access.name.text}("drain", ...) (register it as its own statement)`,
|
||||
);
|
||||
}
|
||||
const stmts: IrStmt[] = [];
|
||||
for (const arg of call.arguments.nodes) {
|
||||
const value = lowerer.lowerExprExpecting(arg, DYN);
|
||||
if (!isSafeToDiscard(value)) stmts.push({ kind: "exprStmt", expr: value, loc: value.loc });
|
||||
}
|
||||
// The call's value is discarded (statement position or a void arrow
|
||||
// body), so the lowering is the evaluated arguments alone; a bare
|
||||
// unitLit has no standalone IR value, so the no-op result is the
|
||||
// dynamic undefined a discarded call leaves.
|
||||
const result: IrExpr = dynUndefinedExpr(loc);
|
||||
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: DYN, loc };
|
||||
}
|
||||
}
|
||||
// process.stdout.on("resize", cb) (and once/off/removeListener/
|
||||
// addListener, stderr too): the terminal resize event, registered on the
|
||||
// runtime's process stream object (a SIGWINCH listener that emits when a
|
||||
@@ -1078,6 +1272,33 @@ export function lowerProcessMethodCall(
|
||||
if (stream === "stdout" || stream === "stderr") {
|
||||
const loc = locOf(call);
|
||||
const args = call.arguments.nodes;
|
||||
// In a program that replaces a process stream's write, a write call is
|
||||
// what Node makes it: a call of the stream's CURRENT write property,
|
||||
// with the arguments as passed (the replacement sees byte chunks,
|
||||
// encodings and callbacks). The native method behind the slot is the
|
||||
// same synchronous write, so an unpatched stream behaves as before.
|
||||
if (programPatchesStdio(lowerer) && !args.some(ts.isSpreadElement)) {
|
||||
const handle: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "process.stdioSlot",
|
||||
args: [numLit(stream === "stdout" ? 1 : 2, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const invoke: IrExpr = {
|
||||
kind: "dynInvoke",
|
||||
recv: handle,
|
||||
method: "write",
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// The declared boolean: a replacement returning something else
|
||||
// throws at a use that needs the boolean (types do not lie); a
|
||||
// discarded result is not converted.
|
||||
return resultIsDiscarded(call) ? invoke : lowerer.coerceToExpected(invoke, BOOL);
|
||||
}
|
||||
if (args.length < 1 || args.length > 3 || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`process.${stream}.write with ${args.length} arguments`,
|
||||
@@ -1174,6 +1395,24 @@ export function lowerProcessMethodCall(
|
||||
if (data.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
data = { kind: "dynCheck", value: data, type: STRING, loc };
|
||||
}
|
||||
// A chunk known only at run time (`any`, or a string | Uint8Array
|
||||
// union): the runtime dispatches on the value — a string or a
|
||||
// Uint8Array/Buffer writes, null throws ERR_STREAM_NULL_VALUES, any
|
||||
// other value throws ERR_INVALID_ARG_TYPE, as Node's Writable.write
|
||||
// does before writing anything. Only the one-argument form.
|
||||
if (
|
||||
args.length === 1 &&
|
||||
(data.type.kind === "dyn" || data.type.kind === "union") &&
|
||||
(data.type.kind === "dyn" || lowerer.dynConvertible(data.type))
|
||||
) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "process.stdioWriteDyn",
|
||||
args: [numLit(stream === "stdout" ? 1 : 2, loc), lowerer.coerceToExpected(data, DYN)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
|
||||
if (!isBytes && data.type.kind !== "string") {
|
||||
lowerer.noLowering(
|
||||
@@ -1312,13 +1551,33 @@ export function lowerProcessMethodCall(
|
||||
// the listener to a const so both sites see the same value), and
|
||||
// `removeListener` IS `off` — Node aliases them.
|
||||
const isOff = member === "off" || member === "removeListener";
|
||||
if (member === "on" || member === "once" || isOff) {
|
||||
// process.prependListener(event, cb): Node's front-of-list registration.
|
||||
// The signal, 'exit', and 'uncaughtException'/'uncaughtExceptionMonitor'
|
||||
// registries have a prepend entry; every other family registers by
|
||||
// appending only, so it refuses rather than reordering silently.
|
||||
// prependOnceListener has no runtime entry at all.
|
||||
const isPrepend = member === "prependListener";
|
||||
if (member === "prependOnceListener") {
|
||||
lowerer.noLowering(
|
||||
"process.prependOnceListener",
|
||||
access,
|
||||
"use prependListener or once (prependOnceListener has no lowering)",
|
||||
);
|
||||
}
|
||||
if (member === "on" || member === "once" || isOff || isPrepend) {
|
||||
if (call.arguments.nodes.length !== 2) {
|
||||
lowerer.noLowering(`process.${member} with ${call.arguments.nodes.length} arguments`, call);
|
||||
}
|
||||
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
|
||||
const event = evT.isStringLiteralType() ? evT.value : null;
|
||||
if ((event === "message" || event === "disconnect") && !isOff) {
|
||||
if (isPrepend) {
|
||||
lowerer.noLowering(
|
||||
`process.prependListener("${event}", ...)`,
|
||||
call,
|
||||
"prependListener is supported for signals, 'exit', and 'uncaughtException' (register these families with on/once)",
|
||||
);
|
||||
}
|
||||
if (!resultIsDiscarded(call)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1346,8 +1605,16 @@ export function lowerProcessMethodCall(
|
||||
const monitor = boolLit(event === "uncaughtExceptionMonitor", loc);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: isOff ? "process.offUncaughtException" : "process.onUncaughtException",
|
||||
args: isOff ? [cb, monitor] : [cb, boolLit(member === "once", loc), monitor],
|
||||
fn: isOff
|
||||
? "process.offUncaughtException"
|
||||
: isPrepend
|
||||
? "process.prependUncaughtException"
|
||||
: "process.onUncaughtException",
|
||||
args: isOff
|
||||
? [cb, monitor]
|
||||
: isPrepend
|
||||
? [cb, monitor]
|
||||
: [cb, boolLit(member === "once", loc), monitor],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
@@ -1361,6 +1628,13 @@ export function lowerProcessMethodCall(
|
||||
// a handler attached after delivery fires it once, synchronously
|
||||
// with the promise.
|
||||
if (event === "unhandledRejection" || event === "rejectionHandled") {
|
||||
if (isPrepend) {
|
||||
lowerer.noLowering(
|
||||
`process.prependListener("${event}", ...)`,
|
||||
call,
|
||||
"prependListener is supported for signals, 'exit', and 'uncaughtException' (register these families with on/once)",
|
||||
);
|
||||
}
|
||||
if (!resultIsDiscarded(call)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1384,6 +1658,13 @@ export function lowerProcessMethodCall(
|
||||
// util.deprecate dispatch on the next tick. off/removeListener remove
|
||||
// by closure identity.
|
||||
if (event === "warning" && (member === "on" || isOff)) {
|
||||
if (isPrepend) {
|
||||
lowerer.noLowering(
|
||||
'process.prependListener("warning", ...)',
|
||||
call,
|
||||
"prependListener is supported for signals, 'exit', and 'uncaughtException' (register this family with on)",
|
||||
);
|
||||
}
|
||||
if (!resultIsDiscarded(call)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1418,8 +1699,12 @@ export function lowerProcessMethodCall(
|
||||
const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: isOff ? "process.offSignal" : "process.onSignal",
|
||||
args: isOff ? [signal, cb] : [signal, cb, boolLit(member === "once", loc)],
|
||||
fn: isOff ? "process.offSignal" : isPrepend ? "process.prependSignal" : "process.onSignal",
|
||||
args: isOff
|
||||
? [signal, cb]
|
||||
: isPrepend
|
||||
? [signal, cb]
|
||||
: [signal, cb, boolLit(member === "once", loc)],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
@@ -1489,6 +1774,9 @@ export function lowerProcessMethodCall(
|
||||
if (isOff) {
|
||||
return { kind: "libCall", fn: "process.offExit", args: [cb], type: VOID, loc };
|
||||
}
|
||||
if (isPrepend) {
|
||||
return { kind: "libCall", fn: "process.prependExit", args: [cb], type: VOID, loc };
|
||||
}
|
||||
return { kind: "libCall", fn: "process.onExit", args: [cb, onceArg], type: VOID, loc };
|
||||
}
|
||||
// process.emitWarning(...): the argument vector crosses as ONE dyn
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type IrExpr,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
VOID,
|
||||
arrayOf,
|
||||
isUnitType,
|
||||
} from "../../../ir/ir.js";
|
||||
@@ -62,6 +63,72 @@ export function lowerReflectCall(
|
||||
}
|
||||
if (call.questionDotToken) return null;
|
||||
const reflectMember = lowerer.stdlibGlobalMember(access, "Reflect");
|
||||
// Reflect.has(target, key) — the `in` operator's presence query over
|
||||
// the target's checked-dynamic view (own and inherited names, proxy
|
||||
// `has` traps included). A primitive target is Node's TypeError
|
||||
// ("Reflect.has called on non-object"): a statically primitive target
|
||||
// is refused, and a target held as any, unknown or a union is checked
|
||||
// at run time (dyn.reflectHasTarget).
|
||||
if (
|
||||
reflectMember === "has" &&
|
||||
call.arguments.nodes.length === 2 &&
|
||||
!call.arguments.nodes.some(ts.isSpreadElement)
|
||||
) {
|
||||
const loc = locOf(call);
|
||||
const target = lowerer.lowerExpr(call.arguments.nodes[0]!);
|
||||
const primitive =
|
||||
isUnitType(target.type) ||
|
||||
["bool", "f64", "string", "bigint", "symbol"].includes(target.type.kind);
|
||||
if (primitive)
|
||||
lowerer.noLowering(
|
||||
"Reflect.has on a primitive target",
|
||||
call.arguments.nodes[0]!,
|
||||
"Node throws a TypeError for a primitive target; narrow to an object first",
|
||||
);
|
||||
if (!lowerer.dynConvertible(target.type))
|
||||
lowerer.noLowering("Reflect.has on this target", call.arguments.nodes[0]!);
|
||||
const key = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN);
|
||||
if (target.type.kind !== "dyn" && target.type.kind !== "union") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.hasKeyComputed",
|
||||
args: [lowerer.coerceToExpected(target, DYN), key],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A target held as any, unknown or a union may be a primitive at run
|
||||
// time: both arguments are evaluated, then the target is checked.
|
||||
const targetLocal = lowerer.declareHiddenLocal("%reflectHasTarget", DYN);
|
||||
const keyLocal = lowerer.declareHiddenLocal("%reflectHasKey", DYN);
|
||||
const targetRef = varRef(targetLocal.id, DYN, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: targetLocal.id,
|
||||
init: lowerer.coerceToExpected(target, DYN),
|
||||
loc,
|
||||
},
|
||||
{ kind: "varDecl", localId: keyLocal.id, init: key, loc },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "dyn.reflectHasTarget", args: [targetRef], type: VOID, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.hasKeyComputed",
|
||||
args: [targetRef, varRef(keyLocal.id, DYN, loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Reflect.getOwnPropertyDescriptor and Reflect.defineProperty over
|
||||
// checked objects (execa's Promise mixin): the Object operations, with
|
||||
// defineProperty answering true. A non-object target throws the Object
|
||||
|
||||
@@ -5,6 +5,7 @@ import { staticTextDecoderEncoding } from "../text-decoder-encoding.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -121,6 +122,24 @@ export function lowerStringDecoderMethodCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A chunk that crossed the checked-dynamic boundary (a stream "data"
|
||||
// payload): the runtime answers Node's write — strings pass through
|
||||
// unchanged, ArrayBufferViews decode, anything else throws
|
||||
// ERR_INVALID_ARG_TYPE.
|
||||
if (chunk.type.kind === "dyn") {
|
||||
return {
|
||||
kind: "call",
|
||||
callee: stringDecoderHelper(
|
||||
lowerer,
|
||||
name === "end" ? "endDyn" : "writeDyn",
|
||||
receiver.type.shapeId,
|
||||
loc,
|
||||
),
|
||||
args: [receiver, chunk],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
chunk = checkedOptionalBuiltinArm(lowerer, chunk, BYTES_U8) ?? chunk;
|
||||
if (chunk.type.kind === "bytes" && chunk.type.elem !== "u8") {
|
||||
chunk = {
|
||||
@@ -153,7 +172,7 @@ export function lowerStringDecoderMethodCall(
|
||||
* the pure strdec.* libCalls. */
|
||||
function stringDecoderHelper(
|
||||
lowerer: Lowerer,
|
||||
op: "write" | "end" | "endChunk" | "endString" | "writeString",
|
||||
op: "write" | "end" | "endChunk" | "endString" | "writeString" | "writeDyn" | "endDyn",
|
||||
shapeId: string,
|
||||
loc: SrcLoc,
|
||||
): string {
|
||||
@@ -245,6 +264,100 @@ function stringDecoderHelper(
|
||||
{ kind: "return", value: varRef("s.0", STRING, loc), loc },
|
||||
];
|
||||
}
|
||||
if (op === "writeDyn" || op === "endDyn") {
|
||||
const dynChunk = { localId: "chunk.0", name: "chunk", type: DYN };
|
||||
params.push(dynChunk);
|
||||
locals.splice(1, 0, { id: "chunk.0", name: "chunk", type: DYN, mutable: false });
|
||||
const writeDyn: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "strdec.writeDyn",
|
||||
args: [encRead(), pendingRead(), varRef("chunk.0", DYN, loc)],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
body = [
|
||||
{ kind: "varDecl", localId: "s.0", init: writeDyn, loc },
|
||||
{
|
||||
kind: "recordSet",
|
||||
obj: varRef("d.0", recT, loc),
|
||||
shapeId,
|
||||
field: "%pending",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "strdec.nextDyn",
|
||||
args: [encRead(), pendingRead(), varRef("chunk.0", DYN, loc)],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: varRef("s.0", STRING, loc), loc },
|
||||
];
|
||||
if (op === "endDyn") {
|
||||
// end(buf) is write(buf) followed by the flush of whatever the write
|
||||
// left buffered (Node's end).
|
||||
locals.push({ id: "tail.0", name: "tail", type: STRING, mutable: false });
|
||||
body.splice(
|
||||
body.length - 1,
|
||||
0,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "tail.0",
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "strdec.end",
|
||||
args: [encRead(), pendingRead()],
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "recordSet",
|
||||
obj: varRef("d.0", recT, loc),
|
||||
shapeId,
|
||||
field: "%pending",
|
||||
value: { kind: "numLit", value: 0, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
);
|
||||
body[body.length - 1] = {
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "strConcat",
|
||||
left: varRef("s.0", STRING, loc),
|
||||
right: varRef("tail.0", STRING, loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
// end(undefined) only flushes, as Node's end skips the write when
|
||||
// its chunk is undefined.
|
||||
const [write, next] = body.splice(0, 2, {
|
||||
kind: "varDecl",
|
||||
localId: "s.0",
|
||||
init: { kind: "strLit", value: "", type: STRING, loc },
|
||||
loc,
|
||||
});
|
||||
if (write?.kind !== "varDecl" || write.init === null || next === undefined)
|
||||
throw new InternalCompilerError("StringDecoder.end helper without its write");
|
||||
locals.find((local) => local.id === "s.0")!.mutable = true;
|
||||
body.splice(1, 0, {
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "dynTest",
|
||||
test: "undefined",
|
||||
value: varRef("chunk.0", DYN, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [],
|
||||
else_: [{ kind: "assign", localId: "s.0", value: write.init, loc }, next],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (op === "endString" || op === "writeString") {
|
||||
const inputT = STRING;
|
||||
params.push({ localId: "chunk.0", name: "chunk", type: inputT });
|
||||
|
||||
@@ -228,11 +228,30 @@ function lowerSearchParamsInit(lowerer: Lowerer, arg: ts.Expression, loc: SrcLoc
|
||||
if (ts.isObjectLiteralExpression(arg)) {
|
||||
let acc: IrExpr = { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc };
|
||||
for (const prop of arg.properties.nodes) {
|
||||
// `{ model }` appends the binding's value under its own name, like
|
||||
// the `{ model: model }` spelling.
|
||||
if (ts.isShorthandPropertyAssignment(prop)) {
|
||||
const keyExpr: IrExpr = {
|
||||
kind: "strLit",
|
||||
value: (prop.name as ts.Identifier | ts.StringLiteral).text,
|
||||
type: STRING,
|
||||
loc: locOf(prop.name),
|
||||
};
|
||||
const value = lowerer.coerceInto(prop.name, lowerer.lowerShorthandValue(prop), STRING);
|
||||
acc = {
|
||||
kind: "libCall",
|
||||
fn: "sp.with",
|
||||
args: [acc, keyExpr, value],
|
||||
type: SEARCH_PARAMS_T,
|
||||
loc,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (!ts.isPropertyAssignment(prop) || prop.name === undefined) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
"URLSearchParams record inits with spreads, accessors, or shorthand entries",
|
||||
"URLSearchParams record inits with spreads or accessors",
|
||||
);
|
||||
}
|
||||
let key: string;
|
||||
|
||||
@@ -30,11 +30,11 @@ const HEAP_STATISTICS_FIELDS = [
|
||||
];
|
||||
|
||||
/** `v8.getHeapStatistics()`: one memoryUsage sample (the runtime heap's
|
||||
* total and used bytes), with the machine's memory as the heap limit (the
|
||||
* runtime heap has no fixed ceiling) and the available size the limit
|
||||
* minus used bytes. V8-only figures (executable code space, malloced
|
||||
* memory, global handles, external memory, zapping) read 0, and one
|
||||
* native context exists. The record is the call site's interned HeapInfo
|
||||
* total and used bytes, and external memory, memoryUsage's `external`),
|
||||
* with the machine's memory as the heap limit (the runtime heap has no
|
||||
* fixed ceiling) and the available size the limit minus used bytes.
|
||||
* V8-only figures (executable code space, malloced memory, global handles,
|
||||
* zapping) read 0, and one native context exists. The record is the call site's interned HeapInfo
|
||||
* shape, built by a shared helper per shape. */
|
||||
export function lowerV8HeapStatisticsCall(
|
||||
lowerer: Lowerer,
|
||||
@@ -73,6 +73,8 @@ export function lowerV8HeapStatisticsCall(
|
||||
return sample(1);
|
||||
case "used_heap_size":
|
||||
return sample(2);
|
||||
case "external_memory":
|
||||
return sample(3);
|
||||
case "heap_size_limit":
|
||||
return limit();
|
||||
case "total_available_size":
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { staticFetchMembers } from "../../../compat/fetch-profile.js";
|
||||
import {
|
||||
READABLE_STREAM_WEB_VIEW_CALLS,
|
||||
staticFetchMembers,
|
||||
} from "../../../compat/fetch-profile.js";
|
||||
import { numLit } from "../../../ir/build.js";
|
||||
import { everyStmt } from "../../../ir/traverse.js";
|
||||
import {
|
||||
@@ -16,6 +19,7 @@ import { constituentTypes } from "../../ts7/checker.js";
|
||||
import { ENTRY_NAME, type Lowerer } from "../lowerer.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
import { streamWebImportOf } from "./module-bindings.js";
|
||||
import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
|
||||
|
||||
/* The web stream classes in JavaScript sources: the globals and the
|
||||
* node:stream/web exports, which are the same constructors. Each reference
|
||||
@@ -92,6 +96,60 @@ export function programNamesWebStreamClass(lowerer: Lowerer): boolean {
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The classes a TypeScript source constructs through the web stream
|
||||
* classes once it uses an operation the native streams lack. */
|
||||
const TYPED_CLASSES: ReadonlySet<string> = new Set([
|
||||
"ReadableStream",
|
||||
"WritableStream",
|
||||
"TransformStream",
|
||||
]);
|
||||
const WEB_VIEW_CALLS: ReadonlySet<string> = new Set(READABLE_STREAM_WEB_VIEW_CALLS);
|
||||
|
||||
/** Whether a TypeScript source of the program uses a web stream operation
|
||||
* the native streams lack: a ReadableStream tee(), pipeTo() or
|
||||
* pipeThrough() call, a WritableStream constructor, or a TransformStream
|
||||
* constructor with arguments (a transformer or queuing strategies). Such a
|
||||
* program builds its ReadableStream, WritableStream and TransformStream
|
||||
* values with the web stream classes, as JavaScript does; a program that
|
||||
* uses only getReader(), cancel() and the identity transform keeps the
|
||||
* native streams. */
|
||||
export function programUsesTypedWebStreams(lowerer: Lowerer): boolean {
|
||||
if (lowerer.typedWebStreams !== undefined) return lowerer.typedWebStreams;
|
||||
let found = false;
|
||||
for (const file of lowerer.program.getSourceFiles()) {
|
||||
if (file.isDeclarationFile || isJsSourceFile(file)) continue;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
const member = staticMemberName(node);
|
||||
if (member !== null && WEB_VIEW_CALLS.has(member) && isReadableStreamMember(lowerer, node))
|
||||
found = true;
|
||||
else if (ts.isNewExpression(node)) {
|
||||
const name = stdlibGlobalNameOf(lowerer, unparenthesized(node.expression));
|
||||
if (
|
||||
name === "WritableStream" ||
|
||||
(name === "TransformStream" && (node.arguments?.nodes.length ?? 0) > 0)
|
||||
)
|
||||
found = true;
|
||||
}
|
||||
return found ? "stop" : undefined;
|
||||
});
|
||||
if (found) break;
|
||||
}
|
||||
lowerer.typedWebStreams = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The class a TypeScript `new` expression constructs through the web
|
||||
* stream classes (programUsesTypedWebStreams), as its index in
|
||||
* WEB_STREAM_CLASSES; null otherwise. */
|
||||
function typedWebStreamClassKindOf(lowerer: Lowerer, node: ts.Expression): number | null {
|
||||
const expr = unparenthesized(node);
|
||||
if (!ts.isIdentifier(expr) || isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
|
||||
const name = streamWebImportOf(lowerer, expr) ?? stdlibGlobalNameOf(lowerer, expr);
|
||||
if (name === null || !TYPED_CLASSES.has(name) || !programUsesTypedWebStreams(lowerer))
|
||||
return null;
|
||||
return CLASS_NAMES.indexOf(name);
|
||||
}
|
||||
|
||||
export function webStreamClassValue(kind: number, loc: SrcLoc): IrExpr {
|
||||
return { kind: "libCall", fn: "webstream.class", args: [numLit(kind, loc)], type: DYN, loc };
|
||||
}
|
||||
@@ -102,13 +160,24 @@ export function lowerWebStreamClassRead(lowerer: Lowerer, node: ts.Expression):
|
||||
return kind === null ? null : webStreamClassValue(kind, locOf(node));
|
||||
}
|
||||
|
||||
/** `new X(...args)` on one of the classes in JavaScript. */
|
||||
/** `new X(...args)` on one of the classes in JavaScript, and on
|
||||
* ReadableStream, WritableStream and TransformStream in a TypeScript
|
||||
* program that uses the web stream classes (programUsesTypedWebStreams).
|
||||
* A TypeScript source's object literal argument (a source, sink,
|
||||
* transformer or strategy) becomes a plain object with its methods. */
|
||||
export function lowerWebStreamNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
|
||||
const kind = webStreamClassKindOf(lowerer, expr.expression);
|
||||
const typed = webStreamClassKindOf(lowerer, expr.expression) === null;
|
||||
const kind = typed
|
||||
? typedWebStreamClassKindOf(lowerer, expr.expression)
|
||||
: webStreamClassKindOf(lowerer, expr.expression);
|
||||
if (kind === null) return null;
|
||||
const nodes = expr.arguments?.nodes ?? [];
|
||||
if (nodes.some(ts.isSpreadElement)) return null;
|
||||
const loc = locOf(expr);
|
||||
const argument = (arg: ts.Expression): IrExpr =>
|
||||
typed && ts.isObjectLiteralExpression(arg)
|
||||
? lowerDynObjectLiteral(lowerer, arg)
|
||||
: lowerer.lowerExprExpecting(arg, DYN);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.construct",
|
||||
@@ -116,7 +185,7 @@ export function lowerWebStreamNew(lowerer: Lowerer, expr: ts.NewExpression): IrE
|
||||
webStreamClassValue(kind, loc),
|
||||
{
|
||||
kind: "dynArrLit",
|
||||
elems: nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
elems: nodes.map(argument),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
@@ -244,10 +313,29 @@ function isReadableStreamType(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a member access resolves to a member of the stdlib
|
||||
* ReadableStream interface. */
|
||||
function isReadableStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
const member = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
type MemberAccess = ts.PropertyAccessExpression | ts.ElementAccessExpression;
|
||||
|
||||
/** The member a property access names, or an element access with a string
|
||||
* literal key (`body.tee` and `body["tee"]`); null for any other node. */
|
||||
function staticMemberName(node: ts.Node): string | null {
|
||||
if (ts.isPropertyAccessExpression(node)) return node.name.text;
|
||||
if (ts.isElementAccessExpression(node)) {
|
||||
const key = node.argumentExpression;
|
||||
if (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) return key.text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether a member access (staticMemberName) resolves to a member of the
|
||||
* stdlib ReadableStream interface. */
|
||||
function isReadableStreamMember(lowerer: Lowerer, access: ts.Node): boolean {
|
||||
const key = ts.isPropertyAccessExpression(access)
|
||||
? access.name
|
||||
: ts.isElementAccessExpression(access)
|
||||
? access.argumentExpression
|
||||
: null;
|
||||
if (key === null) return false;
|
||||
const member = lowerer.checker.getSymbolAtLocation(key);
|
||||
if (member === undefined || !lowerer.isStdlibSymbol(member)) return false;
|
||||
let owner: ts.Node | undefined = member.declarations[0]?.parent;
|
||||
while (owner !== undefined && !ts.isInterfaceDeclaration(owner)) owner = owner.parent;
|
||||
@@ -285,6 +373,7 @@ function programLinksFetch(functions: IrFunction[]): boolean {
|
||||
* count in a program that links the fetch unit, since the value can be a
|
||||
* fetch body. */
|
||||
function programUsesWebStreamOnlyMembers(lowerer: Lowerer, functions: IrFunction[]): boolean {
|
||||
if (programUsesTypedWebStreams(lowerer)) return true;
|
||||
const members = staticFetchMembers().readableStreamUnsupported;
|
||||
const iterates = members.has("[Symbol.asyncIterator]");
|
||||
let untyped = false;
|
||||
@@ -293,9 +382,10 @@ function programUsesWebStreamOnlyMembers(lowerer: Lowerer, functions: IrFunction
|
||||
let found = false;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
let receiver: ts.Type | null = null;
|
||||
if (ts.isPropertyAccessExpression(node) && members.has(node.name.text)) {
|
||||
const member = staticMemberName(node);
|
||||
if (member !== null && members.has(member)) {
|
||||
if (isReadableStreamMember(lowerer, node)) found = true;
|
||||
else receiver = lowerer.typeOf(node.expression);
|
||||
else receiver = lowerer.typeOf((node as MemberAccess).expression);
|
||||
} else if (iterates && ts.isForOfStatement(node) && node.awaitModifier !== undefined) {
|
||||
receiver = lowerer.typeOf(node.expression);
|
||||
if (isReadableStreamType(lowerer, receiver)) found = true;
|
||||
@@ -310,7 +400,9 @@ function programUsesWebStreamOnlyMembers(lowerer: Lowerer, functions: IrFunction
|
||||
}
|
||||
|
||||
/** Links the web stream classes into a program whose JavaScript uses a
|
||||
* ReadableStream member the fetch unit's own stream lacks: a fetch body's
|
||||
* ReadableStream member the fetch unit's own stream lacks, or whose
|
||||
* TypeScript uses the web stream classes (programUsesTypedWebStreams): a
|
||||
* fetch body's or a native stream's
|
||||
* handle stands for a web ReadableStream once such a member is used
|
||||
* (scr_fetch.c, sf_stream_web_view). %main installs the classes first. A
|
||||
* program that names one of the classes links them already. */
|
||||
|
||||
@@ -630,6 +630,41 @@ export function checkDefaultParamBodyType(
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a lambda's OWN signature maps to its completed ABI position for
|
||||
* position — the contract its reads carry (mapType spells `x?: T` as the
|
||||
* `T | undefined` slot). Two positions can escape the mapped-TYPE
|
||||
* comparison in requireExactArityValue: a parameter the checker spells
|
||||
* `any` holds the checked-dynamic value that mapType leaves unmapped when
|
||||
* its spelling is context-free, and a *contextually* typed lambda's own
|
||||
* signature need not be the slot's (a comparator with a surplus optional
|
||||
* parameter under a two-parameter slot). */
|
||||
function lambdaSpellsCompletedAbi(
|
||||
lowerer: Lowerer,
|
||||
lambda: ts.ArrowFunction | ts.FunctionExpression,
|
||||
funcType: IrType,
|
||||
): boolean {
|
||||
if (funcType.kind !== "func" || funcType.rest === true) return false;
|
||||
const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(lambda));
|
||||
if (sigs.length !== 1) return false;
|
||||
const sig = sigs[0]!;
|
||||
const params = sig.getParameters();
|
||||
if (params.length !== funcType.params.length) return false;
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const pTs = lowerer.checker.getTypeOfSymbol(params[i]!);
|
||||
const slot = funcType.params[i]!;
|
||||
if ((pTs.flags & ts.TypeFlags.Any) !== 0) {
|
||||
if (slot.kind !== "dyn") return false;
|
||||
continue;
|
||||
}
|
||||
const mappedParam = lowerer.mapTypeOf(pTs);
|
||||
if (!mappedParam || !typeEquals(mappedParam, slot)) return false;
|
||||
}
|
||||
const retTs = lowerer.checker.getReturnTypeOfSignature(sig);
|
||||
if ((retTs.flags & ts.TypeFlags.Any) !== 0) return funcType.ret.kind === "dyn";
|
||||
const mappedRet = lowerer.mapTypeOf(retTs);
|
||||
return mappedRet !== null && typeEquals(mappedRet, funcType.ret);
|
||||
}
|
||||
|
||||
/** DECISION (docs/ir.md): function VALUES carry one completed native ABI.
|
||||
* Optional/default parameters appear as literal `T | undefined` slots;
|
||||
* typed rest parameters appear as one trailing typed-array slot plus the
|
||||
@@ -675,6 +710,20 @@ export function requireExactArityValue(
|
||||
? lowerer.mapTypeOf(lowerer.typeOf(contextual))
|
||||
: null;
|
||||
if (mapped && typeEquals(mapped, funcType)) return;
|
||||
// A lambda whose OWN signature already spells the completed ABI is a
|
||||
// value: every read of it types it that way, and its slot converts it
|
||||
// (the surplus-parameter and undefined-arm adapters). The mapped-type
|
||||
// comparison above misses two such lambdas — one whose `any` parameter
|
||||
// mapType leaves unmapped (`(a: any, b: any, loose?: boolean) => number`
|
||||
// beside a two-parameter slot), and one whose signature is wider than
|
||||
// the contextual slot it is written under.
|
||||
if (
|
||||
contextual !== null &&
|
||||
(ts.isArrowFunction(contextual) || ts.isFunctionExpression(contextual)) &&
|
||||
lambdaSpellsCompletedAbi(lowerer, contextual, funcType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// An 'any' slot (`C.prototype.m = function (a, b) {…}` through an
|
||||
// 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
|
||||
@@ -1062,6 +1111,9 @@ export function lambdaSignature(
|
||||
type: DYN,
|
||||
mode: "required",
|
||||
}));
|
||||
// `arguments` holds the call's own tuple: a console line passes one
|
||||
// or two arguments whatever the replacement declares.
|
||||
appendArgumentsParameter(lowerer, node, shapes, "arguments");
|
||||
return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { DYN, type IrExpr } from "../../ir/ir.js";
|
||||
import { nodeThrowExpr } from "../../ir/build.js";
|
||||
import { probeNodeRequireRefusal } from "../npm.js";
|
||||
import { lowerIslandRequireCall } from "./lower-node-island.js";
|
||||
|
||||
export interface InlineRequire {
|
||||
call: ts.CallExpression;
|
||||
@@ -537,6 +538,9 @@ export function lowerUncompiledRequireCall(
|
||||
);
|
||||
}
|
||||
if (canonicalBuiltinModule(spec) !== null) return null;
|
||||
// A package that runs in a Node island loads at the call (scope B).
|
||||
const island = lowerIslandRequireCall(lowerer, call);
|
||||
if (island !== null) return island;
|
||||
if (!spec.startsWith(".") && !spec.startsWith("/") && !spec.startsWith("#")) {
|
||||
const refusal = probeNodeRequireRefusal(sf.fileName, spec);
|
||||
if (refusal !== null) {
|
||||
|
||||
@@ -93,6 +93,9 @@ import { protocolArm, protocolFieldArm } from "./class-protocol-dispatch.js";
|
||||
import { STREAM_OPERATORS, operatorSignalOptions, reduceSignalSetup } from "./lower-stream.js";
|
||||
import { deletedPropertyNames, fixedFieldFamily } from "./js-class-fields.js";
|
||||
|
||||
/** The receiver local of a call rewritten for a getter-only name. */
|
||||
const GETTER_RECEIVER = "%dispatch.getter.receiver.";
|
||||
|
||||
const STREAM_METHODS = new Set([
|
||||
"on",
|
||||
"addListener",
|
||||
@@ -1211,6 +1214,11 @@ export class ClassDynamicDispatch {
|
||||
* classes included: the value's capsule can be any of its subclasses. */
|
||||
private readonly boxedStatic = new Set<string>();
|
||||
private readonly dispatches = new Map<string, Dispatch>();
|
||||
/** Per receiver local of a call rewritten for a getter-only name, the
|
||||
* classes whose instances it already reads the getter on. A later wave
|
||||
* that reaches another class with the getter rewrites the fallback call
|
||||
* for that class. */
|
||||
private readonly getterReceivers = new Map<string, ReadonlySet<string>>();
|
||||
private readonly properties = new Map<string, PropertyDispatch>();
|
||||
private readonly computed = new Map<
|
||||
string,
|
||||
@@ -1897,6 +1905,108 @@ export class ClassDynamicDispatch {
|
||||
byMethod.set(method, matching);
|
||||
return matching;
|
||||
};
|
||||
// A name some class declares as a getter, and no class as a method: on
|
||||
// an instance of such a class the call reads the member (running the
|
||||
// getter) before it evaluates the arguments, as JavaScript reads the
|
||||
// callee first, and then calls what it read. Any other receiver keeps
|
||||
// the runtime's method dispatch, which answers builtin methods of the
|
||||
// same name (`n.toFixed(2)` while a class declares `get toFixed()`).
|
||||
// The runtime reads fields and other own properties of class instances
|
||||
// that early itself (scr_dyn_prepare_method).
|
||||
const getterCapsules = (method: string): string[] => {
|
||||
const keys = new Set<string>();
|
||||
for (const { info, capsule } of this.propertyReceivers(lowerer, true))
|
||||
if (findMethodOn(lowerer, info, `get:${method}`))
|
||||
keys.add(typeKey({ kind: "object", className: capsule.def.name }));
|
||||
return [...keys];
|
||||
};
|
||||
const getterCall = (
|
||||
fn: IrFunction,
|
||||
invoke: IrExpr & { kind: "dynInvoke" },
|
||||
capsules: readonly string[],
|
||||
covered: ReadonlySet<string> | undefined,
|
||||
): IrExpr => {
|
||||
const loc = invoke.loc;
|
||||
const receiverLocal = {
|
||||
id: `${GETTER_RECEIVER}${this.getterReceivers.size}`,
|
||||
name: "receiver",
|
||||
type: DYN,
|
||||
mutable: false,
|
||||
};
|
||||
fn.locals.push(receiverLocal);
|
||||
const keys = new Set<string>(capsules);
|
||||
if (covered) for (const key of covered) keys.add(key);
|
||||
this.getterReceivers.set(receiverLocal.id, keys);
|
||||
const calleeLocal = {
|
||||
id: `%dispatch.callee.${fn.locals.length}`,
|
||||
name: "callee",
|
||||
type: DYN,
|
||||
mutable: false,
|
||||
};
|
||||
fn.locals.push(calleeLocal);
|
||||
const receiver = varRef(receiverLocal.id, DYN, loc);
|
||||
const isCapsule = (key: string): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.typedRefIs",
|
||||
args: [receiver, { kind: "strLit", value: key, type: STRING, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const cond = capsules.slice(1).reduce<IrExpr>(
|
||||
(rest, key) => ({
|
||||
kind: "ternary",
|
||||
cond: isCapsule(key),
|
||||
then: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
else_: rest,
|
||||
type: BOOL,
|
||||
loc,
|
||||
}),
|
||||
isCapsule(capsules[0]!),
|
||||
);
|
||||
const read: IrExpr = {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: calleeLocal.id,
|
||||
init: {
|
||||
kind: "dynKeyGet",
|
||||
key: { kind: "strLit", value: invoke.method, type: STRING, loc },
|
||||
value: receiver,
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "dynCall",
|
||||
callee: varRef(calleeLocal.id, DYN, loc),
|
||||
receiver,
|
||||
calleeName: invoke.calleeName,
|
||||
...(invoke.calleeNameValue ? { calleeNameValue: invoke.calleeNameValue } : {}),
|
||||
args: invoke.args,
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: receiverLocal.id, init: invoke.recv, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond,
|
||||
then: read,
|
||||
else_: { ...invoke, recv: receiver },
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
};
|
||||
const candidateSets = new Map<string, ReadonlySet<ClassInfo>>();
|
||||
const candidateSet = (method: string): ReadonlySet<ClassInfo> => {
|
||||
let set = candidateSets.get(method);
|
||||
@@ -2964,6 +3074,19 @@ export class ClassDynamicDispatch {
|
||||
const info = lowerer.classes.get(name);
|
||||
return info && findStaticOn(lowerer, info, method)?.method;
|
||||
});
|
||||
if (
|
||||
invoke &&
|
||||
expr.kind === "dynInvoke" &&
|
||||
candidates(invoke.method).length === 0 &&
|
||||
!staticCandidates(invoke.method)
|
||||
) {
|
||||
// The other receivers' call a rewrite left in place tests only
|
||||
// the classes that rewrite did not.
|
||||
const covered =
|
||||
expr.recv.kind === "varRef" ? this.getterReceivers.get(expr.recv.localId) : undefined;
|
||||
const capsules = getterCapsules(invoke.method).filter((key) => !covered?.has(key));
|
||||
if (capsules.length > 0) return getterCall(fn, expr, capsules, covered);
|
||||
}
|
||||
if (
|
||||
!invoke ||
|
||||
(candidates(invoke.method).length === 0 &&
|
||||
|
||||
@@ -100,6 +100,21 @@ export function initializeRuntimeStatics(
|
||||
}
|
||||
if (!member.name) continue;
|
||||
const memberLoc = locOf(member);
|
||||
// A private static field: its native global, assigned in order.
|
||||
if (ts.isPrivateIdentifier(member.name)) {
|
||||
const name = member.name.text;
|
||||
const field = info.staticFields.find((candidate) => candidate.name === name);
|
||||
if (!ts.isPropertyDeclaration(member) || !field) {
|
||||
lowerer.unsupported("SC1090", member, "private static fields without an initializer");
|
||||
}
|
||||
statements.push({
|
||||
kind: "assign",
|
||||
localId: field.globalId,
|
||||
value: lowerer.lowerExprExpecting(member.initializer!, field.type),
|
||||
loc: memberLoc,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const key: IrExpr = ts.isComputedPropertyName(member.name)
|
||||
? computedKeys.get(member)!
|
||||
: lowerer.coerceToExpected(
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
|
||||
/** A `Pick<C, K>` / `Omit<C, K>` view: the lib alias it instantiates and
|
||||
* its source type argument `C`. */
|
||||
export interface ClassView {
|
||||
name: "Pick" | "Omit";
|
||||
source: ts.Type;
|
||||
}
|
||||
|
||||
/** The lib's Pick or Omit alias (declared only in declaration files). */
|
||||
function libViewName(symbol: ts.Symbol): "Pick" | "Omit" | null {
|
||||
const name = ts.symbolName(symbol);
|
||||
if (name !== "Pick" && name !== "Omit") return null;
|
||||
return symbol.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)
|
||||
? name
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `type` as a Pick/Omit view, spelled inline (`Pick<C, K>`) or through
|
||||
* non-generic user aliases (`type View = Pick<C, K>`, `type V2 = View`).
|
||||
* An aliased instantiation carries the USER's alias symbol and no alias
|
||||
* type arguments, so the alias declaration's type node names the view.
|
||||
* Generic user aliases (`type V<T> = Pick<T, K>`) are not resolved. */
|
||||
export function classViewOf(checker: ts.TypeChecker, type: ts.Type, depth = 0): ClassView | null {
|
||||
const alias = type.getAliasSymbol();
|
||||
if (alias === undefined) return null;
|
||||
const direct = libViewName(alias);
|
||||
if (direct !== null) {
|
||||
const source = type.getAliasTypeArguments()[0];
|
||||
return source === undefined ? null : { name: direct, source };
|
||||
}
|
||||
if (depth >= 8 || type.getAliasTypeArguments().length > 0) return null;
|
||||
const decl = alias.declarations.find(ts.isTypeAliasDeclaration);
|
||||
if (decl === undefined || (decl.typeParameters?.nodes.length ?? 0) > 0) return null;
|
||||
let node: ts.TypeNode = decl.type;
|
||||
while (ts.isParenthesizedTypeNode(node)) node = node.type;
|
||||
if (!ts.isTypeReferenceNode(node)) return null;
|
||||
let target = checker.getSymbolAtLocation(node.typeName);
|
||||
if (target !== undefined && (target.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
target = checker.getAliasedSymbol(target);
|
||||
if (target === undefined) return null;
|
||||
const viewName = libViewName(target);
|
||||
if (viewName !== null) {
|
||||
const argument = node.typeArguments?.nodes[0];
|
||||
return argument === undefined
|
||||
? null
|
||||
: { name: viewName, source: checker.getTypeFromTypeNode(argument) };
|
||||
}
|
||||
if ((target.flags & ts.SymbolFlags.TypeAlias) === 0) return null;
|
||||
return classViewOf(checker, checker.getDeclaredTypeOfSymbol(target), depth + 1);
|
||||
}
|
||||
@@ -114,10 +114,28 @@ export function coercibleValue(lowerer: Lowerer, src: IrType, dst: IrType): bool
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether an omitted parameter position completes with the native
|
||||
* undefined value: a checked-dynamic slot takes undefined as-is, and an
|
||||
* undefined-armed union (`x?: T`, `x: T = e`) is the completed ABI
|
||||
* spelling. */
|
||||
export function omittableTrailingParam(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "dyn" || typeEquals(type, UNDEFINED_T)) return true;
|
||||
return type.kind === "union" && lowerer.armTag(type.unionId, UNDEFINED_T) >= 0;
|
||||
}
|
||||
|
||||
/** Whether all parameters and the result adapt without a trap-only path.
|
||||
* Rest signatures are excluded, except a typed-rest source in a slot with
|
||||
* fixed parameters (the rest packing adapter); extra destination
|
||||
* parameters are ignored. */
|
||||
* parameters are ignored. A source with MORE parameters than the slot is
|
||||
* clean when every extra trailing one is omittable — the adapter passes
|
||||
* that absent value explicitly (funcCoerceAdapter's omittedParams), the
|
||||
* same way a call through the slot would. A parameter the slot coercion
|
||||
* converts through a STRUCTURAL width counts (cleanParamPair), which
|
||||
* recurses through a nested function type: a callback parameter of the
|
||||
* value taking a narrower record where the slot's takes its `unknown` view
|
||||
* converts the argument the same way a top-level call does. The result side
|
||||
* likewise accepts what funcCoerceAdapter converts (cleanResult), including
|
||||
* a promise whose payload widens. */
|
||||
export function cleanFuncAdaptable(
|
||||
lowerer: Lowerer,
|
||||
src: IrType & { kind: "func" },
|
||||
@@ -125,13 +143,35 @@ export function cleanFuncAdaptable(
|
||||
): boolean {
|
||||
if (restPackingAdapterFits(lowerer, src, dst)) return true;
|
||||
if (src.rest === true || dst.rest === true) return false;
|
||||
if (src.params.length > dst.params.length) return false;
|
||||
for (let i = 0; i < src.params.length; i++) {
|
||||
if (!lowerer.coercibleValue(dst.params[i]!, src.params[i]!)) return false;
|
||||
// Recursive function types (`type Handler = (h: Handler) => void`) reach
|
||||
// the same pair again one parameter down: assume it converts, the same
|
||||
// coinductive answer recordWidthPlan gives its own back-edges.
|
||||
const key = `funcClean:${typeKey(src)}:${typeKey(dst)}`;
|
||||
if (lowerer.coercions.planning.has(key)) return true;
|
||||
lowerer.coercions.planning.add(key);
|
||||
try {
|
||||
for (let i = dst.params.length; i < src.params.length; i++) {
|
||||
if (!omittableTrailingParam(lowerer, src.params[i]!)) return false;
|
||||
}
|
||||
for (let i = 0; i < src.params.length && i < dst.params.length; i++) {
|
||||
if (!cleanParamPair(lowerer, dst.params[i]!, src.params[i]!)) return false;
|
||||
}
|
||||
return cleanResult(lowerer, src.ret, dst.ret);
|
||||
} finally {
|
||||
lowerer.coercions.planning.delete(key);
|
||||
}
|
||||
if (dst.ret.kind === "void") return true;
|
||||
if (lowerer.coercibleValue(src.ret, dst.ret)) return true;
|
||||
return src.ret.kind === "void" && dst.ret.kind === "dyn";
|
||||
}
|
||||
|
||||
/** Whether the slot's coercion converts the value's RESULT without a
|
||||
* trap-only path: the dispositions funcCoerceAdapter's result side accepts
|
||||
* (a void slot drops the value; coercibleValue; a structural width, which
|
||||
* includes the promise payload widening and a nested function adaptation;
|
||||
* the void-into-unknown drop). */
|
||||
function cleanResult(lowerer: Lowerer, actual: IrType, expected: IrType): boolean {
|
||||
if (expected.kind === "void") return true;
|
||||
if (lowerer.coercibleValue(actual, expected)) return true;
|
||||
if (actual.kind === "void" && expected.kind === "dyn") return true;
|
||||
return lowerer.widthLiftPlan(actual, expected) !== null;
|
||||
}
|
||||
|
||||
/** Whether the slot's argument type `actual` converts into the function's
|
||||
@@ -442,7 +482,7 @@ export function funcCoerceAdapter(
|
||||
strandRet = false;
|
||||
// A record result the slot's result is a structural width of.
|
||||
if (strandRet && lowerer.widthLiftPlan(fromT.ret, toT.ret)) strandRet = false;
|
||||
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
|
||||
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}${lowerer.coercions.checkedRetags > 0 ? ":checked" : ""}`;
|
||||
const existing = lowerer.coercions.retags.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%fn.adapt", key);
|
||||
|
||||
@@ -9,6 +9,13 @@ export class CoercionState {
|
||||
// plan checks its other fields. Remove every assumption on unwind;
|
||||
// generated helpers must intern their names before building their bodies.
|
||||
readonly planning = new Set<string>();
|
||||
/** > 0 while a record width plan plans or builds its fields: method
|
||||
* fields may take checked function adapters (WidthLift funcAdaptChecked). */
|
||||
methodFieldDepth = 0;
|
||||
/** > 0 while a checked function adapter plans or builds: contravariant
|
||||
* parameters may width-lift, and union fields may retag with trapping
|
||||
* arms (WidthLift checkedRetag). */
|
||||
checkedRetags = 0;
|
||||
// Registry revisions invalidate only fully validated copy-only routes.
|
||||
readonly copyRetags = new Map<string, { name: string; shapes: number; unions: number }>();
|
||||
// Provenance for checked extraction; consumers must not infer it from names.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js";
|
||||
import { planRecordUnionLift, planRecordUnionWrap, planUnionRetag } from "../union-retag.js";
|
||||
import type { WidthLift } from "../width-lift.js";
|
||||
import type { IrRecordShape, IrType } from "../../../ir/ir.js";
|
||||
import { canDynCheckTo, isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js";
|
||||
@@ -57,6 +57,33 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
lowerer.armTag(dst.unionId, UNDEFINED_T) >= 0
|
||||
)
|
||||
return { how: "unitRetag" };
|
||||
// Inside a checked method adapter (contravariant positions): source
|
||||
// arms with no destination home trap instead of refusing the pair.
|
||||
if (lowerer.coercions.checkedRetags > 0) {
|
||||
const from = lowerer.unions.get(src.unionId);
|
||||
const to = lowerer.unions.get(dst.unionId);
|
||||
if (from && to) {
|
||||
const trappable: number[] = [];
|
||||
from.arms.forEach((arm, tag) => {
|
||||
if (isUnitType(arm) || lowerer.armTag(dst.unionId, arm) >= 0) return;
|
||||
if (to.arms.some((d) => !isUnitType(d) && lowerer.widthLiftPlan(arm, d) !== null))
|
||||
return;
|
||||
trappable.push(tag);
|
||||
});
|
||||
if (
|
||||
trappable.length > 0 &&
|
||||
trappable.length < from.arms.length &&
|
||||
planUnionRetag(
|
||||
from,
|
||||
to,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(s, d) => lowerer.widthLiftPlan(s, d),
|
||||
new Set(trappable),
|
||||
) !== null
|
||||
)
|
||||
return { how: "checkedRetag", trappable };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// A unit-typed source can't wrap here (unionWrap requires the
|
||||
@@ -71,7 +98,7 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
return { how: "discriminantWrap" };
|
||||
return { how: "wrap", tag };
|
||||
}
|
||||
const candidates: { tag: number; arm: IrType }[] = [];
|
||||
const candidates: { tag: number; arm: IrType; checked: boolean }[] = [];
|
||||
def.arms.forEach((arm, i) => {
|
||||
if (isUnitType(arm)) return;
|
||||
const sameFamily =
|
||||
@@ -87,9 +114,15 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
arm.kind === "array") ||
|
||||
(src.kind === "object" && arm.kind === "record") ||
|
||||
(src.kind === "record" && arm.kind === "object");
|
||||
if (sameFamily && lowerer.widthLiftPlan(src, arm) !== null) candidates.push({ tag: i, arm });
|
||||
const armLift = sameFamily ? lowerer.widthLiftPlan(src, arm) : null;
|
||||
if (armLift !== null)
|
||||
candidates.push({ tag: i, arm, checked: armLift.how === "funcAdaptChecked" });
|
||||
});
|
||||
let selected = candidates;
|
||||
// A clean function candidate always wins over a checked one, so every
|
||||
// selection that resolves today resolves the same way.
|
||||
if (selected.some((c) => c.checked) && selected.some((c) => !c.checked))
|
||||
selected = selected.filter((c) => !c.checked);
|
||||
if (selected.length > 1 && src.kind === "record") {
|
||||
const source = lowerer.shapes.get(src.shapeId);
|
||||
// An inferred JS union may widen its tag while retaining variants
|
||||
@@ -160,6 +193,26 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
if (exact.length === 1) selected = exact;
|
||||
else if (exact.length === 0 && lossless.length === 1) selected = lossless;
|
||||
}
|
||||
// An 'unknown' (dyn) source whose union destination has no dyn arm of
|
||||
// its own (`details: any` into `Details | undefined`, a tool result's
|
||||
// `structuredContent: unknown` into the JSON-value union): the checked
|
||||
// extraction, the same dynCheck coerceToExpected performs for a dyn
|
||||
// value in a union slot. A dyn source has no arm family, so every arm
|
||||
// rule above has passed it by, and every plan that exists for a dyn
|
||||
// source is the one above (its own arm) — this is exactly the case
|
||||
// that returned null (SC2002) before, so no convertible value changes
|
||||
// its lowering.
|
||||
if (
|
||||
selected.length !== 1 &&
|
||||
src.kind === "dyn" &&
|
||||
canDynCheckTo(
|
||||
dst,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
) {
|
||||
return { how: "dynOut" };
|
||||
}
|
||||
if (selected.length !== 1) return null;
|
||||
return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm };
|
||||
}
|
||||
@@ -216,6 +269,22 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
if (dst.kind === "func" && src.kind === "func" && lowerer.cleanFuncAdaptable(src, dst)) {
|
||||
return { how: "funcAdapt" };
|
||||
}
|
||||
// A method FIELD of a record width copy whose parameters convert only
|
||||
// through a checked narrowing (method bivariance across instantiations):
|
||||
// a checked adapter. Top-level function conversions never see it.
|
||||
if (
|
||||
dst.kind === "func" &&
|
||||
src.kind === "func" &&
|
||||
lowerer.coercions.methodFieldDepth > 0 &&
|
||||
lowerer.coercions.checkedRetags === 0
|
||||
) {
|
||||
lowerer.coercions.checkedRetags++;
|
||||
try {
|
||||
if (lowerer.cleanFuncAdaptable(src, dst)) return { how: "funcAdaptChecked" };
|
||||
} finally {
|
||||
lowerer.coercions.checkedRetags--;
|
||||
}
|
||||
}
|
||||
if (dst.kind === "record" && src.kind === "record") {
|
||||
// An INDEX-SIGNATURE destination takes the overflow CAPTURE reshape:
|
||||
// declared fields copy off the source, every other source field keys
|
||||
@@ -370,6 +439,7 @@ export function recordWidthPlan(
|
||||
// fields answers "assume coercible" — see CoercionState.planning.
|
||||
if (lowerer.coercions.planning.has(key)) return new Map();
|
||||
lowerer.coercions.planning.add(key);
|
||||
lowerer.coercions.methodFieldDepth++;
|
||||
try {
|
||||
const plan = new Map<string, FieldLift>();
|
||||
for (const tf of to.fields) {
|
||||
@@ -405,6 +475,7 @@ export function recordWidthPlan(
|
||||
}
|
||||
return plan;
|
||||
} finally {
|
||||
lowerer.coercions.methodFieldDepth--;
|
||||
lowerer.coercions.planning.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,34 @@ export function applyWidthLift(
|
||||
throw new InternalCompilerError("lowerer bug: planned retag lift failed to intern");
|
||||
return { kind: "call", callee: retag, args: [value], type: dst, loc };
|
||||
}
|
||||
case "checkedRetag": {
|
||||
if (dst.kind !== "union" || value.type.kind !== "union")
|
||||
throw new InternalCompilerError("lowerer bug: checked retag lift shape");
|
||||
const retag = lowerer.unionRetagHelper(
|
||||
value.type.unionId,
|
||||
dst.unionId,
|
||||
loc,
|
||||
new Set(lift.trappable),
|
||||
);
|
||||
if (!retag)
|
||||
throw new InternalCompilerError("lowerer bug: planned checked retag lift failed to intern");
|
||||
return { kind: "call", callee: retag, args: [value], type: dst, loc };
|
||||
}
|
||||
case "funcAdaptChecked": {
|
||||
if (dst.kind !== "func" || value.type.kind !== "func")
|
||||
throw new InternalCompilerError("lowerer bug: checked funcAdapt lift shape");
|
||||
lowerer.coercions.checkedRetags++;
|
||||
try {
|
||||
const adapter = lowerer.funcCoerceAdapter(value.type, dst, loc);
|
||||
if (!adapter)
|
||||
throw new InternalCompilerError(
|
||||
"lowerer bug: planned checked funcAdapt lift failed to intern",
|
||||
);
|
||||
return { kind: "call", callee: adapter, args: [value], type: dst, loc };
|
||||
} finally {
|
||||
lowerer.coercions.checkedRetags--;
|
||||
}
|
||||
}
|
||||
case "unitRetag": {
|
||||
if (dst.kind !== "union" || value.type.kind !== "union")
|
||||
throw new InternalCompilerError("lowerer bug: unit retag lift shape");
|
||||
@@ -527,11 +555,16 @@ export function recordWidthHelper(
|
||||
lowerer.coercionOriginNode = null;
|
||||
lowerer.coercionFieldOrigin = null;
|
||||
try {
|
||||
lowerer.liftedFns.push(
|
||||
buildRecordProjection(lowerer, name, fromT, to, plan, loc, (field) =>
|
||||
declared === null ? null : fieldOrigin(lowerer.checker, declared, field),
|
||||
),
|
||||
);
|
||||
lowerer.coercions.methodFieldDepth++;
|
||||
try {
|
||||
lowerer.liftedFns.push(
|
||||
buildRecordProjection(lowerer, name, fromT, to, plan, loc, (field) =>
|
||||
declared === null ? null : fieldOrigin(lowerer.checker, declared, field),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
lowerer.coercions.methodFieldDepth--;
|
||||
}
|
||||
} finally {
|
||||
lowerer.coercionOriginNode = outerNode;
|
||||
lowerer.coercionFieldOrigin = outerField;
|
||||
|
||||
@@ -56,6 +56,7 @@ import { typeKey } from "../../type-mapper.js";
|
||||
import {
|
||||
lowerArrayCallback,
|
||||
requireProducedArrayElement,
|
||||
unitOnlyArrayElement,
|
||||
callbackArrayElement,
|
||||
truthyFilterCallback,
|
||||
} from "./callback-arguments.js";
|
||||
@@ -69,6 +70,7 @@ import {
|
||||
import { dynamicReceiverThrows } from "./dynamic-receivers.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js";
|
||||
import { checkedFilterNarrowing } from "./filter-predicates.js";
|
||||
import { omittableTrailingParam } from "../coercions/function-adapters.js";
|
||||
|
||||
/** `a.map(fn)` / `a.filter(fn)` / `a.forEach(fn)` desugar to a direct
|
||||
* call of a synthetic module function — one per method + element/result
|
||||
@@ -108,7 +110,14 @@ export function lowerArrayHofCall(
|
||||
}
|
||||
const callback = booleanFilter
|
||||
? { fnArg: truthyFilterCallback(lowerer, valueT, loc), arity: 1, unread: false }
|
||||
: lowerArrayCallback(lowerer, argNode, [valueT], arrayOf(elem));
|
||||
: lowerArrayCallback(
|
||||
lowerer,
|
||||
argNode,
|
||||
[valueT],
|
||||
arrayOf(elem),
|
||||
false,
|
||||
method === "map" ? (unitOnlyArrayElement(lowerer, call) ?? undefined) : undefined,
|
||||
);
|
||||
const { fnArg, arity } = callback;
|
||||
// Only an array that can hold elements outside its element type needs to
|
||||
// leave an element its callback ignores unread.
|
||||
@@ -623,7 +632,14 @@ function arrayHofHelper(
|
||||
* original concrete array arm — never a copied `(T | U)[]` — and an
|
||||
* arm-specific function adapter wraps the element and receiver arguments
|
||||
* into the union-wide callback ABI. That preserves sparse-array reads,
|
||||
* callback mutation, and the callback's third-argument identity. */
|
||||
* callback mutation, and the callback's third-argument identity.
|
||||
*
|
||||
* `filter` joins them: the kept elements are the checker's result element
|
||||
* (`(b): b is TextContent` over `(TextContent | ImageContent)[] |
|
||||
* (ThinkingContent | TextContent)[]` answers `TextContent[]`), so each arm
|
||||
* filters its own array and converts its survivors through the checked
|
||||
* narrowing — a predicate that kept an element it does not describe throws
|
||||
* the TypeError. An arm with no such conversion leaves the fence in place. */
|
||||
export function lowerArrayUnionHofCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -631,7 +647,7 @@ export function lowerArrayUnionHofCall(
|
||||
receiverT: IrType & { kind: "union" },
|
||||
): IrExpr | null {
|
||||
const method = access.name.text;
|
||||
if (method !== "map" && method !== "forEach") return null;
|
||||
if (method !== "map" && method !== "forEach" && method !== "filter") return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const def = lowerer.unions.get(receiverT.unionId);
|
||||
if (!def || def.arms.length < 2 || !def.arms.every((arm) => arm.kind === "array")) return null;
|
||||
@@ -659,15 +675,59 @@ export function lowerArrayUnionHofCall(
|
||||
? valueArms[0]!
|
||||
: { kind: "union", unionId: lowerer.unions.intern(valueArms) };
|
||||
const argNode = call.arguments.nodes[0]!;
|
||||
const { fnArg, arity } = lowerArrayCallback(lowerer, argNode, [valueT], receiverT);
|
||||
const { fnArg, arity } = lowerArrayCallback(
|
||||
lowerer,
|
||||
argNode,
|
||||
[valueT],
|
||||
receiverT,
|
||||
false,
|
||||
method === "map" ? (unitOnlyArrayElement(lowerer, call) ?? undefined) : undefined,
|
||||
);
|
||||
const fnRet = fnArg.type.ret;
|
||||
if (method === "map" && fnRet.kind === "void") {
|
||||
// `void[]` has no element (see the plain-receiver map path).
|
||||
lowerer.badType(call, lowerer.typeOf(call));
|
||||
}
|
||||
if (method === "map") requireProducedArrayElement(lowerer, call, "'.map()'", fnRet);
|
||||
const outElem = method === "map" ? callbackArrayElement(lowerer, call, fnRet) : VOID;
|
||||
const resultT: IrType = method === "map" ? arrayOf(outElem) : VOID;
|
||||
if (method === "filter" && !filterPredicateOk(lowerer, fnRet)) {
|
||||
if (fnRet.kind === "void") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
"'.filter()' with a void-returning predicate (the callback return value is erased before its truthiness can be tested)",
|
||||
);
|
||||
}
|
||||
lowerer.badType(call, lowerer.typeOf(call));
|
||||
}
|
||||
// `map` stores the callback's answer; `filter` keeps the elements its
|
||||
// predicate named. The kept element is the CHECKER's result element, which
|
||||
// for a written type predicate is narrower than every arm's own:
|
||||
// `(b): b is TextContent` over `(TextContent | ImageContent)[] |
|
||||
// (ThinkingContent | TextContent)[]` answers `TextContent[]`. Each arm
|
||||
// filters its own concrete array and converts its survivors to that
|
||||
// element through the checked narrowing — a predicate that kept an element
|
||||
// it does not describe throws instead of misreading it. No conversion the
|
||||
// arms cannot make honestly means the fence stays.
|
||||
let outElem: IrType = VOID;
|
||||
const narrowing: (string | null)[] = [];
|
||||
if (method === "map") {
|
||||
outElem = callbackArrayElement(lowerer, call, fnRet);
|
||||
} else if (method === "filter") {
|
||||
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (!result || result.kind !== "array") return null;
|
||||
outElem = result.elem;
|
||||
const typeText = filterTypeText(lowerer, call);
|
||||
for (const array of arrays) {
|
||||
if (typeEquals(array.elem, outElem)) {
|
||||
narrowing.push(null);
|
||||
continue;
|
||||
}
|
||||
const helper = checkedFilterNarrowing(lowerer, array.elem, outElem, typeText, locOf(call));
|
||||
if (helper === null) return null;
|
||||
narrowing.push(helper);
|
||||
}
|
||||
}
|
||||
const resultT: IrType = method === "forEach" ? VOID : arrayOf(outElem);
|
||||
for (const array of arrays) {
|
||||
const armFnT = funcOf(
|
||||
[arrayValueType(lowerer, array.elem), F64, array].slice(0, arity),
|
||||
@@ -684,6 +744,7 @@ export function lowerArrayUnionHofCall(
|
||||
arity,
|
||||
fnRet,
|
||||
outElem,
|
||||
narrowing,
|
||||
locOf(call),
|
||||
);
|
||||
return {
|
||||
@@ -700,24 +761,31 @@ export function lowerArrayUnionHofCall(
|
||||
|
||||
function arrayUnionHofHelper(
|
||||
lowerer: Lowerer,
|
||||
method: "map" | "forEach",
|
||||
method: "map" | "forEach" | "filter",
|
||||
receiverT: IrType & { kind: "union" },
|
||||
arrays: (IrType & { kind: "array" })[],
|
||||
callbackT: IrType & { kind: "func" },
|
||||
arity: number,
|
||||
fnRet: IrType,
|
||||
outElem: IrType,
|
||||
narrowing: (string | null)[],
|
||||
loc: SrcLoc,
|
||||
): string {
|
||||
const resultT: IrType = method === "map" ? arrayOf(outElem) : VOID;
|
||||
const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}`;
|
||||
const resultT: IrType = method === "forEach" ? VOID : arrayOf(outElem);
|
||||
// A filter's per-arm conversions are part of what the helper does, so two
|
||||
// sites that share receiver, callback and result but convert differently
|
||||
// keep their own helper. `map`/`forEach` never narrow, and leaving their
|
||||
// key untouched keeps the helpers a program already emitted byte-identical.
|
||||
const narrowingKey =
|
||||
method === "filter" ? `:${narrowing.map((helper) => helper ?? "").join(",")}` : "";
|
||||
const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}${narrowingKey}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%arr.union", key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const receiver = varRef("a.0", receiverT, loc);
|
||||
const callback = varRef("f.0", callbackT, loc);
|
||||
const branch = (array: IrType & { kind: "array" }): IrStmt[] => {
|
||||
const branch = (array: IrType & { kind: "array" }, index: number): IrStmt[] => {
|
||||
const tag = lowerer.armTag(receiverT.unionId, array);
|
||||
const concrete: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
@@ -735,16 +803,38 @@ function arrayUnionHofHelper(
|
||||
if (!typeEquals(adapted.type, armFnT)) {
|
||||
throw new InternalCompilerError("lowerer bug: union-array callback stopped adapting");
|
||||
}
|
||||
const callee = arrayHofHelper(lowerer, method, array.elem, fnRet, arity, loc, outElem);
|
||||
const invoke: IrExpr = { kind: "call", callee, args: [concrete, adapted], type: resultT, loc };
|
||||
const callee = arrayHofHelper(
|
||||
lowerer,
|
||||
method,
|
||||
array.elem,
|
||||
fnRet,
|
||||
arity,
|
||||
loc,
|
||||
method === "filter" ? array.elem : outElem,
|
||||
);
|
||||
const invoke: IrExpr = {
|
||||
kind: "call",
|
||||
callee,
|
||||
args: [concrete, adapted],
|
||||
type: method === "filter" ? arrayOf(array.elem) : resultT,
|
||||
loc,
|
||||
};
|
||||
// Survivors of this arm's own array, converted to the checker's result
|
||||
// element. The helper throws for a kept element the predicate does not
|
||||
// describe, so a lying predicate never reinterprets a value.
|
||||
const armNarrow = narrowing[index] ?? null;
|
||||
const answer: IrExpr =
|
||||
armNarrow === null
|
||||
? invoke
|
||||
: { kind: "call", callee: armNarrow, args: [invoke], type: resultT, loc };
|
||||
return resultT.kind === "void"
|
||||
? [
|
||||
{ kind: "exprStmt", expr: invoke, loc },
|
||||
{ kind: "return", value: null, loc },
|
||||
]
|
||||
: [{ kind: "return", value: invoke, loc }];
|
||||
: [{ kind: "return", value: answer, loc }];
|
||||
};
|
||||
let body = branch(arrays[arrays.length - 1]!);
|
||||
let body = branch(arrays[arrays.length - 1]!, arrays.length - 1);
|
||||
for (let i = arrays.length - 2; i >= 0; i--) {
|
||||
const array = arrays[i]!;
|
||||
body = [
|
||||
@@ -759,7 +849,7 @@ function arrayUnionHofHelper(
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: branch(array),
|
||||
then: branch(array, i),
|
||||
else_: body,
|
||||
loc,
|
||||
},
|
||||
@@ -1806,10 +1896,10 @@ export function lowerArrayFlatMapCall(
|
||||
/** How each arm of a flatMap callback's union result contributes to the
|
||||
* output: an array arm appends its elements, an array arm the checker
|
||||
* types `never[]` (an empty literal such as `s ? s : []`) appends nothing,
|
||||
* and any other arm appends itself. */
|
||||
* a tuple arm appends its fields, and any other arm appends itself. */
|
||||
interface FlatMapUnionPlan {
|
||||
outElem: IrType;
|
||||
arms: ("spread" | "empty" | "value")[];
|
||||
arms: ("spread" | "empty" | "tuple" | "value")[];
|
||||
}
|
||||
|
||||
/** The plan for a callback returning `T | U[]`: every non-array arm must
|
||||
@@ -1827,15 +1917,18 @@ function flatMapUnionPlan(
|
||||
const outElem = out.elem;
|
||||
const arms = lowerer.unions.get(fnRet.unionId)?.arms;
|
||||
if (!arms) return null;
|
||||
// The `never[]` constituents of the callback's checked result.
|
||||
// The constituents of the callback's checked result that can hold no
|
||||
// element: a `never[]` arm, and the empty tuple a bare `[]` arm infers
|
||||
// when the enclosing call has a contextual array type (`return x ? [a]
|
||||
// : []` in a `f(): T[]`).
|
||||
const emptyArms: IrType[] = [];
|
||||
const signature = lowerer.checker.getCallSignatures(lowerer.typeOf(argNode))[0];
|
||||
if (signature) {
|
||||
const ret = lowerer.checker.getReturnTypeOfSignature(signature);
|
||||
for (const part of ret.isUnionType() ? ts.constituentTypes(ret) : [ret]) {
|
||||
if (!lowerer.checker.isArrayType(part)) continue;
|
||||
const [element] = lowerer.checker.getTypeArguments(part as ts.TypeReference);
|
||||
if (element === undefined || (element.flags & ts.TypeFlags.Never) === 0) continue;
|
||||
if (!lowerer.checker.isArrayType(part) && !lowerer.checker.isTupleType(part)) continue;
|
||||
const element = lowerer.checker.getTypeArguments(part as ts.TypeReference)[0];
|
||||
if (element !== undefined && (element.flags & ts.TypeFlags.Never) === 0) continue;
|
||||
const mapped = lowerer.mapTypeOf(part);
|
||||
if (mapped) emptyArms.push(mapped);
|
||||
}
|
||||
@@ -1847,12 +1940,19 @@ function flatMapUnionPlan(
|
||||
if (outElem.kind !== "union") return false;
|
||||
return lowerer.armTag(outElem.unionId, t) >= 0;
|
||||
};
|
||||
const tupleConverts = (arm: IrType & { kind: "record" }): boolean =>
|
||||
(lowerer.shapes.get(arm.shapeId)?.fields ?? []).every((f) => converts(f.type));
|
||||
const plan: FlatMapUnionPlan["arms"] = [];
|
||||
for (const arm of arms) {
|
||||
if (arm.kind === "array" && emptyArms.some((e) => typeEquals(e, arm))) plan.push("empty");
|
||||
// An array arm copies its elements as they are stored, a present
|
||||
// undefined included, so its element type is the result's.
|
||||
else if (arm.kind === "array" && typeEquals(arm.elem, outElem)) plan.push("spread");
|
||||
// A contextual array type makes a multi-statement body's `[pair]`
|
||||
// infer as a one-element TUPLE (`return x ? [pair] : []`), whose
|
||||
// fields contribute in order.
|
||||
else if (arm.kind === "record" && lowerer.shapes.get(arm.shapeId)?.tuple && tupleConverts(arm))
|
||||
plan.push("tuple");
|
||||
else if (arm.kind !== "array" && converts(arm)) plan.push("value");
|
||||
else return null;
|
||||
}
|
||||
@@ -1908,6 +2008,32 @@ function flatMapUnionHelper(
|
||||
loc,
|
||||
};
|
||||
if (plan.arms[tag] === "empty") return [];
|
||||
if (plan.arms[tag] === "tuple") {
|
||||
const tuple = arm as IrType & { kind: "record" };
|
||||
// The fields are stored by name ("0", "1", "10", "2", …): the
|
||||
// elements append in index order.
|
||||
const elements = [...(lowerer.shapes.get(tuple.shapeId)?.fields ?? [])].sort(
|
||||
(a, b) => Number(a.name) - Number(b.name),
|
||||
);
|
||||
return elements.map((field) => {
|
||||
const read: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj: narrowed,
|
||||
shapeId: tuple.shapeId,
|
||||
field: field.name,
|
||||
type: field.type,
|
||||
loc,
|
||||
};
|
||||
return arrayValueStore(
|
||||
lowerer,
|
||||
outRef(),
|
||||
outLength(),
|
||||
lowerer.coerceToExpected(read, plan.outElem),
|
||||
plan.outElem,
|
||||
loc,
|
||||
);
|
||||
});
|
||||
}
|
||||
if (plan.arms[tag] === "value") {
|
||||
// A unit arm's value is the unit itself, which has no narrowed
|
||||
// representation.
|
||||
@@ -2642,11 +2768,15 @@ export function lowerArraySortCall(
|
||||
let fnArg =
|
||||
undefinedArg ?? lowerOptionalElementComparator(lowerer, argNode, unwrappedArg, elem, arrT);
|
||||
// Reusable JS comparators often accept checked values. Adapt the array's
|
||||
// element ABI just as other array callbacks do before sorting.
|
||||
// element ABI just as other array callbacks do before sorting. A
|
||||
// comparator declaring MORE parameters than sort passes (`(a, b, loose?)`,
|
||||
// the imported semver shape) receives the surplus ones as undefined — the
|
||||
// completion an ordinary two-argument call performs — so it adapts to the
|
||||
// (a, b) slot like any other wider signature.
|
||||
if (
|
||||
fnArg.type.kind === "func" &&
|
||||
fnArg.type.params.length <= 2 &&
|
||||
fnArg.type.params.every((param) => lowerer.coercibleValue(elem, param))
|
||||
fnArg.type.params.slice(2).every((param) => omittableTrailingParam(lowerer, param)) &&
|
||||
fnArg.type.params.slice(0, 2).every((param) => lowerer.coercibleValue(elem, param))
|
||||
) {
|
||||
// Arithmetic on inferred JS values retains a checked result because
|
||||
// it can produce BigInt. The sort callback still requires a number.
|
||||
@@ -2655,7 +2785,7 @@ export function lowerArraySortCall(
|
||||
? F64
|
||||
: fnArg.type.ret;
|
||||
const expected = funcOf(
|
||||
fnArg.type.params.map(() => elem),
|
||||
fnArg.type.params.slice(0, 2).map(() => elem),
|
||||
result,
|
||||
);
|
||||
if (!typeEquals(fnArg.type, expected)) fnArg = lowerer.coerceToExpected(fnArg, expected);
|
||||
|
||||
@@ -56,6 +56,8 @@ import {
|
||||
import {
|
||||
requireProducedArrayElement,
|
||||
lowerArrayCallback,
|
||||
lowerArrayCallbackAnswering,
|
||||
unitOnlyArrayElement,
|
||||
callbackArrayElement,
|
||||
} from "./callback-arguments.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
@@ -1002,6 +1004,8 @@ export function lowerArrayFromCall(
|
||||
args[1]!,
|
||||
[input.element],
|
||||
arrayOf(input.element),
|
||||
false,
|
||||
unitOnlyArrayElement(lowerer, call) ?? undefined,
|
||||
);
|
||||
if (callback.arity > 2)
|
||||
lowerer.noLowering("Array.from mapper with an array parameter", args[1]!);
|
||||
@@ -1085,6 +1089,19 @@ export function lowerArrayFromCall(
|
||||
// and materializes holes while keeping element reference identity.
|
||||
if ((args.length === 1 || args.length === 2) && !ts.isObjectLiteralExpression(args[0]!)) {
|
||||
const source = lowerer.lowerExpr(args[0]!);
|
||||
// Array.from(s): the string iterator's code points, into a fresh
|
||||
// array (scr_str_code_points already builds one, like Array.from).
|
||||
// The checker can narrow the argument to a string while the binding
|
||||
// keeps its nullable storage (`value.includes(x) && Array.from(value)`),
|
||||
// so the site type decides and the read is coerced to that string.
|
||||
if (
|
||||
args.length === 1 &&
|
||||
(source.type.kind === "string" ||
|
||||
lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind === "string")
|
||||
) {
|
||||
const str = source.type.kind === "string" ? source : lowerer.coerceToExpected(source, STRING);
|
||||
return strCharsCall(str, loc);
|
||||
}
|
||||
let src =
|
||||
source.type.kind === "string" && args.length === 2 ? strCharsCall(source, loc) : source;
|
||||
if (src.type.kind === "array") {
|
||||
@@ -1123,7 +1140,14 @@ export function lowerArrayFromCall(
|
||||
if (paramSymbol) lowerer.runtimeOptionalBindingTypes.set(paramSymbol, valueT);
|
||||
let callback: ReturnType<typeof lowerArrayCallback>;
|
||||
try {
|
||||
callback = lowerArrayCallback(lowerer, mapper, [valueT], arrT);
|
||||
callback = lowerArrayCallback(
|
||||
lowerer,
|
||||
mapper,
|
||||
[valueT],
|
||||
arrT,
|
||||
false,
|
||||
unitOnlyArrayElement(lowerer, call) ?? undefined,
|
||||
);
|
||||
} finally {
|
||||
if (paramSymbol) {
|
||||
if (previous === undefined) lowerer.runtimeOptionalBindingTypes.delete(paramSymbol);
|
||||
@@ -1162,7 +1186,14 @@ export function lowerArrayFromCall(
|
||||
// mapper that writes the source changes later reads, as in Node),
|
||||
// so this is a loop over the typed array, not a toArray snapshot.
|
||||
if (src.type.kind === "bytes" && args.length === 2) {
|
||||
const callback = lowerArrayCallback(lowerer, args[1]!, [F64], arrayOf(F64));
|
||||
const callback = lowerArrayCallback(
|
||||
lowerer,
|
||||
args[1]!,
|
||||
[F64],
|
||||
arrayOf(F64),
|
||||
false,
|
||||
unitOnlyArrayElement(lowerer, call) ?? undefined,
|
||||
);
|
||||
if (callback.arity > 2)
|
||||
lowerer.noLowering("Array.from mapper with an array parameter", args[1]!);
|
||||
const fnRet = callback.fnArg.type.ret;
|
||||
@@ -1245,7 +1276,7 @@ export function lowerArrayFromCall(
|
||||
);
|
||||
}
|
||||
if (n.type.kind !== "f64") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!));
|
||||
const fnArg = lowerer.lowerExpr(args[1]!);
|
||||
const fnArg = lowerArrayCallbackAnswering(lowerer, args[1]!, unitOnlyArrayElement(lowerer, call));
|
||||
// The mapper may declare any prefix of (v, i): v is the checker's own
|
||||
// `unknown` (Node passes undefined there — the dyn undefined singleton
|
||||
// here), i the index. The result type must be a legal array element.
|
||||
|
||||
@@ -306,6 +306,42 @@ function readsParameter(argNode: ts.Expression, index: number): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** The result array's element when it is the unit-only union (`undefined[]`,
|
||||
* the sparse-literal/`Array.from(..., () => undefined)` shape): a callback
|
||||
* that answers `undefined` stores a PRESENT unit element, so the producer
|
||||
* gives the callback that union as its contextual return. The standalone
|
||||
* `undefined` in return position otherwise maps to the void convention,
|
||||
* whose answer is erased before it could be stored. Null for other elements. */
|
||||
export function unitOnlyArrayElement(lowerer: Lowerer, call: ts.CallExpression): IrType | null {
|
||||
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (result?.kind !== "array") return null;
|
||||
const elem = result.elem;
|
||||
if (isUnitType(elem)) return elem;
|
||||
return elem.kind === "union" &&
|
||||
lowerer.unions.get(elem.unionId)?.arms.every((arm) => isUnitType(arm)) === true
|
||||
? elem
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Lower a callback whose answer becomes an array element, with the result
|
||||
* element as its contextual return when that element is the unit-only union
|
||||
* (see unitOnlyArrayElement). Other producers lower the callback directly. */
|
||||
export function lowerArrayCallbackAnswering(
|
||||
lowerer: Lowerer,
|
||||
fnNode: ts.Expression,
|
||||
expected: IrType | null,
|
||||
): IrExpr {
|
||||
if (expected === null) return lowerer.lowerExpr(fnNode);
|
||||
const previous = lowerer.contextualFunctionReturns.get(fnNode);
|
||||
lowerer.contextualFunctionReturns.set(fnNode, expected);
|
||||
try {
|
||||
return lowerer.lowerExpr(fnNode);
|
||||
} finally {
|
||||
if (previous === undefined) lowerer.contextualFunctionReturns.delete(fnNode);
|
||||
else lowerer.contextualFunctionReturns.set(fnNode, previous);
|
||||
}
|
||||
}
|
||||
|
||||
/** Preserve the checker-selected payload ABI when a callback's runtime
|
||||
* return gained only undefined. The array state byte carries that value. */
|
||||
export function callbackArrayElement(
|
||||
|
||||
@@ -163,7 +163,13 @@ function conversionBody(
|
||||
target.indexValue
|
||||
)
|
||||
return null;
|
||||
if (source.fields.length !== target.fields.length) return null;
|
||||
// A written predicate usually names FEWER members than the receiver's
|
||||
// element has (`(c): c is { text: string } => ...` over a block with a
|
||||
// `textSignature` and a `type`): the target's members are a subset of
|
||||
// the source's, each read and converted, and the copy keeps the
|
||||
// source's keys in the source's order. A member the source lacks has
|
||||
// no slot to read, so it stays refused.
|
||||
if (target.fields.length > source.fields.length) return null;
|
||||
const fields: { name: string; value: IrExpr }[] = [];
|
||||
for (const field of target.fields) {
|
||||
const own = source.fields.find((f) => f.name === field.name);
|
||||
@@ -216,7 +222,13 @@ function keepAbsence(
|
||||
}
|
||||
|
||||
/** A union value to one of its arms, or to a union of some of its arms:
|
||||
* each arm the target holds returns, any other arm throws. */
|
||||
* each arm the target holds returns, any other arm throws.
|
||||
*
|
||||
* A written predicate may name a structural type that is no arm at all
|
||||
* (`(c): c is { text: string; type: string } => ...` over the block union
|
||||
* above, whose text arm also carries `textSignature`): when no arm matches
|
||||
* exactly, each arm the target can be read from converts by its own shape,
|
||||
* tag by tag, and an arm with no such read throws. */
|
||||
function unionConversion(
|
||||
lowerer: Lowerer,
|
||||
input: IrExpr,
|
||||
@@ -279,7 +291,10 @@ function unionConversion(
|
||||
// Every arm the target names must be one of the union's own arms, or a
|
||||
// record target converts from at least one arm. A record target that is
|
||||
// itself an arm keeps that arm's elements as they are, and the arms with
|
||||
// its fields convert.
|
||||
// its fields convert. Any other structural target that is no arm converts
|
||||
// from each arm it can be read from.
|
||||
if (kept === 0 && recordArmConversions === 0 && to.kind !== "union")
|
||||
return structuralUnionConversion(lowerer, input, unionId, to, typeText, loc);
|
||||
if (kept === 0 ? recordArmConversions === 0 : kept !== targetArms.length) return null;
|
||||
body.push(
|
||||
typeError(
|
||||
@@ -289,3 +304,42 @@ function unionConversion(
|
||||
);
|
||||
return body;
|
||||
}
|
||||
|
||||
/** A union value to a structural type that is no arm: each arm the target
|
||||
* can be read from returns its converted value, any other arm throws. */
|
||||
function structuralUnionConversion(
|
||||
lowerer: Lowerer,
|
||||
input: IrExpr,
|
||||
unionId: string,
|
||||
to: IrType,
|
||||
typeText: string,
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] | null {
|
||||
const source = lowerer.unions.get(unionId);
|
||||
if (!source) return null;
|
||||
const body: IrStmt[] = [];
|
||||
let kept = 0;
|
||||
for (let tag = 0; tag < source.arms.length; tag++) {
|
||||
const arm = source.arms[tag]!;
|
||||
if (isUnitType(arm)) continue;
|
||||
const payload: IrExpr = { kind: "unionNarrow", unionId, tag, value: input, type: arm, loc };
|
||||
const value = converted(lowerer, payload, arm, to, loc);
|
||||
if (value === null) continue;
|
||||
kept++;
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: input, type: BOOL, loc },
|
||||
then: [{ kind: "return", value, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (kept === 0) return null;
|
||||
body.push(
|
||||
typeError(
|
||||
`a kept element is not a '${typeText}' (a type predicate returned true for a value of another type)`,
|
||||
loc,
|
||||
),
|
||||
);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { lowerWrapperToObject, programCreatesWrappers } from "../primitive-wrappers.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js";
|
||||
import { countedFor, numLit, varRef } from "../../../ir/build.js";
|
||||
@@ -21,12 +22,14 @@ import {
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
arrayOf,
|
||||
canDynCheckTo,
|
||||
isRefCounted,
|
||||
isSupportedIndexValue,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { recordMayKeepSource, recordSourceTest } from "../record-sources.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { type WidthLift } from "../lowerer.js";
|
||||
@@ -50,6 +53,13 @@ export function lowerObjectConstructor(
|
||||
}
|
||||
if (args.length !== 1 || ts.isSpreadElement(args[0]!)) return null;
|
||||
const value = lowerer.lowerExpr(args[0]!);
|
||||
// ToObject of a value that may be a primitive, once the program creates
|
||||
// wrapper objects (programCreatesWrappers counts this very call).
|
||||
if (
|
||||
["dyn", "string", "f64", "bool"].includes(value.type.kind) &&
|
||||
(lowerer.createsWrappers ??= programCreatesWrappers(lowerer))
|
||||
)
|
||||
return lowerWrapperToObject(lowerer, lowerer.coerceToExpected(value, DYN), loc);
|
||||
if (value.kind === "dynObjLit" || value.kind === "dynArrLit") return value;
|
||||
if (
|
||||
[
|
||||
@@ -1213,8 +1223,47 @@ export function lowerRecordOvfCaptureHelper(
|
||||
),
|
||||
);
|
||||
}
|
||||
// A CLOSED source can still hold own keys its shape does not declare: a
|
||||
// record built from the checked-dynamic tree keeps them beside it (the
|
||||
// record-sources.ts view), and every JS copy — spread, Object.assign,
|
||||
// JSON — carries them. The field-by-field rebuild above cannot see
|
||||
// them, so when the source kept such a view, rebuild the target from
|
||||
// that live view instead: the validated keyed conversion the checked
|
||||
// record builder performs, which copies exactly the keys Node copies.
|
||||
// The declared-field copies before this branch are pure writes into a
|
||||
// fresh local, so the redone work is only wasted slot stores on the
|
||||
// rare path where the view exists.
|
||||
// The conversion is only honest when the checked-dynamic tree can check it,
|
||||
// and the view can only exist if the program converts something into a
|
||||
// closed record at all. Both facts are settled before the shape helpers
|
||||
// finalize their bodies, so the tail is chosen there.
|
||||
const sourcedFromView =
|
||||
fIv === null &&
|
||||
recordMayKeepSource(lowerer, fromT) &&
|
||||
canDynCheckTo(
|
||||
toT,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
);
|
||||
body.push({ kind: "return", value: outRef, loc });
|
||||
const suffix = body.slice(1 + fieldStmts.size);
|
||||
const keptSourceTail = (): IrStmt[] => {
|
||||
const fromView: IrExpr = {
|
||||
kind: "dynCheck",
|
||||
value: { kind: "dynFrom", value: sRef, type: DYN, loc },
|
||||
type: toT,
|
||||
loc,
|
||||
};
|
||||
const fromViewReturn: IrStmt = { kind: "return", value: fromView, loc };
|
||||
const fromViewBranch: IrStmt = {
|
||||
kind: "if",
|
||||
cond: recordSourceTest(sRef, loc),
|
||||
then: [fromViewReturn],
|
||||
else_: null,
|
||||
loc,
|
||||
};
|
||||
return [fromViewBranch, ...suffix];
|
||||
};
|
||||
const fn: IrFunction = {
|
||||
name,
|
||||
params: [{ localId: "s.0", name: "s", type: fromT }],
|
||||
@@ -1236,13 +1285,15 @@ export function lowerRecordOvfCaptureHelper(
|
||||
lowerer.shapeOrderHelperFinalizers.push(() => {
|
||||
const current = lowerer.shapes.get(fromId) ?? from;
|
||||
const currentOrder = current.declaredOrder ?? current.fields.map((f) => f.name);
|
||||
let tail: IrStmt[] = suffix;
|
||||
if (sourcedFromView && lowerer.recordSourcesPossible) tail = keptSourceTail();
|
||||
fn.body = [
|
||||
body[0]!,
|
||||
...currentOrder.flatMap((field) => {
|
||||
const stmt = fieldStmts.get(field);
|
||||
return stmt ? [stmt] : [];
|
||||
}),
|
||||
...suffix,
|
||||
...tail,
|
||||
];
|
||||
});
|
||||
lowerer.liftedFns.push(fn);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user