@scriptc/runtime 0.0.4 → 0.0.6

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.
@@ -0,0 +1,222 @@
1
+ /* Library mode support: the panic sink, the poisoned-library flag, the
2
+ * outbound-result arena, the reset registry, and the sink-routing trap
3
+ * funnel. Linked ONLY into library artifacts (every TU of a library archive
4
+ * compiles with -DSCR_LIB); executable builds never contain this file —
5
+ * their funnel expansion lives in scr_console.c and their session teardown
6
+ * stays atexit-driven.
7
+ *
8
+ * The profile-NAMED symbols (init, sink registration, reset, collect) are
9
+ * emitted in the program TU and delegate here, so this file stays
10
+ * profile-agnostic and the program TU carries every profile-named symbol —
11
+ * one place for the conformance symbol audit to look.
12
+ *
13
+ * State after a sink call: none is legal. The trap fired mid-operation with
14
+ * no unwinding — heap, arena, and collector state are unspecified; the library
15
+ * is POISONED. Every runtime-touching entry prologue aborts on the flag
16
+ * (deterministic, never heap corruption); recovery is process restart. */
17
+ #include "scr_runtime.h"
18
+
19
+ #ifdef SCR_LIB
20
+
21
+ #include <stdio.h>
22
+ #include <stdlib.h>
23
+
24
+ /* ── sink registration + poison ───────────────────────────────────────── */
25
+
26
+ static ScrLibSinkFn scr_library_sink = NULL;
27
+ static void *scr_library_sink_ctx = NULL;
28
+ static bool scr_library_poisoned = false;
29
+
30
+ void scr_library_set_sink(ScrLibSinkFn fn, void *ctx) {
31
+ /* Latest registration wins; re-registration is permitted before a trap.
32
+ * Deliberately NOT poison-guarded: a pure store, touching no runtime
33
+ * state — but a poisoned library's entries abort regardless of the sink. */
34
+ scr_library_sink = fn;
35
+ scr_library_sink_ctx = ctx;
36
+ }
37
+
38
+ /* ── the trap funnel, library expansion ───────────────────────────────────
39
+ * Poison first (the sink may longjmp to a host frame below the entry — the
40
+ * conforming survival pattern), then deliver exactly once, then abort:
41
+ * before registration, or if the sink returns (the ruled host-contract
42
+ * violation). The address is the funnel frame's return address — the trap
43
+ * site — 0 where the toolchain cannot supply one. */
44
+
45
+ #if defined(__GNUC__) || defined(__clang__)
46
+ #define SCR_TRAP_ADDR() ((uint64_t)(uintptr_t)__builtin_return_address(0))
47
+ #else
48
+ #define SCR_TRAP_ADDR() ((uint64_t)0)
49
+ #endif
50
+
51
+ static _Noreturn void scr_library_trap_deliver(const char *msg, size_t len, uint64_t addr) {
52
+ scr_library_poisoned = true;
53
+ if (scr_library_sink != NULL) {
54
+ scr_library_sink(scr_library_sink_ctx, (const uint8_t *)msg, len, addr);
55
+ }
56
+ abort();
57
+ }
58
+
59
+ __attribute__((noinline)) _Noreturn void scr_trap(const char *msg) {
60
+ scr_library_trap_deliver(msg, strlen(msg), SCR_TRAP_ADDR());
61
+ }
62
+
63
+ __attribute__((noinline)) _Noreturn void scr_trap_fmt(const char *fmt, ...) {
64
+ static char buf[512]; /* no malloc on the invariant-failure path */
65
+ va_list ap;
66
+ va_start(ap, fmt);
67
+ int n = vsnprintf(buf, sizeof buf, fmt, ap);
68
+ va_end(ap);
69
+ size_t len = n < 0 ? 0 : (size_t)n >= sizeof buf ? sizeof buf - 1 : (size_t)n;
70
+ scr_library_trap_deliver(buf, len, SCR_TRAP_ADDR());
71
+ }
72
+
73
+ /* ── entry prologues ──────────────────────────────────────────────────── */
74
+
75
+ void scr_library_entry(bool reset_arena) {
76
+ /* A poisoned library's entries abort deterministically — never through the
77
+ * sink again (it received its exactly-once message when the trap fired),
78
+ * never into a heap whose invariants already failed. */
79
+ if (scr_library_poisoned) abort();
80
+ if (reset_arena) scr_library_arena_reset();
81
+ }
82
+
83
+ /* ── the outbound-result arena ────────────────────────────────────────────
84
+ * Buffer-class results (string/bytes) MOVE in here; the host reads through
85
+ * borrowed pointers until the arena resets (per-entry under the auto
86
+ * posture, host-cycled when the profile declares a reset symbol; init and
87
+ * collect always reset). */
88
+
89
+ typedef struct {
90
+ void *v;
91
+ bool is_str; /* ScrStr vs ScrBytes — picks the release */
92
+ } ScrCoreArenaEnt;
93
+
94
+ static ScrCoreArenaEnt *scr_library_arena = NULL;
95
+ static size_t scr_library_arena_n = 0, scr_library_arena_cap = 0;
96
+
97
+ static void scr_library_arena_keep(void *v, bool is_str) {
98
+ if (scr_library_arena_n == scr_library_arena_cap) {
99
+ scr_library_arena_cap = scr_library_arena_cap ? scr_library_arena_cap * 2 : 16;
100
+ scr_library_arena = realloc(scr_library_arena, scr_library_arena_cap * sizeof *scr_library_arena);
101
+ if (!scr_library_arena) scr_trap("scriptc: out of memory\n");
102
+ }
103
+ scr_library_arena[scr_library_arena_n].v = v;
104
+ scr_library_arena[scr_library_arena_n].is_str = is_str;
105
+ scr_library_arena_n++;
106
+ }
107
+
108
+ void scr_library_arena_reset(void) {
109
+ for (size_t i = 0; i < scr_library_arena_n; i++) {
110
+ if (scr_library_arena[i].is_str) scr_str_release((ScrStr *)scr_library_arena[i].v);
111
+ else scr_bytes_release((ScrBytes *)scr_library_arena[i].v);
112
+ }
113
+ scr_library_arena_n = 0;
114
+ }
115
+
116
+ void scr_library_collect(void) {
117
+ scr_library_arena_reset();
118
+ scr_collect_cycles();
119
+ }
120
+
121
+ /* ── marshalling helpers (both emissions call exactly these) ──────────── */
122
+
123
+ ScrStr *scr_library_str_in(const uint8_t *p, size_t len) {
124
+ /* ptr may be NULL when len is 0 (the contract's empty-buffer form). */
125
+ return scr_str_new(len == 0 ? "" : (const char *)p, len);
126
+ }
127
+
128
+ ScrBytes *scr_library_bytes_in(const uint8_t *p, size_t len) {
129
+ ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)len);
130
+ if (b == NULL) {
131
+ /* scr_bytes_new throws only for lengths past 2^53-1 — an impossible
132
+ * host buffer; funnel the contract violation instead of a NULL deref. */
133
+ scr_exc_clear();
134
+ scr_trap("scriptc: library inbound bytes length out of range\n");
135
+ }
136
+ if (len > 0 && p != NULL) memcpy(b->data, p, len);
137
+ return b;
138
+ }
139
+
140
+ void scr_library_str_out(ScrStr *s, const uint8_t **out, size_t *out_len) {
141
+ scr_library_arena_keep(s, true);
142
+ *out = (const uint8_t *)s->data; /* NUL-terminated after len (ScrStr layout) */
143
+ *out_len = s->len;
144
+ }
145
+
146
+ void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len) {
147
+ scr_library_arena_keep(b, false);
148
+ *out = b->data; /* elem is always u8 at the boundary (v1 bytes class) */
149
+ *out_len = b->len;
150
+ }
151
+
152
+ /* ── the reset registry + session reset ─────────────────────────────────
153
+ * Where an executable's always-linked units atexit() their lazy teardowns,
154
+ * library builds register here (scr_atexit in scr_runtime.h): registered
155
+ * once, drained on EVERY reset — re-registration guards in the units stay
156
+ * satisfied because the entry persists. */
157
+
158
+ #define SCR_LIB_MAX_RESETS 32
159
+ static void (*scr_library_resets[SCR_LIB_MAX_RESETS])(void);
160
+ static size_t scr_library_nresets = 0;
161
+
162
+ void scr_library_register_reset(void (*fn)(void)) {
163
+ for (size_t i = 0; i < scr_library_nresets; i++) {
164
+ if (scr_library_resets[i] == fn) return;
165
+ }
166
+ if (scr_library_nresets == SCR_LIB_MAX_RESETS) {
167
+ scr_trap("scriptc: internal error: library reset registry overflow\n");
168
+ }
169
+ scr_library_resets[scr_library_nresets++] = fn;
170
+ }
171
+
172
+ #ifdef SCR_RC_AUDIT
173
+ extern long scr_str_live_count(void); /* scr_string.c */
174
+ extern long scr_arr_live_count(void); /* scr_array.c */
175
+ extern long scr_map_live_count(void); /* scr_map.c */
176
+ extern long scr_box_live_count(void); /* scr_closure.c */
177
+ extern long scr_closure_live_count(void); /* scr_closure.c */
178
+ extern long scr_obj_live_count(void); /* scr_object.c */
179
+ extern long scr_union_live_count(void); /* scr_union.c */
180
+ extern long scr_dyn_live_count(void); /* scr_json.c */
181
+ extern long scr_bytes_live_count(void); /* scr_bytes.c */
182
+
183
+ /* The per-session heap-emptiness assertion (the audit flavor's determinism
184
+ * seam): after a full reset the live counters must all read zero, or the
185
+ * previous session leaked. A failure is a TRAP through the sink — the
186
+ * executable audit's _Exit(99) stays exe-lane-only. */
187
+ static void scr_library_audit_zero(void) {
188
+ long strings = scr_str_live_count(), arrays = scr_arr_live_count(),
189
+ maps = scr_map_live_count(), boxes = scr_box_live_count(),
190
+ closures = scr_closure_live_count(), objects = scr_obj_live_count(),
191
+ unions = scr_union_live_count(), dyns = scr_dyn_live_count(),
192
+ bytes = scr_bytes_live_count();
193
+ if (strings != 0 || arrays != 0 || maps != 0 || boxes != 0 || closures != 0 ||
194
+ objects != 0 || unions != 0 || dyns != 0 || bytes != 0) {
195
+ scr_trap_fmt(
196
+ "scriptc LIBRARY RC AUDIT FAILED: %ld heap string(s), %ld array(s), "
197
+ "%ld map(s), %ld box(es), %ld closure(s), %ld object(s), "
198
+ "%ld union(s), %ld dyn value(s), %ld bytes value(s) live across re-init\n",
199
+ strings, arrays, maps, boxes, closures, objects, unions, dyns, bytes);
200
+ }
201
+ }
202
+ #endif /* SCR_RC_AUDIT */
203
+
204
+ extern void scr_lib_session_cleanup(void); /* scr_lib.c: the interned process values */
205
+
206
+ void scr_library_reset(void) {
207
+ /* Called by the generated init entry AFTER the program TU released and
208
+ * zeroed its globals (run-once guards included) — the same order the
209
+ * executable exit path runs: library cleanup → cycle collection → RC
210
+ * audit. Everything here is re-runnable; the first call is a cheap
211
+ * no-op pass. */
212
+ scr_exc_clear();
213
+ scr_library_arena_reset();
214
+ for (size_t i = 0; i < scr_library_nresets; i++) scr_library_resets[i]();
215
+ scr_lib_session_cleanup();
216
+ scr_collect_cycles();
217
+ #ifdef SCR_RC_AUDIT
218
+ scr_library_audit_zero();
219
+ #endif
220
+ }
221
+
222
+ #endif /* SCR_LIB */
package/src/scr_map.c CHANGED
@@ -31,8 +31,7 @@ long scr_map_live_count(void) { return scr_live_maps; }
31
31
  #define SCR_MAP_EMPTY SIZE_MAX
32
32
 
33
33
  static void scr_map_oom(void) {
34
- fputs("scriptc: out of memory\n", stderr);
35
- abort();
34
+ scr_trap("scriptc: out of memory\n");
36
35
  }
37
36
 
38
37
  /* ── key normalization + hashing (SameValueZero) ───────────────────────
@@ -462,8 +461,7 @@ bool scr_map_iter_live(const ScrMap *m, double i) {
462
461
 
463
462
  static const ScrMapEntry *scr_map_iter_at(const ScrMap *m, double i) {
464
463
  if (!(i >= 0) || i >= (double)m->nentries || !m->entries[(size_t)i].live) {
465
- fputs("scriptc: internal error: map iteration index out of range\n", stderr);
466
- abort();
464
+ scr_trap("scriptc: internal error: map iteration index out of range\n");
467
465
  }
468
466
  return &m->entries[(size_t)i];
469
467
  }
package/src/scr_path.c CHANGED
@@ -37,8 +37,7 @@ static void pb_init(PathBuf *b) {
37
37
  b->len = 0;
38
38
  b->data = malloc(b->cap);
39
39
  if (!b->data) {
40
- fputs("scriptc: out of memory\n", stderr);
41
- abort();
40
+ scr_trap("scriptc: out of memory\n");
42
41
  }
43
42
  }
44
43
 
@@ -47,8 +46,7 @@ static void pb_reserve(PathBuf *b, size_t extra) {
47
46
  while (b->len + extra > b->cap) b->cap *= 2;
48
47
  char *grown = realloc(b->data, b->cap);
49
48
  if (!grown) {
50
- fputs("scriptc: out of memory\n", stderr);
51
- abort();
49
+ scr_trap("scriptc: out of memory\n");
52
50
  }
53
51
  b->data = grown;
54
52
  }
@@ -226,8 +224,7 @@ ScrStr *scr_path_join(ScrArr *parts) {
226
224
  static void scr_path_cwd(PathBuf *out) {
227
225
  char buf[4096];
228
226
  if (!getcwd(buf, sizeof buf)) {
229
- fputs("scriptc: path.resolve: getcwd failed\n", stderr);
230
- abort();
227
+ scr_trap("scriptc: path.resolve: getcwd failed\n");
231
228
  }
232
229
  pb_append(out, buf, strlen(buf));
233
230
  }
@@ -984,8 +981,7 @@ ScrStr *scr_path_win32_relative(ScrStr *from, ScrStr *to) {
984
981
  char *flow = malloc(rfrom->len ? rfrom->len : 1);
985
982
  char *tlow = malloc(rto->len ? rto->len : 1);
986
983
  if (!flow || !tlow) {
987
- fputs("scriptc: out of memory\n", stderr);
988
- abort();
984
+ scr_trap("scriptc: out of memory\n");
989
985
  }
990
986
  for (size_t k = 0; k < rfrom->len; k++) flow[k] = scr_path_w32_lower(rfrom->data[k]);
991
987
  for (size_t k = 0; k < rto->len; k++) tlow[k] = scr_path_w32_lower(rto->data[k]);
package/src/scr_regex.c CHANGED
@@ -37,8 +37,7 @@
37
37
  #include "libregexp.h"
38
38
 
39
39
  static void scr_regex_oom(void) {
40
- fputs("scriptc: out of memory\n", stderr);
41
- abort();
40
+ scr_trap("scriptc: out of memory\n");
42
41
  }
43
42
 
44
43
  /* ── libregexp host hooks ─────────────────────────────────────────────
@@ -102,7 +101,7 @@ static void scr_note_compiled(ScrRegex *re) {
102
101
  scr_compiled = realloc(scr_compiled, scr_compiled_cap * sizeof *scr_compiled);
103
102
  if (!scr_compiled) scr_regex_oom();
104
103
  }
105
- if (scr_compiled_len == 0) atexit(scr_regex_free_bytecodes);
104
+ if (scr_compiled_len == 0) scr_atexit(scr_regex_free_bytecodes);
106
105
  scr_compiled[scr_compiled_len++] = re;
107
106
  }
108
107
 
@@ -119,9 +118,8 @@ static int scr_lre_flags(const ScrStr *flags) {
119
118
  case 'u': mask |= LRE_FLAG_UNICODE; break;
120
119
  case 'y': mask |= LRE_FLAG_STICKY; break;
121
120
  default:
122
- fprintf(stderr, "scriptc: internal error: unexpected regex flag '%c'\n",
123
- flags->data[i]);
124
- abort();
121
+ scr_trap_fmt("scriptc: internal error: unexpected regex flag '%c'\n",
122
+ flags->data[i]);
125
123
  }
126
124
  }
127
125
  return mask;
@@ -197,9 +195,8 @@ static uint8_t *scr_regex_bc(ScrRegex *re) {
197
195
  * tsc's parser has already caught plain syntax errors, so this is
198
196
  * rare). */
199
197
  fflush(stdout);
200
- fprintf(stderr, "scriptc: SyntaxError: Invalid regular expression: /%s/%s: %s\n",
201
- re->source->data, re->flags->data, error_msg);
202
- abort();
198
+ scr_trap_fmt("scriptc: SyntaxError: Invalid regular expression: /%s/%s: %s\n",
199
+ re->source->data, re->flags->data, error_msg);
203
200
  }
204
201
  re->bc = bc;
205
202
  scr_note_compiled(re);
@@ -315,8 +312,7 @@ static int scr_exec(uint8_t **capture, const uint8_t *bc, const uint16_t *u,
315
312
  int rc = lre_exec(capture, bc, (const uint8_t *)u, index, len, 1, lre_opaque());
316
313
  if (rc < 0) {
317
314
  fflush(stdout);
318
- fputs("scriptc: regular expression execution failed\n", stderr);
319
- abort();
315
+ scr_trap("scriptc: regular expression execution failed\n");
320
316
  }
321
317
  return rc;
322
318
  }
@@ -351,11 +347,9 @@ bool scr_regex_test(ScrRegex *re, ScrStr *s) {
351
347
  * iteration this slice does not model (the frontend rejects the sites
352
348
  * it can see; values that flow through variables land here). */
353
349
  fflush(stdout);
354
- fprintf(stderr,
355
- "scriptc: test() on a regex with the 'g' or 'y' flag is not "
356
- "supported (stateful lastIndex); drop the flag, or use "
357
- "replace/replaceAll/split\n");
358
- abort();
350
+ scr_trap("scriptc: test() on a regex with the 'g' or 'y' flag is not "
351
+ "supported (stateful lastIndex); drop the flag, or use "
352
+ "replace/replaceAll/split\n");
359
353
  }
360
354
  int len;
361
355
  uint16_t *u = scr_to_utf16(s, &len);
@@ -379,11 +373,9 @@ ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re) {
379
373
  uint8_t *bc = scr_regex_bc(re);
380
374
  if (lre_get_flags(bc) & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) {
381
375
  fflush(stdout);
382
- fprintf(stderr,
383
- "scriptc: match() on a regex with the 'g' or 'y' flag is not "
384
- "supported (an every-match array is a different shape); drop the "
385
- "flag, or use replace/replaceAll/split\n");
386
- abort();
376
+ scr_trap("scriptc: match() on a regex with the 'g' or 'y' flag is not "
377
+ "supported (an every-match array is a different shape); drop the "
378
+ "flag, or use replace/replaceAll/split\n");
387
379
  }
388
380
  int len;
389
381
  uint16_t *u = scr_to_utf16(s, &len);
@@ -803,7 +795,7 @@ static bool scr_assert_regex_hits(ScrRegex *re, ScrStr *s) {
803
795
  static ScrStr *scr_assert_regex_render(ScrRegex *re) {
804
796
  size_t cap = re->source->len + re->flags->len + 2;
805
797
  char *buf = malloc(cap);
806
- if (!buf) abort();
798
+ if (!buf) scr_trap("scriptc: out of memory\n");
807
799
  size_t n = 0;
808
800
  buf[n++] = '/';
809
801
  memcpy(buf + n, re->source->data, re->source->len);
@@ -831,7 +823,7 @@ static void scr_assert_regex_input_fail(bool negated, ScrRegex *re, ScrStr *inpu
831
823
  const char *mid = ". Input:\n\n";
832
824
  size_t cap = strlen(head) + rre->len + strlen(mid) + insp->len + 1;
833
825
  char *buf = malloc(cap);
834
- if (!buf) abort();
826
+ if (!buf) scr_trap("scriptc: out of memory\n");
835
827
  size_t n = 0;
836
828
  memcpy(buf + n, head, strlen(head));
837
829
  n += strlen(head);
@@ -924,8 +916,7 @@ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags) {
924
916
  }
925
917
  ScrRegex *re = calloc(1, sizeof *re);
926
918
  if (!re) {
927
- fputs("scriptc: out of memory\n", stderr);
928
- abort();
919
+ scr_trap("scriptc: out of memory\n");
929
920
  }
930
921
  re->rc = 1;
931
922
  re->source = pattern->len > 0 ? scr_str_retain(pattern) : scr_str_new("(?:)", 4);
package/src/scr_runtime.h CHANGED
@@ -39,6 +39,81 @@ char *strcasestr(const char *hay, const char *needle);
39
39
  * flush-at-exit, RC audit registration (when built with -DSCR_RC_AUDIT). */
40
40
  void scr_init(void);
41
41
 
42
+ /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
43
+ * Every unrecoverable runtime trap — OOM, semantic range traps, internal-
44
+ * invariant failures — funnels through this pair instead of open-coded
45
+ * fputs/fprintf + abort. Executable builds expand to exactly the historical
46
+ * behavior (the message's bytes on stderr, then abort — the default lane
47
+ * must not change by a byte). Library builds (-DSCR_LIB) route the message to
48
+ * the host-registered panic sink and abort only as the last resort: before
49
+ * registration, or if the sink returns (the ruled host-contract violation —
50
+ * a conforming sink longjmps to a host frame BELOW the entry, never back
51
+ * into library frames). Messages keep their trailing newline in both lanes so
52
+ * the funnel is a pure indirection over identical bytes. */
53
+ _Noreturn void scr_trap(const char *msg);
54
+ _Noreturn void scr_trap_fmt(const char *fmt, ...);
55
+
56
+ /* ── library mode (scr_library.c, linked only into library artifacts) ─────
57
+ * A library artifact has no main, no event loop, no signal handlers, no
58
+ * atexit registrations, and never touches host stdio modes or buffering:
59
+ * initialization runs inside the profile-named init entry (re-runnable
60
+ * deterministically), traps route to the sink above, and buffer-class
61
+ * results live in a library-owned result arena. Everything here compiles only
62
+ * under -DSCR_LIB; executable builds never contain it. */
63
+ #ifdef SCR_LIB
64
+ typedef struct ScrStr ScrStr; /* full definitions below (C11 repeat) */
65
+ typedef struct ScrBytes ScrBytes;
66
+ /* The host's panic sink: msg is UTF-8, valid only for the duration of the
67
+ * call; address is the trap site's return address (0 when the toolchain
68
+ * cannot supply one); ctx is the registration's opaque pointer. The sink
69
+ * must not call back into any library entry and must not unwind or longjmp
70
+ * back into library frames. */
71
+ typedef void (*ScrLibSinkFn)(void *ctx, const uint8_t *msg, size_t msg_len,
72
+ uint64_t address);
73
+ void scr_library_set_sink(ScrLibSinkFn fn, void *ctx); /* latest wins */
74
+
75
+ /* Entry prologue: aborts deterministically when the library is poisoned (a
76
+ * trap already fired — no profile entry may run again; recovery is process
77
+ * restart). reset_arena additionally drops the result arena (the
78
+ * auto-reset posture, and the reset/collect entries' shared body). */
79
+ void scr_library_entry(bool reset_arena);
80
+ void scr_library_arena_reset(void);
81
+ /* The mode-provided collect entry's body: arena reset + a full cycle
82
+ * collection (snapshot-invariant by construction — collection frees only
83
+ * unreachable cycles). */
84
+ void scr_library_collect(void);
85
+
86
+ /* Full session reset, called by the generated init entry AFTER the program
87
+ * TU released and zeroed its globals: pending-exception clear, arena
88
+ * reset, the reset registry's drains (the units' repointed atexit halves),
89
+ * the library's interned process values, a cycle collection, and — under
90
+ * SCR_RC_AUDIT — the zero-live-heap assertion (a failure is a trap through
91
+ * the sink, never _Exit). */
92
+ void scr_library_reset(void);
93
+ /* Where a unit would atexit() a lazy teardown, library builds register it
94
+ * here instead (called on every scr_library_reset, registered once). */
95
+ void scr_library_register_reset(void (*fn)(void));
96
+ /* An escaped exception at an entry boundary: renders the same "Uncaught
97
+ * ..." text the executable epilogue prints, releases the payload, and
98
+ * routes the text through the trap funnel. No-op when nothing is pending.
99
+ * Defined in scr_exception.c (it owns the cell). */
100
+ void scr_library_check_exc(void);
101
+
102
+ /* Marshalling helpers the generated wrappers call (both emissions share
103
+ * these bodies, which is how the two lanes stay identical by
104
+ * construction). Inbound is borrowed-and-copied; outbound values MOVE into
105
+ * the result arena and stay valid until the next arena reset. String
106
+ * results are NUL-terminated after *out_len bytes (ScrStr's layout). */
107
+ ScrStr *scr_library_str_in(const uint8_t *p, size_t len); /* +1 */
108
+ ScrBytes *scr_library_bytes_in(const uint8_t *p, size_t len); /* +1, u8 */
109
+ void scr_library_str_out(ScrStr *s, const uint8_t **out, size_t *out_len);
110
+ void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len);
111
+
112
+ #define scr_atexit(fn) scr_library_register_reset(fn)
113
+ #else
114
+ #define scr_atexit(fn) atexit(fn)
115
+ #endif /* SCR_LIB */
116
+
42
117
  /* ── cycle collection (scr_cycle.c) ───────────────────────────────────
43
118
  * Reference counting alone cannot free cycles, so every object that can
44
119
  * participate in one — capture boxes, heap closures, unions, promises, and
@@ -2842,14 +2917,48 @@ extern void scr_dyn_listener_fire_err(ScrClosure *cb, ScrStr *msg);
2842
2917
 
2843
2918
  /* Output buffer for the compiler-emitted type-directed JSON serializers.
2844
2919
  * Value-typed and stack-allocated by the emitted code; _finish hands the
2845
- * bytes over as a +1 ScrStr and frees the buffer storage. */
2920
+ * bytes over as a +1 ScrStr and frees the buffer storage (including the
2921
+ * circular-detection stack below). */
2922
+ struct ScrJsonSeenEnt;
2846
2923
  typedef struct {
2847
2924
  char *data;
2848
2925
  size_t len;
2849
2926
  size_t cap;
2927
+ /* Circular-structure detection for RECURSIVE record types (cyclic
2928
+ * values must throw Node's exact TypeError, never recurse forever):
2929
+ * the stack of container values currently being serialized, each with
2930
+ * its outgoing edge label (the member being written). Only walkers over
2931
+ * cycle-CAPABLE types (the collector-fixpoint set) maintain it; acyclic
2932
+ * types keep the zero-cost path. */
2933
+ struct ScrJsonSeenEnt *seen;
2934
+ size_t seen_len;
2935
+ size_t seen_cap;
2850
2936
  } ScrJsonBuf;
2851
2937
 
2852
2938
  void scr_jb_init(ScrJsonBuf *b);
2939
+ /* Push a container onto the circular-detection stack before serializing
2940
+ * its members. If `v` is already ON the stack, throws V8's exact
2941
+ * "Converting circular structure to JSON" TypeError (the --> starting at /
2942
+ * |property/index hops/--- closes the circle rendering, ellipsis rules
2943
+ * included) and returns false — the caller returns immediately; the
2944
+ * emitted stringify site runs the pending check. `is_array` picks the
2945
+ * constructor name in the message ('Array' for arrays AND tuple shapes —
2946
+ * their JS values are arrays — 'Object' for records). */
2947
+ bool scr_jb_enter(ScrJsonBuf *b, const void *v, bool is_array);
2948
+ void scr_jb_leave(ScrJsonBuf *b);
2949
+ /* Record the edge currently being serialized on the stack top: a static
2950
+ * property name (emitted C literal), an overflow key (borrowed for the
2951
+ * duration of the member write), or an array/tuple index. */
2952
+ void scr_jb_edge_prop(ScrJsonBuf *b, const char *name);
2953
+ void scr_jb_edge_key(ScrJsonBuf *b, const ScrStr *key);
2954
+ void scr_jb_edge_idx(ScrJsonBuf *b, size_t i);
2955
+
2956
+ /* Circular guard for the compiler-emitted typed→dyn converters (sc_td_*
2957
+ * over cycle-capable containers): enter TRAPS on a value already being
2958
+ * converted (a cyclic value has no finite DOM copy — Node shares the
2959
+ * reference instead; SEMANTICS.md), else pushes. */
2960
+ void scr_dyn_from_enter(const void *v);
2961
+ void scr_dyn_from_leave(void);
2853
2962
  void scr_jb_putc(ScrJsonBuf *b, char c);
2854
2963
  void scr_jb_puts(ScrJsonBuf *b, const char *s);
2855
2964
  /* JS JSON.stringify number rules: NaN/±Infinity → null, -0 → 0, else
@@ -4020,6 +4129,18 @@ ScrStr *scr_insp_jsval(ScrJsval *v, double recurse, double depth);
4020
4129
  #endif
4021
4130
  void scr_insp_begin(double recurse);
4022
4131
  void scr_insp_entry(ScrStr *s, bool is_num);
4132
+ /* Circular references, Node-exact (<ref *N> / [Circular *N]): the
4133
+ * compiler-emitted helpers over CYCLE-CAPABLE composites call circ_check
4134
+ * FIRST (a value already on the traversal stack answers its circular id,
4135
+ * assigned in discovery order — the caller renders scr_insp_circular and
4136
+ * descends no further), seen_push after begin, and ref_wrap around end's
4137
+ * result (pops the stack; values in the circular map gain the "<ref *N> "
4138
+ * prefix). State resets at each top-level value's first frame
4139
+ * (scr_insp_begin(1)). ref_wrap BORROWS s and returns +1. */
4140
+ double scr_insp_circ_check(const void *v);
4141
+ void scr_insp_seen_push(const void *v);
4142
+ ScrStr *scr_insp_circular(double id);
4143
+ ScrStr *scr_insp_ref_wrap(const void *v, ScrStr *s);
4023
4144
  ScrStr *scr_insp_more_items(double remaining); /* "... N more items" */
4024
4145
  ScrStr *scr_insp_key(ScrStr *k); /* bare-or-quoted property-name ladder; +1 */
4025
4146
  ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse,
@@ -4964,6 +5085,11 @@ void scr_assert_fail_msg(ScrStr *message); /* takes ownership; always throws */
4964
5085
  ScrStr *scr_assert_inspect_str(const ScrStr *s); /* util.inspect quoting; +1 */
4965
5086
  void scr_assert_ok(bool pass, ScrStr *message);
4966
5087
  bool scr_assert_same_value_f64(double a, double b); /* Object.is */
5088
+ /* deepStrictEqual over cyclic values: pair memo (enter answers true for
5089
+ * a pair already being compared — Node's coinductive memo; leave pops).
5090
+ * The compiler-emitted helpers over cycle-capable types wrap with these. */
5091
+ bool scr_assert_deq_enter(const void *a, const void *b);
5092
+ void scr_assert_deq_leave(void);
4967
5093
  void scr_assert_eq_f64(double a, double b, bool negated, bool deep, ScrStr *msg, bool has_msg);
4968
5094
  void scr_assert_eq_str(ScrStr *a, ScrStr *b, bool negated, bool deep, ScrStr *msg, bool has_msg);
4969
5095
  void scr_assert_eq_bool(bool a, bool b, bool negated, bool deep, ScrStr *msg, bool has_msg);
package/src/scr_string.c CHANGED
@@ -15,8 +15,7 @@ long scr_str_live_count(void) { return scr_live_strings; }
15
15
  #endif
16
16
 
17
17
  static void scr_oom(void) {
18
- fputs("scriptc: out of memory\n", stderr);
19
- abort();
18
+ scr_trap("scriptc: out of memory\n");
20
19
  }
21
20
 
22
21
  /* ── UTF-16 index cache ───────────────────────────────────────────────
@@ -519,8 +518,7 @@ ScrStr *scr_str_slice(ScrStr *s, double start, double end) {
519
518
  ScrStr *scr_str_repeat(ScrStr *s, double count) {
520
519
  double n = scr_to_integer_or_infinity(count);
521
520
  if (n < 0 || (isinf(n) && n > 0)) {
522
- fputs("scriptc: RangeError: Invalid count value\n", stderr);
523
- abort();
521
+ scr_trap("scriptc: RangeError: Invalid count value\n");
524
522
  }
525
523
  if (n == 0 || s->len == 0) return scr_str_empty();
526
524
  /* n is a finite non-negative integer here. Reject sizes malloc could not
package/src/scr_symbol.c CHANGED
@@ -44,8 +44,7 @@ void scr_sym_release_v(void *p) { scr_sym_release(p); }
44
44
  ScrSym *scr_sym_new(ScrStr *desc) {
45
45
  ScrSym *s = malloc(sizeof(ScrSym));
46
46
  if (!s) {
47
- fputs("scriptc: out of memory\n", stderr);
48
- abort();
47
+ scr_trap("scriptc: out of memory\n");
49
48
  }
50
49
  s->rc = 1;
51
50
  s->desc = desc ? scr_str_retain(desc) : NULL;
@@ -75,7 +74,7 @@ ScrSym *scr_sym_for(ScrStr *key) {
75
74
  /* First request for this key: a fresh symbol whose description IS the
76
75
  * key (the spec's Symbol.for behavior), chained into the registry with
77
76
  * the registry's own reference. */
78
- if (!g_sym_registry) atexit(scr_sym_registry_cleanup);
77
+ if (!g_sym_registry) scr_atexit(scr_sym_registry_cleanup);
79
78
  ScrSym *s = scr_sym_new(key);
80
79
  s->reg_key = scr_str_retain(key);
81
80
  s->reg_next = g_sym_registry;