diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index b0139aba9..75f18fe37 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -2380,7 +2380,6 @@ export function collectGlobals( // References from separately-declared functions cascade to // their own per-site runtime fences. const factoryType = - !isLet && isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts index bcb675692..c672f82e2 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -6,10 +6,14 @@ import { bindingNeverReassigned } from "./binding-analysis.js"; import { implicitDefaultInstance } from "./generic-functions.js"; import { type FnSig } from "./call-signatures.js"; -/** An ordinary constructor's explicit object return replaces its receiver. - * Without observing this or new.target, it can use the function's call ABI. */ -function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { - if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return false; +/** The JavaScript function declaration (or never-reassigned function + * expression binding) `new` calls, when it is an ordinary constructor: not + * a generator or async function. */ +function jsConstructorFunctionOf( + lowerer: Lowerer, + expr: ts.NewExpression, +): ts.FunctionDeclaration | ts.FunctionExpression | null { + if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return null; // This probe also sees ordinary classes during global collection. Leave // their deferred diagnostics for the class declaration's lowering. const wasCollecting = lowerer.collecting; @@ -20,7 +24,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { } finally { lowerer.collecting = wasCollecting; } - if (!symbol) return false; + if (!symbol) return null; const owner = symbol.declarations.find( (node) => (ts.isFunctionDeclaration(node) && node.body !== undefined) || @@ -45,7 +49,15 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { declaration.asteriskToken || declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ) - return false; + return null; + return declaration; +} + +/** An ordinary constructor's explicit object return replaces its receiver. + * Without observing this or new.target, it can use the function's call ABI. */ +function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { + const declaration = jsConstructorFunctionOf(lowerer, expr); + if (!declaration?.body) return false; const objectReturn = (expression: ts.Expression): boolean => ts.isObjectLiteralExpression(expression) || ts.isArrayLiteralExpression(expression) || @@ -81,8 +93,13 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { /** Collection must register the shared result before reaching factory * bodies: those bodies can reference later globals in the same module. */ +/** The storage of a JavaScript module binding initialized with `new` of an + * ordinary function: a factory's result and a constructor function's + * receiver (`const head = new Segment()`) are both checked-dynamic + * values, which functions declared elsewhere in the module read through + * the global. */ export function objectFactoryGlobalType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null { - return isObjectFactoryNew(lowerer, expr) ? DYN : null; + return jsConstructorFunctionOf(lowerer, expr) !== null ? DYN : null; } export function objectFactorySignature(lowerer: Lowerer, expr: ts.NewExpression): FnSig | null { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json new file mode 100644 index 000000000..d93d9afdc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7680-typed-array-search.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json new file mode 100644 index 000000000..ab91940c0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7681-typed-array-search-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json new file mode 100644 index 000000000..26d05338c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7682-constructor-function-globals.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 85d4989a1..3926bf83e 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -3341,6 +3341,9 @@ static bool scr_bytes_search_start(size_t len, double from, size_t *start) { } double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool last) { + /* A Buffer viewed as a Uint8Array keeps Buffer's own search: the needle + * is taken as a byte, and a NaN byteOffset searches the whole buffer. */ + if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, x, from, !last); SCR_BYTES_SNAPSHOT(b); size_t len = b->len; size_t size = scr_bytes_elem_size(b->elem); @@ -3361,6 +3364,7 @@ double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool la } bool scr_bytes_includes_elem(const ScrBytes *b, double x, double from) { + if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, x, from, true) >= 0; SCR_BYTES_SNAPSHOT(b); size_t len = b->len; size_t size = scr_bytes_elem_size(b->elem); diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 514c7c2ec..7c0a935cc 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -541,10 +541,12 @@ double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd) /* TypedArray indexOf (last false) and lastIndexOf (last true) over any * element kind, with strict equality: NaN is never found and -0 matches 0. * `from` is the fromIndex argument (ToIntegerOrInfinity applies); the - * caller passes 0, or +Infinity for lastIndexOf, when it is omitted. */ + * caller passes 0, or +Infinity for lastIndexOf, when it is omitted. A + * Buffer (a Uint8Array receiver can be one) searches as Buffer's + * indexOf and lastIndexOf do. */ double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool last); /* TypedArray includes: indexOf's search from `from` with SameValueZero, - * so NaN is found. */ + * so NaN is found. A Buffer searches as Buffer's includes does. */ bool scr_bytes_includes_elem(const ScrBytes *b, double x, double from); ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end); ScrBytes *scr_bytes_fill_num(ScrBytes *b, double v, double nargs, double offset, double end); diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 3391f54b1..6e0bf5146 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -388,8 +388,80 @@ static SCR_TL bool scr_stdin_destroyed = false; static SCR_TL bool scr_stdin_paused = false; static SCR_TL bool scr_stdin_resumed = false; static SCR_TL bool scr_stdin_read_eof = false; -static SCR_TL ScrBytes *scr_stdin_buffer; +/* Input held for read() and later consumers, as Node's BufferList: the + * chunks in arrival order and their total length, so an arrival appends + * without copying what is already held. */ +static SCR_TL ScrBytes **scr_stdin_held = NULL; +static SCR_TL size_t scr_stdin_nheld = 0, scr_stdin_held_cap = 0; +static SCR_TL size_t scr_stdin_buffered = 0; static SCR_TL size_t scr_stdin_read_demand; +/* 'readable' listeners (paused mode, as Node's once one is added): input + * stays buffered for read(), and the event fires on the next tick when + * input arrives while a read wanted more (`need`), and once at EOF. */ +static SCR_TL ScrStdinEndL *scr_stdin_readable = NULL; +static SCR_TL size_t scr_stdin_nreadable = 0, scr_stdin_readable_cap = 0; +static SCR_TL bool scr_stdin_readable_need = false; +static SCR_TL bool scr_stdin_readable_queued = false; + +/* Appends an arrived chunk (moves) to the held input. */ +static void scr_stdin_hold(ScrBytes *chunk) { + if (chunk->len == 0) { + scr_bytes_release(chunk); + return; + } + if (scr_stdin_nheld == scr_stdin_held_cap) { + scr_stdin_held_cap = scr_stdin_held_cap ? scr_stdin_held_cap * 2 : 4; + scr_stdin_held = realloc(scr_stdin_held, scr_stdin_held_cap * sizeof *scr_stdin_held); + if (!scr_stdin_held) scr_events_oom(); + } + scr_stdin_held[scr_stdin_nheld++] = chunk; + scr_stdin_buffered += chunk->len; +} + +/* The first n held bytes as one chunk (+1), 0 < n <= scr_stdin_buffered. + * A whole first chunk moves out without a copy; otherwise the bytes are + * joined once, and a partly taken chunk keeps its rest. */ +static ScrBytes *scr_stdin_take(size_t n) { + ScrBytes *first = scr_stdin_held[0]; + size_t used = 0; + ScrBytes *out; + if (n == first->len) { + out = first; + used = 1; + } else if (n < first->len) { + out = scr_bytes_slice(first, 0, (double)n); + scr_stdin_held[0] = scr_bytes_slice(first, (double)n, (double)first->len); + scr_bytes_release(first); + } else { + out = scr_bytes_new(SCR_BYTES_U8, (double)n); + size_t at = 0; + while (at < n) { + ScrBytes *chunk = scr_stdin_held[used]; + size_t part = chunk->len <= n - at ? chunk->len : n - at; + memcpy(out->data + at, chunk->data, part); + at += part; + if (part == chunk->len) { + scr_bytes_release(chunk); + used++; + } else { + scr_stdin_held[used] = scr_bytes_slice(chunk, (double)part, (double)chunk->len); + scr_bytes_release(chunk); + } + } + } + memmove(scr_stdin_held, scr_stdin_held + used, (scr_stdin_nheld - used) * sizeof *scr_stdin_held); + scr_stdin_nheld -= used; + scr_stdin_buffered -= n; + return out; +} + +static void scr_stdin_drop_held(void) { + for (size_t i = 0; i < scr_stdin_nheld; i++) scr_bytes_release(scr_stdin_held[i]); + free(scr_stdin_held); + scr_stdin_held = NULL; + scr_stdin_nheld = scr_stdin_held_cap = 0; + scr_stdin_buffered = 0; +} #ifdef SCR_STDIO_EXTENDED /* process.stdin.setEncoding(enc): the canonical encoding name (NULL until @@ -475,6 +547,78 @@ void scr_stdin_on_end(ScrClosure *cb /*moves*/, bool once) { scr_stdin_nend++; } +static void scr_stdin_finish(bool fire_end); + +static void scr_stdin_emit_readable(void) { + scr_stdin_readable_queued = false; + size_t length = scr_stdin_buffered; + if (scr_stdin_eof || scr_stdin_destroyed || (!length && !scr_stdin_read_eof)) return; + size_t n = scr_stdin_nreadable; + ScrStdinEndL *snap = malloc((n ? n : 1) * sizeof *snap); + if (!snap) scr_events_oom(); + for (size_t i = 0; i < n; i++) { + snap[i] = scr_stdin_readable[i]; + scr_closure_retain(snap[i].cb); + } + for (size_t i = 0; i < n; i++) { + if (snap[i].once) { + for (size_t j = 0; j < scr_stdin_nreadable; j++) { + if (scr_stdin_readable[j].cb == snap[i].cb) { + scr_closure_release(scr_stdin_readable[j].cb); + memmove(scr_stdin_readable + j, scr_stdin_readable + j + 1, + (scr_stdin_nreadable - j - 1) * sizeof *scr_stdin_readable); + scr_stdin_nreadable--; + break; + } + } + } + if (!scr_exc_pending() && !scr_stdin_eof && !scr_stdin_destroyed) + ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb); + scr_closure_release(snap[i].cb); + } + free(snap); + length = scr_stdin_buffered; + /* Another event is due once the buffer reaches the 64 KiB mark, or the + * larger size a listener's read(size) asked for. */ + scr_stdin_readable_need = + !scr_stdin_read_eof && (length <= 65536 || length < scr_stdin_read_demand); +} + +static void scr_stdin_queue_readable(void) { + if (scr_stdin_readable_queued || !scr_stdin_nreadable) return; + scr_stdin_readable_queued = true; + scr_next_tick_raw(scr_stdin_emit_readable); +} + +/* Adding a 'readable' listener starts reading ahead to the 64 KiB mark; a + * buffer that already holds input fires the event at once (next tick). */ +static void scr_stdin_on_readable(ScrClosure *cb /*moves*/, bool once) { + if (scr_stdin_eof || scr_stdin_destroyed) { + scr_closure_release(cb); + return; + } + if (scr_stdin_nreadable == scr_stdin_readable_cap) { + scr_stdin_readable_cap = scr_stdin_readable_cap ? scr_stdin_readable_cap * 2 : 2; + scr_stdin_readable = realloc(scr_stdin_readable, scr_stdin_readable_cap * sizeof *scr_stdin_readable); + if (!scr_stdin_readable) scr_events_oom(); + } + scr_stdin_readable[scr_stdin_nreadable].cb = cb; + scr_stdin_readable[scr_stdin_nreadable].once = once; + scr_stdin_nreadable++; + scr_stdin_readable_need = true; + if (scr_stdin_read_demand < 65536) scr_stdin_read_demand = 65536; + if (scr_stdin_buffered) scr_stdin_queue_readable(); +} + +/* The input ended: with 'readable' listeners the event fires once more and + * 'end' waits for the read that finds nothing left; otherwise 'end' fires + * once the buffer is drained. */ +static void scr_stdin_reached_eof(void) { + scr_stdin_read_eof = true; + if (scr_stdin_nreadable) scr_stdin_queue_readable(); + else if (!scr_stdin_buffered) scr_stdin_finish(true); +} + void scr_stdin_on_error(ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) { if (scr_stdin_eof || scr_stdin_destroyed) { scr_closure_release(cb); @@ -516,10 +660,15 @@ bool scr_stdin_ended(void) { return scr_stdin_eof || scr_stdin_destroyed; } * alive (a paused Node stdin with only those never flows — the process * exits with them registered). */ static bool scr_stdin_watching(void) { - return !scr_stdin_eof && !scr_stdin_destroyed && - ((!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) || - scr_stdin_waiter != NULL || - (!scr_stdin_read_eof && scr_stdin_read_demand > (scr_stdin_buffer ? scr_stdin_buffer->len : 0))); + if (scr_stdin_eof || scr_stdin_destroyed) return false; + /* 'readable' listeners keep the stream paused, as in Node, where + * resume() and 'data' listeners do not make it flow: it reads ahead only + * while a read wants more than is held (the 64 KiB mark, or a larger + * read(size)), and stops until something drains it. */ + if (scr_stdin_nreadable) return !scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered; + return (!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) || + scr_stdin_waiter != NULL || + (!scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered); } bool scr_stdin_pending(void) { return scr_stdin_watching(); } @@ -528,6 +677,10 @@ static void scr_stdin_drop_listeners(void) { for (size_t i = 0; i < scr_stdin_ndata; i++) scr_closure_release(scr_stdin_data[i].cb); for (size_t i = 0; i < scr_stdin_nend; i++) scr_closure_release(scr_stdin_end[i].cb); for (size_t i = 0; i < scr_stdin_nerr; i++) scr_closure_release(scr_stdin_err[i].cb); + for (size_t i = 0; i < scr_stdin_nreadable; i++) scr_closure_release(scr_stdin_readable[i].cb); + free(scr_stdin_readable); + scr_stdin_readable = NULL; + scr_stdin_nreadable = scr_stdin_readable_cap = 0; free(scr_stdin_data); free(scr_stdin_end); free(scr_stdin_err); @@ -593,8 +746,7 @@ static void scr_stdin_finish(bool fire_end) { void scr_stdin_destroy(void) { if (scr_stdin_destroyed) return; scr_stdin_destroyed = true; - scr_bytes_release(scr_stdin_buffer); - scr_stdin_buffer = NULL; + scr_stdin_drop_held(); scr_stdin_read_demand = 0; scr_stdin_settle_done(); scr_stdin_drop_listeners(); @@ -627,16 +779,9 @@ static void scr_stdin_deliver(ScrBytes *chunk); /* A chunk that arrived (moves): delivered, or buffered while paused or * without a consumer. */ static void scr_stdin_arrived(ScrBytes *chunk) { - if (scr_stdin_paused || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) { - if (scr_stdin_buffer) { - size_t before = scr_stdin_buffer->len; - ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + chunk->len)); - memcpy(joined->data, scr_stdin_buffer->data, before); - memcpy(joined->data + before, chunk->data, chunk->len); - scr_bytes_release(scr_stdin_buffer); - scr_bytes_release(chunk); - scr_stdin_buffer = joined; - } else scr_stdin_buffer = chunk; + if (scr_stdin_paused || scr_stdin_nreadable || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) { + if (scr_stdin_nreadable && scr_stdin_readable_need) scr_stdin_queue_readable(); + scr_stdin_hold(chunk); return; } scr_stdin_deliver(chunk); @@ -645,15 +790,18 @@ static void scr_stdin_arrived(ScrBytes *chunk) { static void scr_stdin_service(void) { if (!scr_stdin_watching()) return; - if (!scr_stdin_paused && scr_stdin_buffer) { - ScrBytes *chunk = scr_stdin_buffer; - scr_stdin_buffer = NULL; + /* Buffered input goes to a consumer: 'data' listeners, a resumed + * stream, or a parked for-await read. After the last 'readable' listener + * it stays buffered until one arrives, as in Node. */ + if (!scr_stdin_paused && !scr_stdin_nreadable && scr_stdin_buffered && + (scr_stdin_ndata > 0 || scr_stdin_resumed || scr_stdin_waiter != NULL)) { + ScrBytes *chunk = scr_stdin_take(scr_stdin_buffered); scr_stdin_deliver(chunk); scr_bytes_release(chunk); return; } if (scr_stdin_read_eof) { - if (!scr_stdin_buffer) scr_stdin_finish(true); + if (!scr_stdin_buffered && !scr_stdin_nreadable) scr_stdin_finish(true); return; } if (scr_stdin_fed) { @@ -662,10 +810,7 @@ static void scr_stdin_service(void) { bool ended = false; ScrBytes *chunk = scr_stdin_fed_take(&ended); if (chunk) scr_stdin_arrived(chunk); - else if (ended) { - scr_stdin_read_eof = true; - if (!scr_stdin_buffer) scr_stdin_finish(true); - } + else if (ended) scr_stdin_reached_eof(); return; } #ifdef _WIN32 @@ -727,8 +872,7 @@ static void scr_stdin_service(void) { return; } if (n == 0) { - scr_stdin_read_eof = true; - if (!scr_stdin_buffer) scr_stdin_finish(true); + scr_stdin_reached_eof(); return; } ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n); @@ -802,6 +946,8 @@ static void scr_stdin_deliver(ScrBytes *chunk) { #endif ScrPromise *p = scr_stdin_waiter; scr_stdin_waiter = NULL; + /* Node's stdin yields Buffers. */ + chunk->is_buffer = true; scr_promise_fulfill_ref(p, scr_bytes_retain(chunk), scr_bytes_retain_v, scr_bytes_release_v, NULL); scr_promise_release(p); @@ -878,6 +1024,7 @@ static void scr_stdio_data_text(ScrClosure *cb, ScrStr *text) { } #endif static void scr_stdio_end(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); } +static void scr_stdio_readable(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); } static void scr_stdio_error(ScrClosure *cb, ScrStr *message) { ScrError *error = scr_error_new(SCR_ERR_ERROR, message); ScrDyn *arg = scr_dyn_from_error(error); @@ -971,7 +1118,7 @@ static void scr_stdio_queue_write_error(ScrStdio *stream, ScrDyn *callback, int } static void scr_stdio_finish_read(void) { - if (scr_stdin_read_eof && !scr_stdin_buffer && !scr_stdin_eof && !scr_stdin_destroyed) + if (scr_stdin_read_eof && !scr_stdin_buffered && !scr_stdin_eof && !scr_stdin_destroyed) scr_stdin_finish(true); } @@ -980,21 +1127,21 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) { if (!all && (size->kind != SCR_DYN_NUM || !isfinite(size->v.num) || size->v.num < 0 || size->v.num > 1073741824 || floor(size->v.num) != size->v.num)) return scr_stdio_refusal("read size"); - size_t available = scr_stdin_buffer ? scr_stdin_buffer->len : 0; + size_t available = scr_stdin_buffered; size_t wanted = all ? available : (size_t)size->v.num; if (scr_stdin_read_eof && wanted > available) wanted = available; if (wanted == 0 || wanted > available) { if (!scr_stdin_eof && !scr_stdin_destroyed) { + scr_stdin_readable_need = true; scr_stdin_read_demand = wanted > 65536 ? wanted : 65536; if (scr_stdin_read_eof) scr_next_tick_raw(scr_stdio_finish_read); } return scr_dyn_new_null(); } - ScrBytes *chunk = scr_bytes_slice(scr_stdin_buffer, 0, (double)wanted); - ScrBytes *rest = wanted < available ? scr_bytes_slice(scr_stdin_buffer, (double)wanted, (double)available) : NULL; - scr_bytes_release(scr_stdin_buffer); - scr_stdin_buffer = rest; + ScrBytes *chunk = scr_stdin_take(wanted); + bool rest = scr_stdin_buffered > 0; scr_stdin_read_demand = 65536; + if (!rest && !scr_stdin_read_eof) scr_stdin_readable_need = true; scr_stdin_deliver(chunk); ScrDyn *result = scr_dyn_new_chunk(chunk); scr_bytes_release(chunk); @@ -1150,12 +1297,14 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc bool data = arg->kind == SCR_DYN_STR && arg->v.str->len == 4 && memcmp(event, "data", 4) == 0; bool end = arg->kind == SCR_DYN_STR && arg->v.str->len == 3 && memcmp(event, "end", 3) == 0; bool error = arg->kind == SCR_DYN_STR && arg->v.str->len == 5 && memcmp(event, "error", 5) == 0; + bool readable = stream->fd == 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 8 && + memcmp(event, "readable", 8) == 0; #ifdef SCR_STDIO_EXTENDED bool resize = stream->fd != 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 6 && memcmp(event, "resize", 6) == 0; - if ((!data && !end && !error && !resize) || (stream->fd != 0 && !error && !resize)) { + if ((!data && !end && !error && !readable && !resize) || (stream->fd != 0 && !error && !resize)) { #else - if ((!data && !end && !error) || (stream->fd != 0 && !error)) { + if ((!data && !end && !error && !readable) || (stream->fd != 0 && !error)) { #endif result = scr_stdio_refusal("event"); goto done; @@ -1175,6 +1324,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc if (data) scr_stdin_on_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, once); #endif if (end) scr_stdin_on_end(scr_stdio_callback(listener, (void *)scr_stdio_end), once); + if (readable) scr_stdin_on_readable(scr_stdio_callback(listener, (void *)scr_stdio_readable), once); if (error) scr_stdin_on_error(scr_stdio_callback(listener, (void *)scr_stdio_error), scr_stdio_error, once); } else { #define REMOVE_STDIO_LISTENER(list, count, fire) \ @@ -1187,6 +1337,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc } if (data) { REMOVE_STDIO_LISTENER(scr_stdin_data, scr_stdin_ndata, scr_stdio_data); } if (end) { REMOVE_STDIO_LISTENER(scr_stdin_end, scr_stdin_nend, scr_stdio_end); } + if (readable) { REMOVE_STDIO_LISTENER(scr_stdin_readable, scr_stdin_nreadable, scr_stdio_readable); } if (error) { REMOVE_STDIO_LISTENER(scr_stdin_err, scr_stdin_nerr, scr_stdio_error); } #undef REMOVE_STDIO_LISTENER } @@ -1246,7 +1397,7 @@ static ScrDyn *scr_stdio_get(void *ptr, const char *key, size_t length) { return dimension < 0 ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(dimension); } if (stream->fd == 0 && length == 14 && memcmp(key, "readableLength", 14) == 0) - return scr_dyn_new_num(scr_stdin_buffer ? (double)scr_stdin_buffer->len : 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()); if (stream->fd == 0 && length == 9 && memcmp(key, "destroyed", 9) == 0) return scr_dyn_new_bool(scr_stdin_destroyed); @@ -1531,8 +1682,7 @@ static void scr_events_cleanup_atexit(void) { if (scr_context_is_main()) scr_sig_cleanup(); scr_stdin_settle_done(); scr_stdin_drop_listeners(); - scr_bytes_release(scr_stdin_buffer); - scr_stdin_buffer = NULL; + scr_stdin_drop_held(); if (scr_stdio_initialized) scr_stdio_cleanup(); } diff --git a/tests/corpus/7680-typed-array-search.ts b/tests/corpus/7680-typed-array-search.ts new file mode 100644 index 000000000..7672c3712 --- /dev/null +++ b/tests/corpus/7680-typed-array-search.ts @@ -0,0 +1,70 @@ +// TypedArray.prototype.indexOf, lastIndexOf and includes on every numeric +// element kind: strict equality (NaN never found) for indexOf/lastIndexOf, +// SameValueZero (NaN found, -0 equals 0) for includes, and fromIndex as +// ToIntegerOrInfinity, counted from the end when negative. +const bytes = new Uint8Array([1, 2, 10, 3, 10, 255]); +console.log("u8", bytes.indexOf(10), bytes.indexOf(10, 3), bytes.indexOf(10, 5), bytes.lastIndexOf(10)); +console.log("u8 from", bytes.indexOf(10, -2), bytes.indexOf(10, -100), bytes.indexOf(1, Infinity), bytes.indexOf(10, 2.9)); +console.log("u8 last", bytes.lastIndexOf(10, 3), bytes.lastIndexOf(10, -3), bytes.lastIndexOf(1, -100), bytes.lastIndexOf(255, 99)); +console.log("u8 values", bytes.indexOf(256 + 10), bytes.indexOf(-246), bytes.indexOf(10.5), bytes.includes(255), bytes.includes(0)); +console.log("u8 includes from", bytes.includes(1, 1), bytes.includes(255, -1), bytes.includes(2, -5)); + +const clamped = new Uint8ClampedArray([0, 300, -5, 127.5]); +console.log("u8c", clamped.indexOf(255), clamped.indexOf(0, 1), clamped.lastIndexOf(0), clamped.indexOf(128)); + +const signed = new Int8Array([-1, 127, -128, 0]); +console.log("i8", signed.indexOf(-1), signed.indexOf(255), signed.indexOf(-128), signed.lastIndexOf(0)); + +const shorts = new Int16Array([5, -300, 5, 7]); +const ushorts = new Uint16Array([65535, 1, 65535]); +console.log("16", shorts.indexOf(-300), shorts.lastIndexOf(5), ushorts.indexOf(-1), ushorts.lastIndexOf(65535, 1)); + +const ints = new Int32Array([-2147483648, 0, 2147483647, 0]); +const uints = new Uint32Array([4294967295, 3]); +console.log("32", ints.indexOf(2147483647), ints.lastIndexOf(0), uints.indexOf(4294967295), uints.includes(-1)); + +const floats = new Float64Array([1.5, NaN, -0, 0, Infinity]); +console.log("f64", floats.indexOf(NaN), floats.lastIndexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.indexOf(-0)); +console.log("f64 more", floats.lastIndexOf(0), floats.indexOf(Infinity), floats.includes(-Infinity), floats.includes(NaN, 2)); + +const singles = new Float32Array([1.1, 0.5, NaN]); +console.log("f32", singles.indexOf(1.1), singles.indexOf(Math.fround(1.1)), singles.indexOf(0.5), singles.includes(NaN), singles.indexOf(NaN)); + +const empty = new Float64Array(0); +console.log("empty", empty.indexOf(0), empty.lastIndexOf(0), empty.includes(0), empty.includes(0, -1)); + +// Views search their own window of the buffer. +const buffer = new ArrayBuffer(16); +const whole = new Uint8Array(buffer); +whole.set([9, 9, 1, 2, 9, 3, 9, 9]); +const view = new Uint8Array(buffer, 2, 4); +const sub = whole.subarray(3, 6); +console.log("views", view.indexOf(9), view.lastIndexOf(9), view.indexOf(1), sub.indexOf(9), sub.includes(1), sub.lastIndexOf(3)); +const words = new Uint16Array(buffer, 2, 3); +console.log("word view", words.indexOf(0x0201), words.length); + +// Shared memory searches the same way. +const shared = new Int32Array(new SharedArrayBuffer(16)); +shared[1] = 7; +shared[3] = 7; +console.log("shared", shared.indexOf(7), shared.lastIndexOf(7), shared.includes(0), shared.indexOf(7, 2)); + +// A loop that finds every occurrence, as a line splitter does. +const text = new TextEncoder().encode("one\ntwo\n\nthree\n"); +const lines: number[] = []; +for (let at = text.indexOf(10); at !== -1; at = text.indexOf(10, at + 1)) lines.push(at); +console.log("lines", lines.join(",")); + +// A Buffer passed as a Uint8Array keeps Buffer's search: a number needle +// is taken as a byte, and a NaN byteOffset searches the whole buffer. +function searchView(view: Uint8Array): string { + return [view.indexOf(97 + 256), view.lastIndexOf(98, NaN), view.includes(353), view.indexOf(98, -1)].join(","); +} +console.log(searchView(Buffer.from("abcab")), searchView(new Uint8Array([97, 98, 99, 97, 98]))); + +// An explicit undefined fromIndex converts to NaN, which searches from 0: +// lastIndexOf then looks at index 0 only. +const values = new Int16Array([3, 1, 3, 2, 3]); +const absent: number | undefined = process.argv.length > 99 ? 1 : undefined; +console.log("undefined from", values.indexOf(3, undefined), values.lastIndexOf(3, undefined), values.lastIndexOf(1, undefined), values.includes(2, undefined)); +console.log("maybe undefined from", values.indexOf(3, absent), values.lastIndexOf(3, absent), values.includes(1, absent)); diff --git a/tests/corpus/7681-typed-array-search-dynamic.mjs b/tests/corpus/7681-typed-array-search-dynamic.mjs new file mode 100644 index 000000000..643ba0cfa --- /dev/null +++ b/tests/corpus/7681-typed-array-search-dynamic.mjs @@ -0,0 +1,28 @@ +// TypedArray search on untyped values (parameters of untyped functions and +// object members): needles that are not numbers are never found (strict +// equality), fromIndex may be undefined, a string or a fraction, and +// Buffers keep their own indexOf, which wraps number needles and takes +// strings. +function views(bytes, floats, shorts) { + return { bytes, floats, shorts }; +} +const { bytes, floats, shorts } = views( + new Uint8Array([1, 2, 10, 3, 10]), + new Float64Array([0.5, NaN, -0]), + new Int16Array([-1, 300, -1]), +); +console.log(bytes.indexOf(10), bytes.indexOf(10, 3), bytes.lastIndexOf(10), bytes.includes(3), bytes.includes(4)); +console.log(bytes.lastIndexOf(1, undefined), bytes.lastIndexOf(10), bytes.indexOf(10, undefined), bytes.includes(1, undefined)); +console.log(bytes.indexOf(10, '3'), bytes.indexOf(10, 2.9), bytes.indexOf(10, -1), bytes.lastIndexOf(10, -2), bytes.indexOf(10, null)); +console.log(bytes.indexOf('10'), bytes.includes('1'), bytes.indexOf(10n), bytes.indexOf(), bytes.includes(), bytes.indexOf(266)); +console.log(floats.indexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.lastIndexOf(-0), floats.includes(0.5, 1)); +console.log(shorts.indexOf(-1), shorts.lastIndexOf(-1), shorts.indexOf(300, -2), shorts.includes(65535)); +const holder = { view: bytes.subarray(2) }; +console.log(holder.view.indexOf(10), holder.view.lastIndexOf(10), holder.view.includes(1)); +const buffer = Buffer.from([1, 2, 10, 3, 10]); +const boxed = [buffer]; +console.log(boxed[0].indexOf(10), boxed[0].indexOf(266), boxed[0].indexOf('\n'), boxed[0].includes(Buffer.from([3, 10]))); +console.log(boxed[0].subarray(3).indexOf(10), Buffer.isBuffer(boxed[0].subarray(3)), boxed[0].lastIndexOf(10, -2)); +let count = 0; +for (let at = bytes.indexOf(10); at !== -1; at = bytes.indexOf(10, at + 1)) count++; +console.log('count', count); diff --git a/tests/corpus/7682-constructor-function-globals.js b/tests/corpus/7682-constructor-function-globals.js new file mode 100644 index 000000000..58292e4ac --- /dev/null +++ b/tests/corpus/7682-constructor-function-globals.js @@ -0,0 +1,55 @@ +// Module bindings that hold `new F()` of an ordinary constructor function +// (const, let and var), read and reassigned by functions declared elsewhere +// in the module, such as the head and tail of a linked list. +'use strict'; +function Segment(prev) { + this.prev = prev; + this.next = null; + this.data = new Array(4); + this.pos = 0; +} +const head = new Segment(); +let current = head; +var spare = new Segment(head); + +function append(value) { + if (current.pos === current.data.length) { + current = current.next || (current.next = new Segment(current)); + } + current.data[current.pos++] = value; +} + +function collect() { + const out = []; + for (let node = head; node; node = node.next) out.push(node.data.slice(0, node.pos).join('')); + return out.join('|'); +} + +function reset() { + current = head; + for (let node = head; node; node = node.next) node.pos = 0; +} + +for (const ch of 'abcdefghij') append(ch); +console.log(collect(), current === head, current.prev === head.next ? 'two' : 'more'); +reset(); +append('z'); +console.log(collect(), current === head, head.next !== null); +console.log(spare.prev === head, spare instanceof Segment, typeof spare.data); +spare = new Segment(spare); +console.log(spare.prev.prev === head); + +function Counter(start) { + this.value = start; +} +Counter.prototype.bump = function () { + return ++this.value; +}; +let counter = new Counter(10); +function bumpTwice() { + counter.bump(); + return counter.bump(); +} +console.log(bumpTwice(), counter.value); +counter = new Counter(0); +console.log(bumpTwice()); diff --git a/tests/fixtures/event-loop/stdin-for-await-buffers.mjs b/tests/fixtures/event-loop/stdin-for-await-buffers.mjs new file mode 100644 index 000000000..d64ae460a --- /dev/null +++ b/tests/fixtures/event-loop/stdin-for-await-buffers.mjs @@ -0,0 +1,10 @@ +// for await over process.stdin yields Buffers: Buffer.isBuffer holds and +// indexOf takes a byte and a start offset (only chunking-independent facts +// are printed). +const chunks = []; +for await (const chunk of process.stdin) chunks.push(chunk); +console.log('buffers', chunks.every((chunk) => Buffer.isBuffer(chunk))); +const all = Buffer.concat(chunks); +const newlines = []; +for (let at = all.indexOf(10); at !== -1; at = all.indexOf(10, at + 1)) newlines.push(at); +console.log('newlines', newlines.join(','), all.subarray(newlines[0] + 1).indexOf('#')); diff --git a/tests/fixtures/event-loop/stdio-readable-backpressure.mjs b/tests/fixtures/event-loop/stdio-readable-backpressure.mjs new file mode 100644 index 000000000..df2afb18f --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-backpressure.mjs @@ -0,0 +1,27 @@ +// 'readable', 'data' and resume() together, with no read() for a while: +// 'readable' keeps the stream paused, so it stops reading at its read-ahead +// mark until a read drains it, and 'data' sees what read() returns. +const input = process.stdin; +let draining = false; +let peak = 0; +let dataBytes = 0; +let readBytes = 0; +const drain = () => { + let chunk; + while ((chunk = input.read()) !== null) readBytes += chunk.length; +}; +input.on('data', (chunk) => { + dataBytes += chunk.length; +}); +input.on('readable', () => { + peak = Math.max(peak, input.readableLength); + if (draining) drain(); +}); +input.resume(); +setTimeout(() => { + peak = Math.max(peak, input.readableLength); + console.log('held below 128 KiB while not draining', peak <= 131072); + draining = true; + drain(); +}, 400); +input.on('end', () => console.log('read', readBytes, 'data', dataBytes)); diff --git a/tests/fixtures/event-loop/stdio-readable-large-size.mjs b/tests/fixtures/event-loop/stdio-readable-large-size.mjs new file mode 100644 index 000000000..f1f44edce --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-large-size.mjs @@ -0,0 +1,9 @@ +// read(size) above the 64 KiB read-ahead mark: 'readable' keeps firing as +// input arrives until the requested size is buffered. +const input = process.stdin; +const sizes = []; +input.on('readable', () => { + let chunk; + while ((chunk = input.read(150000)) !== null) sizes.push(chunk.length); +}); +input.on('end', () => console.log(sizes.join(','))); diff --git a/tests/fixtures/event-loop/stdio-readable-once-rest.mjs b/tests/fixtures/event-loop/stdio-readable-once-rest.mjs new file mode 100644 index 000000000..81eae144b --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-once-rest.mjs @@ -0,0 +1,12 @@ +// A once('readable') listener that reads part of the input: the rest stays +// buffered for a 'data' listener added afterwards. +const input = process.stdin; +input.once('readable', () => { + const head = input.read(3); + console.log('head', head === null ? null : head.toString('ascii')); + setTimeout(() => { + const rest = []; + input.on('data', (chunk) => rest.push(chunk.toString('ascii'))); + input.on('end', () => console.log('rest', rest.join(''))); + }, 50); +}); diff --git a/tests/fixtures/event-loop/stdio-readable-size.mjs b/tests/fixtures/event-loop/stdio-readable-size.mjs new file mode 100644 index 000000000..9ff76dafe --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-size.mjs @@ -0,0 +1,18 @@ +// Fixed-size reads of stdin: read(n) answers null until n bytes are +// buffered, and the rest once the input ended. +const input = process.stdin; +const chunks = []; +const counts = []; +input.on('readable', () => { + let reads = 0; + let chunk; + while ((chunk = input.read(4)) !== null) { + chunks.push(chunk.toString('ascii')); + reads++; + } + counts.push(reads); +}); +input.on('end', () => { + console.log(chunks.join('|')); + console.log(counts.join(',')); +}); diff --git a/tests/fixtures/event-loop/stdio-readable.mjs b/tests/fixtures/event-loop/stdio-readable.mjs new file mode 100644 index 000000000..324a4b57b --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable.mjs @@ -0,0 +1,19 @@ +// process.stdin in paused mode through 'readable': each event drains the +// buffer with read(), one more event follows EOF, and 'end' comes after the +// read that finds the buffer empty. +const input = process.stdin; +const events = []; +let text = ''; +input.on('readable', () => { + let reads = 0; + let chunk; + while ((chunk = input.read()) !== null) { + text += String(chunk); + reads++; + } + events.push(`readable ${reads}`); +}); +input.on('end', () => { + console.log(events.join(', ')); + console.log(JSON.stringify(text), input.readableLength); +}); diff --git a/tests/harness/corpus-costs/7680-typed-array-search.ts.json b/tests/harness/corpus-costs/7680-typed-array-search.ts.json new file mode 100644 index 000000000..37f8b6e89 --- /dev/null +++ b/tests/harness/corpus-costs/7680-typed-array-search.ts.json @@ -0,0 +1 @@ +{ "plain": 2204, "san": 3060 } diff --git a/tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json b/tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json new file mode 100644 index 000000000..b6fcd5eff --- /dev/null +++ b/tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 1649, "san": 958 } diff --git a/tests/harness/corpus-costs/7682-constructor-function-globals.js.json b/tests/harness/corpus-costs/7682-constructor-function-globals.js.json new file mode 100644 index 000000000..90fcede92 --- /dev/null +++ b/tests/harness/corpus-costs/7682-constructor-function-globals.js.json @@ -0,0 +1 @@ +{ "plain": 1623, "san": 962 } diff --git a/tests/harness/event-loop-cases.ts b/tests/harness/event-loop-cases.ts index 40bc7988d..50a7d2b85 100644 --- a/tests/harness/event-loop-cases.ts +++ b/tests/harness/event-loop-cases.ts @@ -34,6 +34,66 @@ export const eventLoopCases: EventLoopCase[] = [ fixture: "stdio-paused-exit.mjs", script: { writes: [], end: false }, }, + { + title: "stdio values: 'readable' drains with read(), then fires once more at EOF", + fixture: "stdio-readable.mjs", + script: { + writes: [ + { delayMs: 0, data: "alpha " }, + { delayMs: 60, data: "beta\n" }, + ], + end: true, + }, + }, + { + title: "stdio values: 'readable' with read(size) holds a partial tail until EOF", + fixture: "stdio-readable-size.mjs", + script: { writes: [{ delayMs: 0, data: "abcdefghij" }], end: true }, + }, + { + title: "stdio values: input a once('readable') listener leaves stays buffered for 'data'", + fixture: "stdio-readable-once-rest.mjs", + script: { writes: [{ delayMs: 0, data: "abcdefgh" }], end: true }, + }, + { + title: "stdio values: 'readable' with a read(size) above the read-ahead mark", + fixture: "stdio-readable-large-size.mjs", + script: { + writes: [ + { delayMs: 0, data: "a".repeat(70000) }, + { delayMs: 30, data: "b".repeat(70000) }, + { delayMs: 30, data: "c".repeat(60000) }, + ], + end: true, + }, + }, + { + title: "stdio values: 'readable' with 'data' and resume() holds input at the read-ahead mark", + fixture: "stdio-readable-backpressure.mjs", + script: { + writes: Array.from({ length: 16 }, (_, i) => ({ + delayMs: i === 0 ? 0 : 5, + data: String.fromCharCode(97 + i).repeat(65536), + })), + end: true, + }, + }, + { + title: "stdio values: 'readable' on an immediately ended pipe", + fixture: "stdio-readable.mjs", + script: { writes: [], end: true }, + }, + { + title: "for-await: process.stdin yields Buffers", + fixture: "stdin-for-await-buffers.mjs", + script: { + writes: [ + { delayMs: 0, data: "#one\nabcd\n" }, + { delayMs: 40, data: "#two\nefgh\n" }, + ], + end: true, + }, + }, { title: "read-all: one instant write, then EOF", fixture: "stdin-read-all.ts",