pi wave 5: node process APIs, stdlib singletons, misc lowering (#2548)

This commit is contained in:
Chris Tate
2026-10-30 01:00:51 -05:00
committed by GitHub
100 changed files with 1955 additions and 125 deletions
@@ -44328,6 +44328,7 @@
"tests/corpus/5570-fs-cp-async.ts",
"tests/corpus/6207-fs-constants-object.ts",
"tests/corpus/6208-fs-constants-require.cjs",
"tests/corpus/8427-fs-copyfile-writefile-flags.ts",
"tests/corpus/fs-native-realpath.ts",
"tests/corpus/fs-watch-file-names.ts",
"tests/corpus/fs-watch-file-polling.ts",
@@ -45353,7 +45354,8 @@
"evidence": "surface-manifest:node-builtin.fs.promises.copyFile",
"tests": [
"tests/corpus/5542-fs-promises-parity.cjs",
"tests/corpus/5543-fs-promises-typed.ts"
"tests/corpus/5543-fs-promises-typed.ts",
"tests/corpus/8427-fs-copyfile-writefile-flags.ts"
]
},
"anchorSource": "exact"
@@ -46094,7 +46096,8 @@
"tests/corpus/1403-buffer-fs.ts",
"tests/corpus/2595-fs-arg-ladders.cjs",
"tests/corpus/2919-fs-function-values.ts",
"tests/corpus/2920-fs-promises-function-values.ts"
"tests/corpus/2920-fs-promises-function-values.ts",
"tests/corpus/8427-fs-copyfile-writefile-flags.ts"
]
},
"anchorSource": "exact"
@@ -47967,7 +47970,8 @@
"tests/corpus/1403-buffer-fs.ts",
"tests/corpus/2595-fs-arg-ladders.cjs",
"tests/corpus/2919-fs-function-values.ts",
"tests/corpus/2920-fs-promises-function-values.ts"
"tests/corpus/2920-fs-promises-function-values.ts",
"tests/corpus/8427-fs-copyfile-writefile-flags.ts"
]
},
"anchorSource": "exact"
@@ -49019,7 +49023,8 @@
"tests/corpus/1403-buffer-fs.ts",
"tests/corpus/2595-fs-arg-ladders.cjs",
"tests/corpus/2919-fs-function-values.ts",
"tests/corpus/2920-fs-promises-function-values.ts"
"tests/corpus/2920-fs-promises-function-values.ts",
"tests/corpus/8427-fs-copyfile-writefile-flags.ts"
]
},
"anchorSource": "exact"
@@ -91579,6 +91584,7 @@
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
"tests/corpus/5882-string-decoder-runtime-encoding.ts",
"tests/corpus/8428-string-decoder-dynamic-chunks.ts",
"tests/corpus/node24-string-decoder.ts"
]
},
@@ -91607,7 +91613,8 @@
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
"tests/corpus/node24-string-decoder.ts",
"tests/corpus/5882-string-decoder-runtime-encoding.ts"
"tests/corpus/5882-string-decoder-runtime-encoding.ts",
"tests/corpus/8428-string-decoder-dynamic-chunks.ts"
]
},
"anchorSource": "exact"
@@ -91635,7 +91642,8 @@
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
"tests/corpus/node24-string-decoder.ts",
"tests/corpus/5882-string-decoder-runtime-encoding.ts"
"tests/corpus/5882-string-decoder-runtime-encoding.ts",
"tests/corpus/8428-string-decoder-dynamic-chunks.ts"
]
},
"anchorSource": "exact"
@@ -91663,7 +91671,8 @@
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
"tests/corpus/node24-string-decoder.ts",
"tests/corpus/5882-string-decoder-runtime-encoding.ts"
"tests/corpus/5882-string-decoder-runtime-encoding.ts",
"tests/corpus/8428-string-decoder-dynamic-chunks.ts"
]
},
"anchorSource": "exact"
@@ -99989,6 +99998,7 @@
"tests/corpus/5229-url-legacy-errors.cjs",
"tests/corpus/5833-url-search-params-iterators.ts",
"tests/corpus/5834-url-search-params-iterators-dynamic.mjs",
"tests/corpus/8423-urlsearchparams-implicit-string.ts",
"tests/corpus/node24-file-url-buffer.ts",
"tests/corpus/node24-file-url-errors.js",
"tests/corpus/node24-file-url-options.ts",
@@ -101238,7 +101248,8 @@
"status": "partial",
"evidence": "surface-manifest:node-builtin.url.urlToHttpOptions",
"tests": [
"tests/corpus/1794-searchparams-url-live.ts"
"tests/corpus/1794-searchparams-url-live.ts",
"tests/corpus/8423-urlsearchparams-implicit-string.ts"
]
},
"anchorSource": "exact"
+7 -4
View File
@@ -59,6 +59,8 @@
"node-builtin.v8.getHeapStatistics": ["tests/corpus/v8-heap-statistics.ts", "tests/corpus/v8-require-optional.cjs"],
"node-builtin.events": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"],
"node-builtin.fs": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts"],
"node-builtin.fs.copyFileSync": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/8427-fs-copyfile-writefile-flags.ts"],
"node-builtin.fs.writeFileSync": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/8427-fs-copyfile-writefile-flags.ts"],
"node-builtin.fs.realpathSync.native": ["tests/corpus/fs-native-realpath.ts"],
"node-builtin.fs.fchmodSync": ["tests/corpus/2950-fs-descriptor-publication.ts"],
"node-builtin.fs.fstatSync": ["tests/corpus/2950-fs-descriptor-publication.ts"],
@@ -71,7 +73,8 @@
"node-builtin.fs.promises.utimes": ["tests/corpus/node24-fs-timestamps-promises.ts"],
"node-builtin.fs.promises.access": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
"node-builtin.fs.promises.appendFile": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
"node-builtin.fs.promises.copyFile": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
"node-builtin.fs.promises.copyFile": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts", "tests/corpus/8427-fs-copyfile-writefile-flags.ts"],
"node-builtin.fs.promises.writeFile": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/8427-fs-copyfile-writefile-flags.ts"],
"node-builtin.fs.promises.mkdtemp": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
"node-builtin.fs.promises.rmdir": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
"node-builtin.fs.promises.truncate": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
@@ -169,12 +172,12 @@
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"],
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
"node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"],
"node-builtin.string_decoder": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts"],
"node-builtin.string_decoder": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/8428-string-decoder-dynamic-chunks.ts"],
"node-builtin.test": ["tests/harness/node-test.test.ts"],
"node-builtin.timers": ["tests/corpus/1804-timers-module.ts", "tests/corpus/2093-timers-promises.ts"],
"node-builtin.tls": ["tests/corpus/2331-tls-server-options-typed.ts", "tests/corpus/2557-tls-ca-store.ts"],
"node-builtin.tty": ["tests/corpus/1448-stream-columns.ts", "tests/corpus/terminal-stream-properties.ts", "tests/harness/terminal-geometry.test.ts"],
"node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts"],
"node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts", "tests/corpus/8423-urlsearchparams-implicit-string.ts"],
"node-builtin.url.pathToFileURL": ["tests/corpus/path-to-file-url-options.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js"],
"node-builtin.url.fileURLToPath": ["tests/corpus/file-url-percent-decoding.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js"],
"node-builtin.url.fileURLToPathBuffer": ["tests/corpus/node24-file-url-buffer.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js", "tests/fixtures/node-types/file-url-conversion.ts"],
@@ -475,7 +478,7 @@
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
{ "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] },
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs", "tests/corpus/7385-als-child-process-events.ts", "tests/corpus/7386-als-child-fork-events/main.mjs", "tests/corpus/7387-als-http2-events.mjs", "tests/corpus/7388-als-http2-compat-events.mjs", "tests/corpus/7389-als-http2-secure-events.mjs", "tests/corpus/7391-als-response-end-and-finished.mjs"] },
{ "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] },
{ "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts", "tests/corpus/8428-string-decoder-dynamic-chunks.ts"] },
{ "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] },
{ "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] },
{ "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs", "tests/corpus/writable-error-callbacks.ts"] },
+6 -1
View File
@@ -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
+1 -1
View File
@@ -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>;
+9 -5
View File
@@ -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,
+17
View File
@@ -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 {
+14
View File
@@ -341,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;
@@ -375,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). */
@@ -1765,6 +1777,8 @@ 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;
@@ -21,6 +21,9 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
...(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 } : {}),
@@ -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}`);
@@ -473,6 +473,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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
@@ -621,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",
@@ -896,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-
@@ -936,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",
@@ -1141,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",
@@ -1290,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",
@@ -1478,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",
@@ -38,6 +38,15 @@ export interface NativeLinkFeatures {
/** 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;
@@ -175,6 +175,15 @@ export interface CcOptions {
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;
@@ -160,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
@@ -449,6 +454,13 @@ const RUNTIME_DEFINES: readonly {
// 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 },
@@ -66,6 +66,9 @@ export interface EarlyExecutableNativeFeatures {
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. */
@@ -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;
}
@@ -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);
@@ -36,7 +36,16 @@ 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 {
@@ -336,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 (
@@ -312,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;
@@ -326,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;
}
@@ -443,6 +444,8 @@ export function lowerProcessStreamProperty(
member !== "columns" &&
member !== "rows" &&
member !== "writableLength" &&
member !== "fd" &&
member !== "isRaw" &&
member !== "writableHighWaterMark" &&
member !== "writableNeedDrain"
)
@@ -558,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",
@@ -1526,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",
@@ -1560,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,
};
@@ -1575,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",
@@ -1598,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",
@@ -1632,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,
};
@@ -1703,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;
@@ -1089,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") {
@@ -714,6 +714,10 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr
if (e.type.kind === "string") return e;
if (e.type.kind === "url")
return { kind: "libCall", fn: "url.href", args: [e], type: STRING, loc: e.loc };
// String(sp) / `${new URLSearchParams(...)}`: URLSearchParams.toString,
// the serialized list (`?` stripped, pairs encoded).
if (e.type.kind === "searchParams")
return { kind: "libCall", fn: "sp.toString", args: [e], type: STRING, loc: e.loc };
// String(date) / `${date}` / "" + date: Date's ToPrimitive prefers
// toString().
if (e.type.kind === "date")
@@ -245,6 +245,21 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
if (active || captured) {
const use = runtimeOptionalUseOf(expr);
if (use?.complex) {
// A DECLARED absent arm is honest storage: when the checker itself
// types this use with a unit arm (an optional property read, an
// optional parameter, a `T | undefined` local), the read needs no
// narrowing aid — the union value flows into the surrounding
// logical/conditional operator, which handles the absent arm
// itself (`(entries ?? []).map(...)`, `x?.y ?? z`). Only a hidden
// absence the checker does not admit needs the refusal below.
const checked = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (
checked?.kind === "union" &&
local.type.kind === "union" &&
lowerer.armTag(checked.unionId, UNDEFINED_T) >= 0 &&
lowerer.armTag(local.type.unionId, UNDEFINED_T) >= 0
)
return { kind: "varRef", localId: local.id, type: local.type, loc };
lowerer.unsupported(
"SC1090",
expr,
@@ -590,7 +590,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
if (
isJsSourceFile(ts.getSourceFileOfNode(node)) &&
ts.isPropertyAccessExpression(node) &&
["isTTY", "columns", "rows"].includes(node.name.text) &&
["isTTY", "columns", "rows", "isRaw"].includes(node.name.text) &&
lowerer.isStdlibMember(node)
)
return expr;
@@ -714,7 +714,9 @@ export function lowerPropertyAccessExpr(
};
}
}
if ((expr.name.text === "index" || expr.name.text === "input") && !expr.questionDotToken) {
// `re.exec(s)?.index` takes the same read: the chain lowering binds the
// guarded receiver (non-nullish inside the guard) before re-dispatching.
if (expr.name.text === "index" || expr.name.text === "input") {
const represented = tryLowerExpression(lowerer, expr.expression);
if (
represented &&
@@ -4074,8 +4074,7 @@ function lowerDynReceiverMethodCall(
(lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 ||
// A receiver of the boxed String type may be a wrapper object.
(lowerer.createsWrappers ??= programCreatesWrappers(lowerer))) &&
!call.questionDotToken &&
!access.questionDotToken &&
!lowerer.chainBlocked(call, access) &&
!call.arguments.nodes.some(ts.isSpreadElement)
)
return lowerStringOrMemberCall(lowerer, call, access, recv);
@@ -4091,8 +4090,6 @@ function lowerDynReceiverMethodCall(
// runtime dispatch.
if (
!ownCallable &&
!call.questionDotToken &&
!access.questionDotToken &&
isDynPrimitiveKindMethod(access.name.text) &&
!lowerer.chainBlocked(access, call)
) {
@@ -4240,9 +4237,13 @@ function lowerDynReceiverMethodCall(
!stringOnlyName
)
return null;
// Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain
// machinery's short-circuit semantics — not modeled here yet.
if (call.questionDotToken || access.questionDotToken) return null;
// An UNHANDLED `?.` step (`obj.cb?.()`, `obj?.cb()`) belongs to the chain
// machinery's short-circuit semantics. Inside the guarded body of a chain
// the active lowering has ALREADY proven the receiver present, so the
// step is a plain read-and-call there — the same "read the member, call
// it" shape below, with `dynCall` throwing Node's catchable
// "obj.cb is not a function" for a present but non-callable member.
if (lowerer.chainBlocked(call, access)) return null;
const loc = locOf(call);
const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN);
const receiver: IrExpr = { kind: "varRef", localId: receiverLocal.id, type: DYN, loc };
@@ -8040,6 +8041,16 @@ function lowerObjectStaticCall(
isUnitType(object.type)
)
object = { kind: "dynFrom", value: object, type: DYN, loc: locOf(call) };
// An array or class instance reflects through its own checked-dynamic
// view (own fields and elements, in property order). A record keeps its
// static key walk below when it has one — `getOwnPropertyNames` over a
// fixed record is exactly Object.keys' construction, and boxing a typed
// record would link the dynamic tier into a program that needs none of
// it. The descriptor and symbol forms have no static walk, so a record
// takes the view there.
const staticWalk = member === "getOwnPropertyNames" && object.type.kind === "record";
if (!staticWalk && object.type.kind !== "dyn" && lowerer.dynConvertible(object.type))
object = lowerer.coerceToExpected(object, DYN);
if (object.type.kind === "dyn")
return {
kind: "libCall",
@@ -2111,7 +2111,14 @@ export function collectClassShapeInner(
const emitterBase = lowerer.builtinEmitterInfoOf(resolved);
const streamBaseNs = builtinStreamInfoOf(lowerer, resolved);
if (emitterBase || streamBaseNs) {
if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes");
// An EventEmitter's type argument is the listener MAP, a pure
// typing aid: the emitter base is one runtime class whatever the
// map says, and the map's keys never reach the layout (Node's
// own generic erases at runtime), so `extends
// EventEmitter<MyEvents>` adopts the same base. A stream base's
// argument IS the element layout, which still has to be concrete.
if (!emitterBase && t.typeArguments?.nodes)
lowerer.unsupported("SC1090", t, "extending generic classes");
base = (emitterBase ?? streamBaseNs)!;
continue;
}
@@ -2341,7 +2348,13 @@ export function collectClassShapeInner(
base = instBase;
continue;
}
if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes");
// An EventEmitter base's type argument is the listener MAP — a pure
// typing aid (the runtime base is one emitter class whatever the map
// says, and Node erases it too), so `extends EventEmitter<Events>`
// adopts the same base. Every other generic base still refuses:
// its argument can carry a layout.
if (t.typeArguments?.nodes && lowerer.builtinEmitterInfoOf(symbol) === null)
lowerer.unsupported("SC1090", t, "extending generic classes");
if (named?.extendsArray)
lowerer.unsupported(
"SC1090",
@@ -4,6 +4,7 @@ import { globalValuesInstall, installsGlobalValues } from "./global-values.js";
import type { TypeFailureSlot } from "../../diagnostics/diagnostic.js";
import { THREADS_MODULE } from "../threads-import.js";
import { collectIslandImport, collectIslandRequires } from "./lower-node-island.js";
import { staticString } from "./builtins/module-resolution.js";
import { NODE_API_MODULE } from "../node-api-import.js";
import { positionTag } from "./structural-positions.js";
import { EXPERIMENTAL_MODULE } from "../experimental-import.js";
@@ -799,9 +800,10 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void {
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
node.arguments.nodes.length >= 1 &&
node.arguments.nodes[0] !== undefined &&
ts.isStringLiteralLike(node.arguments.nodes[0])
staticString(lowerer, node.arguments.nodes[0]) !== null
) {
const spec = node.arguments.nodes[0].text;
// A constant expression names its module as surely as a literal.
const spec = staticString(lowerer, node.arguments.nodes[0]!)!;
if (lowerer.externalTypes.has(spec)) {
return undefined;
}
@@ -820,9 +822,14 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void {
return undefined;
}
const modSym = lowerer.checker.getSymbolAtLocation(node.arguments.nodes[0]);
// A folded specifier carries no module symbol on its argument (the
// text comes from a `const`, not a literal), so fall back to the
// checker's program import resolution.
const folded = resolveImport(lowerer.program, sf, spec);
const ownModule =
modSym !== undefined &&
modSym.declarations.some((d) => ts.isSourceFile(d) && !d.isDeclarationFile);
(modSym !== undefined &&
modSym.declarations.some((d) => ts.isSourceFile(d) && !d.isDeclarationFile)) ||
(folded !== null && !folded.isDeclarationFile);
const builtin = canonicalBuiltinModule(spec);
const res = ownModule
? ({ kind: "program-module" } as const)
@@ -2216,6 +2216,54 @@ function lowerNetServerMethodCall(
'"connection", "request", "checkContinue", "checkExpectation", "drop", "stream", "session", "upgrade", "connect", "error", "close", "listening", and "sessionError" are the supported server events (as literals)',
);
}
if ((name === "off" || name === "removeListener") && args.length === 2) {
// server.off(event, cb): the LAST listener standing for the same
// function leaves that event's list (Node's removeListener order).
// Only the event families whose registry keeps a removable list; the
// rest refuse rather than silently leaving the listener in place.
requireStatementPosition(lowerer, call, `server.${name}(...)`);
const evT = lowerer.typeOf(args[0]!);
const event = evT.isStringLiteralType() ? evT.value : null;
const kind =
event === null
? null
: event === "error"
? 1
: event === "close"
? 2
: event === "listening"
? 3
: event === "connection" || event === "secureConnection"
? 4
: event === "timeout"
? 5
: event === "drop"
? 6
: null;
if (kind === null) {
lowerer.noLowering(
`server.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
args[0]!,
'"error", "close", "listening", "connection", "secureConnection", "timeout", and "drop" are the events off/removeListener removes (as literals)',
);
}
const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T);
const listener = lowerer.lowerExpr(args[1]!);
if (listener.type.kind !== "func") {
lowerer.unsupported(
"SC1090",
args[1]!,
`server.${name} with a listener that is not a function`,
);
}
return {
kind: "libCall",
fn: "net.serverOff",
args: [receiver, listener, numLit(kind, loc)],
type: VOID,
loc,
};
}
if (name === "emit") {
// The demux route: emit("connection", socket) hands an accepted
// socket to ANOTHER server (portless's first-byte TLS peek). Only
@@ -2,6 +2,8 @@ import { dynUndefinedExpr, nodeThrowExpr } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import { webStreamClassKindOf } from "./builtins/web-streams-dyn.js";
import { staticString } from "./builtins/module-resolution.js";
import { resolveImport } from "../program.js";
import * as ts from "../ts7/adapter.js";
import { lowerToNumberArguments } from "./lower-exprs.js";
import type { Lowerer } from "./lowerer.js";
@@ -2230,7 +2232,11 @@ export function lowerStaticReadableStreamReaderCall(
export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
if (call.expression.kind !== ts.SyntaxKind.ImportKeyword) return null;
const arg = call.arguments.nodes[0];
if (arg === undefined || !ts.isStringLiteralLike(arg)) {
// The specifier text must be known at build time; a constant expression
// (`import("node:" + "fs")`, `import(FS)`) names its module just as a
// literal does. collectDynamicImports keys its table by the same text.
const specifier = arg === undefined ? null : staticString(lowerer, arg);
if (specifier === null) {
lowerer.unsupported(
"SC1090",
call,
@@ -2243,11 +2249,11 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression
if (call.arguments.nodes.length !== 1) {
lowerer.unsupported("SC1090", call, "dynamic import() with import attributes");
}
const res = lowerer.dynImports.get(`${ts.getSourceFileOfNode(call).fileName}\u0000${arg.text}`);
const res = lowerer.dynImports.get(`${ts.getSourceFileOfNode(call).fileName}\u0000${specifier}`);
if (res === undefined) lowerer.refuseUnsupportedApi("'import()'", call);
if (res.kind === "rejected") return lowerRejectedImport(lowerer, call, res.crash);
if (res.kind === "program-module") {
return lowerOwnStaticModuleImport(lowerer, call, arg);
return lowerOwnStaticModuleImport(lowerer, call, specifier);
}
return lowerStaticNamespaceImport(lowerer, call, {
kind: "moduleNs",
@@ -2318,13 +2324,24 @@ function lowerRejectedImport(
};
}
/** The checker-resolved program source behind one literal import(). */
function programImportTarget(lowerer: Lowerer, arg: ts.StringLiteralLike): ts.SourceFile | null {
const modSym = lowerer.checker.getSymbolAtLocation(arg);
for (const d of modSym ? modSym.declarations : []) {
if (ts.isSourceFile(d) && !d.isDeclarationFile) return d;
/** The compiled module a literal specifier names. A FOLDED specifier
* (a `const` or a `+` concatenation) carries no module symbol on its
* argument node, so it resolves through the checker's program import
* resolution instead; only modules already in the program resolve. */
function programImportTarget(
lowerer: Lowerer,
call: ts.CallExpression,
specifier: string,
): ts.SourceFile | null {
const arg = call.arguments.nodes[0];
if (arg !== undefined && ts.isStringLiteralLike(arg)) {
const modSym = lowerer.checker.getSymbolAtLocation(arg);
for (const d of modSym ? modSym.declarations : []) {
if (ts.isSourceFile(d) && !d.isDeclarationFile) return d;
}
}
return null;
const resolved = resolveImport(lowerer.program, ts.getSourceFileOfNode(call), specifier);
return resolved !== null && !resolved.isDeclarationFile ? resolved : null;
}
/** Static literal import of one compiled ESM module. The async helper's
@@ -2335,14 +2352,14 @@ function programImportTarget(lowerer: Lowerer, arg: ts.StringLiteralLike): ts.So
function lowerOwnStaticModuleImport(
lowerer: Lowerer,
call: ts.CallExpression,
arg: ts.StringLiteralLike,
specifier: string,
): IrExpr {
const dep = programImportTarget(lowerer, arg);
const dep = programImportTarget(lowerer, call, specifier);
if (dep !== null && (dep.fileName.endsWith(".cts") || isCjsJsFile(dep, lowerer.program))) {
lowerer.unsupported(
"SC1090",
call,
`dynamic import of the program's own CommonJS module '${arg.text}' ` +
`dynamic import of the program's own CommonJS module '${specifier}' ` +
"(its namespace comes from module.exports through Node's CJS lexer, " +
"which has no compiled story — require it, or import it statically)",
);
@@ -2351,7 +2368,7 @@ function lowerOwnStaticModuleImport(
lowerer.unsupported(
"SC1090",
call,
`dynamic import of the program's own module '${arg.text}' ` +
`dynamic import of the program's own module '${specifier}' ` +
"(this module is not part of the compiled module graph)",
);
}
@@ -1261,9 +1261,9 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable {
// truncateSync's omitted len completes to 0 in lowerBuiltinModuleCall,
// like accessSync's mode.
truncateSync: { fn: "fs.truncateSync", params: [STRING, F64], result: VOID },
// The 2-argument form only: Node's mode flags (COPYFILE_EXCL, ...)
// land on the arity fence.
copyFileSync: { fn: "fs.copyFileSync", params: [STRING, STRING], result: VOID },
// The mode is libuv's copyfile flag bits (COPYFILE_EXCL, ...); the
// 2- and 3-argument forms lower in lowerBuiltinModuleCall.
copyFileSync: { fn: "fs.copyFileSync", params: [STRING, STRING, F64], result: VOID },
// The callback form is special-cased in lowerBuiltinModuleCall: its
// Error | null parameter is program-shaped and needs an emitted
// adapter. The row still projects the static surface and routes the
@@ -1469,10 +1469,10 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable {
params: [STRING, STRING],
result: { kind: "promise", inner: VOID },
},
// The 2-argument form only, like copyFileSync.
// The 2- and 3-argument forms, like copyFileSync.
copyFile: {
fn: "fsp.copyFile",
params: [STRING, STRING],
params: [STRING, STRING, F64],
result: { kind: "promise", inner: VOID },
},
mkdtemp: { fn: "fsp.mkdtemp", params: [STRING], result: { kind: "promise", inner: STRING } },
@@ -2271,15 +2271,20 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
],
// The string source's check before a URL destination converts
// (fs-paths.ts).
copyFileSync: ["fs.checkPath"],
copyFileSync: ["fs.checkPath", "fs.copyFileFlagsSync"],
renameSync: ["fs.checkPath"],
copyFile: ["fs.checkPath"],
copyFile: ["fs.checkPath", "fsp.copyFileFlags"],
rename: ["fs.checkPath"],
readlinkSync: ["fs.readlinkSyncStr", "fs.readlinkSyncBuffer"],
// The bytes-data form and the { mode } options form.
// The bytes-data form, the { mode } options form, and the
// file-descriptor data form.
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync", "fs.writeFileSyncFd"],
writeFileSync: [
"fs.writeFileSyncBytes",
"fs.writeFileModeSync",
"fs.writeFileFlagsSync",
"fs.writeFileSyncFd",
],
appendFileSync: ["fs.appendFileSyncBytes"],
// The fs stream members (bytesRead/bytesWritten, pending, path, close).
createReadStream: [
@@ -2330,8 +2335,8 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
// The Buffer form (no encoding) and the runtime-encoding forms.
readFile: ["fsp.readFileBytes", "fsp.readFileEncoded"],
readlink: ["fsp.readlinkStr", "fsp.readlinkBuffer"],
// The string-data { mode } options form.
writeFile: ["fsp.writeFileMode"],
// The string-data { mode, flag } options forms.
writeFile: ["fsp.writeFileMode", "fsp.writeFileFlags"],
// The { withFileTypes: true } Dirent form.
readdir: ["fsp.readdirTypes"],
glob: ["fsp.globTypes"],
+3
View File
@@ -765,6 +765,9 @@ async function compileExecutableNative(
...(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 } : {}),
@@ -91,6 +91,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"child.onDyn",
"text.decoderEncoding",
"strdec.encoding",
"strdec.writeDyn",
"strdec.nextDyn",
"dyn.construct",
"dyn.reflectApply",
"dyn.reflectGet",
@@ -252,8 +254,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dc.tcTracePromise",
"process.onUncaughtException",
"process.offUncaughtException",
"process.prependUncaughtException",
"process.onSignal",
"process.offSignal",
"process.prependSignal",
"process.onUnhandledRejection",
"process.onRejectionHandled",
"async.awaitDyn",
@@ -745,6 +749,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.keyDelete",
"dyn.keyDeleteComputed",
"dyn.reflectDeleteProperty",
"dyn.reflectHasTarget",
"dyn.hasKeyComputed",
"dyn.hasOwnComputed",
"dyn.propertyIsEnumerableComputed",
@@ -1077,6 +1082,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dir.readSync",
"dir.closeSync",
"fs.copyFileSync",
"fs.copyFileFlagsSync",
"fs.renameSync",
"fs.renameCb",
"fs.lstatSync",
@@ -1099,6 +1105,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fs.readlinkSyncStr",
"fs.readlinkSyncBuffer",
"fs.writeFileModeSync",
"fs.writeFileFlagsSync",
"fs.mkdirModeSync",
"fs.mkdirRecursiveModeSync",
"fs.openSync",
@@ -255,6 +255,8 @@ function buildLibFnSignatures() {
"dyn.keyDelete": { argTypes: [DYN, STRING, BOOL], result: VOID },
"dyn.keyDeleteComputed": { argTypes: [DYN, DYN, BOOL], result: VOID },
"dyn.reflectDeleteProperty": { argTypes: [DYN, DYN], result: BOOL },
/** Reflect.has's target check: Node's TypeError for a primitive. */
"dyn.reflectHasTarget": { argTypes: [DYN], result: VOID },
"dyn.hasKeyComputed": { argTypes: [DYN, DYN], result: BOOL },
"dyn.hasOwnComputed": { argTypes: [DYN, DYN], result: BOOL },
"dyn.propertyIsEnumerableComputed": { argTypes: [DYN, DYN], result: BOOL },
@@ -603,6 +605,11 @@ function buildLibFnSignatures() {
* alive. Invalid callbacks and uncatchable signals throw. */
"process.onSignal": { argTypes: [STRING, DYN, BOOL], result: VOID },
"process.offSignal": { argTypes: [STRING, DYN], result: VOID },
/** process.prependListener(<signal>, cb): the same listener in front of
* the signal's existing registrations (Node appends with `on`, so the
* two differ only in dispatch order). Never `once` — that is
* prependOnceListener, which has no lowering. */
"process.prependSignal": { argTypes: [STRING, DYN], result: VOID },
/** The process 'exit' event — process.on/once/off("exit"). args: [cb,
* once bool] / [cb]. Listeners run SYNCHRONOUSLY at termination
* (normal exit, process.exit(), the uncaught/unhandled exit-1 paths)
@@ -611,6 +618,9 @@ function buildLibFnSignatures() {
* picks the runtime adapter. Never throw. */
"process.onExit": { argTypes: [null, BOOL], result: VOID },
"process.offExit": { argTypes: [null], result: VOID },
/** process.prependListener("exit", cb): the same listener in front of
* the exit registry's existing entries. */
"process.prependExit": { argTypes: [null], result: VOID },
/** process.stdin listener registration — stdin.on/once("data" | "end" |
* "error", cb). args: [cb, once bool]. data callbacks: () => void or
* (chunk: Uint8Array) => void; end: () => void; error: () => void or
@@ -1486,6 +1496,12 @@ function buildLibFnSignatures() {
* clamped like ftruncateSync's) and closes it. */
"fs.truncateSync": { argTypes: [STRING, F64], result: VOID },
"fs.copyFileSync": { argTypes: [STRING, STRING], result: VOID },
/** copyFileSync(src, dest, mode): libuv's uv_fs_copyfile flag bits —
* COPYFILE_EXCL (1) fails on an existing destination, COPYFILE_FICLONE
* (2) requests a reflink and falls back to a copy, COPYFILE_FICLONE_FORCE
* (4) requires one. Out-of-range modes throw the RangeError Node throws
* ("mode is out of range: >= 0 && <= 7"). */
"fs.copyFileFlagsSync": { argTypes: [STRING, STRING, F64], result: VOID },
/** renameSync is the direct two-path syscall wrapper. renameCb defers
* the operation to the loop and fires a program-shaped Error | null
* callback through a backend-emitted adapter. */
@@ -1493,6 +1509,10 @@ function buildLibFnSignatures() {
// Callback type is program-dependent (zero params or Error | null).
"fs.renameCb": { argTypes: [STRING, STRING, null], result: VOID },
"fs.writeFileModeSync": { argTypes: [STRING, STRING, F64], result: VOID },
/** writeFileSync(path, data, { mode, flag }): the same open(2) write
* with the disposition flag — flag 1 is "wx" (O_EXCL: EEXIST when the
* path exists), 0 is "w" (the default truncate-or-create). */
"fs.writeFileFlagsSync": { argTypes: [STRING, STRING, F64, F64], result: VOID },
"fs.mkdirModeSync": { argTypes: [STRING, F64], result: VOID },
"fs.mkdirRecursiveModeSync": { argTypes: [STRING, F64], result: VOID },
"atomics.wait": { argTypes: [bytesOf("i32"), F64, F64, F64], result: STRING },
@@ -1668,6 +1688,12 @@ function buildLibFnSignatures() {
result: VOID,
},
"net.serverOnConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
/** server.off(event, listener) / removeListener: [server, listener
* closure, list kind]. The kind selects the event's registry (1 error,
* 2 close, 3 listening, 4 connection/secureConnection, 5 timeout,
* 6 drop — the scr_net.h SCR_NET_OFF_* values); Node removes the most
* recently added matching listener. Never throws. */
"net.serverOff": { argTypes: [NETSERVER_T, null, F64], result: VOID },
/** 'secureConnection' — the TLS server's deferred 'connection' list
* (handshake-completion timing); on a server without deferred
* connections (a plain net server) the registration never fires,
@@ -3516,6 +3542,17 @@ function buildLibFnSignatures() {
argTypes: [STRING, STRING, F64],
result: { kind: "promise", inner: VOID },
},
/** fs.promises.writeFile(path, data, { mode, flag }) and
* fs.promises.copyFile(src, dest, mode): the settled-promise twins of
* fs.writeFileFlagsSync and fs.copyFileFlagsSync. */
"fsp.writeFileFlags": {
argTypes: [STRING, STRING, F64, F64],
result: { kind: "promise", inner: VOID },
},
"fsp.copyFileFlags": {
argTypes: [STRING, STRING, F64],
result: { kind: "promise", inner: VOID },
},
"fsp.mkdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
/** The fs/promises option/member tail the certs pipeline uses: mkdir's
* literal { recursive?, mode? } options (the mkdirSync matrix behind
@@ -4995,6 +5032,11 @@ function buildLibFnSignatures() {
* packed NEW pending; end: (enc, pending) → the buffered partial's
* flush (+1). Node-exact per encoding (oracle-pinned); none throws. */
"strdec.write": { argTypes: [STRING, F64, BYTES_U8], result: STRING },
/** The checked-dynamic chunk forms (a stream "data" payload): a string
* passes through unchanged without touching the buffered state, an
* ArrayBufferView decodes, anything else throws ERR_INVALID_ARG_TYPE. */
"strdec.writeDyn": { argTypes: [STRING, F64, DYN], result: STRING },
"strdec.nextDyn": { argTypes: [STRING, F64, DYN], result: F64 },
"strdec.next": { argTypes: [STRING, F64, BYTES_U8], result: F64 },
"strdec.end": { argTypes: [STRING, F64], result: STRING },
/** A runtime encoding argument's canonical name (Node's normalizeEncoding:
@@ -5220,6 +5262,9 @@ function buildLibFnSignatures() {
* non-function. */
/** Uncaught exception handlers and monitors: [callback dyn, once, monitor]. */
"process.onUncaughtException": { argTypes: [DYN, BOOL, BOOL], result: VOID },
/** Uncaught handlers registered in FRONT of the registry's existing
* entries: [callback dyn, monitor] (never `once`). */
"process.prependUncaughtException": { argTypes: [DYN, BOOL], result: VOID },
"process.offUncaughtException": { argTypes: [DYN, BOOL], result: VOID },
"process.onUnhandledRejection": { argTypes: [DYN, BOOL], result: VOID },
"process.offUnhandledRejection": { argTypes: [DYN], result: VOID },
@@ -5484,6 +5529,10 @@ function buildLibFnSignatures() {
"process.columns": { argTypes: [F64], result: VOID },
/** Terminal height, with the same fd and optional-number contract. */
"process.rows": { argTypes: [F64], result: VOID },
/** process.stdin.isRaw: the raw-mode flag of a TTY stdin as the
* module's interned `boolean | undefined` union (a pipe-backed stdin
* has no such flag — Node answers undefined there). Never throws. */
"process.stdinIsRaw": { argTypes: [], result: VOID },
/** process.stdin.destroy(): a deliberate no-op (no stream machinery
* exists to tear down; documented in SEMANTICS.md). */
"process.stdinDestroy": { argTypes: [], result: VOID },
@@ -100,6 +100,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.offWarning",
"process.onRejectionHandled",
"process.onUncaughtException",
"process.prependUncaughtException",
"process.onUnhandledRejection",
"process.onWarning",
"process.setDeprecationFlag",
@@ -646,7 +647,10 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.offSignal",
"process.onExit",
"process.onSignal",
"process.prependExit",
"process.prependSignal",
"process.stdio",
"process.stdinIsRaw",
"process.stdioSlot",
"process.stdioWriteDyn",
"stdin.nextChunk",
@@ -1297,6 +1301,33 @@ const RUNTIME_FEATURE_RULES = {
"tls.sockGetCertificate",
],
}),
/** writeFile's `flag` disposition ("w" / "wx") and copyFile's mode
* argument: scr_fs_flags.c and the SCR_FS_FLAGS build of scr_fs_cp.c.
* Programs that never pass either keep both units' default objects. */
fsFlags: rule({
fns: [
"fs.copyFileFlagsSync",
"fs.writeFileFlagsSync",
"fsp.copyFileFlags",
"fsp.writeFileFlags",
],
}),
/** string_decoder's checked-dynamic chunk forms (a stream "data" payload):
* scr_bytes_strdec_dyn.c. */
strdecDyn: rule({ fns: ["strdec.nextDyn", "strdec.writeDyn"] }),
/** The process and stream event registries' extras — prependListener for
* signals, 'exit' and uncaught exceptions, stdin.isRaw, and 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: rule({
fns: [
"net.serverOff",
"process.prependExit",
"process.prependSignal",
"process.prependUncaughtException",
"process.stdinIsRaw",
],
}),
zlib: rule({ prefixes: ["zlib."] }),
/** node:zlib's Brotli forms: zlib.brotliInstall, which %main calls when
* the program uses Brotli (scr_brotli.c). */
@@ -1339,6 +1370,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
eventsOnceSignal: false,
fetch: false,
fileHandle: false,
fsFlags: false,
fsWatch: false,
fsWatchFile: false,
http2: false,
@@ -1360,6 +1392,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
pki: false,
ed25519: false,
processEvents: false,
processExtras: false,
qs: false,
regex: false,
searchParams: false,
@@ -1371,6 +1404,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
moduleNamespaceDyn: false,
primitiveWrapper: false,
stream: false,
strdecDyn: false,
streamDyn: false,
handleProps: false,
childLifecycle: false,
+14
View File
@@ -4502,6 +4502,20 @@ function validateFunction(
}
return true;
}
if (e.fn === "process.stdinIsRaw") {
// Result is the module's interned `boolean | undefined` union
// (Node's undefined on a pipe-backed stdin).
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "bool" &&
def.arms[1]!.kind === "undefinedT";
if (!ok) {
err(`libCall process.stdinIsRaw must return the 'boolean | undefined' union`, e.loc);
}
return true;
}
return false;
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8423-process-prepend-listeners.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8423-urlsearchparams-implicit-string.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8424-child-optional-chain.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8424-regex-match-index-chain.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8425-array-from-string-nullable.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8425-server-off.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8426-process-stream-fd-israw.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8426-reflect-object-reflections.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8427-emitter-generic-base.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8427-fs-copyfile-writefile-flags.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8428-spawn-detached-runtime.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8428-string-decoder-dynamic-chunks.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8500-optional-method-call-receivers.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8501-nullish-optional-capture-receivers.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8502-constant-module-specifiers.ts"
],
"diags": []
}
@@ -62,6 +62,12 @@ export const BASE_RUNTIME_SOURCES = [
export const OPTIONAL_RUNTIME_SOURCES = [
["scr_copying.c", "copying"],
["scr_file_handle.c", "fileHandle"],
// writeFile's `flag` disposition and the fs/promises twins; the copyFile
// mode form is the SCR_FS_FLAGS build of scr_fs_cp.c.
["scr_fs_flags.c", "fsFlags"],
// string_decoder's checked-dynamic chunk forms, apart from scr_bytes.c's
// typed twins so a typed program keeps that object byte for byte.
["scr_bytes_strdec_dyn.c", "strdecDyn"],
// BigIntStats member reads (`statSync(p, { bigint: true })` members). The
// bigstats gate is narrower than bigint, which every bigint value turns on.
["scr_stats_bigint.c", "bigstats"],
@@ -318,6 +324,37 @@ export function variantsFor(source) {
when: { warningReplace: true },
defines: ["SCR_WARNING_REPLACE"],
});
// The process-prepend registries (processExtras) add
// scr_process_prepend_uncaught_exception, in a build of its own so the
// default object stays put for every program.
variants.push({
id: "process-extras",
when: { processExtras: true },
defines: ["SCR_PROCESS_EXTRAS"],
});
variants.push({
id: "warning-replace-process-extras",
when: { warningReplace: true, processExtras: true },
defines: ["SCR_WARNING_REPLACE", "SCR_PROCESS_EXTRAS"],
});
}
// copyFileSync's mode argument reuses scr_fs_cp.c's reflink walk, so the
// entry lives in a build of this unit rather than a unit of its own.
if (source === "scr_fs_cp.c") {
variants.push({
id: "flags",
when: { fsFlags: true },
defines: ["SCR_FS_FLAGS"],
});
}
// server.off / removeListener: the listener-removal entry, in a build of
// its own so a program that never removes a listener keeps the default.
if (source === "scr_net.c") {
variants.push({
id: "server-off",
when: { processExtras: true },
defines: ["SCR_NET_SERVER_OFF"],
});
}
// Programs with web streams link the promise unit with native promise
// reactions, the builtin-class unit whose instanceof knows their classes
@@ -371,6 +408,29 @@ export function variantsFor(source) {
when: { stdioExtended: true, nodeIsland: true },
defines: ["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"],
});
// processExtras adds the prepend registrations and stdin's raw-mode
// flag; the combinations above then need their own builds, since the
// variant with the most conditions is the one selected.
variants.push({
id: "process-extras",
when: { processExtras: true },
defines: ["SCR_PROCESS_EXTRAS"],
});
variants.push({
id: "process-extras-stdio-extended",
when: { processExtras: true, stdioExtended: true },
defines: ["SCR_PROCESS_EXTRAS", "SCR_STDIO_EXTENDED"],
});
variants.push({
id: "process-extras-node-island",
when: { processExtras: true, nodeIsland: true },
defines: ["SCR_PROCESS_EXTRAS", "SCR_NODE_ISLANDS"],
});
variants.push({
id: "process-extras-stdio-extended-node-island",
when: { processExtras: true, stdioExtended: true, nodeIsland: true },
defines: ["SCR_PROCESS_EXTRAS", "SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"],
});
// The typed process output write slot (the write method as a value, its
// replacement, run-time-typed chunks): the shared write body, the
// dynamic-chunk entry point and Node's anonymous write function. Every
@@ -394,6 +454,31 @@ export function variantsFor(source) {
],
])
variants.push({ id, when, defines });
// A program with both the process extras and the write slot needs both
// defines in one build.
for (const [id, when, defines] of [
[
"process-extras-write-slot",
{ processExtras: true, stdioWriteSlot: true },
["SCR_PROCESS_EXTRAS", "SCR_STDIO_WRITE_SLOT"],
],
[
"process-extras-stdio-extended-write-slot",
{ processExtras: true, stdioExtended: true, stdioWriteSlot: true },
["SCR_PROCESS_EXTRAS", "SCR_STDIO_EXTENDED", "SCR_STDIO_WRITE_SLOT"],
],
[
"process-extras-node-island-write-slot",
{ processExtras: true, nodeIsland: true, stdioWriteSlot: true },
["SCR_PROCESS_EXTRAS", "SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"],
],
[
"process-extras-stdio-extended-node-island-write-slot",
{ processExtras: true, stdioExtended: true, nodeIsland: true, stdioWriteSlot: true },
["SCR_PROCESS_EXTRAS", "SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS", "SCR_STDIO_WRITE_SLOT"],
],
])
variants.push({ id, when, defines });
}
// Programs with Node islands link the event loop with the Node island slot;
// every other program keeps the default object, byte for byte.
+18
View File
@@ -476,6 +476,24 @@ void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor) {
scr_uncaught_sync_hook();
}
#ifdef SCR_PROCESS_EXTRAS
/* process.prependListener("uncaughtException"[Monitor], fn): the same
* listener in FRONT of the registry's existing entries (Node's
* EventEmitter order). The append path above registers it and the new entry
* then moves to the front, so the two share one registration; a refused
* listener (a non-function) left the list alone and nothing moves. */
void scr_process_prepend_uncaught_exception(ScrDyn *fn, bool monitor) {
size_t k = monitor ? 1 : 0;
size_t before = scr_uncaught_n[k];
scr_process_on_uncaught_exception(fn, false, monitor);
if (scr_uncaught_n[k] != before + 1) return;
ScrRejListener entry = scr_uncaught_ls[k][scr_uncaught_n[k] - 1];
memmove(scr_uncaught_ls[k] + 1, scr_uncaught_ls[k],
(scr_uncaught_n[k] - 1) * sizeof *scr_uncaught_ls[k]);
scr_uncaught_ls[k][0] = entry;
}
#endif
void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor) {
if (!scr_rej_check_listener(fn)) return;
size_t k = monitor ? 1 : 0;
+5
View File
@@ -189,6 +189,11 @@ double scr_bytes_to_u8_clamp(double value);
* incomplete-tail walk, base64's mod-3 groups, utf16le's odd-byte and
* trailing-lead-surrogate holds. None throws. */
ScrStr *scr_strdec_write(const ScrStr *enc, double pending, const ScrBytes *chunk);
/* The checked-dynamic chunk forms: a string passes through unchanged (and
* leaves the buffered state alone); an ArrayBufferView decodes; anything
* else throws Node's ERR_INVALID_ARG_TYPE. */
ScrStr *scr_strdec_write_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk);
double scr_strdec_next_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk);
double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk);
ScrStr *scr_strdec_end(const ScrStr *enc, double pending);
ScrStr *scr_strdec_encoding(const ScrDyn *enc); /* +1, or NULL with ERR_UNKNOWN_ENCODING pending */
@@ -0,0 +1,48 @@
/* string_decoder's checked-dynamic chunk forms.
*
* A stream's "data" payload crosses the checked-dynamic boundary, so
* decoder.write and decoder.end receive it as an ScrDyn: a string passes
* through unchanged without touching the buffered state, an ArrayBufferView
* (Buffer, TypedArray, DataView) decodes through scr_bytes.c's per-encoding
* helpers with the packed-f64 pending state, and anything else throws Node's
* ERR_INVALID_ARG_TYPE for "buf". They live in their own unit because the
* static twins in scr_bytes.c belong only to programs whose chunks are
* already typed; those programs keep that object byte for byte. */
#include "scr_runtime.h"
/* The bytes view of a checked-dynamic write chunk, or NULL for a string
* chunk (Node's write returns it unchanged, never touching the decoder).
* Anything that is not a string or an ArrayBufferView throws Node's
* ERR_INVALID_ARG_TYPE for "buf". */
static ScrBytes *scr_strdec_dyn_view(const ScrDyn *chunk, bool *is_string) {
*is_string = chunk->kind == SCR_DYN_STR;
if (*is_string) return NULL;
/* An ArrayBuffer or SharedArrayBuffer is not a view: Node throws. */
if (chunk->kind == SCR_DYN_TYPED_REF || chunk->kind == SCR_DYN_BYTES)
return scr_bytes_buffer_source(chunk);
scr_dyn_arg_type_fail("buf", "an instance of Buffer, TypedArray, or DataView", chunk);
return NULL;
}
/* decoder.write(chunk) where the chunk crossed the checked-dynamic
* boundary (`stream.on("data", (data) => decoder.write(data))`). */
ScrStr *scr_strdec_write_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
bool is_string = false;
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
if (is_string) return scr_str_retain(chunk->v.str);
if (!view) return NULL;
ScrStr *out = scr_strdec_write(enc, pending, view);
scr_bytes_release(view);
return out;
}
/* The decoder state after that write: a string chunk leaves it alone. */
double scr_strdec_next_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
bool is_string = false;
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
if (is_string) return pending;
if (!view) return 0;
double hold = scr_strdec_next(enc, pending, view);
scr_bytes_release(view);
return hold;
}
+2
View File
@@ -1156,6 +1156,8 @@ void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored);
void scr_dyn_symbol_field_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored);
void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key);
bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key);
/* Reflect.has's target check: Node's TypeError pending for a primitive. */
void scr_dyn_reflect_has_target(const ScrDyn *target);
bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key);
bool scr_dyn_property_is_enumerable_computed(const ScrDyn *value, const ScrDyn *key);
void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *key, bool strict);
+55 -3
View File
@@ -246,6 +246,34 @@ void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) {
#endif
}
#ifdef SCR_PROCESS_EXTRAS
/* process.prependListener(<signal>, cb): the same listener in FRONT of the
* signal's existing registrations (Node's EventEmitter order). The append
* path above registers it and the new entry then moves to the front, so the
* two share one registration; a refused or inert registration (a throw, a
* non-main context) left the list alone and nothing moves. */
void scr_signal_prepend(ScrStr *name, ScrDyn *cb) {
#ifdef __wasi__
scr_signal_on(name, cb, false); /* traps, exactly as the append form does */
#else
int sig = scr_signal_from_name(name);
if (sig < 0) sig = 0;
ScrSigReg *reg = scr_sig_regs;
while (reg && reg->sig != sig) reg = reg->next;
size_t before = reg ? reg->n : 0;
scr_signal_on(name, cb, false);
if (!reg) {
reg = scr_sig_regs;
while (reg && reg->sig != sig) reg = reg->next;
}
if (!reg || reg->n != before + 1) return;
ScrSigListener entry = reg->ls[reg->n - 1];
memmove(reg->ls + 1, reg->ls, (reg->n - 1) * sizeof *reg->ls);
reg->ls[0] = entry;
#endif
}
#endif
static void scr_sig_reg_drop(ScrSigReg *reg) {
if (reg->sig > 0) {
#ifdef _WIN32
@@ -1064,7 +1092,6 @@ static const char *const scr_stdio_names[] = {
#endif
static SCR_TL ScrDyn *scr_stdio_methods[STDIO_METHOD_COUNT];
static SCR_TL bool scr_stdio_initialized;
static SCR_TL bool scr_stdio_raw;
static ScrDyn *scr_stdio_refusal(const char *name) {
char message[192];
@@ -1496,7 +1523,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
result = scr_stdio_read(arg);
} else if (stream->fd == 0 && method == STDIO_RAW) {
scr_process_stdin_set_raw_mode(scr_dyn_truthy(arg));
if (!scr_exc_pending()) { scr_stdio_raw = scr_dyn_truthy(arg); result = scr_dyn_retain(self); }
if (!scr_exc_pending()) result = scr_dyn_retain(self);
#ifdef SCR_STDIO_EXTENDED
} else if (stream->fd == 0 && method == STDIO_SET_ENCODING) {
ScrStr *enc = scr_strdec_encoding(arg);
@@ -1527,7 +1554,7 @@ static ScrDyn *scr_stdio_get(void *ptr, const char *key, size_t length) {
if (stream->fd == 0 && length == 14 && memcmp(key, "readableLength", 14) == 0)
return scr_dyn_new_num((double)scr_stdin_buffered);
if (stream->fd == 0 && length == 5 && memcmp(key, "isRaw", 5) == 0)
return scr_process_is_tty(0) ? scr_dyn_new_bool(scr_stdio_raw) : scr_dyn_retain(scr_dyn_undefined());
return scr_process_is_tty(0) ? scr_dyn_new_bool(scr_process_stdin_raw()) : scr_dyn_retain(scr_dyn_undefined());
if (stream->fd == 0 && length == 9 && memcmp(key, "destroyed", 9) == 0) return scr_dyn_new_bool(scr_stdin_destroyed);
if (length == 3 && memcmp(key, "off", 3) == 0) return scr_dyn_retain(scr_stdio_methods[STDIO_OFF]);
if (length == 11 && memcmp(key, "addListener", 11) == 0) return scr_dyn_retain(scr_stdio_methods[STDIO_ON]);
@@ -1792,6 +1819,31 @@ void scr_process_on_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, doub
scr_exit_n++;
}
#ifdef SCR_PROCESS_EXTRAS
/* process.stdin.isRaw: the raw-mode flag of a TTY stdin (1/0); a
* pipe-backed stdin has no such flag and answers Node's undefined, which
* the emitter's union construction spells -1. */
double scr_process_stdin_is_raw(void) {
if (!scr_context_is_main()) return -1;
if (!scr_process_is_tty(0)) return -1;
return scr_process_stdin_raw() ? 1.0 : 0.0;
}
/* process.prependListener("exit", cb): the same listener in FRONT of the
* registry's existing entries (Node's EventEmitter order). The append path
* above registers it and the new entry then moves to the front, so the two
* share one registration; a registry already swept at exit released the
* closure and left the list empty, and nothing moves. */
void scr_process_prepend_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, double)) {
size_t before = scr_exit_n;
scr_process_on_exit(cb, fn, false);
if (scr_exit_n != before + 1) return;
ScrExitListener entry = scr_exit_ls[scr_exit_n - 1];
memmove(scr_exit_ls + 1, scr_exit_ls, (scr_exit_n - 1) * sizeof *scr_exit_ls);
scr_exit_ls[0] = entry;
}
#endif
#ifdef SCR_NODE_ISLANDS
/* Island code's 'exit' listeners, at their registration position among
* the program's own. */
+3
View File
@@ -35,8 +35,11 @@
typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
void scr_events_install(void);
void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once);
void scr_signal_prepend(ScrStr *name, ScrDyn *cb);
void scr_signal_off(ScrStr *name, ScrDyn *cb);
void scr_process_on_exit(ScrClosure *cb, void (*fn)(ScrClosure *, double), bool once);
void scr_process_prepend_exit(ScrClosure *cb, void (*fn)(ScrClosure *, double));
double scr_process_stdin_is_raw(void);
void scr_process_off_exit(ScrClosure *cb);
void scr_run_exit_listeners(double code);
void scr_exit_thunk0(ScrClosure *cb, double code);
+1
View File
@@ -229,6 +229,7 @@ void scr_exc_print_uncaught(void);
extern SCR_TL int (*scr_uncaught_exception_hook)(bool from_promise);
bool scr_exc_handle_uncaught(bool from_promise);
void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor);
void scr_process_prepend_uncaught_exception(ScrDyn *fn, bool monitor);
void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor);
/* The value Node reports for an unhandled rejection (scr_async_dyn.c): an
* error-like reason itself, otherwise an UnhandledPromiseRejection Error
+25
View File
@@ -589,6 +589,31 @@ static void cp_copyfile(const ScrStr *src, const ScrStr *dest, int mode, const c
if (e) scr_fs_throw2(e, syscall, src, dest);
}
#if defined(SCR_FS_FLAGS) && !defined(_WIN32)
/* copyFileSync(src, dest, mode) and copyFile(src, dest, mode): libuv's
* uv_fs_copyfile with Node's flag bits (the C++ binding's range check and
* message: any value in [0, 7], truncated to an integer — 1.5 is EXCL).
* COPYFILE_EXCL (1) fails EEXIST on an existing destination,
* COPYFILE_FICLONE (2) requests a reflink and falls back to a copy, and
* COPYFILE_FICLONE_FORCE (4) fails where no reflink is possible.
*
* This is the SCR_FS_FLAGS build of the unit: cp_copyfile is the reflink
* walk, and keeping the entry behind a define leaves every program that
* never passes a mode with the default object, byte for byte. The Windows
* arm (CopyFileW and its ENOSYS for FICLONE_FORCE) sits beside the Win32 fs
* helpers in scr_lib.c. */
void scr_fs_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
if (!(mode >= 0 && mode <= 7)) {
static const char text[] = "mode is out of range: >= 0 && <= 7";
scr_throw_error_msg_code(SCR_ERR_RANGE, text, sizeof text - 1, "ERR_OUT_OF_RANGE");
return;
}
if (!scr_fs_path_ok(src, "src")) return;
if (!scr_fs_path_ok(dest, "dest")) return;
cp_copyfile(src, dest, (int)mode, "copyfile");
}
#endif
/* utimes(dest, atime, mtime) in seconds, through the utimesSync path. */
static bool cp_utime(const ScrStr *dest, double atime, double mtime) {
scr_fs_utimes((ScrStr *)dest, atime, mtime, false);
+102
View File
@@ -0,0 +1,102 @@
/* fs.writeFile's `flag` option ("w", "wx") and the fs/promises twins of the
* flag-aware write and copy forms.
*
* The write is scr_lib.c's scr_fs_write_file_mode body with the disposition
* flag added (the same portable open(2) calls, so it builds for every
* target). It lives in this unit rather than beside that function so a
* program that never passes `flag` keeps scr_lib.c's object byte for byte;
* the fs.copyFile mode form rides scr_fs_cp.c's SCR_FS_FLAGS build, whose
* reflink helper it reuses, and that form's Windows arm sits beside the
* Win32 path helpers in scr_lib.c. */
#include "scr_runtime.h"
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <stdio.h>
#ifdef _WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
/* O_BINARY: Windows-only (CRT text mode would translate \n on fd writes);
* zero everywhere else. scr_lib.c carries the same guard for its writes. */
#ifndef O_BINARY
#define O_BINARY 0
#endif
/* Node's mode validation for the options-record write: an integer in
* open(2)'s mode_t range. Throws ERR_OUT_OF_RANGE and answers false. */
static bool scr_fs_write_mode_ok(double mode) {
char recv[48], msg[176];
scr_num_received(mode, recv);
if (!(isfinite(mode) && trunc(mode) == mode)) {
int len = snprintf(msg, sizeof msg,
"The value of \"mode\" is out of range. It must be an integer. Received %s",
recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
if (mode < 0 || mode > 4294967295.0) {
int len = snprintf(msg, sizeof msg,
"The value of \"mode\" is out of range. It must be >= 0 && <= 4294967295. Received %s",
recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
return true;
}
/* The open(2) write behind the flag form: O_TRUNC plus `extra` (only O_EXCL
* today), creation mode applying at CREATION only (umask applying) — an
* existing file keeps its permissions, exactly Node (which never chmods
* here). Same error shapes as the plain form. */
static void scr_fs_write_file_open(ScrStr *path, ScrStr *data, mode_t mode, int extra) {
if (!scr_fs_path_ok(path, "path")) return;
/* O_BINARY: zero on POSIX; on Windows it keeps the CRT from translating
* \n in these byte-exact writes (fopen's "wb" path already does). */
int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | extra, mode);
if (fd < 0) {
scr_fs_throw(errno, "open", path);
return;
}
char *owned;
const char *bytes = scr_str_utf8(data, &owned);
size_t at = 0;
while (at < data->len) {
ssize_t wrote = write(fd, bytes + at, data->len - at);
if (wrote < 0) {
if (errno == EINTR) continue;
int e = errno;
close(fd);
scr_str_utf8_end(owned);
scr_fs_throw(e, "write", path);
return;
}
at += (size_t)wrote;
}
scr_str_utf8_end(owned);
if (close(fd) != 0) scr_fs_throw(errno, "close", path);
}
/* writeFileSync(p, data, { mode, flag }): Node's open(2) with the
* disposition flag. `exclusive` is "wx" (O_EXCL: EEXIST when the path
* exists); 0 is "w", the default truncate-or-create. */
void scr_fs_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
if (!scr_fs_write_mode_ok(mode)) return;
scr_fs_write_file_open(path, data, (mode_t)mode, exclusive != 0 ? O_EXCL : 0);
}
/* fs.promises.writeFile(path, data, { mode, flag }) and
* fs.promises.copyFile(src, dest, mode): the settled-promise twins of
* fs.writeFileFlagsSync and fs.copyFileFlagsSync. */
ScrPromise *scr_fsp_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
scr_fs_write_file_flags(path, data, mode, exclusive);
return scr_promise_settled_void();
}
ScrPromise *scr_fsp_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
scr_fs_copyfile_flags(src, dest, mode);
return scr_promise_settled_void();
}
+19
View File
@@ -9408,6 +9408,25 @@ static bool scr_dyn_key_probe_computed(const ScrDyn *value, const ScrDyn *raw_ke
bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key) { return scr_dyn_key_probe_computed(value, key, 0); }
bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key) { return scr_dyn_key_probe_computed(value, key, 1); }
/* Reflect.has's target check: Node's TypeError for a primitive target. */
void scr_dyn_reflect_has_target(const ScrDyn *target) {
switch (target->kind) {
case SCR_DYN_NULL:
case SCR_DYN_UNDEF:
case SCR_DYN_BOOL:
case SCR_DYN_NUM:
case SCR_DYN_STR:
case SCR_DYN_BIGINT:
case SCR_DYN_SYMBOL: {
static const char message[] = "Reflect.has called on non-object";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
default:
return;
}
}
bool scr_dyn_property_is_enumerable_computed(const ScrDyn *value, const ScrDyn *key) { return scr_dyn_key_probe_computed(value, key, 2); }
static void scr_json_delete_member(ScrDyn *object, const ScrStr *key);
+71 -1
View File
@@ -4768,6 +4768,69 @@ static int scr_fs_win_errno(DWORD error) {
return EIO;
}
}
/* libuv's fs__stat_impl_from_path identity comparison (volume serial and
* file index): the same-file CopyFileW failure below is a success. */
static bool scr_fs_win_same_file(const WCHAR *a, const WCHAR *b) {
BY_HANDLE_FILE_INFORMATION first, second;
HANDLE handle = CreateFileW(a, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (handle == INVALID_HANDLE_VALUE) return false;
BOOL ok = GetFileInformationByHandle(handle, &first);
CloseHandle(handle);
if (!ok) return false;
handle = CreateFileW(b, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (handle == INVALID_HANDLE_VALUE) return false;
ok = GetFileInformationByHandle(handle, &second);
CloseHandle(handle);
if (!ok) return false;
return first.dwVolumeSerialNumber == second.dwVolumeSerialNumber &&
first.nFileIndexHigh == second.nFileIndexHigh &&
first.nFileIndexLow == second.nFileIndexLow;
}
/* The Win32 arm of copyFile's mode form (scr_fs_cp.c's SCR_FS_FLAGS build
* has the POSIX one; writeFile's flag form is portable and lives in
* scr_fs_flags.c). It sits here because the wide-path and errno helpers
* above are this file's; _WIN32 excludes it from the POSIX builds, so every
* other platform's scr_lib.c object is unchanged. */
/* copyFileSync(src, dest, mode) and copyFile(src, dest, mode): libuv's
* fs__copyfile in src/win/fs.c. CopyFileW carries only COPYFILE_EXCL (its
* bFailIfExists flag); COPYFILE_FICLONE (2) falls back to a plain copy and
* COPYFILE_FICLONE_FORCE (4) is the ENOSYS uv_fs_copyfile reports. The
* POSIX arm lives in scr_fs_cp.c with its reflink handling. */
void scr_fs_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
if (!(mode >= 0 && mode <= 7)) {
static const char text[] = "mode is out of range: >= 0 && <= 7";
scr_throw_error_msg_code(SCR_ERR_RANGE, text, sizeof text - 1, "ERR_OUT_OF_RANGE");
return;
}
if (!scr_fs_path_ok(src, "src")) return;
if (!scr_fs_path_ok(dest, "dest")) return;
if (((int)mode & 4) != 0) {
scr_fs_throw2(ENOSYS, "copyfile", src, dest);
return;
}
WCHAR *from = scr_fs_win_wide(src);
WCHAR *to = scr_fs_win_wide(dest);
if (!from || !to) {
DWORD error = GetLastError();
free(from);
free(to);
scr_fs_throw2(scr_fs_win_errno(error), "copyfile", src, dest);
return;
}
BOOL ok = CopyFileW(from, to, ((int)mode & 1) ? TRUE : FALSE);
int error = ok ? 0 : scr_fs_win_errno(GetLastError());
if (!ok && error == EBUSY && scr_fs_win_same_file(from, to)) error = 0;
free(from);
free(to);
if (error != 0) scr_fs_throw2(error, "copyfile", src, dest);
}
#endif
int scr_fs_rename_raw(const ScrStr *oldpath, const ScrStr *newpath) {
@@ -5284,7 +5347,12 @@ double scr_process_rows(double fd) { return scr_process_dimension(fd, true); }
* setRawMode(false) restores the termios saved at the first raw entry
* (libuv's orig_termios), a no-op when raw mode was never entered.
* NON-TTY stdin: Node's process.stdin is a Socket with no setRawMode
* member at all, so the call throws Node's exact catchable TypeError. */
* member at all, so the call throws Node's exact catchable TypeError.
* A TTY call records the mode for process.stdin.isRaw. */
static SCR_TL bool scr_stdin_raw;
bool scr_process_stdin_raw(void) { return scr_stdin_raw; }
#ifdef _WIN32
static SCR_TL DWORD scr_stdin_cooked;
static SCR_TL bool scr_stdin_cooked_saved = false;
@@ -5295,6 +5363,7 @@ void scr_process_stdin_set_raw_mode(bool raw) {
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return;
}
scr_stdin_raw = raw;
/* libuv's uv_tty_set_mode(UV_TTY_MODE_RAW) console half: drop line
* buffering, echo, and Ctrl-C cooking; restore the entry mode on the
* way back — the termios save/restore shape, translated. */
@@ -5328,6 +5397,7 @@ void scr_process_stdin_set_raw_mode(bool raw) {
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return;
}
scr_stdin_raw = raw;
if (raw) {
struct termios t;
if (tcgetattr(0, &t) != 0) return;
+9
View File
@@ -353,6 +353,9 @@ bool scr_fs_access_mode_valid(double mode);
bool scr_fs_truncate_len_valid(double length);
void scr_fs_truncate(ScrStr *path, double length);
void scr_fs_copyfile(ScrStr *src, ScrStr *dest);
/* copyFileSync(src, dest, mode): libuv's copyfile flag bits (EXCL 1,
* FICLONE 2, FICLONE_FORCE 4); out-of-range modes throw Node's RangeError. */
void scr_fs_copyfile_flags(ScrStr *src, ScrStr *dest, double mode);
/* The two-path form of scr_fs_throw: "ENOENT: no such file or directory,
* copyfile 'src' -> 'dest'", with path and dest own properties. */
void scr_fs_throw2(int e, const char *op, const ScrStr *src, const ScrStr *dest);
@@ -374,6 +377,8 @@ void scr_fs_rename(ScrStr *oldpath, ScrStr *newpath);
int scr_fs_rename_raw(const ScrStr *oldpath, const ScrStr *newpath);
void scr_fs_rename_error(int error, const ScrStr *oldpath, const ScrStr *newpath);
void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode);
/* writeFileSync(path, data, { mode, flag }): `exclusive` is "wx" (O_EXCL). */
void scr_fs_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive);
void scr_fs_mkdir_mode(ScrStr *path, double mode);
void scr_fs_mkdir_recursive_mode(ScrStr *path, double mode);
/* The same walk, answering the first directory created (+1) or NULL
@@ -396,6 +401,8 @@ bool scr_process_is_tty(double fd);
bool scr_process_is_tty_value(const ScrDyn *fd);
void scr_process_stdin_destroy(void);
void scr_process_stdin_set_raw_mode(bool raw);
/* The mode the last TTY setRawMode call set (process.stdin.isRaw). */
bool scr_process_stdin_raw(void);
/* Terminal width for process.stdout/stderr.columns (fd 1/2):
* ioctl(TIOCGWINSZ), -1 when the fd is not a TTY or the ioctl refuses —
@@ -534,6 +541,7 @@ ScrDyn *scr_stats_dyn(ScrStats *s, bool bigint);
ScrPromise *scr_fsp_read_file(ScrStr *path);
ScrPromise *scr_fsp_write_file(ScrStr *path, ScrStr *data);
ScrPromise *scr_fsp_write_file_mode(ScrStr *path, ScrStr *data, double mode);
ScrPromise *scr_fsp_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive);
ScrPromise *scr_fsp_mkdir(ScrStr *path);
ScrPromise *scr_fsp_mkdir_mode(ScrStr *path, double mode);
ScrPromise *scr_fsp_mkdir_recursive(ScrStr *path);
@@ -546,6 +554,7 @@ ScrPromise *scr_fsp_access(ScrStr *path, double mode);
ScrPromise *scr_fsp_append_file(ScrStr *path, ScrStr *data);
ScrPromise *scr_fsp_append_file_bytes(ScrStr *path, ScrBytes *data);
ScrPromise *scr_fsp_copyfile(ScrStr *src, ScrStr *dest);
ScrPromise *scr_fsp_copyfile_flags(ScrStr *src, ScrStr *dest, double mode);
ScrPromise *scr_fsp_mkdtemp(ScrStr *prefix);
ScrPromise *scr_fsp_read_file_encoded(ScrStr *path, ScrStr *encoding);
ScrPromise *scr_fsp_rmdir(ScrStr *path);
+28
View File
@@ -2535,6 +2535,34 @@ void scr_net_server_on_close(ScrNetServer *s, ScrClosure *cb /*moves*/, bool onc
scr_net_ls_add(&s->close_ls, cb, NULL, once);
}
#ifdef SCR_NET_SERVER_OFF
/* server.off(event, cb) / removeListener: the LAST listener standing for
* the same function leaves the event's list — Node's EventEmitter removes
* the most recently added match, so the scan runs back to front. The
* SCR_NET_SERVER_OFF build of the unit carries it, so a program that never
* removes a listener keeps the default object byte for byte. */
void scr_net_server_off(ScrNetServer *s, ScrClosure *cb /*borrowed*/, double kind) {
ScrNetLs *l = kind == SCR_NET_OFF_ERROR ? &s->err_ls
: kind == SCR_NET_OFF_CLOSE ? &s->close_ls
: kind == SCR_NET_OFF_LISTENING ? &s->listening_cbs
: kind == SCR_NET_OFF_CONNECTION ? &s->conn_ls
: kind == SCR_NET_OFF_TIMEOUT ? &s->timeout_ls
: kind == SCR_NET_OFF_DROP ? &s->drop_ls
: NULL;
if (l == NULL) return;
ScrClosure *root = cb->identity ? cb->identity : cb;
for (size_t left = l->n; left > 0; left--) {
size_t i = left - 1;
if (scr_net_listener_root(&l->ls[i]) != root) continue;
scr_closure_release(l->ls[i].cb);
scr_net_once_release(l->ls[i].shared_once);
memmove(&l->ls[i], &l->ls[i + 1], (l->n - i - 1) * sizeof *l->ls);
l->n--;
return;
}
}
#endif
/* server.on('listening', cb): joins the deferred 'listening' list the
* listen(port, cb) callback rides (the sweep fires it once after a
* successful bind — Node's next-tick emit). A registration AFTER the
+12
View File
@@ -199,6 +199,18 @@ void scr_net_server_on_close(ScrNetServer *s, ScrClosure *cb /*moves*/, bool onc
* cb) rides — fires once per successful bind; late registrations on an
* already-listening server never fire (Node's once-per-listen emit). */
void scr_net_server_on_listening(ScrNetServer *s, ScrClosure *cb /*moves*/, bool once);
/* server.off(event, cb) / removeListener: the LAST listener standing for
* the same function leaves that event's list (Node's EventEmitter
* removal order). `kind` selects the list (SCR_NET_OFF_*). */
enum {
SCR_NET_OFF_ERROR = 1,
SCR_NET_OFF_CLOSE = 2,
SCR_NET_OFF_LISTENING = 3,
SCR_NET_OFF_CONNECTION = 4,
SCR_NET_OFF_TIMEOUT = 5,
SCR_NET_OFF_DROP = 6,
};
void scr_net_server_off(ScrNetServer *s, ScrClosure *cb /*borrowed*/, double kind);
void scr_net_server_on_connection(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrNetConnFn fn, bool once);
/* 'secureConnection' — the deferred-connection list (TLS handshake
* timing); released unread on a server without deferral (a plain net
@@ -0,0 +1,24 @@
// process.prependListener registers the listener at the FRONT of an
// event's listener list (Node's EventEmitter order) for the registries
// that own a prepend entry: 'exit' and the signal family. on/once keep
// appending, so a prepend registered between them still dispatches first.
import { spawn } from "node:child_process";
process.on("exit", (code) => console.log(`exit on 1 (${code})`));
process.prependListener("exit", (code) => console.log(`exit prepend 0 (${code})`));
process.once("exit", (code) => console.log(`exit once 2 (${code})`));
process.prependListener("exit", () => console.log("exit prepend last-registered, still first"));
let ints = 0;
const onInt = () => {
ints++;
console.log("SIGINT on", ints);
};
process.on("SIGINT", onInt);
process.prependListener("SIGINT", () => console.log("SIGINT prepend", ints));
const child = spawn("/bin/sh", ["-c", "sleep 0.2; kill -INT $PPID; sleep 0.2"], { stdio: "ignore" });
child.on("exit", (code) => {
console.log("child exit", code ?? -1, "ints", ints);
});
console.log("main done");
@@ -0,0 +1,15 @@
// URLSearchParams crossing an implicit ToString: a template span, string
// concatenation, String() and an explicit toString() all serialize the
// pair list in insertion order, percent-encoded exactly as Node does. The
// record init accepts shorthand members (`{ model }`) like `{ model: model }`.
const model = "m 1";
const sp = new URLSearchParams({ model, autoload: "false", q: "a&b" });
console.log(`${sp}`);
console.log("prefix:" + sp);
console.log(String(sp));
console.log(sp.toString());
console.log(sp.get("model"), sp.get("q"), sp.get("missing"));
const empty = new URLSearchParams({});
console.log(`[${empty}]`, JSON.stringify(empty.toString()));
const pairs = [...sp];
console.log(pairs.length, pairs[0]![0], pairs[0]![1]);
+27
View File
@@ -0,0 +1,27 @@
// The optional-chain form of the ChildProcess members: `this.child?.kill()`
// and `this.child?.on(...)` reach the same lowerings as the plain calls, so
// an absent child is a no-op and a present one keeps Node's shapes.
import { spawn, type ChildProcess } from "node:child_process";
class Runner {
child: ChildProcess | undefined;
constructor(child: ChildProcess | undefined) {
this.child = child;
}
kill(): void {
this.child?.kill("SIGTERM");
console.log("kill attempted, child present:", this.child !== undefined);
}
watch(): void {
this.child?.on("exit", (code) => console.log("exit", code));
}
}
const withChild = new Runner(spawn("sleep", ["5"], { stdio: "ignore" }));
withChild.watch();
withChild.kill();
const withoutChild = new Runner(undefined);
withoutChild.watch();
withoutChild.kill();
console.log("done");
@@ -0,0 +1,12 @@
// A match row read through an optional chain: `re.exec(s)?.index` and the
// `?.input` beside it take the same member reads as the guarded form.
const re = /b(c)/g;
const text = "abcz";
const m = re.exec(text);
console.log(m !== null ? m.index : -1);
console.log(re.exec(text)?.index);
console.log(re.exec(text)?.input);
console.log(/(z)/.exec(text)?.index);
console.log(/q/.exec(text)?.index);
const groups = /(?<lead>b)/.exec(text)?.groups;
console.log(groups?.lead);
@@ -0,0 +1,19 @@
// Array.from over a string the checker narrowed through a guard, and over
// code points (surrogate pairs stay whole), each a fresh array.
const values: (string | undefined)[] = ["abc", undefined, "a\u{1F600}b"];
for (const value of values) {
if (value === undefined) {
console.log("skip");
continue;
}
const chars = Array.from(value);
console.log(chars.length, chars.join("|"));
}
const maybe: string | undefined = "xy";
if (maybe !== undefined) console.log(Array.from(maybe).length);
const plain: string = "q";
console.log(Array.from(plain), Array.from(plain).length);
function guarded(v: string | undefined): boolean {
return v !== undefined && Array.from(v).length > 1;
}
console.log(guarded("ab"), guarded("a"), guarded(undefined));
+27
View File
@@ -0,0 +1,27 @@
// server.off / removeListener: the LAST registered listener for the same
// function leaves that event's list (Node's removeListener order), the
// remaining listeners still run, and removing an unregistered function is
// a no-op. The EADDRINUSE from a second listen on a bound port is the real
// event here.
import * as net from "node:net";
const first = net.createServer();
first.listen(0, "127.0.0.1", () => {
const port = first.address().port;
const second = net.createServer();
const removed = (err: Error) => console.log("removed listener ran", (err as NodeJS.ErrnoException).code);
const kept = (err: Error) => console.log("kept listener ran", (err as NodeJS.ErrnoException).code);
second.on("error", removed);
second.on("error", kept);
second.off("error", removed);
second.off("error", () => console.log("never registered"));
second.listen(port, "127.0.0.1", () => {
console.log("unexpected: second listen succeeded");
second.close();
});
setTimeout(() => {
console.log("closing");
second.close();
first.close(() => console.log("first closed"));
}, 20);
});
@@ -0,0 +1,11 @@
// The process streams' descriptors and stdin's raw-mode flag: fd is the
// compile-time 0/1/2 Node reports for a TTY, a pipe, and a redirected file
// alike; isRaw is the TTY's raw-mode flag and Node's undefined when stdin
// is not a terminal (the harness's own pipe).
import { writeSync } from "node:fs";
console.log("stdin fd", process.stdin.fd);
console.log("stdout fd", process.stdout.fd);
console.log("stderr fd", process.stderr.fd);
console.log("stdin isRaw", process.stdin.isRaw, typeof process.stdin.isRaw);
writeSync(process.stdout.fd, "writeSync through process.stdout.fd\n");
@@ -0,0 +1,47 @@
// Reflect.has over the checked-dynamic view, and the Object reflection
// operations over records, arrays and class instances (own fields and
// elements in property order). Reflect.has is [[HasProperty]], so inherited
// names (Array.prototype's, Object.prototype's, a class's methods) answer
// true; a primitive target is Node's TypeError and stays a lowerer fence
// (SC2020) rather than a silent `false`.
class Point {
x = 1;
y = 2;
sum(): number {
return this.x + this.y;
}
}
const record = { a: 1, b: "two" };
const list = [10, 20];
const point = new Point();
console.log(Reflect.has(record, "a"), Reflect.has(record, "z"));
console.log(Reflect.has(list, "0"), Reflect.has(list, "length"), Reflect.has(list, "9"));
console.log(Reflect.has(point, "x"), Reflect.has(point, "sum"));
const key = "b";
console.log(Reflect.has(record, key), Reflect.has(record, key.toUpperCase()));
console.log(Reflect.has(record, "toString"), Reflect.has(record, "constructor"));
console.log(Object.getOwnPropertyNames(record).join(","));
console.log(Object.getOwnPropertyNames(point).join(","));
console.log(Object.getOwnPropertyNames(list).length);
const descriptors = Object.getOwnPropertyDescriptors(record);
console.log(Object.keys(descriptors).join(","));
console.log(Object.getOwnPropertySymbols(record).length);
// A primitive target held as any, unknown or a union throws Node's
// TypeError; its key is evaluated first.
function hasThrough(target: unknown, name: string): string {
try {
return String(Reflect.has(target as any, name));
} catch (e) {
return `${(e as Error).name}: ${(e as Error).message}`;
}
}
const loose: any = "text";
const either: string | { a: number } = Math.random() > 2 ? { a: 1 } : "text";
console.log(hasThrough(loose, "length"), hasThrough(42, "x"), hasThrough(undefined, "x"));
console.log(hasThrough({ a: 1 }, "a"), hasThrough(either, "a"));
let keys = 0;
try {
Reflect.has(loose, String(++keys));
} catch (e) {
console.log((e as Error).name, keys);
}
+31
View File
@@ -0,0 +1,31 @@
// A class extending EventEmitter<EventMap>: the type argument is the
// listener MAP — a pure typing aid with no runtime layout of its own, so
// the emitted class adopts the same emitter base (Node erases it too).
// on/emit dispatch keeps Node's order, and the subclass's own members
// stay alongside the inherited ones.
import { EventEmitter } from "node:events";
interface BufferEvents {
data: [chunk: string];
end: [];
}
class StdinBuffer extends EventEmitter<BufferEvents> {
private chunks: string[] = [];
push(chunk: string): void {
this.chunks.push(chunk);
this.emit("data", chunk);
}
finish(): void {
console.log("joined", this.chunks.join(""));
this.emit("end");
}
}
const buffer = new StdinBuffer();
buffer.on("data", (chunk) => console.log("data", chunk));
buffer.on("end", () => console.log("end"));
buffer.push("a");
buffer.push("b");
buffer.finish();
console.log("listeners", buffer.listenerCount("data"));
@@ -0,0 +1,63 @@
// copyFileSync/copyFile's third argument (libuv's flag bits) and
// writeFileSync/writeFile's `flag` option ("w" and the exclusive "wx").
import { copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync, constants } from "node:fs";
import { copyFile, writeFile, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const dir = mkdtempSync(join(tmpdir(), "scriptc-flags-"));
const src = join(dir, "src.txt");
const dst = join(dir, "dst.txt");
writeFileSync(src, "payload");
copyFileSync(src, dst, constants.COPYFILE_EXCL);
console.log(readFileSync(dst, "utf8"));
try {
copyFileSync(src, dst, constants.COPYFILE_EXCL);
console.log("no throw");
} catch (e) {
const err = e as NodeJS.ErrnoException;
console.log(err.code, err.syscall);
}
copyFileSync(src, dst);
console.log(readFileSync(dst, "utf8"));
try {
copyFileSync(src, join(dir, "x.txt"), 8);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
const wx = join(dir, "wx.txt");
writeFileSync(wx, "one", { flag: "wx" });
console.log(readFileSync(wx, "utf8"));
try {
writeFileSync(wx, "two", { flag: "wx" });
console.log("no throw");
} catch (e) {
const err = e as NodeJS.ErrnoException;
console.log(err.code, err.syscall);
}
writeFileSync(wx, "three", { flag: "w" });
console.log(readFileSync(wx, "utf8"));
const modeFile = join(dir, "mode.txt");
writeFileSync(modeFile, "m", { mode: 0o600, flag: "wx" });
console.log(readFileSync(modeFile, "utf8"));
await copyFile(src, join(dir, "p.txt"), constants.COPYFILE_EXCL);
console.log(await readFile(join(dir, "p.txt"), "utf8"));
try {
await copyFile(src, join(dir, "p.txt"), constants.COPYFILE_EXCL);
console.log("no throw");
} catch (e) {
const err = e as NodeJS.ErrnoException;
console.log(err.code, err.syscall);
}
const pwx = join(dir, "pwx.txt");
await writeFile(pwx, "async-one", { flag: "wx" });
console.log(await readFile(pwx, "utf8"));
try {
await writeFile(pwx, "async-two", { flag: "wx" });
console.log("no throw");
} catch (e) {
const err = e as NodeJS.ErrnoException;
console.log(err.code, err.syscall);
}
rmSync(dir, { recursive: true, force: true });
@@ -0,0 +1,36 @@
// spawn's `detached` option as a RUNTIME boolean — the portable daemon
// idiom `detached: process.platform !== "win32"` — reaches the same
// POSIX_SPAWN_SETSID flag as the literal form: a detached child leads its
// own process group (pgid == pid), a non-detached one stays in the
// parent's group.
import { spawn } from "node:child_process";
const PGID = "ps -o pgid= -p $$ | tr -d ' '";
const mode = process.platform !== "win32";
const lead = spawn("/bin/sh", ["-c", PGID], { detached: mode, stdio: ["ignore", "pipe", "ignore"] });
let leadOut = "";
if (lead.stdout) {
lead.stdout.setEncoding("utf8");
lead.stdout.on("data", (chunk: string) => {
leadOut += chunk;
});
}
lead.on("close", () => {
console.log("detached leads its own group:", leadOut.trim() === String(lead.pid));
const joined = spawn("/bin/sh", ["-c", PGID], {
detached: false,
stdio: ["ignore", "pipe", "ignore"],
});
let joinedOut = "";
if (joined.stdout) {
joined.stdout.setEncoding("utf8");
joined.stdout.on("data", (chunk: string) => {
joinedOut += chunk;
});
}
joined.on("close", () => {
console.log("non-detached stays behind:", joinedOut.trim() !== String(joined.pid));
});
});
@@ -0,0 +1,71 @@
// StringDecoder.write over a chunk that crossed the checked-dynamic
// boundary: a string passes through unchanged, an ArrayBufferView decodes,
// and anything else throws Node's ERR_INVALID_ARG_TYPE. The decoder state
// survives an interleaved string chunk.
import { StringDecoder } from "node:string_decoder";
// The chunk arrives untyped (a stream "data" payload), like pi's exec.ts.
function write(decoder: StringDecoder, chunk: any): string {
return decoder.write(chunk);
}
const decoder = new StringDecoder("utf8");
console.log(JSON.stringify(write(decoder, "plain")));
console.log(JSON.stringify(write(decoder, Buffer.from([0x61, 0xe2]))));
console.log(JSON.stringify(write(decoder, Buffer.from([0x82, 0xac, 0x62]))));
console.log(JSON.stringify(write(decoder, "mid")));
try {
write(decoder, 123);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
try {
write(decoder, {});
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
const ender = new StringDecoder("base64");
console.log(JSON.stringify(write(ender, Buffer.from([0x66]))));
console.log(JSON.stringify(ender.end()));
// end(chunk) is write(chunk) followed by the flush, over the same dynamic
// chunk shapes.
function endWith(decoder: StringDecoder, chunk: any): string {
return decoder.end(chunk);
}
const tail = new StringDecoder("utf8");
console.log(JSON.stringify(write(tail, "x")));
console.log(JSON.stringify(endWith(tail, Buffer.from([0xe2, 0x82]))));
console.log(JSON.stringify(endWith(new StringDecoder("utf8"), "mid")));
try {
endWith(new StringDecoder("utf8"), 123);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
// end(undefined) only flushes; null is not a chunk.
const flushed = new StringDecoder("utf8");
console.log(JSON.stringify(write(flushed, Buffer.from([0x61, 0xe2, 0x82]))));
console.log(JSON.stringify(endWith(flushed, undefined)));
try {
endWith(new StringDecoder("utf8"), null);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
// An ArrayBuffer or SharedArrayBuffer is not an ArrayBufferView.
for (const chunk of [new ArrayBuffer(2), new SharedArrayBuffer(2)]) {
try {
write(new StringDecoder("utf8"), chunk);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
try {
endWith(new StringDecoder("utf8"), chunk);
} catch (e) {
const err = e as Error;
console.log(err.name, (err as NodeJS.ErrnoException).code, err.message);
}
}
@@ -0,0 +1,79 @@
// Optional-chained METHOD calls on structural receivers: a class seen
// through `Pick<Class, ...>`, an interface implemented by program classes,
// and the same receivers held in optional fields. Node calls the method
// when the receiver is present and answers undefined (no call) when not.
class CacheWarmer {
status = "idle";
onAgentSettled(): void {
console.log("settled", this.status);
}
onModeChanged(): void {
console.log("mode", this.status);
}
}
interface Renderer {
requestRender(): void;
}
class MainScreen implements Renderer {
requestRender(): void {
console.log("main");
}
}
class AltScreen implements Renderer {
requestRender(): void {
console.log("alt");
}
}
function settle(w: Pick<CacheWarmer, "onAgentSettled"> | undefined): void {
w?.onAgentSettled();
}
function tick(tui: Renderer | undefined): void {
tui?.requestRender();
}
class Session {
private cacheWarmer?: Pick<CacheWarmer, "onModeChanged" | "status">;
private renderer: Renderer | undefined;
constructor(w: CacheWarmer | undefined, r: Renderer | undefined) {
this.cacheWarmer = w;
this.renderer = r;
}
settle(): void {
this.cacheWarmer?.onModeChanged();
this.renderer?.requestRender();
}
describe(): string {
return `${this.cacheWarmer?.status ?? "none"}/${this.renderer === undefined ? "none" : "ui"}`;
}
}
settle(new CacheWarmer());
settle(undefined);
tick(new MainScreen());
tick(new AltScreen());
tick(undefined);
new Session(new CacheWarmer(), new MainScreen()).settle();
new Session(undefined, undefined).settle();
console.log(new Session(new CacheWarmer(), undefined).describe());
console.log(new Session(undefined, new AltScreen()).describe());
// A present but non-callable member still throws Node's catchable TypeError;
// the `?.` step only skips the call when the RECEIVER is absent.
const loose: any = { render: 42 };
try {
loose?.render();
} catch (e) {
console.log("call:", e instanceof TypeError, (e as Error).message);
}
const looseStep: any = { render: 42 };
try {
looseStep.render?.();
} catch (e) {
console.log("step:", e instanceof TypeError, (e as Error).message);
}
@@ -0,0 +1,28 @@
// `(x ?? fallback).method(...)` over storage the checker types
// `T | undefined`: the default takes over exactly when the stored value is
// absent — an optional record field, an optional parameter, a callback
// parameter, and an unchecked element read.
interface Options {
tools?: string[];
}
const DEFAULTS = ["read", "write"];
function active(options: Options): string[] {
const selected = options.tools;
return (selected ?? DEFAULTS).filter((name) => name !== "write");
}
function collect(...failures: Array<string[] | undefined>): string[] {
return failures.flatMap((entries) => (entries ?? []).map((entry) => entry.toUpperCase()));
}
function totals(groups: number[][] | undefined): number {
return (groups ?? []).reduce((sum, group) => sum + group.length, 0);
}
console.log(active({ tools: ["read", "write"] }).join(","));
console.log(active({}).join(","));
console.log(collect(["a"], undefined, ["b"]).join(","));
console.log(totals([[1, 2], [3]]), totals(undefined));
@@ -0,0 +1,23 @@
// Module specifiers the compiler can evaluate: a `const` binding and a `+`
// concatenation of literals name their module exactly as a string literal
// does, so dynamic import() and import.meta.resolve stay build-time
// resolutions instead of computed-specifier refusals.
import { readFileSync } from "node:fs";
import { join } from "node:path";
const FS_SPECIFIER = "node:" + "fs";
const PATH_SPECIFIER = "node:path" as const;
const PREFIX = "node:";
const JOIN_SPECIFIER = PREFIX + "path";
async function loadBuiltin(): Promise<void> {
const fs = await import(FS_SPECIFIER);
const path = await import(JOIN_SPECIFIER);
console.log(fs.existsSync("/"), fs.existsSync("/definitely-not-here-xyz"));
console.log(path.join("a", "b") === join("a", "b"), path.basename("/a/b.ts"));
}
await loadBuiltin();
console.log(import.meta.resolve(PATH_SPECIFIER).startsWith("node:path"));
console.log(import.meta.resolve(FS_SPECIFIER).startsWith("node:fs"));
@@ -7,15 +7,6 @@ stdlib-fence.ts:17:23 - error SC2020: 'Object.assign' is part of the standard li
hint: spread instead: { ...a, ...b } builds the merged record; what lowers: the empty-target literal-source shape (Object.assign({}, { ... }) IS the source literal) and merges INTO an index-signature record whose value slot every source value enters — other aliased targets are real mutation, and a function target (Object.assign(fn, { prop })) is a function-with-properties value the model cannot represent: bind the property separately
stdlib-fence.ts:18:14 - error SC2020: 'Reflect.has' is part of the standard library types but has no scriptc lowering yet
17 | const aliasedAssign = Object.assign(merged, { b: 2 }); // aliased targets stay fenced (real mutation of a live object)
18 | const refl = Reflect.has({ a: 1 }, "a");
| ^~~~~~~~~~~
19 | const parsedMs = Date.parse;
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
stdlib-fence.ts:19:18 - error SC2020: 'Date.parse' is part of the standard library types but has no scriptc lowering yet
18 | const refl = Reflect.has({ a: 1 }, "a");
@@ -0,0 +1 @@
{ "plain": 2850, "san": 21650 }
@@ -0,0 +1 @@
{ "plain": 4163, "san": 7431 }
@@ -0,0 +1 @@
{ "plain": 2300, "san": 4100 }
@@ -0,0 +1 @@
{ "plain": 2579, "san": 9345 }
@@ -0,0 +1 @@
{ "plain": 2724, "san": 5297 }
@@ -0,0 +1 @@
{ "plain": 2350, "san": 5950 }
@@ -0,0 +1 @@
{ "plain": 2000, "san": 2600 }
@@ -0,0 +1 @@
{ "plain": 3121, "san": 5843 }
@@ -0,0 +1 @@
{ "plain": 2450, "san": 5700 }
@@ -0,0 +1 @@
{ "plain": 2673, "san": 4238 }
@@ -0,0 +1 @@
{ "plain": 4250, "san": 2850 }
@@ -0,0 +1 @@
{ "plain": 2575, "san": 3939 }
@@ -0,0 +1 @@
{ "plain": 2800, "san": 4200 }
@@ -0,0 +1 @@
{ "plain": 2850, "san": 4300 }
@@ -0,0 +1 @@
{ "plain": 2750, "san": 4100 }