diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 3da043d37..8aa3a59b5 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -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" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 50d41994e..933ef8610 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -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"] }, diff --git a/packages/compiler/ambient/node-fallback/events.d.ts b/packages/compiler/ambient/node-fallback/events.d.ts index dda1cd086..ca55ca0c8 100644 --- a/packages/compiler/ambient/node-fallback/events.d.ts +++ b/packages/compiler/ambient/node-fallback/events.d.ts @@ -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` adopts the same emitter base. + * The map stays unconstrained here — the members below are the untyped + * `...args: any[]` surface either way. */ + class EventEmitter> { constructor(); static defaultMaxListeners: number; /* Listeners registered under this symbol observe 'error' emits first diff --git a/packages/compiler/ambient/node-fallback/fs-promises.d.ts b/packages/compiler/ambient/node-fallback/fs-promises.d.ts index 6f3f2c460..e5d7827d0 100644 --- a/packages/compiler/ambient/node-fallback/fs-promises.d.ts +++ b/packages/compiler/ambient/node-fallback/fs-promises.d.ts @@ -210,7 +210,7 @@ declare module "fs/promises" { export function access(path: string | URL, mode?: number): Promise; export function truncate(path: string | URL, len?: number): Promise; export function appendFile(path: string | URL, data: string | Uint8Array): Promise; - export function copyFile(src: string | URL, dest: string | URL): Promise; + export function copyFile(src: string | URL, dest: string | URL, mode?: number): Promise; export function mkdtemp(prefix: string | URL): Promise; export function rmdir(path: string | URL): Promise; export function rename(oldPath: string | URL, newPath: string | URL): Promise; diff --git a/packages/compiler/ambient/node-fallback/fs.d.ts b/packages/compiler/ambient/node-fallback/fs.d.ts index 4aca82386..4cdfad934 100644 --- a/packages/compiler/ambient/node-fallback/fs.d.ts +++ b/packages/compiler/ambient/node-fallback/fs.d.ts @@ -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, diff --git a/packages/compiler/ambient/node-fallback/net.d.ts b/packages/compiler/ambient/node-fallback/net.d.ts index 806039fb5..3520518e7 100644 --- a/packages/compiler/ambient/node-fallback/net.d.ts +++ b/packages/compiler/ambient/node-fallback/net.d.ts @@ -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 { diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index bccbac7be..c9e5e87cd 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -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; diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 1e668bfa9..2b7fe1319 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -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 } : {}), diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index c888d6e23..d7e52e925 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -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}`); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index caa8696b7..7231e9820 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -473,6 +473,7 @@ export const LIB_FN_SYMS: Record = { "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_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 = { // 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 = { "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 = { "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 = { "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 = { "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", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 1b078eecc..59c206a3e 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -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; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 16bd108c9..fcddc9f3f 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -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; diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 82a5022c6..3977e8677 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -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 }, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index c4ed94fa5..e556aebbb 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -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. */ diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts index cdd0b73c0..c1a6ea89b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index 9096316a3..b82984326 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -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; } diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index 9bb9ee404..f86393e55 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -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: , encoding: "utf8" }', + 'the supported options are { mode: , encoding: "utf8", flag: "w" | "wx" }', ); } } @@ -1159,7 +1190,7 @@ export function lowerFsWriteOptionsCall( lowerer.noLowering( `${operation} with 3 arguments`, optsNode, - 'the supported options are { mode: , encoding: "utf8" } over string data', + 'the supported options are { mode: , encoding: "utf8", flag: "w" | "wx" } over string data', ); } return finishWrite(optionValues); diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index 1dbedd395..5e3c6c4b6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -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" && diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index 3911188e4..4e7234a48 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -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("") 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 ( diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index f250bf0f3..f4053db29 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index ef55fa7e5..b90c921c2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 39156940e..ae63652ed 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -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 }); diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index 8619b4997..1c4736cd7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index e286a02b8..107808d2f 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -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") { diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index c204b40f4..9a1c403ce 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -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") diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index a790f57a6..66cc8c1c7 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -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, diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 04f1d0d07..5fc67138e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 189a0e79c..d522c2cde 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -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 && diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index e5f0f9f7a..88aee8466 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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", diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 6fc6020ec..28fbadfa9 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -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` 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` + // 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", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 36ea4aa93..a5fada25e 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -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) diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index d3132d02f..7ba3f7191 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index bf9251569..b07984ab9 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -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)", ); } diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 71a2ace6c..7e1270290 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -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"], diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 0e609116d..ca9211c12 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -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 } : {}), diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 4ee5fc609..3ee94312a 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -91,6 +91,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = 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 = 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 = 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 = 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 = new Set([ "fs.readlinkSyncStr", "fs.readlinkSyncBuffer", "fs.writeFileModeSync", + "fs.writeFileFlagsSync", "fs.mkdirModeSync", "fs.mkdirRecursiveModeSync", "fs.openSync", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 9d9ad9933..802d190a5 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -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(, 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 }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 2b34aad66..1b6913916 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -100,6 +100,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = 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 = 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, diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index a84bcfa73..19ee0f781 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -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; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8423-process-prepend-listeners.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8423-process-prepend-listeners.ts.json new file mode 100644 index 000000000..4b045387c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8423-process-prepend-listeners.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8423-process-prepend-listeners.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8423-urlsearchparams-implicit-string.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8423-urlsearchparams-implicit-string.ts.json new file mode 100644 index 000000000..511710dcf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8423-urlsearchparams-implicit-string.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8423-urlsearchparams-implicit-string.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8424-child-optional-chain.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8424-child-optional-chain.ts.json new file mode 100644 index 000000000..ffd41c325 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8424-child-optional-chain.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8424-child-optional-chain.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8424-regex-match-index-chain.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8424-regex-match-index-chain.ts.json new file mode 100644 index 000000000..46f66548d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8424-regex-match-index-chain.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8424-regex-match-index-chain.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8425-array-from-string-nullable.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8425-array-from-string-nullable.ts.json new file mode 100644 index 000000000..24a32bab3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8425-array-from-string-nullable.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8425-array-from-string-nullable.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8425-server-off.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8425-server-off.ts.json new file mode 100644 index 000000000..42d0a2b16 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8425-server-off.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8425-server-off.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-process-stream-fd-israw.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-process-stream-fd-israw.ts.json new file mode 100644 index 000000000..814d16007 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-process-stream-fd-israw.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8426-process-stream-fd-israw.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json new file mode 100644 index 000000000..4081fc5ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8426-reflect-object-reflections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8427-emitter-generic-base.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8427-emitter-generic-base.ts.json new file mode 100644 index 000000000..5ebf8b665 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8427-emitter-generic-base.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8427-emitter-generic-base.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8427-fs-copyfile-writefile-flags.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8427-fs-copyfile-writefile-flags.ts.json new file mode 100644 index 000000000..3c06d6664 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8427-fs-copyfile-writefile-flags.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8427-fs-copyfile-writefile-flags.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8428-spawn-detached-runtime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8428-spawn-detached-runtime.ts.json new file mode 100644 index 000000000..5ffd8c9bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8428-spawn-detached-runtime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8428-spawn-detached-runtime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8428-string-decoder-dynamic-chunks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8428-string-decoder-dynamic-chunks.ts.json new file mode 100644 index 000000000..4d1ce9721 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8428-string-decoder-dynamic-chunks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8428-string-decoder-dynamic-chunks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8500-optional-method-call-receivers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8500-optional-method-call-receivers.ts.json new file mode 100644 index 000000000..5b5c4b640 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8500-optional-method-call-receivers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8500-optional-method-call-receivers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8501-nullish-optional-capture-receivers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8501-nullish-optional-capture-receivers.ts.json new file mode 100644 index 000000000..e90405629 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8501-nullish-optional-capture-receivers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8501-nullish-optional-capture-receivers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8502-constant-module-specifiers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8502-constant-module-specifiers.ts.json new file mode 100644 index 000000000..8b78bd3c3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8502-constant-module-specifiers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8502-constant-module-specifiers.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 82bf652d3..81c5f9deb 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -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. diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index cd93defcc..a3e56e2b2 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -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; diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index e3846737d..178062895 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -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 */ diff --git a/packages/runtime/src/scr_bytes_strdec_dyn.c b/packages/runtime/src/scr_bytes_strdec_dyn.c new file mode 100644 index 000000000..c23d54523 --- /dev/null +++ b/packages/runtime/src/scr_bytes_strdec_dyn.c @@ -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; +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index e95ba05ae..ca8931742 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -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); diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 349a1e3c1..dbd5a606d 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -246,6 +246,34 @@ void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) { #endif } +#ifdef SCR_PROCESS_EXTRAS +/* process.prependListener(, 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. */ diff --git a/packages/runtime/src/scr_events.h b/packages/runtime/src/scr_events.h index a0025783e..5e99aa607 100644 --- a/packages/runtime/src/scr_events.h +++ b/packages/runtime/src/scr_events.h @@ -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); diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h index 6c5f83358..cb3bea325 100644 --- a/packages/runtime/src/scr_exception.h +++ b/packages/runtime/src/scr_exception.h @@ -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 diff --git a/packages/runtime/src/scr_fs_cp.c b/packages/runtime/src/scr_fs_cp.c index 8a9ebec8b..8c49dddb2 100644 --- a/packages/runtime/src/scr_fs_cp.c +++ b/packages/runtime/src/scr_fs_cp.c @@ -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); diff --git a/packages/runtime/src/scr_fs_flags.c b/packages/runtime/src/scr_fs_flags.c new file mode 100644 index 000000000..29f3e07d6 --- /dev/null +++ b/packages/runtime/src/scr_fs_flags.c @@ -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 +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#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(); +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 0a1616dd9..311596597 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 6846b260e..6556605ab 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -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; diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 217f78561..9250b1bca 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -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); diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 92fd7c9f1..a31cd4688 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -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 diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index af4b8e1b8..f22943717 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -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 diff --git a/tests/corpus/8423-process-prepend-listeners.ts b/tests/corpus/8423-process-prepend-listeners.ts new file mode 100644 index 000000000..b12d4d336 --- /dev/null +++ b/tests/corpus/8423-process-prepend-listeners.ts @@ -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"); diff --git a/tests/corpus/8423-urlsearchparams-implicit-string.ts b/tests/corpus/8423-urlsearchparams-implicit-string.ts new file mode 100644 index 000000000..da32d4906 --- /dev/null +++ b/tests/corpus/8423-urlsearchparams-implicit-string.ts @@ -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]); diff --git a/tests/corpus/8424-child-optional-chain.ts b/tests/corpus/8424-child-optional-chain.ts new file mode 100644 index 000000000..59581df88 --- /dev/null +++ b/tests/corpus/8424-child-optional-chain.ts @@ -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"); diff --git a/tests/corpus/8424-regex-match-index-chain.ts b/tests/corpus/8424-regex-match-index-chain.ts new file mode 100644 index 000000000..4245b161d --- /dev/null +++ b/tests/corpus/8424-regex-match-index-chain.ts @@ -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 = /(?b)/.exec(text)?.groups; +console.log(groups?.lead); diff --git a/tests/corpus/8425-array-from-string-nullable.ts b/tests/corpus/8425-array-from-string-nullable.ts new file mode 100644 index 000000000..210eb1b2a --- /dev/null +++ b/tests/corpus/8425-array-from-string-nullable.ts @@ -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)); diff --git a/tests/corpus/8425-server-off.ts b/tests/corpus/8425-server-off.ts new file mode 100644 index 000000000..f53a05c5a --- /dev/null +++ b/tests/corpus/8425-server-off.ts @@ -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); +}); diff --git a/tests/corpus/8426-process-stream-fd-israw.ts b/tests/corpus/8426-process-stream-fd-israw.ts new file mode 100644 index 000000000..073f4c48d --- /dev/null +++ b/tests/corpus/8426-process-stream-fd-israw.ts @@ -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"); diff --git a/tests/corpus/8426-reflect-object-reflections.ts b/tests/corpus/8426-reflect-object-reflections.ts new file mode 100644 index 000000000..6dea53e79 --- /dev/null +++ b/tests/corpus/8426-reflect-object-reflections.ts @@ -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); +} diff --git a/tests/corpus/8427-emitter-generic-base.ts b/tests/corpus/8427-emitter-generic-base.ts new file mode 100644 index 000000000..7ffc03f55 --- /dev/null +++ b/tests/corpus/8427-emitter-generic-base.ts @@ -0,0 +1,31 @@ +// A class extending EventEmitter: 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 { + 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")); diff --git a/tests/corpus/8427-fs-copyfile-writefile-flags.ts b/tests/corpus/8427-fs-copyfile-writefile-flags.ts new file mode 100644 index 000000000..44b7c4e9b --- /dev/null +++ b/tests/corpus/8427-fs-copyfile-writefile-flags.ts @@ -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 }); diff --git a/tests/corpus/8428-spawn-detached-runtime.ts b/tests/corpus/8428-spawn-detached-runtime.ts new file mode 100644 index 000000000..a16f24c3e --- /dev/null +++ b/tests/corpus/8428-spawn-detached-runtime.ts @@ -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)); + }); +}); diff --git a/tests/corpus/8428-string-decoder-dynamic-chunks.ts b/tests/corpus/8428-string-decoder-dynamic-chunks.ts new file mode 100644 index 000000000..7ec060e56 --- /dev/null +++ b/tests/corpus/8428-string-decoder-dynamic-chunks.ts @@ -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); + } +} diff --git a/tests/corpus/8500-optional-method-call-receivers.ts b/tests/corpus/8500-optional-method-call-receivers.ts new file mode 100644 index 000000000..ddf480641 --- /dev/null +++ b/tests/corpus/8500-optional-method-call-receivers.ts @@ -0,0 +1,79 @@ +// Optional-chained METHOD calls on structural receivers: a class seen +// through `Pick`, 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 | undefined): void { + w?.onAgentSettled(); +} + +function tick(tui: Renderer | undefined): void { + tui?.requestRender(); +} + +class Session { + private cacheWarmer?: Pick; + 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); +} diff --git a/tests/corpus/8501-nullish-optional-capture-receivers.ts b/tests/corpus/8501-nullish-optional-capture-receivers.ts new file mode 100644 index 000000000..e4079dc5d --- /dev/null +++ b/tests/corpus/8501-nullish-optional-capture-receivers.ts @@ -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[] { + 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)); diff --git a/tests/corpus/8502-constant-module-specifiers.ts b/tests/corpus/8502-constant-module-specifiers.ts new file mode 100644 index 000000000..f3bf78c07 --- /dev/null +++ b/tests/corpus/8502-constant-module-specifiers.ts @@ -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 { + 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")); diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 3daa5357e..35295f87b 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -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"); diff --git a/tests/harness/corpus-costs/8423-process-prepend-listeners.ts.json b/tests/harness/corpus-costs/8423-process-prepend-listeners.ts.json new file mode 100644 index 000000000..f315263c8 --- /dev/null +++ b/tests/harness/corpus-costs/8423-process-prepend-listeners.ts.json @@ -0,0 +1 @@ +{ "plain": 2850, "san": 21650 } diff --git a/tests/harness/corpus-costs/8423-urlsearchparams-implicit-string.ts.json b/tests/harness/corpus-costs/8423-urlsearchparams-implicit-string.ts.json new file mode 100644 index 000000000..846fa300d --- /dev/null +++ b/tests/harness/corpus-costs/8423-urlsearchparams-implicit-string.ts.json @@ -0,0 +1 @@ +{ "plain": 4163, "san": 7431 } diff --git a/tests/harness/corpus-costs/8424-child-optional-chain.ts.json b/tests/harness/corpus-costs/8424-child-optional-chain.ts.json new file mode 100644 index 000000000..0f324b400 --- /dev/null +++ b/tests/harness/corpus-costs/8424-child-optional-chain.ts.json @@ -0,0 +1 @@ +{ "plain": 2300, "san": 4100 } diff --git a/tests/harness/corpus-costs/8424-regex-match-index-chain.ts.json b/tests/harness/corpus-costs/8424-regex-match-index-chain.ts.json new file mode 100644 index 000000000..2520fba26 --- /dev/null +++ b/tests/harness/corpus-costs/8424-regex-match-index-chain.ts.json @@ -0,0 +1 @@ +{ "plain": 2579, "san": 9345 } diff --git a/tests/harness/corpus-costs/8425-array-from-string-nullable.ts.json b/tests/harness/corpus-costs/8425-array-from-string-nullable.ts.json new file mode 100644 index 000000000..b7845d83e --- /dev/null +++ b/tests/harness/corpus-costs/8425-array-from-string-nullable.ts.json @@ -0,0 +1 @@ +{ "plain": 2724, "san": 5297 } diff --git a/tests/harness/corpus-costs/8425-server-off.ts.json b/tests/harness/corpus-costs/8425-server-off.ts.json new file mode 100644 index 000000000..4c1984f33 --- /dev/null +++ b/tests/harness/corpus-costs/8425-server-off.ts.json @@ -0,0 +1 @@ +{ "plain": 2350, "san": 5950 } diff --git a/tests/harness/corpus-costs/8426-process-stream-fd-israw.ts.json b/tests/harness/corpus-costs/8426-process-stream-fd-israw.ts.json new file mode 100644 index 000000000..1e5609a66 --- /dev/null +++ b/tests/harness/corpus-costs/8426-process-stream-fd-israw.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2600 } diff --git a/tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json b/tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json new file mode 100644 index 000000000..a1a93d6be --- /dev/null +++ b/tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json @@ -0,0 +1 @@ +{ "plain": 3121, "san": 5843 } diff --git a/tests/harness/corpus-costs/8427-emitter-generic-base.ts.json b/tests/harness/corpus-costs/8427-emitter-generic-base.ts.json new file mode 100644 index 000000000..f8c3504c6 --- /dev/null +++ b/tests/harness/corpus-costs/8427-emitter-generic-base.ts.json @@ -0,0 +1 @@ +{ "plain": 2450, "san": 5700 } diff --git a/tests/harness/corpus-costs/8427-fs-copyfile-writefile-flags.ts.json b/tests/harness/corpus-costs/8427-fs-copyfile-writefile-flags.ts.json new file mode 100644 index 000000000..495c7b29b --- /dev/null +++ b/tests/harness/corpus-costs/8427-fs-copyfile-writefile-flags.ts.json @@ -0,0 +1 @@ +{ "plain": 2673, "san": 4238 } diff --git a/tests/harness/corpus-costs/8428-spawn-detached-runtime.ts.json b/tests/harness/corpus-costs/8428-spawn-detached-runtime.ts.json new file mode 100644 index 000000000..2447b70e5 --- /dev/null +++ b/tests/harness/corpus-costs/8428-spawn-detached-runtime.ts.json @@ -0,0 +1 @@ +{ "plain": 4250, "san": 2850 } diff --git a/tests/harness/corpus-costs/8428-string-decoder-dynamic-chunks.ts.json b/tests/harness/corpus-costs/8428-string-decoder-dynamic-chunks.ts.json new file mode 100644 index 000000000..5c6c747fe --- /dev/null +++ b/tests/harness/corpus-costs/8428-string-decoder-dynamic-chunks.ts.json @@ -0,0 +1 @@ +{ "plain": 2575, "san": 3939 } diff --git a/tests/harness/corpus-costs/8500-optional-method-call-receivers.ts.json b/tests/harness/corpus-costs/8500-optional-method-call-receivers.ts.json new file mode 100644 index 000000000..feabbe802 --- /dev/null +++ b/tests/harness/corpus-costs/8500-optional-method-call-receivers.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 4200 } diff --git a/tests/harness/corpus-costs/8501-nullish-optional-capture-receivers.ts.json b/tests/harness/corpus-costs/8501-nullish-optional-capture-receivers.ts.json new file mode 100644 index 000000000..8ee6a84fa --- /dev/null +++ b/tests/harness/corpus-costs/8501-nullish-optional-capture-receivers.ts.json @@ -0,0 +1 @@ +{ "plain": 2850, "san": 4300 } diff --git a/tests/harness/corpus-costs/8502-constant-module-specifiers.ts.json b/tests/harness/corpus-costs/8502-constant-module-specifiers.ts.json new file mode 100644 index 000000000..a23eac02f --- /dev/null +++ b/tests/harness/corpus-costs/8502-constant-module-specifiers.ts.json @@ -0,0 +1 @@ +{ "plain": 2750, "san": 4100 }