@scriptc/runtime 0.0.17 → 0.0.18

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.17",
3
+ "version": "0.0.18",
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
@@ -106,7 +106,7 @@ struct ScrPromise {
106
106
  size_t ncbs, cbs_cap;
107
107
  /* Unhandled-rejection tracking: set when rejected, cleared on await. */
108
108
  bool rejection_observed;
109
- /* Set when the loop-end report delivered THIS promise to
109
+ /* Set when the checkpoint report delivered THIS promise to
110
110
  * 'unhandledRejection' listeners — a handler attached after that is
111
111
  * Node's 'rejectionHandled' moment (scr_prom_observe below). */
112
112
  bool reported_unhandled;
@@ -193,7 +193,7 @@ ScrPromise *scr_promise_new(void) {
193
193
 
194
194
  /* The 'rejectionHandled' hook (scr_async_dyn.c installs it at listener
195
195
  * registration — the scr_urj_deliver_fn pattern, so listener-free
196
- * binaries keep their size class): called when a promise the loop-end
196
+ * binaries keep their size class): called when a promise the checkpoint
197
197
  * report already delivered as unhandled gains a handler. */
198
198
  void (*scr_rjh_notify_fn)(ScrPromise *p) = NULL;
199
199
 
@@ -212,12 +212,15 @@ static void scr_prom_observe(ScrPromise *p) {
212
212
  }
213
213
 
214
214
  /* The attach-time handled mark (scr_async_dyn.c's dyn then/catch with a
215
- * rejection handler): Node marks a rejection handled at ATTACH, not when
216
- * the reaction runs and the loop-exhaustion window (a .catch inside an
217
- * 'unhandledRejection' listener) has no later fiber turn whose await
218
- * could observe it. */
215
+ * rejection handler, plus the module loader's ownership of every module
216
+ * evaluation promise): Node marks a promise handled at ATTACH, including
217
+ * while it is still pending, not when a later reaction runs. The pending
218
+ * mark is essential for a module dependency whose importer aborts while
219
+ * evaluating a later sibling — the loader still owns that dependency's
220
+ * eventual rejection, so it must never surface as an unrelated unhandled
221
+ * rejection. */
219
222
  void scr_promise_mark_handled(ScrPromise *p) {
220
- if (p->state == SCR_PROM_REJECTED) scr_prom_observe(p);
223
+ scr_prom_observe(p);
221
224
  }
222
225
 
223
226
  ScrPromise *scr_promise_retain(ScrPromise *p) {
@@ -1229,24 +1232,31 @@ static void scr_await_yield(void) {
1229
1232
  * turn — the same hop a settled promise takes. */
1230
1233
  void scr_await_hop(void) { scr_await_yield(); }
1231
1234
 
1232
- /* Await result extraction. Rejection re-throws into the awaiter (payload
1233
- * RETAINED, not moved — a promise can be awaited more than once). */
1235
+ /* Copies a rejection into the active execution context's exception cell.
1236
+ * The payload is RETAINED, not moved — a promise can be awaited more than
1237
+ * once, and the executable's top-level completion probe consumes the same
1238
+ * settlement from the main stack after the loop drains. */
1239
+ static void scr_promise_rethrow(ScrPromise *p) {
1240
+ switch (p->payload_kind) {
1241
+ case SCR_EXC_F64: scr_throw_f64(p->f64); break;
1242
+ case SCR_EXC_BOOL: scr_throw_bool(p->b); break;
1243
+ case SCR_EXC_STR: scr_throw_str(scr_str_retain((ScrStr *)p->payload)); break;
1244
+ case SCR_EXC_REF: scr_throw_ref(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
1245
+ case SCR_EXC_OBJ: scr_throw_obj(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
1246
+ case SCR_EXC_NONE:
1247
+ case SCR_EXC_GENRET: /* unreachable: the sentinel never settles a promise */
1248
+ scr_throw_str(scr_str_new("undefined", 9));
1249
+ break;
1250
+ }
1251
+ }
1252
+
1253
+ /* Await result extraction. Rejection re-throws into the awaiter. */
1234
1254
  static bool scr_await_settled(ScrPromise *p) {
1235
1255
  if (p->state != SCR_PROM_PENDING) scr_await_yield();
1236
1256
  while (p->state == SCR_PROM_PENDING) scr_await_park(p);
1237
1257
  scr_prom_observe(p);
1238
1258
  if (p->state == SCR_PROM_REJECTED) {
1239
- switch (p->payload_kind) {
1240
- case SCR_EXC_F64: scr_throw_f64(p->f64); break;
1241
- case SCR_EXC_BOOL: scr_throw_bool(p->b); break;
1242
- case SCR_EXC_STR: scr_throw_str(scr_str_retain((ScrStr *)p->payload)); break;
1243
- case SCR_EXC_REF: scr_throw_ref(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
1244
- case SCR_EXC_OBJ: scr_throw_obj(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
1245
- case SCR_EXC_NONE:
1246
- case SCR_EXC_GENRET: /* unreachable: the sentinel never settles a promise */
1247
- scr_throw_str(scr_str_new("undefined", 9));
1248
- break;
1249
- }
1259
+ scr_promise_rethrow(p);
1250
1260
  return false;
1251
1261
  }
1252
1262
  return true;
@@ -1263,6 +1273,32 @@ void *scr_await_ref(ScrPromise *p) {
1263
1273
  }
1264
1274
  void scr_await_void(ScrPromise *p) { scr_await_settled(p); }
1265
1275
 
1276
+ /* ECMAScript's INTERNAL module-dependency wait. Unlike a user-authored
1277
+ * await, an already-completed dependency does not introduce a promise-job
1278
+ * hop: the evaluator continues synchronously into the importer. A pending
1279
+ * dependency still parks this module fiber, and rejection propagates into
1280
+ * it exactly like an ordinary await. */
1281
+ void scr_module_await(ScrPromise *p) {
1282
+ while (p->state == SCR_PROM_PENDING) scr_await_park(p);
1283
+ scr_prom_observe(p);
1284
+ if (p->state == SCR_PROM_REJECTED) scr_promise_rethrow(p);
1285
+ }
1286
+
1287
+ int scr_promise_finish_top_level(ScrPromise *p) {
1288
+ if (p->state == SCR_PROM_PENDING) {
1289
+ /* ECMAScript module evaluation is still pending, but Node's ref'd
1290
+ * event-loop work is exhausted. Node exits with its dedicated
1291
+ * unsettled-top-level-await status. */
1292
+ scr_exit_code_note(13);
1293
+ return 13;
1294
+ }
1295
+ scr_prom_observe(p);
1296
+ return p->state == SCR_PROM_REJECTED ? 1 : 0;
1297
+ }
1298
+
1299
+ void scr_promise_rethrow_top_level(ScrPromise *p) {
1300
+ if (p->state == SCR_PROM_REJECTED) scr_promise_rethrow(p);
1301
+ }
1266
1302
 
1267
1303
 
1268
1304
 
@@ -1704,7 +1740,7 @@ void scr_loop_set_stream(bool (*pending)(void), void (*dispatch)(void)) {
1704
1740
  * microtask checkpoints between macrotasks). */
1705
1741
  bool scr_loop_has_ready(void) { return scr_ready_len > 0; }
1706
1742
 
1707
- void scr_loop_run(void) {
1743
+ bool scr_loop_run(ScrPromise *top_level) {
1708
1744
  /* The FIRST checkpoint after the synchronous main body runs promise
1709
1745
  * jobs BEFORE the first tick drain: Node's main-module evaluation is
1710
1746
  * itself awaited (the runMain continuation is a microtask queued after
@@ -1712,7 +1748,9 @@ void scr_loop_run(void) {
1712
1748
  * scheduled during the body exactly once, at startup — differentially
1713
1749
  * pinned. Every later checkpoint drains ticks first. */
1714
1750
  bool first_checkpoint = true;
1751
+ bool rejection_failed = false;
1715
1752
  for (;;) {
1753
+ if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break;
1716
1754
  /* process.nextTick callbacks BEFORE promise jobs (Node's checkpoint
1717
1755
  * order): the tick queue to exhaustion, then the microtask queue to
1718
1756
  * exhaustion, and back while either has work — a microtask's
@@ -1733,7 +1771,7 @@ void scr_loop_run(void) {
1733
1771
  } else {
1734
1772
  raw(); /* one stream tick, FIFO with the user ticks around it */
1735
1773
  }
1736
- if (scr_exc_pending()) return;
1774
+ if (scr_exc_pending()) return false;
1737
1775
  }
1738
1776
  }
1739
1777
  /* Microtasks to exhaustion (Node: promise jobs before timers). */
@@ -1743,7 +1781,26 @@ void scr_loop_run(void) {
1743
1781
  scr_resume_fiber(f);
1744
1782
  }
1745
1783
  first_checkpoint = false;
1784
+ /* The ESM loader observes a rejected entry evaluation at a promise-job
1785
+ * checkpoint and terminates before later ref'd timers or I/O can run.
1786
+ * Wait until this checkpoint's ready queue is drained — the root's
1787
+ * rejection handler is itself promise-job ordered — then leave through
1788
+ * the normal teardown below. A fulfilled root deliberately does not
1789
+ * stop the loop. */
1790
+ if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break;
1746
1791
  if (scr_nt_head != NULL) continue;
1792
+ /* Node decides unhandled rejections at the END of each complete
1793
+ * nextTick/microtask checkpoint, before advancing to timers or I/O.
1794
+ * A rejected executable module root wins over OTHER rejections from
1795
+ * this same checkpoint (the root check above); rejections from an
1796
+ * earlier checkpoint have already delivered here. Handled listeners
1797
+ * may enqueue more jobs or ref'd work, so return to the checkpoint
1798
+ * head instead of declaring the loop exhausted underneath them. */
1799
+ if (scr_report_unhandled_rejections()) {
1800
+ rejection_failed = true;
1801
+ break;
1802
+ }
1803
+ if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) continue;
1747
1804
  /* Quiescent between turns (microtasks drained, nothing running):
1748
1805
  * collect any cycles the turn left behind. No-op on an empty buffer. */
1749
1806
  scr_collect_cycles();
@@ -1754,7 +1811,7 @@ void scr_loop_run(void) {
1754
1811
  * so those drain first. */
1755
1812
  if (scr_stream_dispatch_fn != NULL) {
1756
1813
  scr_stream_dispatch_fn();
1757
- if (scr_exc_pending()) return; /* uncaught throw in a listener */
1814
+ if (scr_exc_pending()) return false; /* uncaught throw in a listener */
1758
1815
  if (scr_ready_len > 0) continue;
1759
1816
  if (scr_stream_pending_fn != NULL && scr_stream_pending_fn()) continue;
1760
1817
  }
@@ -1765,7 +1822,7 @@ void scr_loop_run(void) {
1765
1822
  * microtasks — restart the turn so those drain first. */
1766
1823
  if (scr_events_dispatch_fn != NULL) {
1767
1824
  scr_events_dispatch_fn();
1768
- if (scr_exc_pending()) return; /* uncaught throw in a listener */
1825
+ if (scr_exc_pending()) return false; /* uncaught throw in a listener */
1769
1826
  if (scr_ready_len > 0) continue;
1770
1827
  }
1771
1828
  /* Net dispatch (scr_net.c, when linked): accepts, arrived data,
@@ -1774,7 +1831,7 @@ void scr_loop_run(void) {
1774
1831
  * microtasks — restart the turn so those drain first. */
1775
1832
  if (scr_net_dispatch_fn != NULL) {
1776
1833
  scr_net_dispatch_fn();
1777
- if (scr_exc_pending()) return; /* uncaught throw in a listener */
1834
+ if (scr_exc_pending()) return false; /* uncaught throw in a listener */
1778
1835
  if (scr_ready_len > 0) continue;
1779
1836
  }
1780
1837
  /* Dgram dispatch (scr_dgram.c, when linked): arrived datagrams and
@@ -1782,7 +1839,7 @@ void scr_loop_run(void) {
1782
1839
  * dns.lookup callbacks) fire now — the net hook's exact station. */
1783
1840
  if (scr_dgram_dispatch_fn != NULL) {
1784
1841
  scr_dgram_dispatch_fn();
1785
- if (scr_exc_pending()) return; /* uncaught throw in a listener */
1842
+ if (scr_exc_pending()) return false; /* uncaught throw in a listener */
1786
1843
  if (scr_ready_len > 0) continue;
1787
1844
  }
1788
1845
  /* Watch dispatch (scr_watch.c, when linked): file events queued on
@@ -1790,7 +1847,7 @@ void scr_loop_run(void) {
1790
1847
  * net hook's exact station. */
1791
1848
  if (scr_watch_dispatch_fn != NULL) {
1792
1849
  scr_watch_dispatch_fn();
1793
- if (scr_exc_pending()) return; /* uncaught throw in a listener */
1850
+ if (scr_exc_pending()) return false; /* uncaught throw in a listener */
1794
1851
  if (scr_ready_len > 0) continue;
1795
1852
  }
1796
1853
  /* Reap spawned children and fire their listeners (before timers,
@@ -1817,7 +1874,7 @@ void scr_loop_run(void) {
1817
1874
  (scr_watch_pending_fn != NULL && scr_watch_pending_fn());
1818
1875
  if (held) {
1819
1876
  scr_children_poll();
1820
- if (scr_exc_pending()) return; /* uncaught throw in a listener */
1877
+ if (scr_exc_pending()) return false; /* uncaught throw in a listener */
1821
1878
  if (scr_ready_len > 0) continue;
1822
1879
  }
1823
1880
  }
@@ -2018,7 +2075,7 @@ void scr_loop_run(void) {
2018
2075
  scr_closure_release(t.cb);
2019
2076
  }
2020
2077
  scr_firing_id = 0;
2021
- if (scr_exc_pending()) return; /* uncaught throw in a callback: main handles it */
2078
+ if (scr_exc_pending()) return false; /* uncaught throw in a callback: main handles it */
2022
2079
  if (scr_ready_len > 0) break; /* drain microtasks before more timers */
2023
2080
  }
2024
2081
  /* The check phase: immediates queued BEFORE this phase started run
@@ -2042,12 +2099,13 @@ void scr_loop_run(void) {
2042
2099
  }
2043
2100
  ((void (*)(ScrClosure *))cb->fn)(cb);
2044
2101
  scr_closure_release(cb);
2045
- if (scr_exc_pending()) return; /* uncaught throw: main handles it */
2102
+ if (scr_exc_pending()) return false; /* uncaught throw: main handles it */
2046
2103
  while (scr_ready_len > 0) {
2047
2104
  ScrFiber *f = scr_ready[scr_ready_head++];
2048
2105
  scr_ready_len--;
2049
2106
  scr_resume_fiber(f);
2050
2107
  }
2108
+ if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break;
2051
2109
  }
2052
2110
  /* Fully drained: reset the cursor so the array reuses its slots. */
2053
2111
  if (scr_immediates_head == scr_nimmediates) {
@@ -2056,12 +2114,11 @@ void scr_loop_run(void) {
2056
2114
  }
2057
2115
  }
2058
2116
  }
2059
- /* Normal exit can now leave UNREF'd timers armed in the heap (the loop
2060
- * stopped keeping itself alive for them, Node's unref semantics) — they
2061
- * never fire, so release their closures here or the RC audit counts them
2062
- * as leaks. Timers left on the uncaught/unhandled paths (the loop
2063
- * `return`s above, skipping this) are covered by the abandoned-fiber
2064
- * audit skip. */
2117
+ /* Exit can now leave UNREF'd timers armed in the heap (ordinary
2118
+ * exhaustion, a fatal module root, or an unhandled rejection). They
2119
+ * never fire, so release their closures here or the RC audit counts
2120
+ * them as leaks. Uncaught callback throws still return above for main
2121
+ * to report through the existing exceptional teardown path. */
2065
2122
  scr_timers_teardown();
2066
2123
  /* Same story for unref'd children the loop never reaped: release the
2067
2124
  * registry's references (their listeners never fire — the process is
@@ -2069,6 +2126,7 @@ void scr_loop_run(void) {
2069
2126
  scr_children_teardown();
2070
2127
  scr_fibers_abandoned = scr_fibers_live;
2071
2128
  scr_note_abandoned_fibers(scr_fibers_abandoned);
2129
+ return rejection_failed;
2072
2130
  }
2073
2131
 
2074
2132
  /* The island's half of the unhandled-rejection report (scr_island.c
@@ -2077,21 +2135,18 @@ void scr_loop_run(void) {
2077
2135
  * first-unhandled-rejection death. Returns whether the island had any.
2078
2136
  * Static builds never set it. */
2079
2137
  static bool (*scr_island_rejections_fn)(bool print) = NULL;
2138
+ static int (*scr_island_jobs_drain_fn)(void) = NULL;
2080
2139
 
2081
- void scr_loop_set_island_rejections(bool (*fn)(bool print)) {
2140
+ void scr_loop_set_island_rejections(bool (*fn)(bool print),
2141
+ int (*drain_jobs)(void)) {
2082
2142
  scr_island_rejections_fn = fn;
2143
+ scr_island_jobs_drain_fn = drain_jobs;
2083
2144
  }
2084
2145
 
2085
2146
  /* ── process.on('unhandledRejection') ─────────────────────────────────
2086
- * dyn listeners called per never-observed rejection instead of the
2087
- * default report below. Node fires the event at end-of-turn; the
2088
- * compiled runtime fires at loop exhaustion, where the ledger is
2089
- * decided — the same values, later (SEMANTICS.md; mustCall-style
2090
- * assertions and reason identity observe no difference, cross-turn
2091
- * interleaving would). A registered listener suppresses the report and
2092
- * the exit-1, exactly Node's handled-event contract. */
2093
-
2094
-
2147
+ * Dyn listeners are called per never-observed rejection at the completed
2148
+ * nextTick/microtask checkpoint. A registered listener suppresses the
2149
+ * default report and exit 1, exactly Node's handled-event contract. */
2095
2150
 
2096
2151
  /* The unhandled-rejection LISTENER hook (scr_async_dyn.c installs it at
2097
2152
  * registration — the loop-hook pattern, so listener-free binaries keep
@@ -2099,11 +2154,25 @@ void scr_loop_set_island_rejections(bool (*fn)(bool print)) {
2099
2154
  * listener threw (the uncaught crash path). */
2100
2155
  bool (*scr_urj_deliver_fn)(ScrPromise *p) = NULL;
2101
2156
 
2102
- /* Unhandled rejections at loop end: Node prints an error and exits 1. */
2157
+ /* Unhandled rejections at a completed nextTick/microtask checkpoint:
2158
+ * Node prints an error and exits 1 when no listener handles the event.
2159
+ * Snapshot the ledger: a listener can reject another promise, but that
2160
+ * new rejection belongs to the NEXT checkpoint rather than this report. */
2103
2161
  bool scr_report_unhandled_rejections(void) {
2162
+ /* Static fibers drain before engine jobs in this runtime's documented
2163
+ * island ordering. Complete BOTH halves of that microtask checkpoint
2164
+ * before deciding either rejection ledger; engine reactions can attach
2165
+ * a handler to a rejection that would otherwise look unhandled here.
2166
+ * A host callback can wake a static fiber/tick, in which case the loop
2167
+ * must drain that work before reporting too. */
2168
+ if (scr_island_jobs_drain_fn != NULL) {
2169
+ scr_island_jobs_drain_fn();
2170
+ if (scr_ready_len > 0 || scr_nt_head != NULL) return false;
2171
+ }
2104
2172
  bool any = false;
2105
2173
  bool crashed = false;
2106
- for (size_t i = 0; i < scr_nunhandled; i++) {
2174
+ size_t report_count = scr_nunhandled;
2175
+ for (size_t i = 0; i < report_count; i++) {
2107
2176
  ScrPromise *p = scr_maybe_unhandled[i];
2108
2177
  if (p->state == SCR_PROM_REJECTED && !p->rejection_observed && !crashed) {
2109
2178
  if (scr_urj_deliver_fn != NULL) {
@@ -2145,7 +2214,12 @@ bool scr_report_unhandled_rejections(void) {
2145
2214
  }
2146
2215
  scr_promise_release(p);
2147
2216
  }
2148
- scr_nunhandled = 0;
2217
+ size_t remaining = scr_nunhandled - report_count;
2218
+ if (remaining > 0) {
2219
+ memmove(scr_maybe_unhandled, scr_maybe_unhandled + report_count,
2220
+ remaining * sizeof *scr_maybe_unhandled);
2221
+ }
2222
+ scr_nunhandled = remaining;
2149
2223
  if (crashed) {
2150
2224
  scr_exc_print_uncaught();
2151
2225
  scr_exit_code_note(1);
@@ -2155,20 +2229,22 @@ bool scr_report_unhandled_rejections(void) {
2155
2229
  bool island = scr_island_rejections_fn(!any);
2156
2230
  any = any || island;
2157
2231
  }
2158
- /* An 'unhandledRejection' listener can spawn fibers the exhausted loop
2159
- * will never run (a .catch attach mints a reaction fiber): they are
2160
- * abandoned by construction — re-note them so the RC audit's skip
2161
- * covers their parked state, exactly the loop-teardown accounting. */
2162
- if (scr_fibers_live > scr_fibers_abandoned) {
2163
- scr_fibers_abandoned = scr_fibers_live;
2164
- scr_note_abandoned_fibers(scr_fibers_abandoned);
2165
- }
2166
2232
  /* main returns 1 on a reported rejection — the 'exit' listeners (atexit)
2167
2233
  * must see that code, like Node's. */
2168
2234
  if (any) scr_exit_code_note(1);
2169
2235
  return any;
2170
2236
  }
2171
2237
 
2238
+ /* A fatal executable-module rejection suppresses unrelated rejections
2239
+ * created in the SAME checkpoint. Drop their retained ledger references
2240
+ * without delivering process events or a competing default report. */
2241
+ void scr_discard_unhandled_rejections(void) {
2242
+ for (size_t i = 0; i < scr_nunhandled; i++) {
2243
+ scr_promise_release(scr_maybe_unhandled[i]);
2244
+ }
2245
+ scr_nunhandled = 0;
2246
+ }
2247
+
2172
2248
  /* ── new Promise(executor) ────────────────────────────────────────────── */
2173
2249
 
2174
2250
  /* resolve closures: caps[0] is a box whose slot holds the promise (+1). */
@@ -403,7 +403,7 @@ ScrDyn *scr_dyn_promise_then(ScrPromise *src, ScrDyn *onf, ScrDyn *onr, ScrDyn *
403
403
  /* A rejection HANDLER marks the source handled at attach (Node's
404
404
  * moment; the reaction fiber's await re-marks harmlessly) — this is
405
405
  * also what lets a .catch inside an 'unhandledRejection' listener fire
406
- * 'rejectionHandled' when no fiber turn remains. */
406
+ * 'rejectionHandled' immediately at the attach point. */
407
407
  if (onr != NULL) scr_promise_mark_handled(src);
408
408
  ScrDynThenPack *a = malloc(sizeof *a);
409
409
  if (!a) scr_ad_oom();
@@ -720,4 +720,3 @@ ScrDyn *scr_dyn_new_promise_adapting(ScrPromise *src,
720
720
  ScrPromise *scr_dyn_promise_of(const ScrDyn *d) {
721
721
  return d->kind == SCR_DYN_PROMISE ? d->v.promise : NULL;
722
722
  }
723
-
package/src/scr_bytes.c CHANGED
@@ -1503,4 +1503,3 @@ double scr_bytes_write_var(ScrBytes *b, double value, double offset, double byte
1503
1503
  }
1504
1504
  return offset + (double)width;
1505
1505
  }
1506
-
@@ -0,0 +1,157 @@
1
+ #include "scr_runtime.h"
2
+
3
+ #include <math.h>
4
+ #include <stdio.h>
5
+ #include <stdint.h>
6
+
7
+ /* ES2023 Array/TypedArray copying methods and the typed-array-to-number[]
8
+ * bridge live in their own archive member. Programs that do not use this
9
+ * surface therefore pull in none of it — the static binary size contract. */
10
+
11
+ static uint64_t copying_slot_from_ptr(void *p) {
12
+ return (uint64_t)(uintptr_t)p;
13
+ }
14
+
15
+ static void *copying_slot_to_ptr(uint64_t slot) {
16
+ return (void *)(uintptr_t)slot;
17
+ }
18
+
19
+ static bool copying_elem_is_ref(ScrElemKind kind) {
20
+ return kind == SCR_ELEM_STR || kind == SCR_ELEM_ARR ||
21
+ kind == SCR_ELEM_BYTES || kind == SCR_ELEM_REF;
22
+ }
23
+
24
+ static ScrArr *copying_arr_new_like(const ScrArr *a, size_t cap) {
25
+ return a->elem == SCR_ELEM_REF
26
+ ? scr_arr_new_ref(a->elem_retain, a->elem_release,
27
+ a->elem_trace, cap ? cap : 1)
28
+ : scr_arr_new(a->elem, cap ? cap : 1);
29
+ }
30
+
31
+ static uint64_t copying_arr_retain_slot(const ScrArr *a, uint64_t slot) {
32
+ if (!copying_elem_is_ref(a->elem)) return slot;
33
+ void *p = copying_slot_to_ptr(slot);
34
+ if (a->elem == SCR_ELEM_STR) p = scr_str_retain((ScrStr *)p);
35
+ else if (a->elem == SCR_ELEM_ARR) p = scr_arr_retain((ScrArr *)p);
36
+ else if (a->elem == SCR_ELEM_BYTES) p = scr_bytes_retain((ScrBytes *)p);
37
+ else p = a->elem_retain(p);
38
+ return copying_slot_from_ptr(p);
39
+ }
40
+
41
+ static void copying_arr_copy_slot(ScrArr *out, const ScrArr *src, size_t i) {
42
+ out->data[out->len++] = copying_arr_retain_slot(src, src->data[i]);
43
+ }
44
+
45
+ ScrArr *scr_arr_to_reversed(const ScrArr *a) {
46
+ ScrArr *out = copying_arr_new_like(a, a->len);
47
+ for (size_t i = a->len; i > 0; i--) {
48
+ copying_arr_copy_slot(out, a, i - 1);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ ScrArr *scr_arr_to_spliced(const ScrArr *a, double start,
54
+ double delete_count, const ScrArr *items) {
55
+ double len = (double)a->len;
56
+ double s0 = isnan(start) ? 0 : trunc(start);
57
+ if (s0 < 0) s0 += len;
58
+ size_t from = s0 <= 0 ? 0 : s0 >= len ? a->len : (size_t)s0;
59
+ double avail = len - (double)from;
60
+ double d0 = isnan(delete_count) ? 0 : trunc(delete_count);
61
+ size_t ndelete =
62
+ d0 <= 0 ? 0 : d0 >= avail ? (size_t)avail : (size_t)d0;
63
+ if (items->len > SIZE_MAX - (a->len - ndelete)) {
64
+ scr_trap("scriptc: out of memory\n");
65
+ }
66
+ size_t out_len = a->len - ndelete + items->len;
67
+ ScrArr *out = copying_arr_new_like(a, out_len);
68
+ for (size_t i = 0; i < from; i++) copying_arr_copy_slot(out, a, i);
69
+ for (size_t i = 0; i < items->len; i++) {
70
+ out->data[out->len++] =
71
+ copying_arr_retain_slot(a, items->data[i]);
72
+ }
73
+ for (size_t i = from + ndelete; i < a->len; i++) {
74
+ copying_arr_copy_slot(out, a, i);
75
+ }
76
+ return out;
77
+ }
78
+
79
+ static bool copying_arr_with_index(const ScrArr *a, double index,
80
+ size_t *out) {
81
+ double rel = isnan(index) ? 0 : trunc(index);
82
+ double actual = rel >= 0 ? rel : (double)a->len + rel;
83
+ if (!(actual >= 0) || actual >= (double)a->len) {
84
+ char num[32];
85
+ size_t numlen = scr_f64_to_str(index, num);
86
+ char msg[80];
87
+ int mlen = snprintf(msg, sizeof msg, "Invalid index : %.*s",
88
+ (int)numlen, num);
89
+ scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)mlen);
90
+ return false;
91
+ }
92
+ *out = (size_t)actual;
93
+ return true;
94
+ }
95
+
96
+ ScrArr *scr_arr_with_f64(ScrArr *a, double index, double value) {
97
+ size_t i;
98
+ if (!copying_arr_with_index(a, index, &i)) return NULL;
99
+ ScrArr *out = scr_arr_slice(a, 0, INFINITY);
100
+ scr_arr_set_f64(out, (double)i, value);
101
+ return out;
102
+ }
103
+
104
+ ScrArr *scr_arr_with_bool(ScrArr *a, double index, bool value) {
105
+ size_t i;
106
+ if (!copying_arr_with_index(a, index, &i)) return NULL;
107
+ ScrArr *out = scr_arr_slice(a, 0, INFINITY);
108
+ scr_arr_set_bool(out, (double)i, value);
109
+ return out;
110
+ }
111
+
112
+ ScrArr *scr_arr_with_ref(ScrArr *a, double index, void *value) {
113
+ size_t i;
114
+ if (!copying_arr_with_index(a, index, &i)) return NULL;
115
+ ScrArr *out = scr_arr_slice(a, 0, INFINITY);
116
+ uint64_t retained =
117
+ copying_arr_retain_slot(a, copying_slot_from_ptr(value));
118
+ scr_arr_set_ref(out, (double)i, copying_slot_to_ptr(retained));
119
+ return out;
120
+ }
121
+
122
+ ScrBytes *scr_bytes_to_reversed(const ScrBytes *b) {
123
+ ScrBytes *out = scr_bytes_new(b->elem, (double)b->len);
124
+ for (size_t i = 0; i < b->len; i++) {
125
+ scr_bytes_set(out, (double)i,
126
+ scr_bytes_get(b, (double)(b->len - i - 1)));
127
+ }
128
+ return out;
129
+ }
130
+
131
+ ScrBytes *scr_bytes_with(const ScrBytes *b, double index, double value) {
132
+ double rel = isnan(index) ? 0 : trunc(index);
133
+ double actual = rel >= 0 ? rel : (double)b->len + rel;
134
+ if (!(actual >= 0) || actual >= (double)b->len) {
135
+ static const char msg[] = "Invalid typed array index";
136
+ scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
137
+ return NULL;
138
+ }
139
+ ScrBytes *out = scr_bytes_copy(b);
140
+ scr_bytes_set(out, actual, value);
141
+ return out;
142
+ }
143
+
144
+ ScrArr *scr_bytes_to_arr(const ScrBytes *b) {
145
+ ScrArr *out = scr_arr_new(SCR_ELEM_F64, b->len ? b->len : 1);
146
+ for (size_t i = 0; i < b->len; i++) {
147
+ scr_arr_push_f64(out, scr_bytes_get(b, (double)i));
148
+ }
149
+ return out;
150
+ }
151
+
152
+ ScrStr *scr_bytes_join(const ScrBytes *b, const ScrStr *separator) {
153
+ ScrArr *values = scr_bytes_to_arr(b);
154
+ ScrStr *out = scr_arr_join(values, (ScrStr *)separator);
155
+ scr_arr_release(values);
156
+ return out;
157
+ }
package/src/scr_island.c CHANGED
@@ -267,14 +267,14 @@ void scr_island_set_netmod(void (*attach)(void *jsctx, void *host_obj), void (*t
267
267
  * false when a promise rejects with no reaction attached (tracked here,
268
268
  * promise and reason retained), is_handled == true when a handler is
269
269
  * attached to it later (the rescission: unlinked and freed — a
270
- * handled-later rejection never reports). At loop exit the ledger joins
271
- * scr_report_unhandled_rejections through the hook registered at boot:
272
- * the FIRST never-observed rejection prints in the static runtime's exact
273
- * voice ("Unhandled promise rejection: <String(reason)>", stderr, exit 1
274
- * — an Error reason renders "name: message" through its toString, same
275
- * as the static ledger). Retaining the promise value keeps its identity
276
- * stable for the rescission (the engine cannot recycle the object while
277
- * the ledger holds it). */
270
+ * handled-later rejection never reports). At the completed microtask
271
+ * checkpoint the ledger joins scr_report_unhandled_rejections through
272
+ * the hook registered at boot: the FIRST never-observed rejection prints
273
+ * in the static runtime's exact voice ("Unhandled promise rejection:
274
+ * <String(reason)>", stderr, exit 1 — an Error reason renders "name:
275
+ * message" through its toString, same as the static ledger). Retaining
276
+ * the promise value keeps its identity stable for the rescission (the
277
+ * engine cannot recycle the object while the ledger holds it). */
278
278
  typedef struct IslRejection {
279
279
  JSValue promise; /* owned; identity for the rescission */
280
280
  JSValue reason; /* owned */
@@ -555,10 +555,13 @@ static void isl_init(void) {
555
555
  * progress exactly where Node runs its microtasks. */
556
556
  scr_loop_set_io(isl_io_pending, isl_io_poll);
557
557
  /* Unhandled island rejections: tracked as they happen (and rescinded
558
- * when handled later), reported at loop exit through the SAME report the
559
- * static promise ledger uses — one voice, exit 1, like Node. */
558
+ * when handled later), reported at the same completed microtask
559
+ * checkpoint as the static promise ledger — one voice, exit 1, like
560
+ * Node. The report hook drains engine jobs first so a same-checkpoint
561
+ * handler attachment gets its chance to rescind. */
560
562
  JS_SetHostPromiseRejectionTracker(isl_rt, isl_rejection_tracker, NULL);
561
- scr_loop_set_island_rejections(isl_report_rejections);
563
+ scr_loop_set_island_rejections(isl_report_rejections,
564
+ scr_island_drain_jobs);
562
565
  /* Armed island timers (AbortSignal.timeout) cap the loop's idle sleep
563
566
  * so they fire on time while the poller waits on socket readiness —
564
567
  * without keeping the loop alive by themselves (Node's unref'd timer).
@@ -2743,8 +2746,10 @@ static JSValue isl_host_exit(JSContext *ctx, JSValueConst this_val, int argc,
2743
2746
  * mirrored here by the shim's setter, read by the emitted main after the
2744
2747
  * loop drains — Node's a-program-that-sets-it-and-returns contract. */
2745
2748
  static int isl_exit_code = 0;
2749
+ static size_t isl_exit_code_version = 0;
2746
2750
 
2747
2751
  int scr_island_exit_code(void) { return isl_exit_code; }
2752
+ size_t scr_island_exit_code_version(void) { return isl_exit_code_version; }
2748
2753
 
2749
2754
  static JSValue isl_host_set_exit_code(JSContext *ctx, JSValueConst this_val,
2750
2755
  int argc, JSValueConst *argv) {
@@ -2753,6 +2758,7 @@ static JSValue isl_host_set_exit_code(JSContext *ctx, JSValueConst this_val,
2753
2758
  int32_t code = 0;
2754
2759
  JS_ToInt32(ctx, &code, argv[0]);
2755
2760
  isl_exit_code = code;
2761
+ isl_exit_code_version++;
2756
2762
  return JS_UNDEFINED;
2757
2763
  }
2758
2764
 
package/src/scr_runtime.h CHANGED
@@ -317,6 +317,10 @@ bool scr_str_eq(ScrStr *a, ScrStr *b);
317
317
  * <0, 0, >0. */
318
318
  int scr_str_cmp(ScrStr *a, ScrStr *b);
319
319
 
320
+ /* ECMAScript string-list ordering: compare UTF-16 code units even though
321
+ * ScrStr stores well-formed UTF-8. Returns <0, 0, >0. */
322
+ int scr_str_cmp_u16(ScrStr *a, ScrStr *b);
323
+
320
324
  /* ── class objects (classes as first-class values) ────────────────────
321
325
  * The class STATIC side as a runtime value: one emitted IMMORTAL static
322
326
  * per class the program takes as a value (`const X = C`, class
@@ -818,6 +822,16 @@ void scr_arr_set_ref(ScrArr *a, double i, void *v);
818
822
  * elements retain into the copy. Borrows a. */
819
823
  ScrArr *scr_arr_slice(ScrArr *a, double start, double end);
820
824
 
825
+ /* ES2023 copying methods. All borrow their inputs and return fresh +1
826
+ * shallow copies. with() raises Node's catchable RangeError for an invalid
827
+ * relative index; the ref variant retains the borrowed replacement. */
828
+ ScrArr *scr_arr_to_reversed(const ScrArr *a);
829
+ ScrArr *scr_arr_to_spliced(const ScrArr *a, double start,
830
+ double delete_count, const ScrArr *items);
831
+ ScrArr *scr_arr_with_f64(ScrArr *a, double index, double value);
832
+ ScrArr *scr_arr_with_bool(ScrArr *a, double index, bool value);
833
+ ScrArr *scr_arr_with_ref(ScrArr *a, double index, void *value);
834
+
821
835
  /* push returns the new length (JS-exact); _ref takes ownership. */
822
836
  double scr_arr_push_f64(ScrArr *a, double v);
823
837
  double scr_arr_push_bool(ScrArr *a, bool v);
@@ -3338,6 +3352,18 @@ bool scr_await_bool(ScrPromise *p);
3338
3352
  ScrStr *scr_await_str(ScrPromise *p); /* +1 */
3339
3353
  void *scr_await_ref(ScrPromise *p); /* +1 via the stored retain */
3340
3354
  void scr_await_void(ScrPromise *p);
3355
+ /* Internal ESM dependency wait: parks while pending but, unlike a
3356
+ * JavaScript await expression, does not hop when already settled. */
3357
+ void scr_module_await(ScrPromise *p);
3358
+ /* After the root-aware event loop returns, inspect the executable's async
3359
+ * module-root promise without another microtask hop: 0 = fulfilled,
3360
+ * 1 = rejected, 13 = still pending with no ref'd work capable of settling
3361
+ * it (Node's unsettled top-level-await exit status). A rejected root is
3362
+ * marked observed here but re-thrown separately; earlier-checkpoint
3363
+ * rejections were already decided by the loop and same-checkpoint
3364
+ * competitors are suppressed by the executable-module verdict. */
3365
+ int scr_promise_finish_top_level(ScrPromise *p);
3366
+ void scr_promise_rethrow_top_level(ScrPromise *p);
3341
3367
  /* The checked-dynamic tree-crossing await (SCR_DYN_PROMISE's boundary contract): the
3342
3368
  * payload as a dyn value (+1; void fulfillments answer the undefined
3343
3369
  * value), or NULL with the rejection re-thrown into the awaiter. */
@@ -3388,23 +3414,19 @@ ScrDyn *scr_als_run(double id, ScrDyn *value, ScrDyn *fn, ScrDyn *args);
3388
3414
  ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args);
3389
3415
 
3390
3416
  /* process.on/once('unhandledRejection', fn): dyn listeners dispatched
3391
- * per never-observed rejection at loop end (reason, promise),
3392
- * suppressing the default report and the exit-1 (Node's handled-event
3393
- * contract; the end-of-turn vs loop-end timing is a SEMANTICS.md
3394
- * divergence). `once` auto-removes after one delivery; off removes by
3395
- * closure identity (the warning registry's story). Throws Node's
3417
+ * per never-observed rejection at the end of its nextTick/microtask
3418
+ * checkpoint — (reason, promise), suppressing the default report and the
3419
+ * exit-1. `once` auto-removes after one delivery; off removes by closure
3420
+ * identity (the warning registry's story). Throws Node's
3396
3421
  * ERR_INVALID_ARG_TYPE on a non-function. */
3397
3422
  void scr_process_on_unhandled_rejection(ScrDyn *fn, bool once);
3398
3423
  void scr_process_off_unhandled_rejection(ScrDyn *fn);
3399
- /* process.on/once/off('rejectionHandled', fn): the sibling registry.
3400
- * Under the loop-exhaustion model the event's ONE firing window is a
3401
- * handler attached DURING an unhandledRejection listener (a rejection
3402
- * handled any earlier never enters the report at all — the same
3403
- * documented divergence); dispatch is synchronous at the attach, with
3404
- * the promise, Node's payload. */
3424
+ /* process.on/once/off('rejectionHandled', fn): the sibling registry. A
3425
+ * promise handled after its unhandledRejection delivery fires once, with
3426
+ * the promise as Node's payload. */
3405
3427
  void scr_process_on_rejection_handled(ScrDyn *fn, bool once);
3406
3428
  void scr_process_off_rejection_handled(ScrDyn *fn);
3407
- /* The loop-end delivery hook the registration above installs
3429
+ /* The checkpoint delivery hook the registration above installs
3408
3430
  * (scr_async_dyn.c → scr_report_unhandled_rejections; NULL = default
3409
3431
  * report). Runtime-internal. */
3410
3432
  extern bool (*scr_urj_deliver_fn)(ScrPromise *p);
@@ -3414,9 +3436,10 @@ extern bool (*scr_urj_deliver_fn)(ScrPromise *p);
3414
3436
  * Runtime-internal. */
3415
3437
  extern void (*scr_rjh_notify_fn)(ScrPromise *p);
3416
3438
  /* The attach-time handled mark (a dyn then/catch carrying a rejection
3417
- * handler): marks an already-rejected source observed Node's attach
3418
- * moment firing the late-handled hook when the report already
3419
- * delivered it. Runtime-internal. */
3439
+ * handler, or the module loader taking ownership of an evaluation
3440
+ * promise): marks pending and rejected sources observed at Node's attach
3441
+ * moment, firing the late-handled hook when the report already delivered
3442
+ * it. Runtime-internal. */
3420
3443
  void scr_promise_mark_handled(ScrPromise *p);
3421
3444
  /* A rejected promise's reason as a dyn value (identity-preserving for
3422
3445
  * dyn payloads and %Error instances). +1. */
@@ -3646,7 +3669,13 @@ int scr_exit_code_hint_get(void);
3646
3669
  * poll(2) sleep, or -1 when it can't wake for every pending child. */
3647
3670
  int scr_children_wake_fd(void);
3648
3671
  double scr_now_ms(void); /* the loop's monotonic clock, in ms */
3649
- void scr_loop_run(void);
3672
+ /* Run to ordinary loop exhaustion, except that a non-NULL executable
3673
+ * module-root promise stops the loop as soon as it is rejected at a
3674
+ * microtask checkpoint. Fulfilled roots do not stop the loop: Node keeps
3675
+ * running ref'd work scheduled by a successfully evaluated module.
3676
+ * Returns true when a default/listener-crashing unhandled rejection
3677
+ * already selected and reported exit status 1. */
3678
+ bool scr_loop_run(ScrPromise *top_level);
3650
3679
  /* External I/O hook, polled at loop quiescence like the child registry:
3651
3680
  * `pending` keeps the loop alive; `poll` makes progress and may SLEEP up
3652
3681
  * to max_wait_ms (on real fds — socket readiness wakes it early), so the
@@ -3655,12 +3684,14 @@ void scr_loop_run(void);
3655
3684
  * never set it. */
3656
3685
  void scr_loop_set_io(bool (*pending)(void), void (*poll)(double max_wait_ms));
3657
3686
  bool scr_report_unhandled_rejections(void); /* true = exit 1 */
3687
+ void scr_discard_unhandled_rejections(void);
3658
3688
  /* The island's unhandled-rejection ledger joins the report above (one
3659
3689
  * registrant, set at engine boot; static builds never set it): called with
3660
3690
  * print=true when the static ledger reported nothing, prints its FIRST
3661
3691
  * never-observed rejection in the same voice, frees the ledger either way,
3662
3692
  * and returns whether it had any. */
3663
- void scr_loop_set_island_rejections(bool (*fn)(bool print));
3693
+ void scr_loop_set_island_rejections(bool (*fn)(bool print),
3694
+ int (*drain_jobs)(void));
3664
3695
  /* The island's earliest armed timer deadline (scr_island_timers_deadline;
3665
3696
  * HUGE_VAL when none) joins the loop's sleep computation: an armed
3666
3697
  * AbortSignal.timeout must fire on time while the loop sleeps on socket
@@ -3874,8 +3905,10 @@ bool scr_zlib_inflate_exact(const unsigned char *src, size_t src_len,
3874
3905
 
3875
3906
  /* The island process shim's implicit exit status (process.exitCode):
3876
3907
  * Node's set-it-and-return-normally contract. The emitted main returns
3877
- * this after the loop drains; 0 when never set. */
3908
+ * this after the loop drains; 0 when never set. The version advances on
3909
+ * every assignment so an exit listener can override a provisional status. */
3878
3910
  int scr_island_exit_code(void);
3911
+ size_t scr_island_exit_code_version(void);
3879
3912
 
3880
3913
  /* ── web-platform globals (scr_web.c) ─────────────────────────────────
3881
3914
  * The pure-JS prelude defining the island's WHATWG subset (streams et
@@ -4441,6 +4474,17 @@ void scr_bytes_set(ScrBytes *b, double i, double v);
4441
4474
  * result is a fresh same-kind copy. Never throws. */
4442
4475
  ScrBytes *scr_bytes_slice(const ScrBytes *b, double start, double end); /* +1 */
4443
4476
 
4477
+ /* ES2023 typed-array copying methods. Both preserve the receiver's element
4478
+ * kind and return a fresh +1 owner. with() raises Node's catchable
4479
+ * "Invalid typed array index" RangeError for an invalid relative index. */
4480
+ ScrBytes *scr_bytes_to_reversed(const ScrBytes *b); /* +1 */
4481
+ ScrBytes *scr_bytes_with(const ScrBytes *b, double index, double value); /* +1 */
4482
+
4483
+ /* Numeric typed-array iteration drained into number[], and Uint8Array.join.
4484
+ * Inputs borrowed; results fresh +1. */
4485
+ ScrArr *scr_bytes_to_arr(const ScrBytes *b); /* +1 */
4486
+ ScrStr *scr_bytes_join(const ScrBytes *b, const ScrStr *separator); /* +1 */
4487
+
4444
4488
  /* TypedArray.prototype.fill on non-u8 receivers: per-element fill with
4445
4489
  * the element write's coercion, slice-clamped relative indices; answers
4446
4490
  * the receiver +1 (chaining). Never throws. */
package/src/scr_string.c CHANGED
@@ -198,6 +198,64 @@ int scr_str_cmp(ScrStr *a, ScrStr *b) {
198
198
  return a->len < b->len ? -1 : (a->len > b->len ? 1 : 0);
199
199
  }
200
200
 
201
+ /* UTF-16 code-unit comparison over well-formed UTF-8 (the default
202
+ * Array.sort/toSorted and URLSearchParams.sort order). Byte order ALMOST
203
+ * matches — the exception is U+E000..U+FFFF (3-byte UTF-8, single high code
204
+ * units) vs supplementary code points (4-byte UTF-8, surrogate pairs
205
+ * 0xD800..0xDFFF): bytes put the 4-byte form last, code units put it first.
206
+ * Decode code points and compare their leading UTF-16 units. */
207
+ static uint32_t scr_str_lead_u16(const unsigned char *s, size_t len,
208
+ size_t *adv) {
209
+ unsigned char b = s[0];
210
+ uint32_t cp;
211
+ size_t n;
212
+ if (b < 0x80) {
213
+ cp = b; n = 1;
214
+ } else if ((b & 0xe0) == 0xc0) {
215
+ cp = b & 0x1fu; n = 2;
216
+ } else if ((b & 0xf0) == 0xe0) {
217
+ cp = b & 0x0fu; n = 3;
218
+ } else {
219
+ cp = b & 0x07u; n = 4;
220
+ }
221
+ if (n > len) n = len; /* defensive: strings are well-formed by contract */
222
+ for (size_t i = 1; i < n; i++) cp = (cp << 6) | (s[i] & 0x3fu);
223
+ *adv = n;
224
+ if (cp >= 0x10000) return 0xd800 + ((cp - 0x10000) >> 10);
225
+ return cp;
226
+ }
227
+
228
+ int scr_str_cmp_u16(ScrStr *a, ScrStr *b) {
229
+ size_t ia = 0, ib = 0;
230
+ while (ia < a->len && ib < b->len) {
231
+ size_t na, nb;
232
+ uint32_t ua = scr_str_lead_u16(
233
+ (const unsigned char *)a->data + ia, a->len - ia, &na);
234
+ uint32_t ub = scr_str_lead_u16(
235
+ (const unsigned char *)b->data + ib, b->len - ib, &nb);
236
+ if (ua != ub) return ua < ub ? -1 : 1;
237
+ /* Equal leading units: equal whole code points (both single units or
238
+ * both pairs with equal highs — lows only differ if cps differ). */
239
+ if (na == 4 && nb == 4) {
240
+ uint32_t cpa = 0, cpb = 0;
241
+ for (size_t i = 0; i < 4; i++) {
242
+ cpa = (cpa << 6) |
243
+ (i == 0 ? (unsigned char)a->data[ia] & 0x07u
244
+ : (unsigned char)a->data[ia + i] & 0x3fu);
245
+ cpb = (cpb << 6) |
246
+ (i == 0 ? (unsigned char)b->data[ib] & 0x07u
247
+ : (unsigned char)b->data[ib + i] & 0x3fu);
248
+ }
249
+ if (cpa != cpb) return cpa < cpb ? -1 : 1;
250
+ }
251
+ ia += na;
252
+ ib += nb;
253
+ }
254
+ if (ia < a->len) return 1;
255
+ if (ib < b->len) return -1;
256
+ return 0;
257
+ }
258
+
201
259
  /* ── interned strings ─────────────────────────────────────────────────
202
260
  * The empty string and every single-character ASCII string are immortal
203
261
  * statics (same layout the emitter uses for literals): charAt/slice churn
@@ -1272,4 +1330,3 @@ ScrStr *scr_str_decode_uri_component(ScrStr *s) {
1272
1330
  }
1273
1331
  return out;
1274
1332
  }
1275
-
@@ -467,57 +467,6 @@ bool scr_sp_has_value(ScrSearchParams *sp, ScrStr *name, ScrStr *value) {
467
467
  return false;
468
468
  }
469
469
 
470
- /* UTF-16 code-unit comparison over well-formed UTF-8 (the spec's sort
471
- * order). Byte order ALMOST matches — the exception is U+E000..U+FFFF
472
- * (3-byte UTF-8, single high code units) vs supplementary code points
473
- * (4-byte UTF-8, surrogate pairs 0xD800..0xDFFF): bytes put the 4-byte
474
- * form last, code units put it first. Decode code points and compare
475
- * their leading UTF-16 units. */
476
- static uint32_t sp_lead_unit(const unsigned char *s, size_t len, size_t *adv) {
477
- unsigned char b = s[0];
478
- uint32_t cp;
479
- size_t n;
480
- if (b < 0x80) {
481
- cp = b; n = 1;
482
- } else if ((b & 0xe0) == 0xc0) {
483
- cp = b & 0x1fu; n = 2;
484
- } else if ((b & 0xf0) == 0xe0) {
485
- cp = b & 0x0fu; n = 3;
486
- } else {
487
- cp = b & 0x07u; n = 4;
488
- }
489
- if (n > len) n = len; /* defensive: strings are well-formed by contract */
490
- for (size_t i = 1; i < n; i++) cp = (cp << 6) | (s[i] & 0x3fu);
491
- *adv = n;
492
- if (cp >= 0x10000) return 0xd800 + ((cp - 0x10000) >> 10); /* high surrogate leads */
493
- return cp;
494
- }
495
-
496
- static int sp_cmp_u16(const ScrStr *a, const ScrStr *b) {
497
- size_t ia = 0, ib = 0;
498
- while (ia < a->len && ib < b->len) {
499
- size_t na, nb;
500
- uint32_t ua = sp_lead_unit((const unsigned char *)a->data + ia, a->len - ia, &na);
501
- uint32_t ub = sp_lead_unit((const unsigned char *)b->data + ib, b->len - ib, &nb);
502
- if (ua != ub) return ua < ub ? -1 : 1;
503
- /* Equal LEADING units: equal whole code points (both single units or
504
- * both pairs with equal highs — lows only differ if cps differ). */
505
- if (na == 4 && nb == 4) {
506
- uint32_t cpa = 0, cpb = 0;
507
- for (size_t i = 0; i < 4; i++) {
508
- cpa = (cpa << 6) | ((i == 0 ? a->data[ia] & 0x07 : a->data[ia + i] & 0x3f));
509
- cpb = (cpb << 6) | ((i == 0 ? b->data[ib] & 0x07 : b->data[ib + i] & 0x3f));
510
- }
511
- if (cpa != cpb) return cpa < cpb ? -1 : 1;
512
- }
513
- ia += na;
514
- ib += nb;
515
- }
516
- if (ia < a->len) return 1;
517
- if (ib < b->len) return -1;
518
- return 0;
519
- }
520
-
521
470
  /* Stable insertion sort by name (lists are small; equal names keep their
522
471
  * relative order — the spec's stability requirement). */
523
472
  void scr_sp_sort(ScrSearchParams *sp) {
@@ -525,7 +474,7 @@ void scr_sp_sort(ScrSearchParams *sp) {
525
474
  ScrStr *n = sp->names[i];
526
475
  ScrStr *v = sp->vals[i];
527
476
  size_t j = i;
528
- while (j > 0 && sp_cmp_u16(sp->names[j - 1], n) > 0) {
477
+ while (j > 0 && scr_str_cmp_u16(sp->names[j - 1], n) > 0) {
529
478
  sp->names[j] = sp->names[j - 1];
530
479
  sp->vals[j] = sp->vals[j - 1];
531
480
  j--;