diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index f4290a15a..0fdc92436 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -20,6 +20,9 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ ...(features.wasm ? { wasm: true } : {}), ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), + ...(features.fsFlags ? { fsFlags: true } : {}), + ...(features.strdecDyn ? { strdecDyn: true } : {}), + ...(features.processExtras ? { processExtras: true } : {}), ...(features.nodeIsland ? { nodeIsland: true } : {}), ...(features.moduleNamespace ? { moduleNamespace: true } : {}), ...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}), diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 6a40a1be7..a76aea1c8 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -34,6 +34,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; /** Calls into Node.js (@scriptc/node): scr_node_island.c and the * SCR_NODE_ISLANDS variant of the event loop unit. */ nodeIsland?: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index f093d6483..522b355b2 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -161,6 +161,15 @@ export interface CcOptions { warningReplace?: 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; /** Calls into Node.js (@scriptc/node): compiles scr_node_island.c and * the event loop with SCR_NODE_ISLANDS (its helper slot). */ nodeIsland?: boolean; diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 01efc7e34..8d8979185 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -153,6 +153,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"), // win32 targets compile the libc-shim TU (stpcpy, arc4random_buf, // gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares // them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036, @@ -412,6 +417,13 @@ const RUNTIME_DEFINES: readonly { { define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true }, { define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace }, { 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_async.c's native promise reactions, which the web streams unit uses, // scr_json.c's and scr_dyn_builtin_class.c's instanceof over its classes, // and scr_fetch.c's bodies of its streams. diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 25a77c10d..141fad738 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -65,6 +65,9 @@ export interface EarlyExecutableNativeFeatures { wasm?: boolean; warningReplace?: boolean; stdioExtended?: boolean; + fsFlags?: boolean; + strdecDyn?: boolean; + processExtras?: boolean; nodeIsland?: boolean; moduleNamespace?: boolean; moduleNamespaceDyn?: boolean; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 0719112cc..c9ca2ffa9 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -700,6 +700,9 @@ async function compileExecutableNative( ...(features.wasm ? { wasm: true } : {}), ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), + ...(features.fsFlags ? { fsFlags: true } : {}), + ...(features.strdecDyn ? { strdecDyn: true } : {}), + ...(features.processExtras ? { processExtras: true } : {}), ...(features.nodeIsland ? { nodeIsland: true } : {}), ...(features.moduleNamespace ? { moduleNamespace: true } : {}), ...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}), diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 2c9f6d57f..f0bb7bea6 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1248,6 +1248,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). */ @@ -1289,6 +1316,7 @@ function noRuntimeFeatures(): RuntimeFeatures { eventsOnceSignal: false, fetch: false, fileHandle: false, + fsFlags: false, fsWatch: false, http2: false, http: false, @@ -1308,6 +1336,7 @@ function noRuntimeFeatures(): RuntimeFeatures { pki: false, ed25519: false, processEvents: false, + processExtras: false, qs: false, regex: false, searchParams: false, @@ -1316,6 +1345,7 @@ function noRuntimeFeatures(): RuntimeFeatures { moduleNamespace: false, moduleNamespaceDyn: false, stream: false, + strdecDyn: false, streamDyn: false, handleProps: false, childLifecycle: false, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index d83d8393c..478ee5d24 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"], ["scr_regex.c", "regex"], ["scr_glob.c", "regex"], ["scr_idna.c", "regex"], @@ -278,6 +284,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 @@ -331,6 +368,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"], + }); } // 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.c b/packages/runtime/src/scr_async.c index f26d520f9..0225269f4 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -3234,11 +3234,6 @@ ScrPromise *scr_fsp_write_file_mode(ScrStr *path, ScrStr *data, double mode) { return scr_promise_settled_void(); } -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_mkdir(ScrStr *path) { scr_fs_mkdir(path); return scr_promise_settled_void(); @@ -3299,11 +3294,6 @@ ScrPromise *scr_fsp_copyfile(ScrStr *src, ScrStr *dest) { 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(); -} - /* readFile(path, encoding | { encoding }): the encoding is checked first, * so an unknown name rejects before the read. */ ScrPromise *scr_fsp_read_file_encoded(ScrStr *path, ScrStr *encoding) { diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 99c17eeca..6ad771b17 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -341,11 +341,7 @@ static bool scr_rej_check_listener(ScrDyn *fn) { return false; } -/* The registration body: `prepend` puts the listener in FRONT of the - * registry's existing entries (process.prependListener — Node's - * EventEmitter order), the plain form appends. */ -static void scr_rej_push_at(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once, - bool prepend) { +static void scr_rej_push(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once) { if (*n == *cap) { *cap = *cap ? *cap * 2 : 4; *list = realloc(*list, *cap * sizeof **list); @@ -354,18 +350,7 @@ static void scr_rej_push_at(ScrRejListener **list, size_t *n, size_t *cap, ScrDy ScrRejState *state = calloc(1, sizeof *state); if (!state) scr_ad_oom(); state->refs = 1; - ScrRejListener entry = (ScrRejListener){scr_dyn_retain(fn), once, ++scr_rej_id, state}; - if (prepend) { - memmove(*list + 1, *list, *n * sizeof **list); - (*list)[0] = entry; - (*n)++; - } else { - (*list)[(*n)++] = entry; - } -} - -static void scr_rej_push(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once) { - scr_rej_push_at(list, n, cap, fn, once, false); + (*list)[(*n)++] = (ScrRejListener){scr_dyn_retain(fn), once, ++scr_rej_id, state}; } static void scr_rej_remove(ScrRejListener *list, size_t *n, ScrDyn *fn) { @@ -482,32 +467,32 @@ static void scr_uncaught_teardown(void) { scr_uncaught_sync_hook(); } -/* Arm the registry's exit-time teardown once, whichever entry point - * registers first. */ -static void scr_uncaught_arm(void) { - static SCR_TL bool armed = false; - if (armed) return; - armed = true; - scr_atexit(scr_uncaught_teardown); -} - void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor) { if (!scr_rej_check_listener(fn)) return; - scr_uncaught_arm(); + static SCR_TL bool armed = false; + if (!armed) { armed = true; scr_atexit(scr_uncaught_teardown); } size_t k = monitor ? 1 : 0; scr_rej_push(&scr_uncaught_ls[k], &scr_uncaught_n[k], &scr_uncaught_cap[k], fn, once); scr_uncaught_sync_hook(); } +#ifdef SCR_PROCESS_EXTRAS /* process.prependListener("uncaughtException"[Monitor], fn): the same - * listener in front of the registry's existing entries. */ + * 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) { - if (!scr_rej_check_listener(fn)) return; - scr_uncaught_arm(); size_t k = monitor ? 1 : 0; - scr_rej_push_at(&scr_uncaught_ls[k], &scr_uncaught_n[k], &scr_uncaught_cap[k], fn, false, true); - scr_uncaught_sync_hook(); + 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; diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 3e7277dc7..6bb56195a 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -1509,45 +1509,6 @@ double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk) return hold; } -/* 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; - if ( - chunk->kind == SCR_DYN_TYPED_REF || chunk->kind == SCR_DYN_BYTES || - scr_buffer_storage_is(chunk)) { - 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; -} - /* decoder.end(): the buffered partial flushes — utf8's truncated sequence * becomes its replacement chars (the maximal-subpart rule), base64's * remainder encodes (padded; unpadded for base64url), utf16le's held 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..1c9bd2998 --- /dev/null +++ b/packages/runtime/src/scr_bytes_strdec_dyn.c @@ -0,0 +1,50 @@ +/* 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; + if ( + chunk->kind == SCR_DYN_TYPED_REF || chunk->kind == SCR_DYN_BYTES || + scr_buffer_storage_is(chunk)) { + 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_events.c b/packages/runtime/src/scr_events.c index a159c2fdc..aa550a5f3 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -192,10 +192,7 @@ static bool scr_signal_check(ScrStr *name, ScrDyn *cb) { return true; } -/* The registration body both entry points share: `prepend` puts the - * listener in FRONT of the signal's existing ones (process.prependListener - * — Node's EventEmitter order), the plain form appends. */ -static void scr_signal_add(ScrStr *name, ScrDyn *cb, bool once, bool prepend) { +void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) { if (!scr_signal_check(name, cb)) return; if (!scr_context_is_main()) return; #ifdef __wasi__ @@ -245,25 +242,37 @@ static void scr_signal_add(ScrStr *name, ScrDyn *cb, bool once, bool prepend) { reg->ls = realloc(reg->ls, reg->cap * sizeof *reg->ls); if (!reg->ls) scr_events_oom(); } - ScrSigListener entry = - (ScrSigListener){scr_dyn_retain(cb), scr_str_retain(name), once, ++scr_sig_id}; - if (prepend) { - memmove(reg->ls + 1, reg->ls, reg->n * sizeof *reg->ls); - reg->ls[0] = entry; - reg->n++; - } else { - reg->ls[reg->n++] = entry; - } + reg->ls[reg->n++] = (ScrSigListener){scr_dyn_retain(cb), scr_str_retain(name), once, ++scr_sig_id}; #endif } -void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) { - scr_signal_add(name, cb, once, false); +#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 } - -/* process.prependListener(, cb): the same listener in front of the - * signal's existing registrations. */ -void scr_signal_prepend(ScrStr *name, ScrDyn *cb) { scr_signal_add(name, cb, false, true); } +#endif static void scr_sig_reg_drop(ScrSigReg *reg) { if (reg->sig > 0) { @@ -1708,6 +1717,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_stdio_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. */ @@ -1734,37 +1768,6 @@ void scr_process_off_exit_island(uint32_t island) { } #endif -/* 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_stdio_raw ? 1.0 : 0.0; -} - -/* process.prependListener("exit", cb): the same listener in front of the - * registry's existing entries. */ -void scr_process_prepend_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, double)) { - if (scr_exit_ran) { - scr_closure_release(cb); - return; - } - if (scr_exit_n == scr_exit_cap) { - scr_exit_cap = scr_exit_cap ? scr_exit_cap * 2 : 2; - scr_exit_ls = realloc(scr_exit_ls, scr_exit_cap * sizeof *scr_exit_ls); - if (!scr_exit_ls) scr_events_oom(); - } - memmove(scr_exit_ls + 1, scr_exit_ls, scr_exit_n * sizeof *scr_exit_ls); - scr_exit_ls[0].cb = cb; - scr_exit_ls[0].fn = fn; - scr_exit_ls[0].once = false; -#ifdef SCR_NODE_ISLANDS - scr_exit_ls[0].island = 0; -#endif - scr_exit_n++; -} - void scr_process_off_exit(ScrClosure *cb /*borrowed*/) { for (size_t i = 0; i < scr_exit_n; i++) { #ifdef SCR_NODE_ISLANDS diff --git a/packages/runtime/src/scr_fs_cp.c b/packages/runtime/src/scr_fs_cp.c index f6c1fd13b..8c49dddb2 100644 --- a/packages/runtime/src/scr_fs_cp.c +++ b/packages/runtime/src/scr_fs_cp.c @@ -589,14 +589,19 @@ 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 file - * excludes _WIN32, whose arm (CopyFileW and its ENOSYS for FICLONE_FORCE) - * lives in scr_lib.c beside the Win32 fs helpers. */ + * 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"; @@ -607,6 +612,7 @@ void scr_fs_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) { 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) { 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_lib.c b/packages/runtime/src/scr_lib.c index d4d84a6e8..418c3bfe2 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -3531,9 +3531,12 @@ void scr_fs_write_file(ScrStr *path, ScrStr *data) { scr_fs_write_common(path, data, "wb"); } -/* 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) { +/* writeFileSync(p, data, { mode }): the mode is open(2)'s O_CREAT + * argument — it applies at CREATION only (umask applying), and an + * existing file keeps its permissions, exactly Node (which never chmods + * here). Same error shapes as the plain form. */ +void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode) { + if (!scr_fs_path_ok(path, "path")) return; char recv[48], msg[176]; scr_num_received(mode, recv); if (!(isfinite(mode) && trunc(mode) == mode)) { @@ -3541,27 +3544,18 @@ static bool scr_fs_write_mode_ok(double mode) { "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; + return; } 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; } - return true; -} - -/* The open(2) write behind the mode/flag forms: 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); + int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, (mode_t)mode); if (fd < 0) { scr_fs_throw(errno, "open", path); return; @@ -3585,22 +3579,6 @@ static void scr_fs_write_file_open(ScrStr *path, ScrStr *data, mode_t mode, int if (close(fd) != 0) scr_fs_throw(errno, "close", path); } -/* writeFileSync(p, data, { mode }). */ -void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode) { - if (!scr_fs_path_ok(path, "path")) return; - if (!scr_fs_write_mode_ok(mode)) return; - scr_fs_write_file_open(path, data, (mode_t)mode, 0); -} - -/* 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_path_ok(path, "path")) return; - if (!scr_fs_write_mode_ok(mode)) return; - scr_fs_write_file_open(path, data, (mode_t)mode, exclusive != 0 ? O_EXCL : 0); -} - void scr_fs_append_file(ScrStr *path, ScrStr *data) { scr_fs_write_common(path, data, "ab"); } @@ -4694,6 +4672,12 @@ static bool scr_fs_win_same_file(const WCHAR *a, const WCHAR *b) { 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 diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index c17c8b004..477fd9ab7 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -2438,9 +2438,12 @@ 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 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 @@ -2461,6 +2464,7 @@ void scr_net_server_off(ScrNetServer *s, ScrClosure *cb /*borrowed*/, double kin return; } } +#endif /* server.on('listening', cb): joins the deferred 'listening' list the * listen(port, cb) callback rides (the sweep fires it once after a