mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
pi wave 5: node process APIs, stdlib singletons, misc lowering (#2548)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
@@ -5,7 +5,12 @@
|
||||
* checks annotated listener parameters against it (unannotated non-empty
|
||||
* parameter lists have no static types and fence at the registration). */
|
||||
declare module "events" {
|
||||
class EventEmitter {
|
||||
/* Generic like @types/node's class: the type argument is the listener
|
||||
* MAP, a pure typing aid with no runtime layout (Node erases it too), so
|
||||
* `class Sink extends EventEmitter<Map>` adopts the same emitter base.
|
||||
* The map stays unconstrained here — the members below are the untyped
|
||||
* `...args: any[]` surface either way. */
|
||||
class EventEmitter<EventMap = Record<string, any[]>> {
|
||||
constructor();
|
||||
static defaultMaxListeners: number;
|
||||
/* Listeners registered under this symbol observe 'error' emits first
|
||||
|
||||
@@ -210,7 +210,7 @@ declare module "fs/promises" {
|
||||
export function access(path: string | URL, mode?: number): Promise<void>;
|
||||
export function truncate(path: string | URL, len?: number): Promise<void>;
|
||||
export function appendFile(path: string | URL, data: string | Uint8Array): Promise<void>;
|
||||
export function copyFile(src: string | URL, dest: string | URL): Promise<void>;
|
||||
export function copyFile(src: string | URL, dest: string | URL, mode?: number): Promise<void>;
|
||||
export function mkdtemp(prefix: string | URL): Promise<string>;
|
||||
export function rmdir(path: string | URL): Promise<void>;
|
||||
export function rename(oldPath: string | URL, newPath: string | URL): Promise<void>;
|
||||
|
||||
+9
-5
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8423-process-prepend-listeners.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8423-urlsearchparams-implicit-string.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8424-child-optional-chain.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8424-regex-match-index-chain.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8426-process-stream-fd-israw.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8426-reflect-object-reflections.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8427-emitter-generic-base.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8427-fs-copyfile-writefile-flags.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8428-spawn-detached-runtime.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8428-string-decoder-dynamic-chunks.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8500-optional-method-call-receivers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8501-nullish-optional-capture-receivers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user