mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Search TypedArrays, read stdin through 'readable', and assign constructor-function globals
- TypedArray indexOf, lastIndexOf and includes on every element kind, typed
and checked-dynamic: strict equality (SameValueZero for includes) and
fromIndex as ToIntegerOrInfinity. A Buffer reached through a Uint8Array
value keeps Buffer's search.
- process.stdin 'readable' with read() and read(size), including sizes
above the read-ahead mark and input a once('readable') listener leaves
for a later 'data' listener. for await over process.stdin yields Buffers.
- Module bindings holding `new F()` of a constructor function can be read
and reassigned from other functions.
This commit is contained in:
@@ -936,6 +936,22 @@ export function emitBytesIntrinsic(
|
||||
B.line(`${t} = fcmp one double ${idx.name}, ${f64Lit(-1)}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "indexOfElem":
|
||||
case "lastIndexOfElem":
|
||||
case "includesElem": {
|
||||
const includes = method === "includesElem";
|
||||
const idx = call(
|
||||
"scr_bytes_ta_index_of",
|
||||
"double (ptr, double, double, i1 zeroext, i1 zeroext, i1 zeroext)",
|
||||
`ptr ${r.name}, double ${args[0]!.name}, double ${args[1]?.name ?? f64Lit(0)}, i1 ${args.length > 1}, i1 ${method !== "lastIndexOfElem"}, i1 ${includes}`,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
if (!includes) return idx;
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = fcmp one double ${idx.name}, ${f64Lit(-1)}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "fillElem":
|
||||
// Per-element TypedArray fill (non-u8): slice-style index
|
||||
// defaults, never throws; the receiver comes back +1.
|
||||
|
||||
@@ -907,9 +907,10 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
* (shared view), toReversed, copyWithin, set(src,
|
||||
* offset?), and the u8-only Buffer surface: toString(enc?) plus the
|
||||
* whole numeric read/write family (fixed widths BE/LE and the
|
||||
* variable-width read/writeUIntLE quartet — BUF_NUM_METHODS). Everything
|
||||
* else the lib declares (fill, indexOf, reverse, ...) falls through to
|
||||
* the SC2020 member fence. Null when this isn't a bytes method call. */
|
||||
* variable-width read/writeUIntLE quartet — BUF_NUM_METHODS), and
|
||||
* TypedArray fill, indexOf, lastIndexOf and includes. Everything else the
|
||||
* lib declares (reverse, sort, ...) falls through to the SC2020 member
|
||||
* fence. Null when this isn't a bytes method call. */
|
||||
export function lowerBytesMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -1101,6 +1102,43 @@ export function lowerBytesMethodCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
(name === "indexOf" || name === "lastIndexOf" || name === "includes") &&
|
||||
!(() => {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
symbol.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration.parent) &&
|
||||
declaration.parent.name.text === "Buffer",
|
||||
)
|
||||
);
|
||||
})()
|
||||
) {
|
||||
// TypedArray search: strict equality (includes: SameValueZero) with a
|
||||
// relative fromIndex. Buffer's same-named methods keep their byte,
|
||||
// string and Buffer needles.
|
||||
if (nArgs < 1 || nArgs > 2 || call.arguments.nodes.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(`.${name} with ${nArgs} arguments on typed arrays`, call);
|
||||
}
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr);
|
||||
const args = call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, F64));
|
||||
const method =
|
||||
name === "indexOf"
|
||||
? "indexOfElem"
|
||||
: name === "lastIndexOf"
|
||||
? "lastIndexOfElem"
|
||||
: "includesElem";
|
||||
return {
|
||||
kind: "bytesIntrinsic",
|
||||
method,
|
||||
receiver,
|
||||
args,
|
||||
type: name === "includes" ? BOOL : F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "set") {
|
||||
if (nArgs < 1 || nArgs > 2) {
|
||||
lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call);
|
||||
@@ -1210,9 +1248,8 @@ export function lowerBytesMethodCall(
|
||||
// The Buffer-declared comparison/search/mutation surface. All of
|
||||
// these resolve against the Buffer interface (the checker keeps most
|
||||
// off plain typed arrays; where the lib DOES declare a same-named
|
||||
// TypedArray member — indexOf, includes, fill — the semantics differ,
|
||||
// so only Buffer-declared resolutions lower and u8 receivers gate the
|
||||
// rest).
|
||||
// TypedArray member — indexOf, includes, fill — the semantics differ:
|
||||
// those resolutions lowered above, and u8 receivers gate the rest).
|
||||
const declOnBuffer = (() => {
|
||||
const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
return (
|
||||
|
||||
@@ -2361,7 +2361,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) &&
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2484,6 +2484,13 @@ export type IrBytesIntrinsicMethod =
|
||||
* (same observable number-fill result; Buffer's throwing offset
|
||||
* validation). */
|
||||
| "fillElem"
|
||||
/** TypedArray.prototype.indexOf / lastIndexOf / includes on any
|
||||
* receiver: [f64 searchElement, f64 fromIndex?] → f64 index (-1 when
|
||||
* absent) or bool; strict equality (includes: SameValueZero), a present
|
||||
* fromIndex counts from the end when negative, never throws. */
|
||||
| "indexOfElem"
|
||||
| "lastIndexOfElem"
|
||||
| "includesElem"
|
||||
| "copy"
|
||||
| "swap16"
|
||||
| "swap32"
|
||||
|
||||
@@ -2196,6 +2196,10 @@ function validateFunction(
|
||||
fillNum: { argTypes: [F64, F64, F64], minArgs: 1, result: BYTES_U8 },
|
||||
// Per-element TypedArray fill (any elem — the non-u8 fill path).
|
||||
fillElem: { argTypes: [F64, F64, F64], minArgs: 1, result: bytesOf(recv.elem) },
|
||||
// TypedArray search (any elem).
|
||||
indexOfElem: { argTypes: [F64, F64], minArgs: 1, result: F64 },
|
||||
lastIndexOfElem: { argTypes: [F64, F64], minArgs: 1, result: F64 },
|
||||
includesElem: { argTypes: [F64, F64], minArgs: 1, result: BOOL },
|
||||
fillStr: { argTypes: [STRING, STRING, F64, F64], minArgs: 2, result: BYTES_U8 },
|
||||
copy: { argTypes: [BYTES_U8, F64, F64, F64], minArgs: 1, result: F64 },
|
||||
swap16: { argTypes: [], minArgs: 0, result: BYTES_U8 },
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7680-typed-array-search.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7681-typed-array-search-dynamic.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7682-constructor-function-globals.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -3300,6 +3300,50 @@ double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd)
|
||||
return scr_bytes_index_of(b, &needle, off, 1, fwd);
|
||||
}
|
||||
|
||||
/* TypedArray.prototype.indexOf and lastIndexOf (strict equality, so NaN
|
||||
* never matches) and includes (`same_value_zero`: NaN matches NaN). `from`
|
||||
* is the fromIndex argument when `has_from` (ToIntegerOrInfinity, negative
|
||||
* counts from the end), else the default: 0, or len - 1 searching
|
||||
* backwards. Returns the element index, or -1. Never throws. */
|
||||
double scr_bytes_ta_index_of(const ScrBytes *b, double v, double from, bool has_from, bool forward,
|
||||
bool same_value_zero) {
|
||||
// A Buffer viewed as a Uint8Array keeps Buffer's own search methods, whose
|
||||
// byte needle and byteOffset rules differ (an absent offset is NaN there).
|
||||
if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, v, has_from ? from : NAN, forward);
|
||||
size_t len = b->len;
|
||||
if (len == 0) return -1;
|
||||
double k = has_from ? (from != from ? 0 : trunc(from)) : forward ? 0 : (double)len - 1;
|
||||
bool nan = v != v;
|
||||
if (nan && !same_value_zero) return -1;
|
||||
bool bytes = (b->elem == SCR_BYTES_U8 || b->elem == SCR_BYTES_U8C) && !b->shared;
|
||||
if (forward) {
|
||||
if (k >= (double)len) return -1;
|
||||
if (k < 0) k = k + (double)len < 0 ? 0 : k + (double)len;
|
||||
size_t start = (size_t)k;
|
||||
if (bytes) {
|
||||
if (!(v >= 0 && v <= 255 && v == trunc(v))) return -1;
|
||||
const uint8_t *hit = memchr(b->data + start, (int)v, len - start);
|
||||
return hit ? (double)(hit - b->data) : -1;
|
||||
}
|
||||
for (size_t i = start; i < len; i++) {
|
||||
double e = scr_bytes_load(b, i);
|
||||
if (e == v || (nan && e != e)) return (double)i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (k < 0) {
|
||||
k += (double)len;
|
||||
if (k < 0) return -1;
|
||||
} else if (k > (double)len - 1) {
|
||||
k = (double)len - 1;
|
||||
}
|
||||
for (size_t i = (size_t)k + 1; i-- > 0;) {
|
||||
double e = scr_bytes_load(b, i);
|
||||
if (e == v || (nan && e != e)) return (double)i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* fill's shared core over a normalized byte pattern. nargs counts the
|
||||
* present offset/end args. An EMPTY pattern zero-fills when zero_ok
|
||||
* (Node's '' string) and is the constant TypeError otherwise (an empty
|
||||
|
||||
@@ -499,6 +499,8 @@ ScrEmitter *scr_emitter_set_max_chk(ScrEmitter *em, const ScrDyn *n);
|
||||
void scr_emitter_set_default_max_chk(const ScrDyn *n, const ScrStr *name);
|
||||
double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd);
|
||||
double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd);
|
||||
double scr_bytes_ta_index_of(const ScrBytes *b, double v, double from, bool has_from, bool forward,
|
||||
bool same_value_zero);
|
||||
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);
|
||||
ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
|
||||
|
||||
@@ -1704,6 +1704,21 @@ static ScrDyn *scr_dyn_invoke_impl(
|
||||
scr_str_release(joined);
|
||||
return out;
|
||||
}
|
||||
if (!recv->buffer &&
|
||||
((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || (mid == DYN_M_includes))) {
|
||||
/* TypedArray search: strict equality (includes: SameValueZero), so a
|
||||
* needle that is not a number matches nothing; fromIndex counts as
|
||||
* given whenever it is passed, undefined included. */
|
||||
double from = 0;
|
||||
if (argc > 1) {
|
||||
if (!scr_dyn_number_coerce_js(args[1], &from)) return NULL;
|
||||
}
|
||||
bool includes = mid == DYN_M_includes;
|
||||
double found = argc > 0 && args[0]->kind == SCR_DYN_NUM
|
||||
? scr_bytes_ta_index_of(bytes, args[0]->v.num, from, argc > 1, mid != DYN_M_lastIndexOf, includes)
|
||||
: -1;
|
||||
return includes ? scr_dyn_new_bool(found >= 0) : scr_dyn_new_num(found);
|
||||
}
|
||||
if (dyn_bytes_proto_real(mid)) {
|
||||
dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method);
|
||||
return NULL;
|
||||
|
||||
@@ -390,6 +390,13 @@ static SCR_TL bool scr_stdin_resumed = false;
|
||||
static SCR_TL bool scr_stdin_read_eof = false;
|
||||
static SCR_TL ScrBytes *scr_stdin_buffer;
|
||||
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;
|
||||
|
||||
#ifdef SCR_STDIO_EXTENDED
|
||||
/* process.stdin.setEncoding(enc): the canonical encoding name (NULL until
|
||||
@@ -475,6 +482,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_buffer ? scr_stdin_buffer->len : 0;
|
||||
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_buffer ? scr_stdin_buffer->len : 0;
|
||||
/* 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_buffer && scr_stdin_buffer->len) 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_buffer) 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);
|
||||
@@ -528,6 +607,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);
|
||||
@@ -627,7 +710,8 @@ 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_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();
|
||||
if (scr_stdin_buffer) {
|
||||
size_t before = scr_stdin_buffer->len;
|
||||
ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + chunk->len));
|
||||
@@ -645,7 +729,11 @@ 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) {
|
||||
/* 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_buffer &&
|
||||
(scr_stdin_ndata > 0 || scr_stdin_resumed || scr_stdin_waiter != NULL)) {
|
||||
ScrBytes *chunk = scr_stdin_buffer;
|
||||
scr_stdin_buffer = NULL;
|
||||
scr_stdin_deliver(chunk);
|
||||
@@ -653,7 +741,7 @@ static void scr_stdin_service(void) {
|
||||
return;
|
||||
}
|
||||
if (scr_stdin_read_eof) {
|
||||
if (!scr_stdin_buffer) scr_stdin_finish(true);
|
||||
if (!scr_stdin_buffer && !scr_stdin_nreadable) scr_stdin_finish(true);
|
||||
return;
|
||||
}
|
||||
if (scr_stdin_fed) {
|
||||
@@ -662,10 +750,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 +812,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 +886,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 +964,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);
|
||||
@@ -985,6 +1072,7 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) {
|
||||
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);
|
||||
}
|
||||
@@ -995,6 +1083,7 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) {
|
||||
scr_bytes_release(scr_stdin_buffer);
|
||||
scr_stdin_buffer = rest;
|
||||
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 +1239,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 +1266,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 +1279,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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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])));
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
@@ -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('#'));
|
||||
@@ -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(',')));
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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(','));
|
||||
});
|
||||
+19
@@ -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);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2204, "san": 3060 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1649, "san": 958 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1623, "san": 962 }
|
||||
@@ -34,6 +34,55 @@ 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' 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",
|
||||
|
||||
Reference in New Issue
Block a user