@scriptc/runtime 0.0.12 → 0.0.14

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.12",
3
+ "version": "0.0.14",
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,6 +106,10 @@ 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
110
+ * 'unhandledRejection' listeners — a handler attached after that is
111
+ * Node's 'rejectionHandled' moment (scr_prom_observe below). */
112
+ bool reported_unhandled;
109
113
  };
110
114
 
111
115
  #ifdef SCR_RC_AUDIT
@@ -187,6 +191,35 @@ ScrPromise *scr_promise_new(void) {
187
191
  return p;
188
192
  }
189
193
 
194
+ /* The 'rejectionHandled' hook (scr_async_dyn.c installs it at listener
195
+ * registration — the scr_urj_deliver_fn pattern, so listener-free
196
+ * binaries keep their size class): called when a promise the loop-end
197
+ * report already delivered as unhandled gains a handler. */
198
+ void (*scr_rjh_notify_fn)(ScrPromise *p) = NULL;
199
+
200
+ /* Every handler attach funnels here: mark the rejection observed, and
201
+ * fire Node's 'rejectionHandled' when the attach arrived AFTER the
202
+ * report delivered this promise to 'unhandledRejection' listeners (the
203
+ * model's one late-handling window — earlier handling keeps the promise
204
+ * out of the report entirely). The flag clears on the first attach, so
205
+ * one report fires at most one 'rejectionHandled' — Node's pairing. */
206
+ static void scr_prom_observe(ScrPromise *p) {
207
+ p->rejection_observed = true;
208
+ if (p->reported_unhandled) {
209
+ p->reported_unhandled = false;
210
+ if (scr_rjh_notify_fn != NULL) scr_rjh_notify_fn(p);
211
+ }
212
+ }
213
+
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. */
219
+ void scr_promise_mark_handled(ScrPromise *p) {
220
+ if (p->state == SCR_PROM_REJECTED) scr_prom_observe(p);
221
+ }
222
+
190
223
  ScrPromise *scr_promise_retain(ScrPromise *p) {
191
224
  if (p && p->rc != SIZE_MAX) {
192
225
  p->rc++;
@@ -855,7 +888,7 @@ static void scr_promise_settle_from(ScrPromise *dst, ScrPromise *src) {
855
888
  }
856
889
  scr_promise_settle_wake(dst);
857
890
  }
858
- if (src->state == SCR_PROM_REJECTED) src->rejection_observed = true;
891
+ if (src->state == SCR_PROM_REJECTED) scr_prom_observe(src);
859
892
  }
860
893
 
861
894
  /* The emitted same-type Promise.race adapter (see raceAdapterFor). */
@@ -1201,7 +1234,7 @@ void scr_await_hop(void) { scr_await_yield(); }
1201
1234
  static bool scr_await_settled(ScrPromise *p) {
1202
1235
  if (p->state != SCR_PROM_PENDING) scr_await_yield();
1203
1236
  while (p->state == SCR_PROM_PENDING) scr_await_park(p);
1204
- p->rejection_observed = true;
1237
+ scr_prom_observe(p);
1205
1238
  if (p->state == SCR_PROM_REJECTED) {
1206
1239
  switch (p->payload_kind) {
1207
1240
  case SCR_EXC_F64: scr_throw_f64(p->f64); break;
@@ -2076,8 +2109,12 @@ bool scr_report_unhandled_rejections(void) {
2076
2109
  if (scr_urj_deliver_fn != NULL) {
2077
2110
  /* Listener dispatch (scr_async_dyn.c installed the hook at
2078
2111
  * registration): the event handles it, like Node — per entry,
2079
- * no report, exit 0. A listener throw is the uncaught crash. */
2112
+ * no report, exit 0. A listener throw is the uncaught crash.
2113
+ * reported_unhandled arms the 'rejectionHandled' window: a
2114
+ * handler the listener itself attaches fires the sibling event
2115
+ * (scr_prom_observe). */
2080
2116
  p->rejection_observed = true;
2117
+ p->reported_unhandled = true;
2081
2118
  if (!scr_urj_deliver_fn(p)) crashed = true;
2082
2119
  } else if (!any) {
2083
2120
  any = true;
@@ -2118,6 +2155,14 @@ bool scr_report_unhandled_rejections(void) {
2118
2155
  bool island = scr_island_rejections_fn(!any);
2119
2156
  any = any || island;
2120
2157
  }
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
+ }
2121
2166
  /* main returns 1 on a reported rejection — the 'exit' listeners (atexit)
2122
2167
  * must see that code, like Node's. */
2123
2168
  if (any) scr_exit_code_note(1);
@@ -146,55 +146,166 @@ ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args) {
146
146
  return scr_als_call_in(scr_als_enter_absent(id), fn, args);
147
147
  }
148
148
 
149
- static ScrDyn **scr_urj_listeners = NULL;
149
+ /* The two rejection-event registries share one shape: listeners with a
150
+ * `once` flag (auto-removed after one delivery, Node's once) and
151
+ * identity-based removal (the offWarning stance). */
152
+ typedef struct {
153
+ ScrDyn *fn;
154
+ bool once;
155
+ } ScrRejListener;
156
+
157
+ static ScrRejListener *scr_urj_listeners = NULL;
150
158
  static size_t scr_nurj = 0, scr_urj_cap = 0;
159
+ static ScrRejListener *scr_rjh_listeners = NULL;
160
+ static size_t scr_nrjh = 0, scr_rjh_cap = 0;
151
161
 
152
162
  static void scr_urj_teardown(void) {
153
- for (size_t i = 0; i < scr_nurj; i++) scr_dyn_release(scr_urj_listeners[i]);
163
+ for (size_t i = 0; i < scr_nurj; i++) scr_dyn_release(scr_urj_listeners[i].fn);
154
164
  free(scr_urj_listeners);
155
165
  scr_urj_listeners = NULL;
156
166
  scr_nurj = scr_urj_cap = 0;
157
167
  }
158
168
 
159
- static bool scr_urj_dispatch(ScrPromise *p);
169
+ static void scr_rjh_teardown(void) {
170
+ for (size_t i = 0; i < scr_nrjh; i++) scr_dyn_release(scr_rjh_listeners[i].fn);
171
+ free(scr_rjh_listeners);
172
+ scr_rjh_listeners = NULL;
173
+ scr_nrjh = scr_rjh_cap = 0;
174
+ }
160
175
 
161
- void scr_process_on_unhandled_rejection(ScrDyn *fn) {
162
- if (fn->kind != SCR_DYN_FUNC) {
163
- const char *msg = "The \"listener\" argument must be of type function";
164
- scr_throw_error_msg_code(SCR_ERR_TYPE, msg, strlen(msg), "ERR_INVALID_ARG_TYPE");
165
- return;
176
+ static bool scr_urj_dispatch(ScrPromise *p);
177
+ static void scr_rjh_dispatch(ScrPromise *p);
178
+
179
+ /* Node's ERR_INVALID_ARG_TYPE for a non-function listener; true when the
180
+ * value is fine. */
181
+ static bool scr_rej_check_listener(ScrDyn *fn) {
182
+ if (fn->kind == SCR_DYN_FUNC) return true;
183
+ const char *msg = "The \"listener\" argument must be of type function";
184
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg, strlen(msg), "ERR_INVALID_ARG_TYPE");
185
+ return false;
186
+ }
187
+
188
+ static void scr_rej_push(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once) {
189
+ if (*n == *cap) {
190
+ *cap = *cap ? *cap * 2 : 4;
191
+ *list = realloc(*list, *cap * sizeof **list);
192
+ if (!*list) scr_ad_oom();
166
193
  }
167
- if (scr_nurj == scr_urj_cap) {
168
- scr_urj_cap = scr_urj_cap ? scr_urj_cap * 2 : 4;
169
- scr_urj_listeners = realloc(scr_urj_listeners, scr_urj_cap * sizeof *scr_urj_listeners);
170
- if (!scr_urj_listeners) scr_ad_oom();
194
+ (*list)[(*n)++] = (ScrRejListener){scr_dyn_retain(fn), once};
195
+ }
196
+
197
+ static void scr_rej_remove(ScrRejListener *list, size_t *n, ScrDyn *fn) {
198
+ for (size_t i = 0; i < *n; i++) {
199
+ ScrDyn *l = list[i].fn;
200
+ bool same = l == fn || (l->kind == SCR_DYN_FUNC && fn->kind == SCR_DYN_FUNC &&
201
+ l->v.fn.clo == fn->v.fn.clo);
202
+ if (same) {
203
+ scr_dyn_release(l);
204
+ memmove(list + i, list + i + 1, (*n - i - 1) * sizeof *list);
205
+ (*n)--;
206
+ return;
207
+ }
171
208
  }
172
- if (scr_nurj == 0) {
209
+ }
210
+
211
+ /* The hooks arm exactly while their registry is non-empty: Node with
212
+ * every listener removed (off, or once-consumed) reverts to the default
213
+ * report/silence, and the report loop consults the hook per promise. */
214
+ static void scr_urj_sync_hook(void) {
215
+ scr_urj_deliver_fn = scr_nurj > 0 ? scr_urj_dispatch : NULL;
216
+ }
217
+
218
+ static void scr_rjh_sync_hook(void) {
219
+ scr_rjh_notify_fn = scr_nrjh > 0 ? scr_rjh_dispatch : NULL;
220
+ }
221
+
222
+ void scr_process_on_unhandled_rejection(ScrDyn *fn, bool once) {
223
+ if (!scr_rej_check_listener(fn)) return;
224
+ static bool teardown_armed = false;
225
+ if (!teardown_armed) {
226
+ teardown_armed = true;
173
227
  atexit(scr_urj_teardown);
174
- scr_urj_deliver_fn = scr_urj_dispatch; /* arm the loop-end report */
175
228
  }
176
- scr_urj_listeners[scr_nurj++] = scr_dyn_retain(fn);
229
+ scr_rej_push(&scr_urj_listeners, &scr_nurj, &scr_urj_cap, fn, once);
230
+ scr_urj_sync_hook();
231
+ }
232
+
233
+ void scr_process_off_unhandled_rejection(ScrDyn *fn) {
234
+ scr_rej_remove(scr_urj_listeners, &scr_nurj, fn);
235
+ scr_urj_sync_hook();
236
+ }
237
+
238
+ void scr_process_on_rejection_handled(ScrDyn *fn, bool once) {
239
+ if (!scr_rej_check_listener(fn)) return;
240
+ static bool teardown_armed = false;
241
+ if (!teardown_armed) {
242
+ teardown_armed = true;
243
+ atexit(scr_rjh_teardown);
244
+ }
245
+ scr_rej_push(&scr_rjh_listeners, &scr_nrjh, &scr_rjh_cap, fn, once);
246
+ scr_rjh_sync_hook();
247
+ }
248
+
249
+ void scr_process_off_rejection_handled(ScrDyn *fn) {
250
+ scr_rej_remove(scr_rjh_listeners, &scr_nrjh, fn);
251
+ scr_rjh_sync_hook();
252
+ }
253
+
254
+ /* One registry pass: call every listener with `args`, removing once-
255
+ * listeners BEFORE their call (Node's once removes at dispatch, so a
256
+ * re-registration inside the listener sticks). The registry is accessed
257
+ * THROUGH its pointers per step — a listener that registers can realloc
258
+ * the array mid-pass. Returns false when a listener threw (the caller's
259
+ * crash path). */
260
+ static bool scr_rej_fire(ScrRejListener **list, size_t *n, ScrDyn **args, size_t argc) {
261
+ size_t i = 0;
262
+ bool ok = true;
263
+ while (i < *n && ok) {
264
+ /* Own +1 across the call: a once-removal (here) or the listener
265
+ * removing itself (off inside the body) must not free a running
266
+ * function. */
267
+ ScrDyn *fn = scr_dyn_retain((*list)[i].fn);
268
+ if ((*list)[i].once) {
269
+ scr_dyn_release((*list)[i].fn);
270
+ memmove(*list + i, *list + i + 1, (*n - i - 1) * sizeof **list);
271
+ (*n)--;
272
+ } else {
273
+ i++;
274
+ }
275
+ ScrDyn *r = scr_dyn_call(fn, args, argc, "listener");
276
+ if (r == NULL) ok = false;
277
+ else scr_dyn_release(r);
278
+ scr_dyn_release(fn);
279
+ }
280
+ return ok;
177
281
  }
178
282
 
179
283
  /* Dispatch one unhandled rejection to the registered listeners —
180
284
  * (reason, promise), Node's signature. A listener throw is an uncaught
181
- * exception (Node crashes there too): print it and exit 1. Returns
182
- * false on that crash path. */
285
+ * exception (Node crashes there too): the caller prints it and exits 1.
286
+ * A once-consumed-to-empty registry disarms the hook on the way out, so
287
+ * the report's NEXT promise takes the default print — Node's
288
+ * listener-less behavior. */
183
289
  static bool scr_urj_dispatch(ScrPromise *p) {
184
290
  ScrDyn *reason = scr_promise_reason_dyn(p);
185
291
  ScrDyn *boxed = scr_dyn_new_promise(p);
186
292
  ScrDyn *args[2] = {reason, boxed};
187
- bool ok = true;
188
- for (size_t i = 0; i < scr_nurj && ok; i++) {
189
- ScrDyn *r = scr_dyn_call(scr_urj_listeners[i], args, 2, "listener");
190
- if (r == NULL) ok = false;
191
- else scr_dyn_release(r);
192
- }
293
+ bool ok = scr_rej_fire(&scr_urj_listeners, &scr_nurj, args, 2);
193
294
  scr_dyn_release(reason);
194
295
  scr_dyn_release(boxed);
296
+ scr_urj_sync_hook();
195
297
  return ok;
196
298
  }
197
299
 
300
+ /* 'rejectionHandled' delivery — (promise), Node's payload. A listener
301
+ * throw propagates as a pending exception through the attach site. */
302
+ static void scr_rjh_dispatch(ScrPromise *p) {
303
+ ScrDyn *boxed = scr_dyn_new_promise(p);
304
+ (void)scr_rej_fire(&scr_rjh_listeners, &scr_nrjh, &boxed, 1);
305
+ scr_dyn_release(boxed);
306
+ scr_rjh_sync_hook(); /* a once-consumed-to-empty registry disarms */
307
+ }
308
+
198
309
  /* `new Promise(setImmediate)` (the Node-suite early-exit shape): the
199
310
  * executor IS setImmediate, so resolve rides the immediate queue — a
200
311
  * fresh promise an armed immediate fulfills with the undefined dyn value
@@ -289,6 +400,11 @@ static void scr_dyn_then_entry(ScrFiber *self, void *ap) {
289
400
  }
290
401
 
291
402
  ScrDyn *scr_dyn_promise_then(ScrPromise *src, ScrDyn *onf, ScrDyn *onr, ScrDyn *onfin) {
403
+ /* A rejection HANDLER marks the source handled at attach (Node's
404
+ * moment; the reaction fiber's await re-marks harmlessly) — this is
405
+ * also what lets a .catch inside an 'unhandledRejection' listener fire
406
+ * 'rejectionHandled' when no fiber turn remains. */
407
+ if (onr != NULL) scr_promise_mark_handled(src);
292
408
  ScrDynThenPack *a = malloc(sizeof *a);
293
409
  if (!a) scr_ad_oom();
294
410
  a->src = scr_promise_retain(src);
@@ -671,9 +671,33 @@ ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, si
671
671
  return NULL;
672
672
  }
673
673
 
674
- if (recv->kind == SCR_DYN_BYTES && dyn_bytes_proto_real(method)) {
675
- dyn_throw_unsupported("Uint8Array", method);
676
- return NULL;
674
+ if (recv->kind == SCR_DYN_BYTES) {
675
+ ScrBytes *bytes = recv->v.bytes;
676
+ size_t blen = bytes->len;
677
+ if (dyn_name_is(method, "at")) {
678
+ double iD = dyn_index_arg(args, argc, 0, 0, what);
679
+ if (scr_exc_pending()) return NULL;
680
+ double idx = iD < 0 ? (double)blen + iD : iD;
681
+ if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined());
682
+ return scr_dyn_new_num((double)bytes->data[(size_t)idx]);
683
+ }
684
+ if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) {
685
+ /* Both COPY (no views in this runtime — the static lane's
686
+ * documented divergence for subarray/Buffer.slice); the result
687
+ * keeps the receiver's Buffer flavor. */
688
+ double startD = dyn_index_arg(args, argc, 0, 0, what);
689
+ if (scr_exc_pending()) return NULL;
690
+ double endD = dyn_index_arg(args, argc, 1, (double)blen, what);
691
+ if (scr_exc_pending()) return NULL;
692
+ ScrBytes *out = scr_bytes_slice(bytes, startD, endD);
693
+ ScrDyn *d = recv->buffer ? scr_dyn_new_buffer_copy(out) : scr_dyn_new_bytes_copy(out);
694
+ scr_bytes_release(out);
695
+ return d;
696
+ }
697
+ if (dyn_bytes_proto_real(method)) {
698
+ dyn_throw_unsupported("Uint8Array", method);
699
+ return NULL;
700
+ }
677
701
  }
678
702
 
679
703
  /* NUM/BOOL/BYTES-remainder: the name is no method of this kind — JS's