@scriptc/runtime 0.0.5 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scriptc/runtime",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
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_assert.c CHANGED
@@ -150,6 +150,37 @@ void scr_assert_ok(bool pass, ScrStr *message) {
150
150
 
151
151
  /* SameValue over doubles — Object.is, the comparison of strictEqual AND
152
152
  * deepStrictEqual for numbers (NaN equals NaN; +0 and -0 differ). */
153
+ /* ── deepStrictEqual over cyclic values ────────────────────────────────
154
+ * RECURSIVE record types permit reference cycles; Node's deep equality
155
+ * memoizes (value1, value2) pairs so equal cyclic structures compare
156
+ * true instead of recursing forever. The compiler-emitted per-type
157
+ * helpers over cycle-capable types wrap their walks in enter/leave: a
158
+ * PAIR already being compared answers equal (the coinductive step —
159
+ * Node's memo behavior exactly). The stack is global (comparisons never
160
+ * interleave; the emitted walks cannot throw mid-compare). */
161
+ static struct { const void *a, *b; } *g_deq_stack;
162
+ static size_t g_deq_len;
163
+ static size_t g_deq_cap;
164
+
165
+ bool scr_assert_deq_enter(const void *a, const void *b) {
166
+ for (size_t i = 0; i < g_deq_len; i++) {
167
+ if (g_deq_stack[i].a == a && g_deq_stack[i].b == b) return true;
168
+ }
169
+ if (g_deq_len == g_deq_cap) {
170
+ g_deq_cap = g_deq_cap ? g_deq_cap * 2 : 8;
171
+ g_deq_stack = realloc(g_deq_stack, g_deq_cap * sizeof *g_deq_stack);
172
+ if (!g_deq_stack) scr_assert_oom();
173
+ }
174
+ g_deq_stack[g_deq_len].a = a;
175
+ g_deq_stack[g_deq_len].b = b;
176
+ g_deq_len++;
177
+ return false;
178
+ }
179
+
180
+ void scr_assert_deq_leave(void) {
181
+ if (g_deq_len > 0) g_deq_len--;
182
+ }
183
+
153
184
  bool scr_assert_same_value_f64(double a, double b) {
154
185
  if (a == b) return a != 0.0 || signbit(a) == signbit(b);
155
186
  return isnan(a) && isnan(b);
package/src/scr_inspect.c CHANGED
@@ -245,10 +245,13 @@ static double g_cur_depth; /* ctx.currentDepth (last-entered composite) */
245
245
 
246
246
  static InspFrame *insp_top(void) { return &g_frames[g_nframes - 1]; }
247
247
 
248
+ static void insp_circ_reset(void); /* circular-reference state, below */
249
+
248
250
  /* Begin a non-empty composite: formatRaw's `recurseTimes += 1;
249
251
  * ctx.currentDepth = recurseTimes` plus the uniform +2 the children
250
252
  * format under. `recurse` is the CHILDREN's recursion depth. */
251
253
  void scr_insp_begin(double recurse) {
254
+ if (recurse == 1) insp_circ_reset(); /* a fresh top-level value: Node's per-inspect ctx */
252
255
  if (g_nframes == g_frames_cap) {
253
256
  g_frames_cap = g_frames_cap ? g_frames_cap * 2 : 8;
254
257
  g_frames = realloc(g_frames, g_frames_cap * sizeof(InspFrame));
@@ -277,6 +280,85 @@ void scr_insp_entry(ScrStr *s, bool is_num) {
277
280
  if (!is_num) f->all_num = false;
278
281
  }
279
282
 
283
+ /* ── circular references (Node's <ref *N> / [Circular *N]) ────────────
284
+ * Recursive record/class types permit runtime reference cycles, and Node
285
+ * renders them with formatValue's seen/circular machinery: a value
286
+ * already ON the current traversal stack renders as `[Circular *N]` (N
287
+ * assigned at first detection, in discovery order), and every rendering
288
+ * of a so-numbered value gets the `<ref *N> ` prefix at its close. The
289
+ * compiler-emitted helpers over CYCLE-CAPABLE composites drive exactly
290
+ * that protocol: circ_check first (before the empty-literal and depth
291
+ * answers — Node's order: a circular value beyond the depth budget still
292
+ * says Circular), seen_push after begin, ref_wrap around end's result.
293
+ * The circular MAP persists for one whole top-level inspect — begin(1)
294
+ * is the per-value reset (a root composite's first frame). */
295
+ typedef struct {
296
+ const void *ptr;
297
+ int id; /* circular id (0 while only on the stack) */
298
+ } InspSeenEnt;
299
+
300
+ static InspSeenEnt *g_seen;
301
+ static size_t g_nseen;
302
+ static size_t g_seen_cap;
303
+ /* Detection-numbered circular targets (persist across the call). */
304
+ static const void *g_circ[64];
305
+ static int g_ncirc;
306
+
307
+ static void insp_circ_reset(void) {
308
+ g_nseen = 0;
309
+ g_ncirc = 0;
310
+ }
311
+
312
+ static int insp_circ_id(const void *v) {
313
+ for (int i = 0; i < g_ncirc; i++) {
314
+ if (g_circ[i] == v) return i + 1;
315
+ }
316
+ return 0;
317
+ }
318
+
319
+ double scr_insp_circ_check(const void *v) {
320
+ for (size_t i = 0; i < g_nseen; i++) {
321
+ if (g_seen[i].ptr != v) continue;
322
+ int id = insp_circ_id(v);
323
+ if (id == 0 && g_ncirc < (int)(sizeof g_circ / sizeof *g_circ)) {
324
+ g_circ[g_ncirc++] = v;
325
+ id = g_ncirc;
326
+ }
327
+ return (double)id;
328
+ }
329
+ return 0;
330
+ }
331
+
332
+ void scr_insp_seen_push(const void *v) {
333
+ if (g_nseen == g_seen_cap) {
334
+ g_seen_cap = g_seen_cap ? g_seen_cap * 2 : 8;
335
+ g_seen = realloc(g_seen, g_seen_cap * sizeof(InspSeenEnt));
336
+ if (!g_seen) insp_oom();
337
+ }
338
+ g_seen[g_nseen].ptr = v;
339
+ g_seen[g_nseen].id = 0;
340
+ g_nseen++;
341
+ }
342
+
343
+ ScrStr *scr_insp_circular(double id) {
344
+ char buf[32];
345
+ int n = snprintf(buf, sizeof buf, "[Circular *%.0f]", id);
346
+ return scr_str_new(buf, (size_t)n);
347
+ }
348
+
349
+ ScrStr *scr_insp_ref_wrap(const void *v, ScrStr *s) {
350
+ if (g_nseen > 0) g_nseen--;
351
+ int id = insp_circ_id(v);
352
+ if (id == 0) return scr_str_retain(s);
353
+ char buf[32];
354
+ int n = snprintf(buf, sizeof buf, "<ref *%d> ", id);
355
+ ScrStr *out = scr_str_alloc_raw(s->len + (size_t)n, s->len + (size_t)n);
356
+ memcpy(out->data, buf, (size_t)n);
357
+ memcpy(out->data + n, s->data, s->len);
358
+ out->data[out->len] = '\0';
359
+ return out;
360
+ }
361
+
280
362
  /* remainingText: the "... N more items" tail entry. */
281
363
  ScrStr *scr_insp_more_items(double remaining) {
282
364
  char buf[64];
package/src/scr_json.c CHANGED
@@ -55,6 +55,9 @@ void scr_jb_init(ScrJsonBuf *b) {
55
55
  b->data = NULL;
56
56
  b->len = 0;
57
57
  b->cap = 0;
58
+ b->seen = NULL;
59
+ b->seen_len = 0;
60
+ b->seen_cap = 0;
58
61
  }
59
62
 
60
63
  static void scr_jb_grow(ScrJsonBuf *b, size_t need) {
@@ -76,6 +79,7 @@ static void scr_jb_grow(ScrJsonBuf *b, size_t need) {
76
79
  /* Abandon a buffer (parser error paths). */
77
80
  static void scr_jb_dispose(ScrJsonBuf *b) {
78
81
  if (b->data) scr_str_release(SCR_JB_STR(b));
82
+ free(b->seen);
79
83
  scr_jb_init(b);
80
84
  }
81
85
 
@@ -143,6 +147,10 @@ void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s) {
143
147
  }
144
148
 
145
149
  ScrStr *scr_jb_finish(ScrJsonBuf *b) {
150
+ free(b->seen);
151
+ b->seen = NULL;
152
+ b->seen_len = 0;
153
+ b->seen_cap = 0;
146
154
  if (!b->data) return scr_str_new("", 0);
147
155
  ScrStr *s = SCR_JB_STR(b);
148
156
  s->len = b->len;
@@ -154,6 +162,144 @@ ScrStr *scr_jb_finish(ScrJsonBuf *b) {
154
162
  return s;
155
163
  }
156
164
 
165
+ /* ── circular-structure detection ──────────────────────────────────────
166
+ * RECURSIVE record types permit runtime reference cycles; JSON.stringify
167
+ * of a cyclic value throws V8's exact TypeError. The compiler-emitted
168
+ * walkers over cycle-CAPABLE containers (records whose shape carries a
169
+ * collector header, arrays of them, tuple shapes alike) bracket their
170
+ * bodies with enter/leave and stamp the current member edge before each
171
+ * cycle-capable member write; everything acyclic pays nothing. Detection
172
+ * is STACK membership (a DAG serializes the shared subtree twice, exactly
173
+ * like Node — only a path back to an ancestor is circular).
174
+ *
175
+ * The message mirrors V8's ConstructCircularStructureErrorMessage byte
176
+ * for byte: the starting object (where the repeat lands), one line per
177
+ * hop from it to the top of the stack ("property 'x' -> object with
178
+ * constructor 'Y'" / "index N -> ..."), the middle elided as "..." when
179
+ * there are more than three hops (first two + last one shown), and the
180
+ * closing edge. Constructor names are exactly 'Object'/'Array' — the only
181
+ * containers JSON-safe types admit. */
182
+ typedef struct ScrJsonSeenEnt {
183
+ const void *ptr;
184
+ bool is_array;
185
+ const char *prop; /* static property edge (emitted C literal) */
186
+ const ScrStr *prop_str; /* overflow-key edge (borrowed for the write) */
187
+ size_t index; /* array/tuple index edge */
188
+ } ScrJsonSeenEnt;
189
+
190
+ static void scr_jb_put_edge(ScrJsonBuf *m, const ScrJsonSeenEnt *e) {
191
+ if (e->prop || e->prop_str) {
192
+ scr_jb_puts(m, "property '");
193
+ if (e->prop) scr_jb_puts(m, e->prop);
194
+ else scr_jb_write(m, e->prop_str->data, e->prop_str->len);
195
+ scr_jb_putc(m, '\'');
196
+ return;
197
+ }
198
+ scr_jb_puts(m, "index ");
199
+ scr_jb_put_f64(m, (double)e->index);
200
+ }
201
+
202
+ static void scr_jb_put_ctor(ScrJsonBuf *m, bool is_array) {
203
+ scr_jb_puts(m, is_array ? "object with constructor 'Array'" : "object with constructor 'Object'");
204
+ }
205
+
206
+ bool scr_jb_enter(ScrJsonBuf *b, const void *v, bool is_array) {
207
+ for (size_t i = 0; i < b->seen_len; i++) {
208
+ if (b->seen[i].ptr != v) continue;
209
+ ScrJsonBuf m;
210
+ scr_jb_init(&m);
211
+ scr_jb_puts(&m, "Converting circular structure to JSON\n --> starting at ");
212
+ scr_jb_put_ctor(&m, b->seen[i].is_array);
213
+ const size_t n = b->seen_len;
214
+ const size_t hops = n - 1 - i; /* intermediate lines (j = i+1 .. n-1) */
215
+ for (size_t j = i + 1; j < n; j++) {
216
+ if (hops > 3 && j - (i + 1) == 2) {
217
+ scr_jb_puts(&m, "\n | ...");
218
+ j = n - 2; /* the loop increment lands on the LAST hop */
219
+ continue;
220
+ }
221
+ scr_jb_puts(&m, "\n | ");
222
+ scr_jb_put_edge(&m, &b->seen[j - 1]);
223
+ scr_jb_puts(&m, " -> ");
224
+ scr_jb_put_ctor(&m, b->seen[j].is_array);
225
+ }
226
+ scr_jb_puts(&m, "\n --- ");
227
+ scr_jb_put_edge(&m, &b->seen[n - 1]);
228
+ scr_jb_puts(&m, " closes the circle");
229
+ scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&m));
230
+ return false;
231
+ }
232
+ if (b->seen_len == b->seen_cap) {
233
+ size_t cap = b->seen_cap ? b->seen_cap * 2 : 8;
234
+ ScrJsonSeenEnt *grown = realloc(b->seen, cap * sizeof *grown);
235
+ if (!grown) scr_json_oom();
236
+ b->seen = grown;
237
+ b->seen_cap = cap;
238
+ }
239
+ ScrJsonSeenEnt *e = &b->seen[b->seen_len++];
240
+ e->ptr = v;
241
+ e->is_array = is_array;
242
+ e->prop = NULL;
243
+ e->prop_str = NULL;
244
+ e->index = 0;
245
+ return true;
246
+ }
247
+
248
+ void scr_jb_leave(ScrJsonBuf *b) {
249
+ if (b->seen_len > 0) b->seen_len--;
250
+ }
251
+
252
+ void scr_jb_edge_prop(ScrJsonBuf *b, const char *name) {
253
+ ScrJsonSeenEnt *e = &b->seen[b->seen_len - 1];
254
+ e->prop = name;
255
+ e->prop_str = NULL;
256
+ }
257
+
258
+ void scr_jb_edge_key(ScrJsonBuf *b, const ScrStr *key) {
259
+ ScrJsonSeenEnt *e = &b->seen[b->seen_len - 1];
260
+ e->prop = NULL;
261
+ e->prop_str = key;
262
+ }
263
+
264
+ void scr_jb_edge_idx(ScrJsonBuf *b, size_t i) {
265
+ ScrJsonSeenEnt *e = &b->seen[b->seen_len - 1];
266
+ e->prop = NULL;
267
+ e->prop_str = NULL;
268
+ e->index = i;
269
+ }
270
+
271
+ /* ── circular guard for the typed→dyn converters (sc_td_*) ────────────
272
+ * A recursive-typed value crossing into a checked-dynamic slot DEEP-
273
+ * COPIES into the DOM; a cyclic value has no finite copy. Node never
274
+ * copies (an unknown-typed binding shares the reference), so there is no
275
+ * Node-exact error to throw — the conversion TRAPS loudly instead
276
+ * (SEMANTICS.md documents the divergence). The emitted converters over
277
+ * cycle-capable containers bracket their walks with enter/leave; the
278
+ * stack is global (conversions never interleave). */
279
+ static const void **g_td_seen;
280
+ static size_t g_td_nseen;
281
+ static size_t g_td_cap;
282
+
283
+ void scr_dyn_from_enter(const void *v) {
284
+ for (size_t i = 0; i < g_td_nseen; i++) {
285
+ if (g_td_seen[i] == v) {
286
+ scr_trap("scriptc: cannot convert a circular structure into a checked-dynamic value "
287
+ "(unknown-typed slots deep-copy; break the cycle first)\n");
288
+ }
289
+ }
290
+ if (g_td_nseen == g_td_cap) {
291
+ g_td_cap = g_td_cap ? g_td_cap * 2 : 8;
292
+ const void **grown = realloc(g_td_seen, g_td_cap * sizeof *grown);
293
+ if (!grown) scr_json_oom();
294
+ g_td_seen = grown;
295
+ }
296
+ g_td_seen[g_td_nseen++] = v;
297
+ }
298
+
299
+ void scr_dyn_from_leave(void) {
300
+ if (g_td_nseen > 0) g_td_nseen--;
301
+ }
302
+
157
303
  /* ── DOM lifecycle ─────────────────────────────────────────────────────
158
304
  * Parse/release churn (a JSON round-trip loop allocates and frees every
159
305
  * node each iteration) runs on freelists instead of calloc/free: one list
package/src/scr_runtime.h CHANGED
@@ -2917,14 +2917,48 @@ extern void scr_dyn_listener_fire_err(ScrClosure *cb, ScrStr *msg);
2917
2917
 
2918
2918
  /* Output buffer for the compiler-emitted type-directed JSON serializers.
2919
2919
  * Value-typed and stack-allocated by the emitted code; _finish hands the
2920
- * 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;
2921
2923
  typedef struct {
2922
2924
  char *data;
2923
2925
  size_t len;
2924
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;
2925
2936
  } ScrJsonBuf;
2926
2937
 
2927
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);
2928
2962
  void scr_jb_putc(ScrJsonBuf *b, char c);
2929
2963
  void scr_jb_puts(ScrJsonBuf *b, const char *s);
2930
2964
  /* JS JSON.stringify number rules: NaN/±Infinity → null, -0 → 0, else
@@ -4095,6 +4129,18 @@ ScrStr *scr_insp_jsval(ScrJsval *v, double recurse, double depth);
4095
4129
  #endif
4096
4130
  void scr_insp_begin(double recurse);
4097
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);
4098
4144
  ScrStr *scr_insp_more_items(double remaining); /* "... N more items" */
4099
4145
  ScrStr *scr_insp_key(ScrStr *k); /* bare-or-quoted property-name ladder; +1 */
4100
4146
  ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse,
@@ -5039,6 +5085,11 @@ void scr_assert_fail_msg(ScrStr *message); /* takes ownership; always throws */
5039
5085
  ScrStr *scr_assert_inspect_str(const ScrStr *s); /* util.inspect quoting; +1 */
5040
5086
  void scr_assert_ok(bool pass, ScrStr *message);
5041
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);
5042
5093
  void scr_assert_eq_f64(double a, double b, bool negated, bool deep, ScrStr *msg, bool has_msg);
5043
5094
  void scr_assert_eq_str(ScrStr *a, ScrStr *b, bool negated, bool deep, ScrStr *msg, bool has_msg);
5044
5095
  void scr_assert_eq_bool(bool a, bool b, bool negated, bool deep, ScrStr *msg, bool has_msg);