#include "scr_runtime.h" #include #include #include #include #include #ifdef _WIN32 #include /* _O_BINARY */ #include /* _setmode, _fileno */ #include #endif /* POSIX executables submit each JavaScript-visible stdout/stderr chunk with * write(2)/writev(2) on the descriptor itself. The C stream layer added a * copy, several stream calls per console line, and — for the unbuffered * stderr stream — one write(2) per argument, separator, and newline. Windows * (CRT text/binary modes, console code pages), WASI, and library artifacts * (whose host owns the process's C streams) keep the C stream path, now fed * one formatted chunk per call. */ #if !defined(_WIN32) && !defined(__wasi__) && !defined(SCR_LIB) #define SCR_STDIO_DIRECT 1 #include #include #if defined(__linux__) #include /* __fpending: glibc and musl */ #endif #else #define SCR_STDIO_DIRECT 0 #endif /* Set by scr_async.c at loop exhaustion; lives here (unconditionally) so * binaries that link the console without the async runtime still link, and * plain builds satisfy scr_async's reference. */ static SCR_TL long scr_abandoned_fibers = 0; void scr_note_abandoned_fibers(long n) { scr_abandoned_fibers = n; } bool scr_exit_frees(void) { #if defined(SCR_RC_AUDIT) || defined(SCR_LSAN_IMMORTAL) return true; #else if (!scr_context_is_main()) { #ifdef SCR_WORKERS scr_context_note_script_end(); #endif return true; } static int forced = -1; /* the main thread reads it, once */ if (forced < 0) { const char *value = getenv("SCRIPTC_EXIT_FREE"); forced = value != NULL && value[0] == '1'; } return forced != 0; #endif } #ifndef SCR_LIB #ifdef SCR_RC_AUDIT extern long scr_str_live_count(void); /* scr_string.c */ extern long scr_arr_live_count(void); /* scr_array.c */ extern long scr_map_live_count(void); /* scr_map.c */ extern long scr_box_live_count(void); /* scr_closure.c */ extern long scr_closure_live_count(void); /* scr_closure.c */ extern long scr_obj_live_count(void); /* scr_object.c */ extern long scr_obj_lifetime_count(void); /* scr_object.c */ extern long scr_union_live_count(void); /* scr_union.c */ extern long scr_dyn_live_count(void); /* scr_json.c */ extern long scr_bytes_live_count(void); /* scr_bytes.c */ static void scr_rc_audit_at_exit(void) { scr_buffer_pool_reset(); /* the pool's own slab reference */ /* Fibers suspended forever at loop exhaustion (awaits nobody resolves — * Node exits 0 there too) still hold their stacks and owned values by * design; a strict audit would only report that deliberate abandonment. */ if (scr_abandoned_fibers > 0) { fprintf(stderr, "scriptc RC audit skipped: %ld fiber(s) never resumed\n", scr_abandoned_fibers); return; } /* ProgramLifetime instances (@scriptc/types) are never freed, and neither * is anything they hold: a strict audit would only report that deliberate * retention. */ long lifetime = scr_obj_lifetime_count(); if (lifetime > 0) { fprintf(stderr, "scriptc RC audit skipped: %ld program-lifetime object(s) never freed\n", lifetime); return; } long strings = scr_str_live_count(); long arrays = scr_arr_live_count(); long maps = scr_map_live_count(); long boxes = scr_box_live_count(); long closures = scr_closure_live_count(); long objects = scr_obj_live_count(); long unions = scr_union_live_count(); long dyns = scr_dyn_live_count(); long bytes = scr_bytes_live_count(); if (strings != 0 || arrays != 0 || maps != 0 || boxes != 0 || closures != 0 || objects != 0 || unions != 0 || dyns != 0 || bytes != 0) { fflush(stdout); fprintf(stderr, "scriptc RC AUDIT FAILED: %ld heap string(s), %ld array(s), " "%ld map(s), %ld box(es), %ld closure(s), %ld object(s), " "%ld union(s), %ld dyn value(s), %ld bytes value(s) live at exit\n", strings, arrays, maps, boxes, closures, objects, unions, dyns, bytes); _Exit(99); } } #endif static void scr_flush_at_exit(void) { fflush(stdout); } static void scr_collect_cycles_at_exit(void) { if (scr_exit_frees()) scr_collect_cycles(); } #ifdef _WIN32 static UINT scr_original_console_output_cp = 0; static void scr_restore_console_output_cp(void) { if (scr_original_console_output_cp != 0) { (void)SetConsoleOutputCP(scr_original_console_output_cp); } } static void scr_enable_utf8_console_output(void) { DWORD mode; HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE); HANDLE err = GetStdHandle(STD_ERROR_HANDLE); bool out_is_console = out != NULL && out != INVALID_HANDLE_VALUE && GetConsoleMode(out, &mode); bool err_is_console = err != NULL && err != INVALID_HANDLE_VALUE && GetConsoleMode(err, &mode); if (!out_is_console && !err_is_console) return; UINT original = GetConsoleOutputCP(); if (original == 0 || original == CP_UTF8) return; if (!SetConsoleOutputCP(CP_UTF8)) return; /* The code page belongs to the shared console, not this process. Restore * it after every scriptc exit hook has finished writing. */ scr_original_console_output_cp = original; if (atexit(scr_restore_console_output_cp) != 0) { (void)SetConsoleOutputCP(original); scr_original_console_output_cp = 0; } } #endif void scr_runtime_abi_v26(void) {} void scr_init(void) { #ifdef SCR_WORKERS /* Generated inline exception polls read the active cell directly. */ scr_exc_swap_cell(NULL); if (scr_context_is_main()) { #endif #ifdef _WIN32 /* The CRT opens std streams in TEXT mode, which writes \n as \r\n. Node * on Windows does NOT translate — console.log emits \n whether stdout is * a pipe or the console (libuv writes the bytes as given) — so the * streams switch to binary here or every line would differ from the * oracle by a byte. stdin too: readFileSync(0) must see the bytes on the * pipe, not a CRLF-collapsed view. */ _setmode(_fileno(stdout), _O_BINARY); _setmode(_fileno(stderr), _O_BINARY); _setmode(_fileno(stdin), _O_BINARY); /* Byte streams stay raw UTF-8. A real Windows console instead decodes * narrow writes through its output code page, which commonly starts as * CP437; select UTF-8 only while this process is attached and restore the * shared setting at exit. */ scr_enable_utf8_console_output(); #endif /* A private formatting buffer coalesces the several stdio calls needed to * render one console line. Every JavaScript-visible stdout write flushes * before returning, so this buffer is never observable as delayed output: * Node hands each console.log/process.stdout.write chunk to its backing * stream immediately. */ static char outbuf[1 << 16]; setvbuf(stdout, outbuf, _IOFBF, sizeof outbuf); #ifdef SCR_WORKERS } #endif /* atexit is LIFO; registration order makes exit run: library cleanup * (registered later, in scr_lib_init) → cycle collection → RC audit → * flush → Windows console-code-page restore. The final collection frees * cycles the program dropped, so the audit (and ASan's leak check) see * them as freed, not leaked. */ scr_atexit(scr_flush_at_exit); #ifdef SCR_RC_AUDIT scr_atexit(scr_rc_audit_at_exit); #endif scr_atexit(scr_collect_cycles_at_exit); } #endif /* !SCR_LIB — a library artifact never touches host stdio modes/buffering and * registers no atexit handlers: scr_init and its exit hooks (the * audit's _Exit(99) included) are executable-lane machinery; the * library session reset lives in scr_library.c. */ /* ONE formatter for both console streams — console.error/warn print * byte-identically to console.log in Node (same inspect rendering), only * the stream differs. */ /* A real definition lets relocatable library links localize this hook; * a tentative common symbol stays externally visible on Mach-O. */ SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, bool wide) = NULL; SCR_TL bool (*scr_stdio_capture)(int fd, const void *data, size_t len) = NULL; SCR_TL bool scr_stdin_fed = false; SCR_TL void (*scr_stdin_fed_attach)(void) = NULL; SCR_TL bool (*scr_stdin_fed_waiting)(void) = NULL; static SCR_TL ScrBytes **scr_stdin_fed_q; static SCR_TL size_t scr_stdin_fed_head, scr_stdin_fed_len, scr_stdin_fed_cap; static SCR_TL bool scr_stdin_fed_end; void scr_stdin_fed_push(ScrBytes *chunk) { if (scr_stdin_fed_end) { if (chunk) scr_bytes_release(chunk); return; } if (!chunk) { scr_stdin_fed_end = true; return; } if (scr_stdin_fed_head + scr_stdin_fed_len == scr_stdin_fed_cap) { if (scr_stdin_fed_head > 0) { memmove(scr_stdin_fed_q, scr_stdin_fed_q + scr_stdin_fed_head, scr_stdin_fed_len * sizeof *scr_stdin_fed_q); scr_stdin_fed_head = 0; } if (scr_stdin_fed_len == scr_stdin_fed_cap) { scr_stdin_fed_cap = scr_stdin_fed_cap ? scr_stdin_fed_cap * 2 : 8; scr_stdin_fed_q = realloc(scr_stdin_fed_q, scr_stdin_fed_cap * sizeof *scr_stdin_fed_q); if (!scr_stdin_fed_q) scr_trap("scriptc: out of memory\n"); } } scr_stdin_fed_q[scr_stdin_fed_head + scr_stdin_fed_len++] = chunk; } ScrBytes *scr_stdin_fed_take(bool *ended) { if (scr_stdin_fed_len == 0) { *ended = scr_stdin_fed_end; return NULL; } *ended = false; ScrBytes *chunk = scr_stdin_fed_q[scr_stdin_fed_head++]; if (--scr_stdin_fed_len == 0) scr_stdin_fed_head = 0; return chunk; } void scr_stdin_fed_clear(void) { for (size_t i = 0; i < scr_stdin_fed_len; i++) scr_bytes_release(scr_stdin_fed_q[scr_stdin_fed_head + i]); free(scr_stdin_fed_q); scr_stdin_fed_q = NULL; scr_stdin_fed_head = scr_stdin_fed_len = scr_stdin_fed_cap = 0; scr_stdin_fed_end = false; scr_stdin_fed = false; scr_stdin_fed_attach = NULL; scr_stdin_fed_waiting = NULL; } /* Lines up to this size render on the C stack (fiber stacks are 256 KiB). */ #define SCR_CONSOLE_STACK_LINE 2048 #if SCR_STDIO_DIRECT /* True when the C stdout buffer holds bytes: runtime-internal output that * went through stdio without flushing. A direct write must submit them * first so stdout keeps source order and merged (2>&1) redirections keep * stdout-before-stderr order. Unknown platforms flush unconditionally. */ static inline bool scr_stdout_pending(void) { #if defined(__linux__) return __fpending(stdout) != 0; #elif defined(__APPLE__) return (stdout->_flags & __SWR) && stdout->_bf._base && stdout->_p > stdout->_bf._base; #else return true; #endif } /* Submit every byte of iov[0..count) to fd. Short writes continue with the * remainder and EINTR retries. Any other failure (EAGAIN on a non-blocking * descriptor, EPIPE, EBADF, ...) returns its errno with the rest of the * chunk dropped — what the C stream flush did before. */ static int scr_fd_writev_all(int fd, struct iovec *iov, int count) { while (count > 0 && iov->iov_len == 0) iov++, count--; while (count > 0) { ssize_t written = count == 1 ? write(fd, iov->iov_base, iov->iov_len) : writev(fd, iov, count); if (written < 0) { if (errno == EINTR) continue; return errno ? errno : EIO; } if (written == 0) return EIO; size_t left = (size_t)written; while (count > 0 && left >= iov->iov_len) left -= iov->iov_len, iov++, count--; if (count > 0) { iov->iov_base = (char *)iov->iov_base + left; iov->iov_len -= left; } } return 0; } /* Worker executables run several runtime threads that all write to the * process descriptors. Hold the matching C stream's lock (recursive, so the * pending-stdout flush inside is fine) around each direct submission, so a * chunk that needs several write(2) calls, and the stdout flush that precedes * it, cannot interleave with another thread's output. */ static inline FILE *scr_fd_stream_lock(int fd) { #ifdef SCR_WORKERS FILE *stream = fd == 2 ? stderr : stdout; flockfile(stream); return stream; #else (void)fd; return NULL; #endif } static inline void scr_fd_stream_unlock(FILE *stream) { #ifdef SCR_WORKERS funlockfile(stream); #else (void)stream; #endif } #endif /* Render args into line (capacity from scr_console_capacity): space-joined * with a trailing newline. Returns the byte count. Tags may carry * SCR_ARG_GLUE, so every kind test masks with SCR_ARG_KIND: a glued string * part must reserve its full length, exactly as scr_console_render copies * it. */ static size_t scr_console_capacity(size_t n, const ScrLogArg *args) { size_t capacity = 1; for (size_t i = 0; i < n; i++) { size_t length = (args[i].tag & SCR_ARG_KIND) == SCR_ARG_STR ? args[i].v.s->len : 32; if (length >= SIZE_MAX - capacity) scr_trap("scriptc: console output too large\n"); capacity += length + 1; } return capacity; } /* *wide records a string argument that may hold a lone surrogate (only * once one may exist). Glued parts of one concatenation that complete a * surrogate pair across their seam render as the paired character. */ static size_t scr_console_render(char *line, size_t n, const ScrLogArg *args, bool *wide) { size_t used = 0; bool lone = scr_wtf8_any(), joins = false, maybe = false; for (size_t i = 0; i < n; i++) { const ScrLogArg *arg = &args[i]; if (i && !(arg->tag & SCR_ARG_GLUE)) line[used++] = ' '; int kind = arg->tag & SCR_ARG_KIND; if (kind == SCR_ARG_STR) { if (lone && !arg->v.s->ascii) { maybe = true; joins |= scr_wtf8_joins(line, used, arg->v.s->data, arg->v.s->len); } memcpy(line + used, arg->v.s->data, arg->v.s->len); used += arg->v.s->len; } else if (kind == SCR_ARG_BOOL) { const char *text = arg->v.b ? "true" : "false"; size_t length = arg->v.b ? 4 : 5; memcpy(line + used, text, length); used += length; } else if (kind == SCR_ARG_F64 && arg->v.f == 0 && signbit(arg->v.f)) { /* console.log renders numbers via inspect, which distinguishes -0 * (String(-0) is "0", but console.log(-0) prints "-0"). A number * inside a concatenation (SCR_ARG_NUM) keeps String()'s "0". */ memcpy(line + used, "-0", 2); used += 2; } else { used += scr_f64_to_str(arg->v.f, line + used); } } if (joins) used = scr_wtf8_fuse(line, used); line[used++] = '\n'; *wide = maybe; return used; } /* The global console's group indentation: console.group installs its * indenting rewrite (scr_console_native.c) while a group is open, so * programs without groups carry none of it. Console instances never use * it. */ SCR_TL char *(*scr_console_indenter)(const char *line, size_t *used) = NULL; /* Console methods submit one formatted chunk to their backing stream before * returning, as Node's console does: a caller observing a live child sees * this line before the next JS turn. Errors are ignored (the global Node * console uses ignoreErrors=true for stream writes). */ static void scr_console_write(int fd, size_t n, const ScrLogArg *args, bool global) { #if SCR_STDIO_DIRECT /* A large single string goes out without a copy: string + newline in one * vectored write. */ if (!scr_stdio_write_hook && !scr_stdio_capture && !(global && scr_console_indenter) && n == 1 && (args[0].tag & SCR_ARG_KIND) == SCR_ARG_STR && args[0].v.s->len >= SCR_CONSOLE_STACK_LINE) { char *owned; const char *bytes = scr_str_utf8(args[0].v.s, &owned); FILE *locked = scr_fd_stream_lock(fd); if (scr_stdout_pending()) fflush(stdout); struct iovec iov[2] = {{(void *)bytes, args[0].v.s->len}, {(void *)"\n", 1}}; (void)scr_fd_writev_all(fd, iov, 2); scr_fd_stream_unlock(locked); scr_str_utf8_end(owned); return; } #endif char stack[SCR_CONSOLE_STACK_LINE]; size_t capacity = scr_console_capacity(n, args); char *line = capacity <= sizeof stack ? stack : malloc(capacity); if (!line) scr_trap("scriptc: out of memory\n"); bool wide = false; size_t used = scr_console_render(line, n, args, &wide); if (global && scr_console_indenter) { char *indented = scr_console_indenter(line, &used); if (line != stack) free(line); line = indented; } if (scr_stdio_write_hook) { /* A patched write receives the JavaScript string, lone surrogates kept. */ scr_stdio_write_hook(fd, line, used, wide); if (scr_exc_pending()) scr_exc_clear(); } else { if (wide) scr_wtf8_to_utf8(line, used); (void)scr_stdio_write_raw(fd, line, used); } if (line != stack) free(line); } void scr_console_log(size_t n, const ScrLogArg *args) { scr_console_write(1, n, args, true); } /* console.error and console.warn. Runtime-internal stdout output that is * still buffered goes out first so it cannot cross this stderr line under a * merged redirection (2>&1); JavaScript-visible stdout writes have already * been submitted. */ void scr_console_error(size_t n, const ScrLogArg *args) { scr_console_write(2, n, args, true); } void scr_console_log_parts(size_t n, const ScrLogArg *args) { scr_console_write(1, n, args, true); } void scr_console_error_parts(size_t n, const ScrLogArg *args) { scr_console_write(2, n, args, true); } /* A Console instance's line on process stdout (fd 1) or stderr (fd 2): * the global console's group indentation does not apply. */ void scr_console_write_fd(int fd, size_t n, const ScrLogArg *args) { scr_console_write(fd == 2 ? 2 : 1, n, args, false); } /* One JavaScript-visible raw write. Node's global console and process stream * methods hand each chunk to the backing stream when called; they do not * retain stdout until a later stderr write, a 64 KiB threshold, or process * exit. The runtime remains synchronous internally, so its backpressure * surface is still constantly true, but the bytes are observable promptly. * * Buffered runtime-internal stdout bytes go out first (for both streams) to * preserve stdout order and the merged-fd ordering convention. */ int scr_stdio_write_raw(int fd, const void *data, size_t len) { if (__builtin_expect(scr_stdio_capture != NULL, 0) && scr_stdio_capture(fd == 2 ? 2 : 1, data, len)) return 0; #if SCR_STDIO_DIRECT FILE *locked = scr_fd_stream_lock(fd == 2 ? 2 : 1); if (scr_stdout_pending()) fflush(stdout); struct iovec iov = {(void *)data, len}; int result = len == 0 ? 0 : scr_fd_writev_all(fd == 2 ? 2 : 1, &iov, 1); scr_fd_stream_unlock(locked); return result; #else FILE *out = fd == 2 ? stderr : stdout; if (fd == 2) fflush(stdout); if (len > 0 && fwrite(data, 1, len, out) != len) return errno ? errno : EIO; if (fflush(out) != 0) return errno ? errno : EIO; return 0; #endif } bool scr_stdio_write(int fd, const void *data, size_t len) { if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, data, len, false); (void)scr_stdio_write_raw(fd, data, len); return true; } /* A string chunk: a patched write receives the string itself; the stream * receives its UTF-8, each lone surrogate as U+FFFD. */ static __attribute__((noinline)) bool scr_stdio_write_wide(int fd, const ScrStr *s) { if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, s->data, s->len, true); char *owned; const char *bytes = scr_str_utf8(s, &owned); (void)scr_stdio_write_raw(fd, bytes, s->len); scr_str_utf8_end(owned); return true; } bool scr_stdio_write_str(int fd, const ScrStr *s) { if (__builtin_expect(scr_wtf8_any(), 0) && !s->ascii) return scr_stdio_write_wide(fd, s); return scr_stdio_write(fd, s->data, s->len); }