@scriptc/runtime 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scriptc/runtime",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "scriptc native runtime — C sources, compiled into every scriptc binary",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://scriptc.dev",
package/src/scr_async.c CHANGED
@@ -591,7 +591,8 @@ ScrArr *scr_active_resources(void) {
591
591
  * left queued on the uncaught paths) never run — the teardown releases
592
592
  * them, like Node dropping the queue at exit. */
593
593
  typedef struct ScrNtick {
594
- ScrClosure *cb; /* owned */
594
+ ScrClosure *cb; /* owned; NULL for a raw C-hook entry */
595
+ void (*raw)(void); /* the hook when cb == NULL (stream tick markers) */
595
596
  struct ScrNtick *next;
596
597
  } ScrNtick;
597
598
 
@@ -612,11 +613,25 @@ void scr_next_tick(ScrClosure *cb /*moves*/) {
612
613
  scr_nt_tail = t;
613
614
  }
614
615
 
616
+ /* A RAW C-hook tick: the stream unit enqueues one marker per deferred
617
+ * stream emission so those emissions interleave with user nextTicks in
618
+ * true FIFO order — in Node they ARE nextTicks (resume_, emitReadable_,
619
+ * endReadableNT, ...). The hook dispatches exactly one stream tick;
620
+ * teardown just drops markers (the stream queue owns its entries). */
621
+ void scr_next_tick_raw(void (*fn)(void)) {
622
+ ScrNtick *t = calloc(1, sizeof *t);
623
+ if (!t) scr_oom();
624
+ t->raw = fn;
625
+ if (scr_nt_tail) scr_nt_tail->next = t;
626
+ else scr_nt_head = t;
627
+ scr_nt_tail = t;
628
+ }
629
+
615
630
  void scr_nticks_teardown(void) {
616
631
  while (scr_nt_head != NULL) {
617
632
  ScrNtick *t = scr_nt_head;
618
633
  scr_nt_head = t->next;
619
- scr_closure_release(t->cb);
634
+ if (t->cb) scr_closure_release(t->cb);
620
635
  free(t);
621
636
  }
622
637
  scr_nt_tail = NULL;
@@ -1677,9 +1692,14 @@ void scr_loop_run(void) {
1677
1692
  scr_nt_head = t->next;
1678
1693
  if (scr_nt_head == NULL) scr_nt_tail = NULL;
1679
1694
  ScrClosure *cb = t->cb;
1695
+ void (*raw)(void) = t->raw;
1680
1696
  free(t);
1681
- ((void (*)(ScrClosure *))cb->fn)(cb);
1682
- scr_closure_release(cb);
1697
+ if (cb) {
1698
+ ((void (*)(ScrClosure *))cb->fn)(cb);
1699
+ scr_closure_release(cb);
1700
+ } else {
1701
+ raw(); /* one stream tick, FIFO with the user ticks around it */
1702
+ }
1683
1703
  if (scr_exc_pending()) return;
1684
1704
  }
1685
1705
  }
package/src/scr_bytes.c CHANGED
@@ -1,6 +1,8 @@
1
1
  /* Typed arrays / Buffer: ONE runtime representation (ScrBytes — see the
2
- * header contract). Element buffers own their storage outright: no views,
3
- * no byteOffset, slice/subarray both copy. Coercions are JS-exact
2
+ * header contract). An ScrBytes either OWNS its storage or is a VIEW into
3
+ * an owner's (backing set, chain depth exactly 1): DataView, subarray(),
4
+ * and Buffer's slice() all alias JS-exactly; only the plain typed arrays'
5
+ * slice() copies. Coercions are JS-exact
4
6
  * (ToUint8/ToUint32 modular truncation, double→float rounding); the
5
7
  * encoding conversions (utf8 with WHATWG replacement, hex, base64) match
6
8
  * Node byte-for-byte — the differential corpus holds them to it. */
@@ -164,7 +166,7 @@ void scr_bytes_set(ScrBytes *b, double i, double v) {
164
166
  }
165
167
  }
166
168
 
167
- /* ── slice / subarray (both COPY) ──────────────────────────────────────── */
169
+ /* ── slice (copy) / subarray (view) ────────────────────────────────────── */
168
170
 
169
171
  /* Relative index: ToIntegerOrInfinity, negatives from the end, clamped. */
170
172
  static size_t scr_bytes_rel_index(double i, size_t len) {
@@ -186,6 +188,41 @@ ScrBytes *scr_bytes_slice(const ScrBytes *b, double start, double end) {
186
188
  return out;
187
189
  }
188
190
 
191
+ /* TypedArray.prototype.fill on non-u8 receivers: per-ELEMENT fill with
192
+ * the element write's JS-exact coercion (ToUint32/ToInt32 wrap, f32
193
+ * rounding), slice-clamped relative indices; answers the receiver +1
194
+ * (chaining). Never throws — Buffer's throwing fill family is separate. */
195
+ ScrBytes *scr_bytes_fill_elem(ScrBytes *b, double v, double start, double end) {
196
+ size_t s = scr_bytes_rel_index(start, b->len);
197
+ size_t e = scr_bytes_rel_index(end, b->len);
198
+ for (size_t i = s; i < e; i++) scr_bytes_set(b, (double)i, v);
199
+ return scr_bytes_retain(b);
200
+ }
201
+
202
+ /* subarray(start, end): a same-elem VIEW over the receiver's storage —
203
+ * TypedArray.prototype.subarray and Buffer's slice()/subarray() all alias
204
+ * in JS (mutations are visible both ways; buffer-swap through a slice is
205
+ * the canonical Node use). Chain depth stays exactly 1: a subarray of a
206
+ * view retains the OWNER, offsets composed here (the DataView rule).
207
+ * Relative indices clamp like slice; never throws. */
208
+ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) {
209
+ size_t s = scr_bytes_rel_index(start, b->len);
210
+ size_t e = scr_bytes_rel_index(end, b->len);
211
+ size_t count = e > s ? e - s : 0;
212
+ ScrBytes *owner = b->backing ? b->backing : b;
213
+ ScrBytes *v = malloc(sizeof(ScrBytes));
214
+ if (!v) scr_bytes_oom();
215
+ v->rc = 1;
216
+ v->len = count;
217
+ v->elem = b->elem;
218
+ v->data = b->data + s * scr_bytes_elem_size(b->elem);
219
+ v->backing = scr_bytes_retain(owner);
220
+ #ifdef SCR_RC_AUDIT
221
+ scr_live_bytes++;
222
+ #endif
223
+ return v;
224
+ }
225
+
189
226
  void scr_bytes_set_from(ScrBytes *dst, const ScrBytes *src, double offset) {
190
227
  double t = (offset != offset) ? 0 : trunc(offset);
191
228
  if (!(t >= 0) || (double)src->len + t > (double)dst->len) {
@@ -85,6 +85,12 @@ typedef struct ScrEeBucket {
85
85
  struct ScrEeReg {
86
86
  ScrEeBucket *head; /* singly linked, first-registration order */
87
87
  double max; /* -1 = unset (follows the default), 0 = unlimited */
88
+ /* Node's kShapeMode: set when names were PRE-CREATED (the stream
89
+ * constructors). In shape mode removeListener KEEPS an emptied name's
90
+ * rank (Node writes events[type] = undefined instead of deleting) —
91
+ * only removeAllListeners deletes: the named form drops the one key,
92
+ * the no-argument wipe resets everything and leaves shape mode. */
93
+ bool shape;
88
94
  };
89
95
 
90
96
  /* The vtable of BARE `new EventEmitter()` instances; the emitted main()
@@ -145,6 +151,21 @@ static ScrEeBucket *scr_ee_bucket_ensure(ScrEeReg *reg, ScrStr *name) {
145
151
  return b;
146
152
  }
147
153
 
154
+ /* Reserve a name's first-registration RANK without any listener: Node's
155
+ * stream classes pre-create their known _events keys (a V8 shape
156
+ * optimization eventNames() order observes — 'error'/'data'/... list
157
+ * before user events added earlier). An empty bucket is ABSENT for every
158
+ * read (n == 0 everywhere); the first add fills it in place, and a bucket
159
+ * emptied by removal still drops — Node deleting the key. */
160
+ void scr_emitter_reserve(ScrEmitter *em, const char *name) {
161
+ ScrEeReg *reg = scr_ee_reg_ensure(em);
162
+ reg->shape = true;
163
+ if (scr_ee_bucket_find(reg, name, strlen(name))) return;
164
+ ScrStr *n = scr_str_new(name, strlen(name));
165
+ scr_ee_bucket_ensure(reg, n);
166
+ scr_str_release(n);
167
+ }
168
+
148
169
  /* Drops an emptied bucket from the list (Node deletes the _events key; a
149
170
  * later add re-appends at the end). */
150
171
  static void scr_ee_bucket_drop(ScrEeReg *reg, ScrEeBucket *b) {
@@ -402,13 +423,14 @@ void scr_ee_inv_fixed4(ScrClosure *cb, va_list ap) {
402
423
 
403
424
  /* Removes one entry (by index) from a bucket's live list and fires the
404
425
  * 'removeListener' meta event AFTER the removal, Node's order. Drops the
405
- * bucket when emptied. */
426
+ * bucket when emptied — except in shape mode, where the emptied name
427
+ * keeps its rank (Node's events[type] = undefined). */
406
428
  static void scr_ee_remove_at(ScrEmitter *em, ScrEeBucket *b, size_t i) {
407
429
  ScrEeEntry *e = b->ls[i];
408
430
  memmove(b->ls + i, b->ls + i + 1, (b->n - i - 1) * sizeof *b->ls);
409
431
  b->n--;
410
432
  ScrStr *name = scr_str_retain(b->name);
411
- if (b->n == 0) scr_ee_bucket_drop(em->reg, b);
433
+ if (b->n == 0 && !em->reg->shape) scr_ee_bucket_drop(em->reg, b);
412
434
  scr_ee_entry_unref(e);
413
435
  scr_ee_emit_meta(em, "removeListener", name);
414
436
  scr_str_release(name);
@@ -456,6 +478,13 @@ void scr_emitter_check_listener(const ScrDyn *cb) {
456
478
  * firing the meta event — and the whole-emitter form does every other
457
479
  * event first (bucket order), 'removeListener' itself LAST. Returns em +1. */
458
480
  static void scr_ee_remove_all_named(ScrEmitter *em, ScrEeBucket *b, bool meta) {
481
+ if (b->n == 0) {
482
+ /* An empty rank-holding bucket reaches here only from the WIPE scan
483
+ * (the named form skips undefined keys — Node no-ops and the rank
484
+ * stays): drop it, the wipe resets _events wholesale. */
485
+ scr_ee_bucket_drop(em->reg, b);
486
+ return;
487
+ }
459
488
  if (!meta) {
460
489
  for (size_t i = 0; i < b->n; i++) scr_ee_entry_unref(b->ls[i]);
461
490
  b->n = 0;
@@ -497,7 +526,26 @@ ScrEmitter *scr_emitter_remove_all(ScrEmitter *em, ScrStr *name, bool all) {
497
526
  bool meta = scr_ee_has(reg, "removeListener");
498
527
  if (!all) {
499
528
  ScrEeBucket *b = scr_ee_bucket_find(reg, name->data, name->len);
500
- if (b) scr_ee_remove_all_named(em, b, meta);
529
+ /* A rank-holding empty bucket is Node's `events[type] === undefined`:
530
+ * the named form no-ops there (the rank survives). A POPULATED name
531
+ * deletes its key even in shape mode — but through the meta path each
532
+ * listener leaves via removeListener, whose shape rule KEEPS the
533
+ * emptied rank (Node's exact split). */
534
+ if (b && b->n > 0) {
535
+ scr_ee_remove_all_named(em, b, meta);
536
+ if (!meta) {
537
+ /* Node's non-meta named branch: the count hitting zero replaces
538
+ * _events wholesale — every rank (pre-created included) goes,
539
+ * though kShapeMode itself stays set (Node's exact quirk). */
540
+ bool any_live = false;
541
+ for (ScrEeBucket *w = reg->head; w; w = w->next) {
542
+ if (w->n > 0) { any_live = true; break; }
543
+ }
544
+ if (!any_live) {
545
+ while (reg->head) scr_ee_bucket_drop(reg, reg->head);
546
+ }
547
+ }
548
+ }
501
549
  return scr_emitter_retain(em);
502
550
  }
503
551
  /* Every event except 'removeListener' first, in bucket order; then
@@ -515,6 +563,10 @@ ScrEmitter *scr_emitter_remove_all(ScrEmitter *em, ScrStr *name, bool all) {
515
563
  }
516
564
  ScrEeBucket *rl = scr_ee_bucket_find(reg, "removeListener", 14);
517
565
  if (rl) scr_ee_remove_all_named(em, rl, meta);
566
+ /* The wipe replaces _events wholesale in Node — pre-created ranks are
567
+ * gone and the emitter leaves shape mode. */
568
+ while (reg->head) scr_ee_bucket_drop(reg, reg->head);
569
+ reg->shape = false;
518
570
  return scr_emitter_retain(em);
519
571
  }
520
572
 
@@ -633,12 +685,13 @@ double scr_emitter_listener_count_fn(ScrEmitter *em, ScrStr *name, ScrClosure *f
633
685
  }
634
686
 
635
687
  /* eventNames(): +1 string[] of the bucket names in first-registration
636
- * order (exactly Node's _events key order). */
688
+ * order (exactly Node's _events key order). Reserved-but-empty buckets
689
+ * (the stream pre-created keys) are invisible until a listener lands. */
637
690
  ScrArr *scr_emitter_event_names(ScrEmitter *em) {
638
691
  ScrArr *out = scr_arr_new(SCR_ELEM_STR, 0);
639
692
  if (em->reg) {
640
693
  for (ScrEeBucket *b = em->reg->head; b; b = b->next) {
641
- scr_arr_push_ref(out, scr_str_retain(b->name));
694
+ if (b->n > 0) scr_arr_push_ref(out, scr_str_retain(b->name));
642
695
  }
643
696
  }
644
697
  return out;
package/src/scr_library.c CHANGED
@@ -251,6 +251,21 @@ ScrBytes *scr_library_bytes_in(const uint8_t *p, size_t len, const char *trap_ms
251
251
  return b;
252
252
  }
253
253
 
254
+ double scr_library_i64_in(int64_t v, const char *trap_msg) {
255
+ /* The inbound declared-integer edge (ask 4): only |v| <= 2^53-1 rides
256
+ * f64 exactly; past it, (double)v silently rounds — a coercion the
257
+ * author never wrote, so the wrapper funnels the host-contract
258
+ * violation instead (same story as an impossible bytes length; the
259
+ * message is the compiler-assembled structured trap-teaching form). */
260
+ if (v > 9007199254740991LL || v < -9007199254740991LL) scr_trap(trap_msg);
261
+ return (double)v;
262
+ }
263
+
264
+ double scr_library_u64_in(uint64_t v, const char *trap_msg) {
265
+ if (v > 9007199254740991ULL) scr_trap(trap_msg);
266
+ return (double)v;
267
+ }
268
+
254
269
  void scr_library_str_out(ScrStr *s, const uint8_t **out, size_t *out_len) {
255
270
  scr_library_arena_keep(s, true);
256
271
  *out = (const uint8_t *)s->data; /* NUL-terminated after len (ScrStr layout) */
package/src/scr_regex.c CHANGED
@@ -484,13 +484,20 @@ ScrArr *scr_regex_match_all_into(ScrStr *s, ScrRegex *re, ScrArr *indices) {
484
484
  * replacements are frontend-fenced): $$, $&, $` and $', $1..$99 (two digits
485
485
  * win when in range — the refined ES2019 rule quickjs implements), with
486
486
  * out-of-range references left literal and unmatched groups substituting
487
- * empty. $<name> stays literal: named capture groups are frontend-fenced,
488
- * so namedCaptures is always undefined here (the spec's literal case).
489
- * The template is scanned byte-wise: every '$' directive is ASCII, and
490
- * UTF-8 continuation bytes can never alias it. */
487
+ * empty. $<name> resolves against the pattern's named capture groups
488
+ * (lre_get_groupnames one NUL-terminated name per capture, "" for
489
+ * unnamed): the first PARTICIPATING capture with that name substitutes
490
+ * (ES2025 duplicates live in distinct alternatives, so at most one
491
+ * participates); a nonexistent or nonparticipating name substitutes
492
+ * empty (the spec's Get→undefined→"" path, Node-exact). When the pattern
493
+ * has NO named groups, namedCaptures is undefined and '$<' stays literal
494
+ * — also Node's unterminated-'$<name' answer either way. The template is
495
+ * scanned byte-wise: every '$' directive is ASCII, and UTF-8 continuation
496
+ * bytes can never alias it. */
491
497
  static void scr_put_substitution(ScrJsonBuf *b, const uint16_t *u, int len,
492
498
  int start, int end, uint8_t **capture,
493
- int capture_count, const ScrStr *rep) {
499
+ int capture_count, const char *groupnames,
500
+ const ScrStr *rep) {
494
501
  const uint8_t *ubase = (const uint8_t *)u;
495
502
  size_t i = 0;
496
503
  while (i < rep->len) {
@@ -534,6 +541,31 @@ static void scr_put_substitution(ScrJsonBuf *b, const uint16_t *u, int len,
534
541
  scr_jb_putc(b, '$');
535
542
  i++;
536
543
  }
544
+ } else if (c1 == '<' && groupnames != NULL) {
545
+ /* $<name>: scan for '>'; absent, the '$' is literal (the rest
546
+ * re-scans verbatim — GetSubstitution's not-found answer). */
547
+ size_t gt = i + 2;
548
+ while (gt < rep->len && rep->data[gt] != '>') gt++;
549
+ if (gt >= rep->len) {
550
+ scr_jb_putc(b, '$');
551
+ i++;
552
+ continue;
553
+ }
554
+ const char *name = rep->data + i + 2;
555
+ size_t name_len = gt - (i + 2);
556
+ const char *p = groupnames; /* capture_count-1 entries: name NUL scope */
557
+ for (int k = 1; k < capture_count; k++) {
558
+ size_t glen = strlen(p);
559
+ if (glen == name_len && memcmp(p, name, name_len) == 0) {
560
+ const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1];
561
+ if (cs && ce) {
562
+ scr_jb_put_utf16(b, u, (int)((cs - ubase) >> 1), (int)((ce - ubase) >> 1));
563
+ break; /* at most one duplicate participates */
564
+ }
565
+ }
566
+ p += glen + LRE_GROUP_NAME_TRAILER_LEN;
567
+ }
568
+ i = gt + 1;
537
569
  } else {
538
570
  scr_jb_putc(b, '$');
539
571
  i++;
@@ -550,6 +582,7 @@ static ScrStr *scr_replace_impl(ScrStr *s, ScrRegex *re, ScrStr *rep) {
550
582
  bool global = (re_flags & LRE_FLAG_GLOBAL) != 0;
551
583
  bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0;
552
584
  int capture_count = lre_get_capture_count(bc);
585
+ const char *groupnames = lre_get_groupnames(bc);
553
586
  int len;
554
587
  uint16_t *u = scr_to_utf16(s, &len);
555
588
  uint8_t **capture = scr_capture_alloc(bc);
@@ -562,7 +595,7 @@ static ScrStr *scr_replace_impl(ScrStr *s, ScrRegex *re, ScrStr *rep) {
562
595
  int start = (int)((capture[0] - ubase) >> 1);
563
596
  int end = (int)((capture[1] - ubase) >> 1);
564
597
  scr_jb_put_utf16(&b, u, next, start);
565
- scr_put_substitution(&b, u, len, start, end, capture, capture_count, rep);
598
+ scr_put_substitution(&b, u, len, start, end, capture, capture_count, groupnames, rep);
566
599
  next = end;
567
600
  if (!global) break;
568
601
  pos = end == start ? scr_advance(u, len, end, unicode) : end;
package/src/scr_runtime.h CHANGED
@@ -163,6 +163,12 @@ ScrStr *scr_library_str_in(const uint8_t *p, size_t len); /* +1 */
163
163
  * (structured trap-teaching bytes naming this entry's symbol), delivered
164
164
  * through the funnel when len falls outside the marshalling class. */
165
165
  ScrBytes *scr_library_bytes_in(const uint8_t *p, size_t len, const char *trap_msg); /* +1, u8 */
166
+ /* The inbound declared-integer edge (ask 4's i64/u64 parameter classes):
167
+ * exact conversion for |v| <= 2^53-1, the host-contract trap (same
168
+ * assembled SC4012 message shape as the bytes trap) past it — silent
169
+ * rounding is a coercion the author never wrote. */
170
+ double scr_library_i64_in(int64_t v, const char *trap_msg);
171
+ double scr_library_u64_in(uint64_t v, const char *trap_msg);
166
172
  void scr_library_str_out(ScrStr *s, const uint8_t **out, size_t *out_len);
167
173
  void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len);
168
174
 
@@ -1362,6 +1368,10 @@ bool scr_emitter_emit_error(ScrEmitter *em, ScrStr *name, ScrError *err);
1362
1368
  double scr_emitter_listener_count(ScrEmitter *em, ScrStr *name);
1363
1369
  double scr_emitter_listener_count_fn(ScrEmitter *em, ScrStr *name, ScrClosure *fn);
1364
1370
  ScrArr *scr_emitter_event_names(ScrEmitter *em); /* +1 string[] */
1371
+ /* Pre-create a name's eventNames() rank with no listener (Node's stream
1372
+ * classes pre-create their known _events keys; empty = absent for every
1373
+ * other read). The stream constructors call this. */
1374
+ void scr_emitter_reserve(ScrEmitter *em, const char *name);
1365
1375
  ScrArr *scr_emitter_listeners(ScrEmitter *em, ScrStr *name); /* +1 closures */
1366
1376
  ScrEmitter *scr_emitter_set_max(ScrEmitter *em, double n); /* returns em +1 */
1367
1377
  double scr_emitter_get_max(ScrEmitter *em);
@@ -1562,6 +1572,13 @@ ScrDyn *scr_stream_done_dyn_l(ScrClosure *clo, ScrDyn *const *args, size_t argc)
1562
1572
  * utf8, Node's decodeStrings default); push answers the below-hwm bool. */
1563
1573
  bool scr_stream_push(ScrStream *s, ScrBytes *chunk);
1564
1574
  bool scr_stream_push_str(ScrStream *s, ScrStr *str);
1575
+ /* push(chunk, enc): the per-call literal encoding (canonical); overrides
1576
+ * the stream's defaultEncoding. Borrows both. */
1577
+ bool scr_stream_push_str_enc(ScrStream *s, ScrStr *str, ScrStr *enc);
1578
+ /* The defaultEncoding option's push side: how push(string) decodes chunks
1579
+ * (Buffer.from(chunk, enc)). Canonical literal, never "utf8". Receiver
1580
+ * answers +1 (the setEncoding chaining shape). */
1581
+ ScrStream *scr_stream_set_push_encoding(ScrStream *s, ScrStr *enc);
1565
1582
  bool scr_stream_push_null(ScrStream *s);
1566
1583
  void scr_stream_unshift(ScrStream *s, ScrBytes *chunk);
1567
1584
  void scr_stream_unshift_str(ScrStream *s, ScrStr *str);
@@ -1591,6 +1608,18 @@ ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s);
1591
1608
  * Streams borrowed; +1 promises. */
1592
1609
  ScrPromise *scr_sp_finished(ScrStream *s);
1593
1610
  ScrPromise *scr_sp_pipeline(double n, ScrStream **streams);
1611
+ /* node:stream/consumers — the promise consumers over the readable
1612
+ * machinery: accumulate every chunk (string chunks as their utf8 bytes)
1613
+ * and settle at the terminal point (right after 'close', the eos timing
1614
+ * Node's consumers share) — text answers the utf8 decode, json parses
1615
+ * the text (malformed input rejects with the parse's SyntaxError; the
1616
+ * result is a +1 DOM tree), buffer the concatenated bytes. Stream
1617
+ * errors reject; an early close rejects ERR_STREAM_PREMATURE_CLOSE; a
1618
+ * stream with no readable side rejects Node's async-iterable TypeError.
1619
+ * Streams borrowed; +1 promises. */
1620
+ ScrPromise *scr_sc_text(ScrStream *s);
1621
+ ScrPromise *scr_sc_json(ScrStream *s);
1622
+ ScrPromise *scr_sc_buffer(ScrStream *s);
1594
1623
  /* Readable.from(array): +1 fully-seeded object-entry stream (one WHOLE
1595
1624
  * chunk per element — strings or Buffers per the flag; hwm 1, already
1596
1625
  * EOF'd). Borrows arr. */
@@ -3487,6 +3516,11 @@ bool scr_immediate_has_ref(double handle);
3487
3516
  * teardown (they must never run yet must not leak). cb ownership moves
3488
3517
  * in. */
3489
3518
  void scr_next_tick(ScrClosure *cb);
3519
+ /* A raw C-hook entry on the SAME queue: the stream unit enqueues one
3520
+ * marker per deferred stream emission, so stream ticks and user
3521
+ * nextTicks run in true FIFO order (in Node they are the same queue).
3522
+ * Teardown drops markers without running them. */
3523
+ void scr_next_tick_raw(void (*fn)(void));
3490
3524
  void scr_nticks_teardown(void);
3491
3525
  /* ── the events unit (scr_events.c — OPTIONAL, link-gated) ────────────
3492
3526
  * Process signal/exit events and the piped-stdin surface. The unit links
@@ -4207,13 +4241,15 @@ double scr_bit_not(double a);
4207
4241
  /* ── typed arrays / Buffer (scr_bytes.c) ──────────────────────────────
4208
4242
  * ONE runtime representation for Uint8Array/Uint32Array/Float32Array,
4209
4243
  * Node's Buffer (a Uint8Array subclass), and DataView: a refcounted,
4210
- * MUTABLE, fixed-length element buffer. Typed arrays OWN their storage
4211
- * (backing == NULL) subarray()/slice() both COPY (slice matches JS;
4212
- * subarray's sharing is a documented divergence, SEMANTICS.md) so a
4213
- * typed array never aliases another and its byteOffset is always 0. The
4214
- * ONE view kind is DataView (scr_dataview_new): a u8-elem ScrBytes whose
4215
- * `data` points INTO an owner's storage and whose `backing` retains that
4216
- * owner, so reads/writes through the view alias the source JS-exactly.
4244
+ * MUTABLE, fixed-length element buffer. An ScrBytes either OWNS its
4245
+ * storage (backing == NULL, byteOffset 0) or is a VIEW: its `data` points
4246
+ * INTO an owner's storage and its `backing` retains that owner (chain
4247
+ * depth is always exactly 1 views over views resolve to the owner at
4248
+ * construction), so reads/writes through the view alias the source
4249
+ * JS-exactly. Views come from DataView (scr_dataview_new) and from
4250
+ * subarray()/Buffer-slice() (scr_bytes_subarray Buffer's slice is
4251
+ * subarray's deprecated alias in Node); only the plain typed arrays'
4252
+ * slice() copies (scr_bytes_slice, matching JS).
4217
4253
  * Elements are scalars only and the backing edge is acyclic by
4218
4254
  * construction (owners point at nothing): never part of a cycle, no
4219
4255
  * trace. Element reads widen to double; writes coerce JS-exactly (ToUint8
@@ -4236,10 +4272,10 @@ typedef struct ScrBytes {
4236
4272
  size_t len; /* ELEMENT count, fixed at construction */
4237
4273
  ScrBytesElem elem;
4238
4274
  uint8_t *data; /* len * elem_size bytes; owned unless backing is set */
4239
- /* NULL for owners (every typed array/Buffer). A DataView sets this to
4240
- * the retained OWNER it aliases (chain depth is always exactly 1: views
4241
- * over a view's .buffer resolve to the owner at construction) and its
4242
- * `data` points into backing->data — released, never freed. */
4275
+ /* NULL for owners. A view (DataView, subarray, Buffer-slice) sets this
4276
+ * to the retained OWNER it aliases (chain depth is always exactly 1:
4277
+ * views over views resolve to the owner at construction) and its `data`
4278
+ * points into backing->data — released, never freed. */
4243
4279
  struct ScrBytes *backing;
4244
4280
  } ScrBytes;
4245
4281
 
@@ -4344,10 +4380,21 @@ void scr_bytes_set(ScrBytes *b, double i, double v);
4344
4380
 
4345
4381
  /* TypedArray.prototype.slice(start, end): relative indices clamp like
4346
4382
  * string/array slice (ToIntegerOrInfinity, negatives from the end); the
4347
- * result is a fresh same-kind copy. subarray() lowers here too — a COPY,
4348
- * the documented divergence. Never throws. */
4383
+ * result is a fresh same-kind copy. Never throws. */
4349
4384
  ScrBytes *scr_bytes_slice(const ScrBytes *b, double start, double end); /* +1 */
4350
4385
 
4386
+ /* TypedArray.prototype.fill on non-u8 receivers: per-element fill with
4387
+ * the element write's coercion, slice-clamped relative indices; answers
4388
+ * the receiver +1 (chaining). Never throws. */
4389
+ ScrBytes *scr_bytes_fill_elem(ScrBytes *b, double v, double start, double end); /* +1 */
4390
+
4391
+ /* TypedArray.prototype.subarray(start, end) — and Buffer's slice(), its
4392
+ * deprecated alias: a same-elem VIEW aliasing the receiver's storage
4393
+ * (mutations visible both ways, JS-exactly). The view retains the OWNER
4394
+ * (chain depth exactly 1, the DataView rule) and its byteOffset composes.
4395
+ * Same index clamping as slice; never throws. */
4396
+ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end); /* +1 */
4397
+
4351
4398
  /* dst.set(src, offset): same-kind bulk copy (memmove — dst may be src).
4352
4399
  * offset goes through ToIntegerOrInfinity; a negative offset or
4353
4400
  * src.len + offset > dst.len THROWS Node's "offset is out of bounds"
package/src/scr_stream.c CHANGED
@@ -13,11 +13,12 @@
13
13
  * nothing.
14
14
  *
15
15
  * EVENT TIMING. Node schedules most stream emissions on process.nextTick;
16
- * here those deferrals ride a FIFO tick queue drained at the top of every
17
- * loop turn (scr_loop_set_stream the station before events/net/timers,
18
- * the closest to nextTick; ticks scheduled from user code on the main
19
- * stack run when the loop starts, i.e. after all synchronous code — the
20
- * nextTick shape). The implemented orderings follow lib/internal/streams:
16
+ * here each deferral enqueues on the stream tick FIFO AND posts a raw
17
+ * marker on the USER nextTick queue (scr_next_tick_raw), so stream
18
+ * emissions and user nextTicks run in true FIFO enqueue order Node's,
19
+ * where they are the same queue. The scr_loop_set_stream station remains
20
+ * as the drain of anything a marker never reached (and the uncaught-throw
21
+ * cleanup). The implemented orderings follow lib/internal/streams:
21
22
  * - on('data') starts flowing on a TICK (resume_): synchronous code
22
23
  * after the registration runs before the first 'data'.
23
24
  * - push() while flowing with an empty buffer emits 'data'
@@ -103,6 +104,10 @@ struct ScrStreamState {
103
104
  bool encoded; /* string-chunk mode (decoder active) */
104
105
  ScrStr *enc; /* owned canonical encoding name, or NULL */
105
106
  double dec_pending; /* scr_strdec packed pending state */
107
+ /* The defaultEncoding option's push-side effect: how push(string)
108
+ * DECODES string chunks into bytes (Node's Buffer.from(chunk,
109
+ * state.defaultEncoding)). Owned canonical name; NULL = utf8. */
110
+ ScrStr *push_enc;
106
111
  /* Readable.from mode: entries are whole OBJECTS (each counts 1
107
112
  * toward length/hwm and delivers undivided — Node's objectMode
108
113
  * accounting for the from() surface; hwm is 1). */
@@ -223,6 +228,7 @@ static void scr_stream_state_drop(ScrStreamState *st, bool gc) {
223
228
  for (size_t i = 0; i < st->r.n; i++) scr_stream_entry_release(st, st->r.buf[i]);
224
229
  free(st->r.buf);
225
230
  if (st->r.enc) scr_str_release(st->r.enc);
231
+ if (st->r.push_enc) scr_str_release(st->r.push_enc);
226
232
  if (!gc && st->r.next_waiter) scr_promise_release(st->r.next_waiter);
227
233
  if (!gc) {
228
234
  if (st->r.read_cb) scr_closure_release(st->r.read_cb);
@@ -350,6 +356,8 @@ typedef struct ScrStreamTick {
350
356
  static ScrStreamTick *scr_st_head = NULL;
351
357
  static ScrStreamTick *scr_st_tail = NULL;
352
358
 
359
+ static void scr_stream_dispatch_one(void);
360
+
353
361
  static void scr_st_tick(ScrStream *s, ScrStreamTickOp op, ScrError *err /*moves*/,
354
362
  ScrClosure *cb /*moves*/) {
355
363
  ScrStreamTick *t = calloc(1, sizeof *t);
@@ -361,6 +369,13 @@ static void scr_st_tick(ScrStream *s, ScrStreamTickOp op, ScrError *err /*moves*
361
369
  if (scr_st_tail) scr_st_tail->next = t;
362
370
  else scr_st_head = t;
363
371
  scr_st_tail = t;
372
+ /* One marker per tick on the USER nextTick queue: stream emissions are
373
+ * process.nextTicks in Node (resume_, emitReadable_, endReadableNT,
374
+ * afterWrite, ...), so they must interleave with user nextTicks in
375
+ * enqueue order — a `push(); on('data'); process.nextTick(assert)`
376
+ * sequence sees its data before the assert runs. The station dispatch
377
+ * below stays as the drain of anything a marker never reached. */
378
+ scr_next_tick_raw(&scr_stream_dispatch_one);
364
379
  }
365
380
 
366
381
  static bool scr_stream_ticks_pending(void) { return scr_st_head != NULL; }
@@ -1222,6 +1237,24 @@ static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r,
1222
1237
  s->reg = NULL;
1223
1238
  s->cls = cls;
1224
1239
  s->st = scr_stream_state_new(has_r, has_w, rhwm, whwm, auto_destroy, emit_close, allow_half_open);
1240
+ /* Node's stream constructors pre-create their known _events keys (a V8
1241
+ * shape optimization) — eventNames() lists these BEFORE user events
1242
+ * added earlier. Same names, same order: Readable close/error/data/end/
1243
+ * readable, Writable close/error/prefinish/finish/drain, Duplex the
1244
+ * union (writable trio first — Node's observed key order). */
1245
+ ScrEmitter *em = (ScrEmitter *)s;
1246
+ scr_emitter_reserve(em, "close");
1247
+ scr_emitter_reserve(em, "error");
1248
+ if (has_w) {
1249
+ scr_emitter_reserve(em, "prefinish");
1250
+ scr_emitter_reserve(em, "finish");
1251
+ scr_emitter_reserve(em, "drain");
1252
+ }
1253
+ if (has_r) {
1254
+ scr_emitter_reserve(em, "data");
1255
+ scr_emitter_reserve(em, "end");
1256
+ scr_emitter_reserve(em, "readable");
1257
+ }
1225
1258
  scr_obj_alloc_note();
1226
1259
  return s;
1227
1260
  }
@@ -2193,6 +2226,133 @@ ScrPromise *scr_sp_pipeline(double n, ScrStream **streams) {
2193
2226
  return p;
2194
2227
  }
2195
2228
 
2229
+ /* ── node:stream/consumers ────────────────────────────────────────────
2230
+ * text/json/buffer over the readable machinery: a native 'data' listener
2231
+ * accumulates every chunk (Buffer chunks as-is; string chunks — an
2232
+ * encoded stream or Readable.from strings — as their utf8 bytes, the
2233
+ * Blob rule Node's consumers share for whole-stream accumulation), and
2234
+ * the finished watcher settles at the terminal point — the accumulated
2235
+ * result on a clean end (right after 'close', Node's own timing: the
2236
+ * consumer's async iterator completes through eos), the stream's error,
2237
+ * or ERR_STREAM_PREMATURE_CLOSE on an early close — and marks lifecycle
2238
+ * errors handled, exactly like the iterator's eos registration. Both
2239
+ * closures share the cap layout: caps[0] the pending promise, caps[1]
2240
+ * the chunk list (Buffer entries). */
2241
+
2242
+ enum { SCR_SC_TEXT = 0, SCR_SC_JSON = 1, SCR_SC_BUFFER = 2 };
2243
+
2244
+ static void scr_sc_settle_ok(ScrClosure *cb, int kind) {
2245
+ ScrPromise *p = scr_box_get_ref(cb->caps[0]); /* +1 */
2246
+ ScrArr *chunks = scr_box_get_ref(cb->caps[1]); /* +1 */
2247
+ ScrBytes *all = scr_bytes_concat(chunks);
2248
+ scr_arr_release(chunks);
2249
+ if (kind == SCR_SC_BUFFER) {
2250
+ scr_promise_fulfill_ref(p, all, scr_bytes_retain_v, scr_bytes_release_v, NULL);
2251
+ scr_promise_release(p);
2252
+ return;
2253
+ }
2254
+ ScrStr *enc = scr_str_new("utf8", 4);
2255
+ ScrStr *text = scr_bytes_to_str(all, enc); /* U+FFFD per invalid subpart */
2256
+ scr_str_release(enc);
2257
+ scr_bytes_release(all);
2258
+ if (kind == SCR_SC_TEXT) {
2259
+ scr_promise_fulfill_str(p, text); /* moves */
2260
+ } else {
2261
+ ScrDyn *doc = scr_json_parse(text);
2262
+ scr_str_release(text);
2263
+ if (doc == NULL) {
2264
+ /* the parse's SyntaxError rides the cell — the rejection, like
2265
+ * Node's json() rejecting with JSON.parse's throw */
2266
+ scr_promise_reject_pending(p);
2267
+ } else {
2268
+ scr_promise_fulfill_ref(p, doc, scr_dyn_retain_v, scr_dyn_release_v, NULL);
2269
+ }
2270
+ }
2271
+ scr_promise_release(p);
2272
+ }
2273
+
2274
+ /* The 'data' accumulation (the emit ABI's two payload slots — exactly
2275
+ * one non-NULL). */
2276
+ static void scr_sc_data(ScrClosure *cb, void *b, void *str) {
2277
+ ScrArr *chunks = scr_box_get_ref(cb->caps[1]); /* +1 */
2278
+ if (b != NULL) {
2279
+ scr_arr_push_ref(chunks, scr_bytes_retain((ScrBytes *)b));
2280
+ } else if (str != NULL) {
2281
+ ScrStr *enc = scr_str_new("utf8", 4);
2282
+ scr_arr_push_ref(chunks, scr_bytes_from_str((ScrStr *)str, enc));
2283
+ scr_str_release(enc);
2284
+ }
2285
+ scr_arr_release(chunks);
2286
+ }
2287
+
2288
+ static void scr_sc_fin(ScrClosure *cb, ScrError *err, int kind) {
2289
+ if (err != NULL) {
2290
+ ScrPromise *p = scr_box_get_ref(cb->caps[0]); /* +1 */
2291
+ scr_throw_obj(scr_error_retain(err), &scr_error_retain_v, &scr_error_release_v,
2292
+ scr_error_trace_arg());
2293
+ scr_promise_reject_pending(p);
2294
+ scr_promise_release(p);
2295
+ return;
2296
+ }
2297
+ scr_sc_settle_ok(cb, kind);
2298
+ }
2299
+
2300
+ static void scr_sc_fin_text(ScrClosure *cb, ScrStream *s, ScrError *err) {
2301
+ (void)s;
2302
+ scr_sc_fin(cb, err, SCR_SC_TEXT);
2303
+ }
2304
+ static void scr_sc_fin_json(ScrClosure *cb, ScrStream *s, ScrError *err) {
2305
+ (void)s;
2306
+ scr_sc_fin(cb, err, SCR_SC_JSON);
2307
+ }
2308
+ static void scr_sc_fin_buffer(ScrClosure *cb, ScrStream *s, ScrError *err) {
2309
+ (void)s;
2310
+ scr_sc_fin(cb, err, SCR_SC_BUFFER);
2311
+ }
2312
+
2313
+ static ScrClosure *scr_sc_closure(void *fn, ScrPromise *p, ScrArr *chunks) {
2314
+ ScrClosure *c = scr_closure_new(fn, 2);
2315
+ c->caps[0] = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, scr_promise_trace_v);
2316
+ scr_box_set_ref(c->caps[0], scr_promise_retain(p));
2317
+ c->caps[1] = scr_box_new_obj(scr_arr_retain_v, scr_arr_release_v, NULL);
2318
+ scr_box_set_ref(c->caps[1], scr_arr_retain(chunks));
2319
+ return c;
2320
+ }
2321
+
2322
+ static ScrPromise *scr_sc_consume(ScrStream *s, int kind) {
2323
+ ScrPromise *p = scr_promise_new();
2324
+ if (s->st == NULL || !s->st->has_r) {
2325
+ /* Node's consumers for-await the argument; a stream with no readable
2326
+ * side has no async iterator — the TypeError rejects. */
2327
+ static const char msg[] = "stream is not async iterable";
2328
+ scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
2329
+ scr_promise_reject_pending(p);
2330
+ return p;
2331
+ }
2332
+ ScrArr *chunks = scr_arr_new_ref(scr_bytes_retain_v, scr_bytes_release_v, NULL, 4);
2333
+ ScrStreamErrInv fin_inv = kind == SCR_SC_TEXT ? &scr_sc_fin_text
2334
+ : kind == SCR_SC_JSON ? &scr_sc_fin_json
2335
+ : &scr_sc_fin_buffer;
2336
+ /* The terminal watcher first (it marks lifecycle errors handled); the
2337
+ * promise form exposes no unhook — the cleanup closure drops. */
2338
+ ScrClosure *cleanup = scr_stream_finished(s, scr_sc_closure((void *)fin_inv, p, chunks), fin_inv);
2339
+ scr_closure_release(cleanup);
2340
+ ScrStr *dn = scr_str_new("data", 4);
2341
+ scr_emitter_release(scr_emitter_on((ScrEmitter *)s, dn,
2342
+ scr_sc_closure((void *)&scr_sc_data, p, chunks),
2343
+ scr_ee_inv_fixed2, false, false));
2344
+ scr_str_release(dn);
2345
+ /* resume() rather than the 'data' hook alone: Node's consumer pulls
2346
+ * through the iterator, which drains a PAUSED stream too. */
2347
+ scr_stream_release(scr_stream_resume(s));
2348
+ scr_arr_release(chunks);
2349
+ return p;
2350
+ }
2351
+
2352
+ ScrPromise *scr_sc_text(ScrStream *s) { return scr_sc_consume(s, SCR_SC_TEXT); }
2353
+ ScrPromise *scr_sc_json(ScrStream *s) { return scr_sc_consume(s, SCR_SC_JSON); }
2354
+ ScrPromise *scr_sc_buffer(ScrStream *s) { return scr_sc_consume(s, SCR_SC_BUFFER); }
2355
+
2196
2356
  /* ── the readable surface ─────────────────────────────────────────────── */
2197
2357
 
2198
2358
  bool scr_stream_push(ScrStream *s, ScrBytes *chunk) {
@@ -2200,13 +2360,41 @@ bool scr_stream_push(ScrStream *s, ScrBytes *chunk) {
2200
2360
  }
2201
2361
 
2202
2362
  bool scr_stream_push_str(ScrStream *s, ScrStr *str) {
2203
- ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)str->len);
2204
- memcpy(b->data, str->data, str->len);
2363
+ ScrStr *enc = s->st->has_r ? s->st->r.push_enc : NULL;
2364
+ ScrBytes *b;
2365
+ if (enc != NULL) {
2366
+ /* Node: Buffer.from(chunk, state.defaultEncoding). */
2367
+ b = scr_bytes_from_str(str, enc);
2368
+ } else {
2369
+ b = scr_bytes_new(SCR_BYTES_U8, (double)str->len);
2370
+ memcpy(b->data, str->data, str->len);
2371
+ }
2372
+ bool ret = scr_stream_add_chunk(s, b, false);
2373
+ scr_bytes_release(b);
2374
+ return ret;
2375
+ }
2376
+
2377
+ /* push(chunk, encoding) with an explicit non-utf8 literal: the per-call
2378
+ * encoding overrides the stream's default. Borrows both strings. */
2379
+ bool scr_stream_push_str_enc(ScrStream *s, ScrStr *str, ScrStr *enc) {
2380
+ ScrBytes *b = scr_bytes_from_str(str, enc);
2205
2381
  bool ret = scr_stream_add_chunk(s, b, false);
2206
2382
  scr_bytes_release(b);
2207
2383
  return ret;
2208
2384
  }
2209
2385
 
2386
+ /* The defaultEncoding option's push side (canonical literal, frontend-
2387
+ * folded; never "utf8" — that stays the NULL fast path). Answers the
2388
+ * receiver +1, the setEncoding chaining shape. */
2389
+ ScrStream *scr_stream_set_push_encoding(ScrStream *s, ScrStr *enc) {
2390
+ ScrStreamState *st = s->st;
2391
+ if (st->has_r) {
2392
+ if (st->r.push_enc) scr_str_release(st->r.push_enc);
2393
+ st->r.push_enc = scr_str_retain(enc);
2394
+ }
2395
+ return scr_stream_retain(s);
2396
+ }
2397
+
2210
2398
  bool scr_stream_push_null(ScrStream *s) {
2211
2399
  ScrStreamState *st = s->st;
2212
2400
  if (!st->has_r || st->destroyed) return false;
@@ -2891,6 +3079,25 @@ static void scr_stream_run_tick(ScrStreamTick *t) {
2891
3079
  }
2892
3080
  }
2893
3081
 
3082
+ static void scr_stream_dispatch(void);
3083
+
3084
+ /* One tick-marker's dispatch: the queue's FIFO head (markers and entries
3085
+ * are enqueued 1:1 in the same order). After an uncaught throw the
3086
+ * remaining entries drop through the station's exc branch below, keeping
3087
+ * the RC audit clean. */
3088
+ static void scr_stream_dispatch_one(void) {
3089
+ ScrStreamTick *t = scr_st_head;
3090
+ if (t == NULL || scr_exc_pending()) return;
3091
+ scr_st_head = t->next;
3092
+ if (scr_st_head == NULL) scr_st_tail = NULL;
3093
+ scr_stream_run_tick(t);
3094
+ scr_stream_release(t->s);
3095
+ if (t->err) scr_error_release(t->err);
3096
+ if (t->cb) scr_closure_release(t->cb);
3097
+ free(t);
3098
+ if (scr_exc_pending()) scr_stream_dispatch(); /* its exc branch drops the rest */
3099
+ }
3100
+
2894
3101
  static void scr_stream_dispatch(void) {
2895
3102
  while (scr_st_head != NULL && !scr_exc_pending() && !scr_loop_has_ready()) {
2896
3103
  ScrStreamTick *t = scr_st_head;