@scriptc/cli-darwin-x64 0.2.4 → 0.2.5

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.
Files changed (123) hide show
  1. package/dist/THIRD_PARTY_NOTICES +1 -1
  2. package/dist/bin/scriptc +0 -0
  3. package/dist/bin/scriptc.json +1 -1
  4. package/dist/lib/declarations/scriptc-node-fallback.d.ts +915 -215
  5. package/dist/lib/declarations/scriptc-overrides.d.ts +5 -1
  6. package/dist/lib/declarations/scriptc.d.ts +22 -2
  7. package/dist/lib/llvm/bin/scriptc-llvm-codegen +0 -0
  8. package/dist/lib/llvm/package.json +1 -1
  9. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_array.o +0 -0
  10. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_bytes.o +0 -0
  11. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_bytes_io.o +0 -0
  12. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_copying.o +0 -0
  13. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_json.o +0 -0
  14. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_lib.o +0 -0
  15. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_map.o +0 -0
  16. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_number.o +0 -0
  17. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_regex.o +0 -0
  18. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_string.o +0 -0
  19. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_util_compare.o +0 -0
  20. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_array.o +0 -0
  21. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_bytes.o +0 -0
  22. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_bytes_io.o +0 -0
  23. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_copying.o +0 -0
  24. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_json.o +0 -0
  25. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_lib.o +0 -0
  26. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_map.o +0 -0
  27. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_number.o +0 -0
  28. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_regex.o +0 -0
  29. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_string.o +0 -0
  30. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_util_compare.o +0 -0
  31. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic-text-decoder-legacy/scr_bytes.o +0 -0
  32. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  33. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_array.o +0 -0
  34. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_bytes.o +0 -0
  35. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_bytes_io.o +0 -0
  36. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_copying.o +0 -0
  37. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_json.o +0 -0
  38. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_lib.o +0 -0
  39. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_map.o +0 -0
  40. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_number.o +0 -0
  41. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_regex.o +0 -0
  42. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_string.o +0 -0
  43. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  44. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_array.o +0 -0
  45. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_bytes.o +0 -0
  46. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_bytes_io.o +0 -0
  47. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_copying.o +0 -0
  48. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_json.o +0 -0
  49. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_lib.o +0 -0
  50. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_map.o +0 -0
  51. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_number.o +0 -0
  52. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_regex.o +0 -0
  53. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_string.o +0 -0
  54. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  55. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_array.o +0 -0
  56. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_bytes.o +0 -0
  57. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_bytes_io.o +0 -0
  58. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_copying.o +0 -0
  59. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_json.o +0 -0
  60. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_lib.o +0 -0
  61. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_map.o +0 -0
  62. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_number.o +0 -0
  63. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_regex.o +0 -0
  64. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_string.o +0 -0
  65. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  66. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_array.o +0 -0
  67. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_bytes.o +0 -0
  68. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_bytes_io.o +0 -0
  69. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_copying.o +0 -0
  70. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_json.o +0 -0
  71. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_lib.o +0 -0
  72. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_map.o +0 -0
  73. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_number.o +0 -0
  74. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_regex.o +0 -0
  75. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_string.o +0 -0
  76. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  77. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_array.o +0 -0
  78. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_bytes.o +0 -0
  79. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_bytes_io.o +0 -0
  80. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_copying.o +0 -0
  81. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_json.o +0 -0
  82. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_lib.o +0 -0
  83. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_map.o +0 -0
  84. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_number.o +0 -0
  85. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_regex.o +0 -0
  86. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_string.o +0 -0
  87. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_util_compare.o +0 -0
  88. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_array.o +0 -0
  89. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_bytes.o +0 -0
  90. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_bytes_io.o +0 -0
  91. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_copying.o +0 -0
  92. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_json.o +0 -0
  93. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_lib.o +0 -0
  94. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_map.o +0 -0
  95. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_number.o +0 -0
  96. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_regex.o +0 -0
  97. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_string.o +0 -0
  98. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_util_compare.o +0 -0
  99. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic-text-decoder-legacy/scr_bytes.o +0 -0
  100. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  101. package/dist/lib/runtime-darwin-x64/package.json +1 -1
  102. package/dist/lib/runtime-darwin-x64/runtime-pack.json +183 -183
  103. package/dist/lib/runtime-sources/package.json +1 -1
  104. package/dist/lib/runtime-sources/src/scr_array.c +267 -6
  105. package/dist/lib/runtime-sources/src/scr_async.test.ts +16 -6
  106. package/dist/lib/runtime-sources/src/scr_bytes.c +437 -216
  107. package/dist/lib/runtime-sources/src/scr_bytes.test.c +85 -0
  108. package/dist/lib/runtime-sources/src/scr_bytes_io.c +0 -34
  109. package/dist/lib/runtime-sources/src/scr_copying.c +12 -2
  110. package/dist/lib/runtime-sources/src/scr_json.c +479 -150
  111. package/dist/lib/runtime-sources/src/scr_json.test.c +323 -1
  112. package/dist/lib/runtime-sources/src/scr_json.test.ts +58 -25
  113. package/dist/lib/runtime-sources/src/scr_key.h +68 -0
  114. package/dist/lib/runtime-sources/src/scr_lib.c +132 -73
  115. package/dist/lib/runtime-sources/src/scr_map.c +50 -66
  116. package/dist/lib/runtime-sources/src/scr_number.c +2 -4
  117. package/dist/lib/runtime-sources/src/scr_numeric.h +25 -0
  118. package/dist/lib/runtime-sources/src/scr_numeric.test.c +37 -0
  119. package/dist/lib/runtime-sources/src/scr_numeric.test.ts +33 -0
  120. package/dist/lib/runtime-sources/src/scr_regex.c +238 -167
  121. package/dist/lib/runtime-sources/src/scr_runtime.h +37 -6
  122. package/dist/lib/runtime-sources/src/scr_string.c +155 -42
  123. package/package.json +1 -1
@@ -22,6 +22,7 @@
22
22
  * serializers (and the error messages here).
23
23
  */
24
24
  #include "scr_runtime.h"
25
+ #include "scr_key.h"
25
26
 
26
27
  static SCR_TL ScrDyn *scr_builtin_object_prototype;
27
28
 
@@ -159,7 +160,9 @@ void scr_jb_init(ScrJsonBuf *b) {
159
160
  }
160
161
 
161
162
  static void scr_jb_grow(ScrJsonBuf *b, size_t need) {
162
- if (b->len + need <= b->cap) return;
163
+ if (need > SIZE_MAX - sizeof(ScrStr) - 1 - b->len) scr_json_oom();
164
+ size_t required = b->len + need;
165
+ if (required <= b->cap) return;
163
166
  if (!b->data) { /* first allocation: len == 0 */
164
167
  size_t cap = scr_jb_hint >= need ? scr_jb_hint : need;
165
168
  ScrStr *s = scr_str_alloc_raw(0, cap);
@@ -168,7 +171,13 @@ static void scr_jb_grow(ScrJsonBuf *b, size_t need) {
168
171
  return;
169
172
  }
170
173
  size_t cap = b->cap;
171
- while (cap < b->len + need) cap *= 2;
174
+ while (cap < required) {
175
+ if (cap > (SIZE_MAX - sizeof(ScrStr) - 1) / 2) {
176
+ cap = required;
177
+ break;
178
+ }
179
+ cap *= 2;
180
+ }
172
181
  ScrStr *s = scr_str_regrow(SCR_JB_STR(b), cap);
173
182
  b->data = s->data;
174
183
  b->cap = cap;
@@ -186,7 +195,8 @@ void scr_jb_putc(ScrJsonBuf *b, char c) {
186
195
  b->data[b->len++] = c;
187
196
  }
188
197
 
189
- static void scr_jb_write(ScrJsonBuf *b, const char *s, size_t n) {
198
+ void scr_jb_write(ScrJsonBuf *b, const char *s, size_t n) {
199
+ if (n == 0) return;
190
200
  scr_jb_grow(b, n);
191
201
  memcpy(b->data + b->len, s, n);
192
202
  b->len += n;
@@ -215,18 +225,45 @@ void scr_jb_put_f64(ScrJsonBuf *b, double v) {
215
225
  scr_jb_write(b, buf, n);
216
226
  }
217
227
 
218
- void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s) {
228
+ /* Find the next quote, backslash or control byte. memcpy keeps the word
229
+ * loads unaligned-safe, and the length guard never reads past the input.
230
+ * The masks only decide whether to inspect a word byte by byte: borrow
231
+ * propagation may mark extra lanes, so it must not determine a byte index.
232
+ * High UTF-8 bytes are ordinary data, independent of host byte order. */
233
+ static size_t scr_json_plain_bytes(const char *data, size_t len) {
234
+ const uint64_t ones = UINT64_C(0x0101010101010101);
235
+ const uint64_t high = UINT64_C(0x8080808080808080);
236
+ size_t i = 0;
237
+ while (len - i >= sizeof(uint64_t)) {
238
+ uint64_t word;
239
+ memcpy(&word, data + i, sizeof word);
240
+ uint64_t quotes = word ^ (ones * '"');
241
+ uint64_t slashes = word ^ (ones * '\\');
242
+ uint64_t special = ((quotes - ones) & ~quotes) |
243
+ ((slashes - ones) & ~slashes) |
244
+ ((word - ones * 0x20) & ~word);
245
+ if (special & high) break;
246
+ i += sizeof word;
247
+ }
248
+ while (i < len) {
249
+ unsigned char c = (unsigned char)data[i];
250
+ if (c < 0x20 || c == '"' || c == '\\') break;
251
+ i++;
252
+ }
253
+ return i;
254
+ }
255
+
256
+ static void scr_jb_put_json_span(ScrJsonBuf *b, const char *data, size_t len) {
219
257
  scr_jb_putc(b, '"');
220
258
  /* Bulk-copy runs of unescaped bytes (UTF-8 passes through verbatim,
221
259
  * like JS); escapes interrupt the run. */
222
- size_t i = 0, run = 0;
223
- while (i + run < s->len) {
224
- unsigned char c = (unsigned char)s->data[i + run];
225
- if (c != '"' && c != '\\' && c >= 0x20) {
226
- run++;
227
- continue;
228
- }
229
- scr_jb_write(b, s->data + i, run);
260
+ size_t i = 0;
261
+ while (i < len) {
262
+ size_t run = scr_json_plain_bytes(data + i, len - i);
263
+ scr_jb_write(b, data + i, run);
264
+ i += run;
265
+ if (i == len) break;
266
+ unsigned char c = (unsigned char)data[i++];
230
267
  switch (c) {
231
268
  case '"': scr_jb_puts(b, "\\\""); break;
232
269
  case '\\': scr_jb_puts(b, "\\\\"); break;
@@ -241,13 +278,14 @@ void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s) {
241
278
  scr_jb_puts(b, esc);
242
279
  }
243
280
  }
244
- i += run + 1;
245
- run = 0;
246
281
  }
247
- scr_jb_write(b, s->data + i, run);
248
282
  scr_jb_putc(b, '"');
249
283
  }
250
284
 
285
+ void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s) {
286
+ scr_jb_put_json_span(b, s->data, s->len);
287
+ }
288
+
251
289
  ScrStr *scr_jb_finish(ScrJsonBuf *b) {
252
290
  free(b->seen);
253
291
  b->seen = NULL;
@@ -413,11 +451,110 @@ void scr_dyn_from_leave(void) {
413
451
  * strict alloc/free balance.
414
452
  */
415
453
  static SCR_TL ScrDyn *scr_builtin_array_prototype;
454
+
455
+ /* Property names are immutable and often repeated across simultaneously live
456
+ * objects. Entries own references to their bytes; this fixed weak table owns
457
+ * none. Collisions only reduce sharing, and the final release removes a weak
458
+ * entry before freeing storage. Long keys bypass lookup entirely. */
459
+ #define SCR_DYN_KEY_SHARE_SLOTS 256
460
+ #define SCR_DYN_KEY_SHARE_LENGTH 64
461
+ typedef struct {
462
+ size_t rc;
463
+ char bytes[];
464
+ } ScrDynKey;
465
+ typedef struct {
466
+ ScrDynKey *key;
467
+ size_t length;
468
+ } ScrDynKeySlot;
469
+ static SCR_TL ScrDynKeySlot scr_dyn_keys[SCR_DYN_KEY_SHARE_SLOTS];
470
+ #ifdef SCR_RC_AUDIT
471
+ static SCR_TL long scr_live_dyn_keys;
472
+ long scr_dyn_key_live_count(void) { return scr_live_dyn_keys; }
473
+ #endif
474
+
475
+ static size_t scr_dyn_key_slot(const char *key, size_t length) {
476
+ size_t hash = length;
477
+ for (size_t i = 0; i < length; i++) hash = hash * 31 + (unsigned char)key[i];
478
+ return hash % SCR_DYN_KEY_SHARE_SLOTS;
479
+ }
480
+
481
+ static char *scr_dyn_key_copy(const char *key, size_t length) {
482
+ ScrDynKeySlot *slot = length <= SCR_DYN_KEY_SHARE_LENGTH
483
+ ? &scr_dyn_keys[scr_dyn_key_slot(key, length)] : NULL;
484
+ if (slot && slot->key && slot->length == length &&
485
+ !memcmp(slot->key->bytes, key, length)) {
486
+ slot->key->rc++;
487
+ return slot->key->bytes;
488
+ }
489
+ if (length > SIZE_MAX - sizeof(ScrDynKey) - 1) scr_json_oom();
490
+ ScrDynKey *owned = malloc(sizeof *owned + length + 1);
491
+ if (!owned) scr_json_oom();
492
+ #ifdef SCR_RC_AUDIT
493
+ scr_live_dyn_keys++;
494
+ #endif
495
+ owned->rc = 1;
496
+ memcpy(owned->bytes, key, length);
497
+ owned->bytes[length] = '\0';
498
+ if (slot) *slot = (ScrDynKeySlot){owned, length};
499
+ return owned->bytes;
500
+ }
501
+
502
+ static void scr_dyn_key_release(char *key, size_t length) {
503
+ if (!key) return;
504
+ ScrDynKey *owned = (ScrDynKey *)(key - offsetof(ScrDynKey, bytes));
505
+ if (--owned->rc) return;
506
+ if (length <= SCR_DYN_KEY_SHARE_LENGTH) {
507
+ ScrDynKeySlot *slot = &scr_dyn_keys[scr_dyn_key_slot(key, length)];
508
+ if (slot->key == owned) slot->key = NULL;
509
+ }
510
+ #ifdef SCR_RC_AUDIT
511
+ scr_live_dyn_keys--;
512
+ #endif
513
+ free(owned);
514
+ }
515
+
416
516
  #ifndef SCR_RC_AUDIT
417
517
  static SCR_TL ScrDyn *scr_dyn_free_arr, *scr_dyn_free_obj, *scr_dyn_free_misc;
418
518
  static SCR_TL size_t scr_dyn_free_count;
419
519
  #define SCR_DYN_FREE_MAX 8192
520
+ /* Recycled small objects may retain key bytes, but never values or an
521
+ * unbounded document's keys. This budget applies across the whole pool. */
522
+ static SCR_TL size_t scr_dyn_cached_key_bytes;
523
+ #define SCR_DYN_KEY_CACHE_MAX ((size_t)64 * 1024)
524
+ #define SCR_DYN_KEY_CACHE_SLOTS 16
525
+ #define SCR_DYN_KEY_CACHE_LENGTH 64
526
+ #endif
527
+
528
+ static void scr_dyn_obj_drop_keys(ScrDyn *d, bool cache) {
529
+ for (size_t i = 0; i < d->v.obj.cap; i++) {
530
+ ScrDynEntry *entry = &d->v.obj.entries[i];
531
+ if (!entry->key) continue;
532
+ #ifndef SCR_RC_AUDIT
533
+ if (cache && d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS &&
534
+ entry->key_len <= SCR_DYN_KEY_CACHE_LENGTH &&
535
+ sizeof(ScrDynKey) + entry->key_len + 1 <= SCR_DYN_KEY_CACHE_MAX - scr_dyn_cached_key_bytes) {
536
+ scr_dyn_cached_key_bytes += sizeof(ScrDynKey) + entry->key_len + 1;
537
+ continue;
538
+ }
539
+ #else
540
+ (void)cache;
420
541
  #endif
542
+ scr_dyn_key_release(entry->key, entry->key_len);
543
+ entry->key = NULL;
544
+ entry->key_len = 0;
545
+ }
546
+ }
547
+
548
+ /* Primitive kinds cannot acquire properties or change kind while live, so
549
+ * an object property edge to one cannot participate in a cycle. Keep its RC edge
550
+ * out of trial deletion and release it in collected teardown instead. Other
551
+ * owners may still trace the same primitive through the uniform header ABI;
552
+ * our untraced reference keeps it alive until that teardown releases it.
553
+ * Entry metadata snapshots this decision during trace, before any white node
554
+ * can be freed. Teardown must never inspect the child's kind again. */
555
+ static bool scr_dyn_member_is_traced(const ScrDyn *value) {
556
+ return value && value->kind > SCR_DYN_STR && value->kind != SCR_DYN_UNDEF;
557
+ }
421
558
 
422
559
  /* Checked objects and functions can close over one another. All heap dyn
423
560
  * nodes carry a collector header so a captured unknown value has one uniform
@@ -425,19 +562,21 @@ static SCR_TL size_t scr_dyn_free_count;
425
562
  * capsules keep their conservative ownership edges. */
426
563
  void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) {
427
564
  ScrDyn *d = ptr;
428
- visit(d->prototype, ctx);
429
- visit(d->symbol_properties, ctx);
430
- visit(d->symbol_keys, ctx);
565
+ if (d->prototype) visit(d->prototype, ctx);
566
+ if (d->symbol_properties) visit(d->symbol_properties, ctx);
567
+ if (d->symbol_keys) visit(d->symbol_keys, ctx);
431
568
  switch (d->kind) {
432
569
  case SCR_DYN_ARR:
433
570
  for (size_t i = 0; i < d->v.arr.len; i++) visit(d->v.arr.items[i], ctx);
434
- visit(d->v.arr.properties, ctx);
571
+ if (d->v.arr.properties) visit(d->v.arr.properties, ctx);
435
572
  break;
436
573
  case SCR_DYN_OBJ:
437
574
  for (size_t i = 0; i < d->v.obj.len; i++) {
438
- visit(d->v.obj.entries[i].value, ctx);
439
- visit(d->v.obj.entries[i].getter, ctx);
440
- visit(d->v.obj.entries[i].setter, ctx);
575
+ ScrDynEntry *entry = &d->v.obj.entries[i];
576
+ entry->value_traced = scr_dyn_member_is_traced(entry->value);
577
+ if (entry->value_traced) visit(entry->value, ctx);
578
+ if (entry->getter) visit(entry->getter, ctx);
579
+ if (entry->setter) visit(entry->setter, ctx);
441
580
  }
442
581
  break;
443
582
  case SCR_DYN_FUNC: visit(d->v.fn.clo, ctx); break;
@@ -539,6 +678,12 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
539
678
  d->v.arr.presence = NULL;
540
679
  } else if (kind == SCR_DYN_OBJ) {
541
680
  d->v.obj.len = 0; /* cap/entries preserved */
681
+ if (d->v.obj.cap <= SCR_DYN_KEY_CACHE_SLOTS) {
682
+ for (size_t i = 0; i < d->v.obj.cap; i++) {
683
+ ScrDynEntry *entry = &d->v.obj.entries[i];
684
+ if (entry->key) scr_dyn_cached_key_bytes -= sizeof(ScrDynKey) + entry->key_len + 1;
685
+ }
686
+ }
542
687
  d->v.obj.source_identity = NULL;
543
688
  d->v.obj.source_access = NULL;
544
689
  } else {
@@ -565,6 +710,10 @@ static void scr_dyn_handle_release(void *h, ScrDynHandleTag tag);
565
710
  static bool scr_dyn_to_primitive_result_is_object(const ScrDyn *d);
566
711
 
567
712
  static void scr_dyn_dispose(ScrDyn *d, bool collected) {
713
+ if (d->kind == SCR_DYN_OBJ) {
714
+ free(d->v.obj.index);
715
+ d->v.obj.index = NULL;
716
+ }
568
717
  if (!collected) scr_dyn_release(d->prototype);
569
718
  if (!collected) scr_dyn_release(d->symbol_properties);
570
719
  if (!collected) scr_dyn_release(d->symbol_keys);
@@ -592,12 +741,15 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) {
592
741
  break;
593
742
  case SCR_DYN_OBJ:
594
743
  for (size_t i = 0; i < d->v.obj.len; i++) {
595
- free(d->v.obj.entries[i].key);
744
+ ScrDyn *value = d->v.obj.entries[i].value;
745
+ if (!collected || !d->v.obj.entries[i].value_traced) scr_dyn_release(value);
596
746
  if (!collected) {
597
- scr_dyn_release(d->v.obj.entries[i].value);
598
747
  scr_dyn_release(d->v.obj.entries[i].getter);
599
748
  scr_dyn_release(d->v.obj.entries[i].setter);
600
749
  }
750
+ d->v.obj.entries[i].value = NULL;
751
+ d->v.obj.entries[i].getter = NULL;
752
+ d->v.obj.entries[i].setter = NULL;
601
753
  }
602
754
  if (d->v.obj.source_identity) {
603
755
  d->v.obj.source_access(d->v.obj.source_identity, false);
@@ -646,6 +798,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) {
646
798
  #endif
647
799
  #ifndef SCR_RC_AUDIT
648
800
  if (!collected && scr_dyn_free_count < SCR_DYN_FREE_MAX) {
801
+ if (d->kind == SCR_DYN_OBJ) scr_dyn_obj_drop_keys(d, true);
649
802
  /* Pooled blocks retain their headers and count toward the bounded
650
803
  * allocated heap, but never remain in a candidate buffer. */
651
804
  scr_weak_dispose(d);
@@ -659,7 +812,10 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) {
659
812
  }
660
813
  #endif
661
814
  if (d->kind == SCR_DYN_ARR) free(d->v.arr.items);
662
- else if (d->kind == SCR_DYN_OBJ) free(d->v.obj.entries);
815
+ else if (d->kind == SCR_DYN_OBJ) {
816
+ scr_dyn_obj_drop_keys(d, false);
817
+ free(d->v.obj.entries);
818
+ }
663
819
  scr_cyc_free(d);
664
820
  }
665
821
 
@@ -678,25 +834,95 @@ void scr_dyn_release(ScrDyn *d) {
678
834
  scr_dyn_dispose(d, false);
679
835
  }
680
836
 
837
+ /* Small objects retain their compact linear representation. A wide object
838
+ * owns a bucket index of entry positions; entries remain insertion ordered.
839
+ * No key/value ownership lives here, and callbacks never retain its slots. */
840
+ #define SCR_DYN_OBJECT_INDEX_MIN 32
841
+ typedef struct ScrDynObjectIndex {
842
+ size_t buckets;
843
+ size_t slots[]; /* zero is empty; occupied slots store entry + 1 */
844
+ } ScrDynObjectIndex;
845
+
846
+ static void scr_dyn_index_insert(ScrDynObjectIndex *index, const ScrDynEntry *entry, size_t position) {
847
+ size_t slot = scr_key_hash(entry->key, entry->key_len) & (index->buckets - 1);
848
+ while (index->slots[slot]) slot = (slot + 1) & (index->buckets - 1);
849
+ index->slots[slot] = position + 1;
850
+ }
851
+
852
+ /* Keep index allocation/probing outside compact object insertion and reads. */
853
+ static __attribute__((noinline)) void scr_dyn_index_append(ScrDyn *object) {
854
+ size_t count = object->v.obj.len;
855
+ ScrDynObjectIndex *index = object->v.obj.index;
856
+ if (!index || count > index->buckets / 2) {
857
+ size_t buckets = index ? index->buckets : 64;
858
+ while (count > buckets / 2) {
859
+ if (buckets > (SIZE_MAX - sizeof *index) / sizeof(size_t) / 2) scr_json_oom();
860
+ buckets *= 2;
861
+ }
862
+ free(index);
863
+ index = calloc(1, sizeof *index + buckets * sizeof(size_t));
864
+ if (!index) scr_json_oom();
865
+ index->buckets = buckets;
866
+ object->v.obj.index = index;
867
+ for (size_t i = 0; i < count; i++) scr_dyn_index_insert(index, &object->v.obj.entries[i], i);
868
+ } else {
869
+ scr_dyn_index_insert(index, &object->v.obj.entries[count - 1], count - 1);
870
+ }
871
+ }
872
+
873
+ /* A checked hint avoids repeating a linear walk for recurring property
874
+ * names. Slots store indices only, never object/key pointers. Every hit
875
+ * checks the current entry, so different shapes, deletion and reordering
876
+ * need no invalidation and collisions fall back to the ordinary search. */
877
+ #define SCR_DYN_PROPERTY_HINTS 64
878
+ static SCR_TL size_t scr_dyn_property_hints[SCR_DYN_PROPERTY_HINTS];
879
+
880
+ static ScrDynEntry *scr_dyn_find_linear_entry(const ScrDyn *object, const char *key, size_t length) {
881
+ for (size_t i = 0; i < object->v.obj.len; i++) {
882
+ ScrDynEntry *entry = &object->v.obj.entries[i];
883
+ if (entry->key_len == length && !memcmp(entry->key, key, length)) return entry;
884
+ }
885
+ return NULL;
886
+ }
887
+
888
+ static __attribute__((noinline)) ScrDynEntry *scr_dyn_find_indexed_entry(
889
+ const ScrDyn *object, const char *key, size_t length) {
890
+ const ScrDynObjectIndex *index = object->v.obj.index;
891
+ size_t slot = scr_key_hash(key, length) & (index->buckets - 1);
892
+ while (index->slots[slot]) {
893
+ ScrDynEntry *entry = &object->v.obj.entries[index->slots[slot] - 1];
894
+ if (entry->key_len == length && !memcmp(entry->key, key, length)) return entry;
895
+ slot = (slot + 1) & (index->buckets - 1);
896
+ }
897
+ return NULL;
898
+ }
899
+
900
+ static ScrDynEntry *scr_dyn_find_entry(const ScrDyn *object, const char *key, size_t length) {
901
+ if (object->kind != SCR_DYN_OBJ) return NULL;
902
+ if (object->v.obj.index) return scr_dyn_find_indexed_entry(object, key, length);
903
+ size_t slot = (length + (length ? (unsigned char)key[0] * 7u : 0)) % SCR_DYN_PROPERTY_HINTS;
904
+ size_t hint = scr_dyn_property_hints[slot];
905
+ if (hint < object->v.obj.len) {
906
+ ScrDynEntry *entry = &object->v.obj.entries[hint];
907
+ if (entry->key_len == length && !memcmp(entry->key, key, length)) return entry;
908
+ }
909
+ ScrDynEntry *entry = scr_dyn_find_linear_entry(object, key, length);
910
+ if (entry) scr_dyn_property_hints[slot] = (size_t)(entry - object->v.obj.entries);
911
+ return entry;
912
+ }
913
+
681
914
  ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len) {
682
915
  if (d->kind == SCR_DYN_PROXY) {
683
916
  scr_dyn_proxy_unsupported("raw object inspection");
684
917
  return NULL;
685
918
  }
686
- if (d->kind != SCR_DYN_OBJ) return NULL;
687
- for (size_t i = 0; i < d->v.obj.len; i++) {
688
- const ScrDynEntry *e = &d->v.obj.entries[i];
689
- if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) return e->value;
690
- }
691
- return NULL;
919
+ ScrDynEntry *entry = scr_dyn_find_entry(d, key, key_len);
920
+ return entry ? entry->value : NULL;
692
921
  }
693
922
 
694
923
  bool scr_dyn_obj_enumerable(const ScrDyn *d, const char *key, size_t key_len) {
695
- for (size_t i = 0; i < d->v.obj.len; i++) {
696
- const ScrDynEntry *e = &d->v.obj.entries[i];
697
- if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) return e->enumerable;
698
- }
699
- return false;
924
+ ScrDynEntry *entry = scr_dyn_find_entry(d, key, key_len);
925
+ return entry && entry->enumerable;
700
926
  }
701
927
 
702
928
  static const ScrDyn *scr_dyn_property_owner(const ScrDyn *object, const char *key, size_t length);
@@ -983,29 +1209,27 @@ ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key) {
983
1209
  return value;
984
1210
  }
985
1211
 
986
- /* Takes ownership of key (malloc'd) and value. Duplicate keys: the LATER
987
- * value wins (like JS JSON.parse) — the old value is released and the new
988
- * key buffer freed (the surviving entry keeps its original, equal key). */
989
- static void scr_dyn_obj_put(ScrDyn *obj, char *key, size_t key_len, ScrDyn *value) {
990
- for (size_t i = 0; i < obj->v.obj.len; i++) {
991
- ScrDynEntry *e = &obj->v.obj.entries[i];
992
- if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) {
993
- if (e->accessor || !e->writable) {
994
- free(key);
995
- scr_dyn_release(value);
996
- static const char message[] = "Cannot assign to read only property";
997
- scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
998
- return;
999
- }
1000
- ScrDyn *old = e->value;
1001
- e->value = value;
1002
- scr_dyn_release(old);
1003
- free(key);
1212
+ /* Borrows key and takes ownership of value. Duplicate keys retain their
1213
+ * original spelling/storage and insertion position; the later value wins.
1214
+ * A recycled entry can reuse equal key bytes without a malloc/free pair. */
1215
+ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) {
1216
+ /* Construction usually appends an absent name. Avoid a read hint that
1217
+ * repeats comparisons on compact objects; wide objects use their index. */
1218
+ ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len)
1219
+ : scr_dyn_find_linear_entry(obj, key, key_len);
1220
+ if (existing) {
1221
+ if (existing->accessor || !existing->writable) {
1222
+ scr_dyn_release(value);
1223
+ static const char message[] = "Cannot assign to read only property";
1224
+ scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
1004
1225
  return;
1005
1226
  }
1227
+ ScrDyn *old = existing->value;
1228
+ existing->value = value;
1229
+ scr_dyn_release(old);
1230
+ return;
1006
1231
  }
1007
1232
  if (obj->non_extensible) {
1008
- free(key);
1009
1233
  scr_dyn_release(value);
1010
1234
  static const char message[] = "Cannot add property, object is not extensible";
1011
1235
  scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
@@ -1015,11 +1239,16 @@ static void scr_dyn_obj_put(ScrDyn *obj, char *key, size_t key_len, ScrDyn *valu
1015
1239
  size_t cap = obj->v.obj.cap ? obj->v.obj.cap * 2 : 4;
1016
1240
  ScrDynEntry *entries = realloc(obj->v.obj.entries, cap * sizeof *entries);
1017
1241
  if (!entries) scr_json_oom();
1242
+ memset(entries + obj->v.obj.cap, 0, (cap - obj->v.obj.cap) * sizeof *entries);
1018
1243
  obj->v.obj.entries = entries;
1019
1244
  obj->v.obj.cap = cap;
1020
1245
  }
1021
1246
  ScrDynEntry *e = &obj->v.obj.entries[obj->v.obj.len++];
1022
- e->key = key;
1247
+ if (!e->key || e->key_len != key_len || memcmp(e->key, key, key_len)) {
1248
+ char *owned = scr_dyn_key_copy(key, key_len);
1249
+ scr_dyn_key_release(e->key, e->key_len);
1250
+ e->key = owned;
1251
+ }
1023
1252
  e->key_len = key_len;
1024
1253
  e->value = value;
1025
1254
  e->getter = NULL;
@@ -1028,6 +1257,7 @@ static void scr_dyn_obj_put(ScrDyn *obj, char *key, size_t key_len, ScrDyn *valu
1028
1257
  e->writable = true;
1029
1258
  e->enumerable = true;
1030
1259
  e->configurable = true;
1260
+ if (obj->v.obj.len >= SCR_DYN_OBJECT_INDEX_MIN) scr_dyn_index_append(obj);
1031
1261
  }
1032
1262
 
1033
1263
  /* ── dyn construction (compiler-emitted converters & overflow reads) ───── */
@@ -1088,6 +1318,22 @@ ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key) {
1088
1318
  }
1089
1319
 
1090
1320
  ScrDyn *scr_dyn_new_num(double n) {
1321
+ /* Small integers are immutable primitives, just like the boolean and null
1322
+ * singletons. Initialize only touched slots; the table neither grows nor
1323
+ * owns heap references. Preserve negative zero as a distinct payload. */
1324
+ if (n >= -128 && n <= 255) {
1325
+ int value = (int)n;
1326
+ if ((double)value == n && (value != 0 || !signbit(n))) {
1327
+ static SCR_TL ScrDyn values[384];
1328
+ ScrDyn *cached = &values[value + 128];
1329
+ if (cached->rc == 0) {
1330
+ cached->rc = SIZE_MAX;
1331
+ cached->kind = SCR_DYN_NUM;
1332
+ cached->v.num = n;
1333
+ }
1334
+ return cached;
1335
+ }
1336
+ }
1091
1337
  ScrDyn *d = scr_dyn_alloc(SCR_DYN_NUM);
1092
1338
  d->v.num = n;
1093
1339
  return d;
@@ -2827,14 +3073,10 @@ static void scr_dyn_handle_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
2827
3073
  scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
2828
3074
  }
2829
3075
 
2830
- /* Public obj insertion: COPIES the key bytes (the internal put takes a
2831
- * malloc'd buffer), owns the value, later duplicate keys win. */
3076
+ /* Public obj insertion copies a new key only when needed, owns the value,
3077
+ * and preserves the existing entry for duplicate keys. */
2832
3078
  void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) {
2833
- char *copy = malloc(key_len + 1);
2834
- if (!copy) scr_json_oom();
2835
- memcpy(copy, key, key_len);
2836
- copy[key_len] = '\0';
2837
- scr_dyn_obj_put(obj, copy, key_len, value);
3079
+ scr_dyn_obj_put(obj, key, key_len, value);
2838
3080
  }
2839
3081
 
2840
3082
  static ScrStr *scr_dyn_property_key(const ScrDyn *key) {
@@ -2842,12 +3084,7 @@ static ScrStr *scr_dyn_property_key(const ScrDyn *key) {
2842
3084
  }
2843
3085
 
2844
3086
  static ScrDynEntry *scr_dyn_entry(ScrDyn *obj, const ScrStr *key) {
2845
- if (obj->kind != SCR_DYN_OBJ) return NULL;
2846
- for (size_t i = 0; i < obj->v.obj.len; i++) {
2847
- ScrDynEntry *entry = &obj->v.obj.entries[i];
2848
- if (entry->key_len == key->len && memcmp(entry->key, key->data, key->len) == 0) return entry;
2849
- }
2850
- return NULL;
3087
+ return scr_dyn_find_entry(obj, key->data, key->len);
2851
3088
  }
2852
3089
 
2853
3090
  static bool scr_dyn_property_same_value(const ScrDyn *a, const ScrDyn *b) {
@@ -3030,6 +3267,7 @@ typedef struct {
3030
3267
  unsigned kind; /* 0: array, 1: string, 2: Map, 3: Set */
3031
3268
  unsigned selection; /* 0: values, 1: keys, 2: entries */
3032
3269
  bool array_like;
3270
+ bool step_done; /* completion of the last compiler-consumed step */
3033
3271
  double limit;
3034
3272
  } ScrNativeIterator;
3035
3273
 
@@ -3071,21 +3309,18 @@ static void scr_native_iterator_release(void *ptr) {
3071
3309
  free(iterator);
3072
3310
  }
3073
3311
 
3074
- static ScrDyn *scr_native_iterator_next(ScrNativeIterator *iterator) {
3312
+ /* Produce an owned value without allocating the protocol result. The
3313
+ * compiler uses this only after checking the captured next function; public
3314
+ * next() calls still receive a fresh, independently mutable result object. */
3315
+ static ScrDyn *scr_native_iterator_value(ScrNativeIterator *iterator, bool *done_out) {
3075
3316
  ScrDyn *source = iterator->source;
3076
- if (source && iterator->kind == 4) {
3077
- ScrDyn *result = scr_dyn_handle_ops_of(source)->iter_step(source->v.handle.ptr, &iterator->index, iterator->selection);
3078
- if (!result) return NULL;
3079
- if (scr_dyn_truthy(scr_dyn_obj_get(result, "done", 4))) {
3080
- iterator->source = NULL;
3081
- scr_dyn_release(source);
3082
- }
3083
- return result;
3084
- }
3317
+ /* An inherited indexed getter can advance this same iterator recursively.
3318
+ * Keep the source alive even if that nested call exhausts the iterator. */
3319
+ if (source) scr_dyn_retain(source);
3085
3320
  /* Native array capsules retain the live array, so refresh its view at each
3086
3321
  * step rather than iterating a snapshot taken when the iterator opened. */
3087
3322
  ScrDyn *view = source && source->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(source) : NULL;
3088
- if (scr_exc_pending()) { scr_dyn_release(view); return NULL; }
3323
+ if (scr_exc_pending()) { scr_dyn_release(view); scr_dyn_release(source); return NULL; }
3089
3324
  ScrDyn *items = view ? view : source;
3090
3325
  ScrMap *map = source && (iterator->kind == 2 || iterator->kind == 3) ? source->v.handle.ptr : NULL;
3091
3326
  if (map) while (iterator->index < scr_map_iter_count(map) && !scr_map_iter_live(map, iterator->index)) iterator->index++;
@@ -3131,6 +3366,24 @@ static ScrDyn *scr_native_iterator_next(ScrNativeIterator *iterator) {
3131
3366
  scr_str_release(point);
3132
3367
  }
3133
3368
  scr_dyn_release(view);
3369
+ scr_dyn_release(source);
3370
+ *done_out = done;
3371
+ return value;
3372
+ }
3373
+
3374
+ static ScrDyn *scr_native_iterator_next(ScrNativeIterator *iterator) {
3375
+ ScrDyn *source = iterator->source;
3376
+ if (source && iterator->kind == 4) {
3377
+ ScrDyn *result = scr_dyn_handle_ops_of(source)->iter_step(source->v.handle.ptr, &iterator->index, iterator->selection);
3378
+ if (!result) return NULL;
3379
+ if (scr_dyn_truthy(scr_dyn_obj_get(result, "done", 4))) {
3380
+ iterator->source = NULL;
3381
+ scr_dyn_release(source);
3382
+ }
3383
+ return result;
3384
+ }
3385
+ bool done;
3386
+ ScrDyn *value = scr_native_iterator_value(iterator, &done);
3134
3387
  if (!value) return NULL;
3135
3388
  ScrDyn *result = scr_dyn_new_obj();
3136
3389
  scr_dyn_obj_set(result, "value", 5, value);
@@ -3180,6 +3433,28 @@ static ScrDyn *scr_native_handle_iterator_next_call(ScrClosure *closure, ScrDyn
3180
3433
  return scr_native_iterator_next_checked(4);
3181
3434
  }
3182
3435
 
3436
+ bool scr_dyn_iterator_can_step(const ScrDyn *iterator, const ScrDyn *next) {
3437
+ if (iterator->kind != SCR_DYN_HANDLE || iterator->v.handle.tag != SCR_DYNH_ITERATOR) return false;
3438
+ unsigned kind = ((ScrNativeIterator *)iterator->v.handle.ptr)->kind;
3439
+ if (kind > 3) return false;
3440
+ size_t index = kind == 3 ? 8 : kind == 2 ? 7 : kind == 1 ? 5 : 0;
3441
+ return next == scr_iterator_functions[index];
3442
+ }
3443
+
3444
+ ScrDyn *scr_dyn_iterator_step(const ScrDyn *value) {
3445
+ ScrNativeIterator *iterator = value->v.handle.ptr;
3446
+ bool done = false;
3447
+ ScrDyn *item = scr_native_iterator_value(iterator, &done);
3448
+ /* Publish after getters finish, so recursive steps cannot overwrite this
3449
+ * step's completion. No user code runs between step and step_done. */
3450
+ iterator->step_done = done;
3451
+ return item;
3452
+ }
3453
+
3454
+ bool scr_dyn_iterator_step_done(const ScrDyn *value) {
3455
+ return ((ScrNativeIterator *)value->v.handle.ptr)->step_done;
3456
+ }
3457
+
3183
3458
  static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) {
3184
3459
  if (len != 4 || memcmp(key, "next", 4)) return NULL;
3185
3460
  unsigned kind = ((ScrNativeIterator *)ptr)->kind;
@@ -5483,10 +5758,7 @@ void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) {
5483
5758
  if (e->value->kind == SCR_DYN_JSVAL && scr_dyn_isl_typeof_is(e->value, "function")) continue; /* engine functions drop too */
5484
5759
  if (!first) scr_jb_putc(b, ',');
5485
5760
  first = false;
5486
- /* Keys escape exactly like string values (put_json_str quotes). */
5487
- ScrStr *k = scr_str_new(e->key, e->key_len);
5488
- scr_jb_put_json_str(b, k);
5489
- scr_str_release(k);
5761
+ scr_jb_put_json_span(b, e->key, e->key_len);
5490
5762
  scr_jb_putc(b, ':');
5491
5763
  scr_jb_put_dyn(b, e->value);
5492
5764
  }
@@ -5647,8 +5919,10 @@ static int32_t scr_json_hex4(ScrJsonP *p) {
5647
5919
  * slow path would produce) and returns -1. */
5648
5920
  static int scr_json_string_span(ScrJsonP *p, const char **span,
5649
5921
  size_t *span_len) {
5650
- size_t i = p->pos + 1;
5651
- while (i < p->len) {
5922
+ *span = p->s + p->pos + 1;
5923
+ *span_len = scr_json_plain_bytes(*span, p->len - p->pos - 1);
5924
+ size_t i = p->pos + 1 + *span_len;
5925
+ if (i < p->len) {
5652
5926
  unsigned char c = (unsigned char)p->s[i];
5653
5927
  if (c == '"') {
5654
5928
  *span = p->s + p->pos + 1;
@@ -5661,7 +5935,6 @@ static int scr_json_string_span(ScrJsonP *p, const char **span,
5661
5935
  scr_json_throw_pos("Bad control character in string literal", i);
5662
5936
  return -1;
5663
5937
  }
5664
- i++;
5665
5938
  }
5666
5939
  scr_json_throw_pos("Unterminated string", p->pos);
5667
5940
  return -1;
@@ -5669,11 +5942,13 @@ static int scr_json_string_span(ScrJsonP *p, const char **span,
5669
5942
 
5670
5943
  /* Slow path: parses the string literal at p->pos (the opening quote),
5671
5944
  * decoding escapes, into a +1 ScrStr. NULL on error (thrown). */
5672
- static ScrStr *scr_json_string_slow(ScrJsonP *p) {
5945
+ static ScrStr *scr_json_string_slow(ScrJsonP *p, size_t prefix) {
5673
5946
  size_t open = p->pos;
5674
5947
  p->pos++; /* opening quote */
5675
5948
  ScrJsonBuf b;
5676
5949
  scr_jb_init(&b);
5950
+ scr_jb_write(&b, p->s + p->pos, prefix);
5951
+ p->pos += prefix;
5677
5952
  for (;;) {
5678
5953
  if (p->pos >= p->len) {
5679
5954
  scr_jb_dispose(&b);
@@ -5691,8 +5966,9 @@ static ScrStr *scr_json_string_slow(ScrJsonP *p) {
5691
5966
  return NULL;
5692
5967
  }
5693
5968
  if (c != '\\') {
5694
- scr_jb_putc(&b, (char)c); /* raw UTF-8 passes through */
5695
- p->pos++;
5969
+ size_t run = scr_json_plain_bytes(p->s + p->pos, p->len - p->pos);
5970
+ scr_jb_write(&b, p->s + p->pos, run);
5971
+ p->pos += run;
5696
5972
  continue;
5697
5973
  }
5698
5974
  p->pos++; /* backslash */
@@ -5767,7 +6043,7 @@ static ScrStr *scr_json_string_scr(ScrJsonP *p) {
5767
6043
  int r = scr_json_string_span(p, &span, &span_len);
5768
6044
  if (r < 0) return NULL;
5769
6045
  if (r > 0) return scr_str_new(span, span_len);
5770
- return scr_json_string_slow(p);
6046
+ return scr_json_string_slow(p, span_len);
5771
6047
  }
5772
6048
 
5773
6049
  static ScrDyn *scr_json_value(ScrJsonP *p);
@@ -5846,20 +6122,17 @@ static ScrDyn *scr_json_number(ScrJsonP *p) {
5846
6122
  }
5847
6123
  exp10 += eneg ? -ev : ev;
5848
6124
  }
5849
- ScrDyn *d = scr_dyn_alloc(SCR_DYN_NUM);
5850
6125
  if (precise && exp10 >= -22 && exp10 <= 22) {
5851
6126
  double v = (double)mant; /* exact: mant < 10^15 < 2^53 */
5852
6127
  if (exp10 > 0) v *= scr_json_pow10[exp10];
5853
6128
  else if (exp10 < 0) v /= scr_json_pow10[-exp10];
5854
- d->v.num = neg ? -v : v;
5855
- return d;
6129
+ return scr_dyn_new_num(neg ? -v : v);
5856
6130
  }
5857
6131
  /* The validated span re-parses with strtod (correctly rounded, and the
5858
6132
  * grammar above is a strict subset of what strtod accepts). The ScrStr
5859
6133
  * data is NUL-terminated, and strtod stops at the first non-number char,
5860
6134
  * so parsing from `start` reads exactly the validated token. */
5861
- d->v.num = strtod(p->s + start, NULL);
5862
- return d;
6135
+ return scr_dyn_new_num(strtod(p->s + start, NULL));
5863
6136
  }
5864
6137
 
5865
6138
  static bool scr_json_lit(ScrJsonP *p, const char *word, size_t n) {
@@ -5926,10 +6199,12 @@ static ScrDyn *scr_json_object(ScrJsonP *p) {
5926
6199
  scr_json_throw_pos("Expected property name or '}'", p->pos);
5927
6200
  return NULL;
5928
6201
  }
5929
- /* Key: fast span path (no escapes) copies straight into the malloc'd
5930
- * key buffer; the slow path decodes into a ScrStr first. */
6202
+ /* Keep the key borrowed until insertion determines whether it already
6203
+ * exists or can reuse a recycled slot. Escaped keys own their decoded
6204
+ * string until the value has been parsed and inserted. */
5931
6205
  size_t key_len = 0;
5932
- char *key;
6206
+ const char *key;
6207
+ ScrStr *decoded_key = NULL;
5933
6208
  {
5934
6209
  const char *span;
5935
6210
  size_t span_len;
@@ -5939,27 +6214,21 @@ static ScrDyn *scr_json_object(ScrJsonP *p) {
5939
6214
  return NULL;
5940
6215
  }
5941
6216
  if (r > 0) {
5942
- key = malloc(span_len + 1);
5943
- if (!key) scr_json_oom();
5944
- memcpy(key, span, span_len);
5945
- key[span_len] = '\0';
6217
+ key = span;
5946
6218
  key_len = span_len;
5947
6219
  } else {
5948
- ScrStr *ks = scr_json_string_slow(p);
5949
- if (!ks) {
6220
+ decoded_key = scr_json_string_slow(p, span_len);
6221
+ if (!decoded_key) {
5950
6222
  scr_dyn_release(obj);
5951
6223
  return NULL;
5952
6224
  }
5953
- key = malloc(ks->len + 1);
5954
- if (!key) scr_json_oom();
5955
- memcpy(key, ks->data, ks->len + 1);
5956
- key_len = ks->len;
5957
- scr_str_release(ks);
6225
+ key = decoded_key->data;
6226
+ key_len = decoded_key->len;
5958
6227
  }
5959
6228
  }
5960
6229
  scr_json_ws(p);
5961
6230
  if (p->pos >= p->len || p->s[p->pos] != ':') {
5962
- free(key);
6231
+ scr_str_release(decoded_key);
5963
6232
  scr_dyn_release(obj);
5964
6233
  if (p->pos >= p->len) scr_json_throw("Unexpected end of JSON input");
5965
6234
  else scr_json_throw_pos("Expected ':' after property name", p->pos);
@@ -5968,11 +6237,12 @@ static ScrDyn *scr_json_object(ScrJsonP *p) {
5968
6237
  p->pos++; /* ':' */
5969
6238
  ScrDyn *value = scr_json_value(p);
5970
6239
  if (!value) {
5971
- free(key);
6240
+ scr_str_release(decoded_key);
5972
6241
  scr_dyn_release(obj);
5973
6242
  return NULL;
5974
6243
  }
5975
6244
  scr_dyn_obj_put(obj, key, key_len, value); /* later duplicate keys win */
6245
+ scr_str_release(decoded_key);
5976
6246
  scr_json_ws(p);
5977
6247
  if (p->pos >= p->len) {
5978
6248
  scr_dyn_release(obj);
@@ -6121,7 +6391,12 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) {
6121
6391
  ScrDynEntry removed = *entry;
6122
6392
  memmove(entry, entry + 1, (object->v.obj.len - i - 1) * sizeof *entry);
6123
6393
  object->v.obj.len--;
6124
- free(removed.key);
6394
+ free(object->v.obj.index);
6395
+ object->v.obj.index = NULL;
6396
+ /* The moved tail aliases a live entry; spare slots own only their own
6397
+ * retained key bytes, never a second pointer to a surviving property. */
6398
+ memset(&object->v.obj.entries[object->v.obj.len], 0, sizeof *entry);
6399
+ scr_dyn_key_release(removed.key, removed.key_len);
6125
6400
  scr_dyn_release(removed.value);
6126
6401
  scr_dyn_release(removed.getter);
6127
6402
  scr_dyn_release(removed.setter);
@@ -6328,12 +6603,76 @@ static void scr_json_gap(ScrJsonBuf *buffer, const ScrStr *gap, size_t depth) {
6328
6603
  for (size_t i = 0; i < depth; i++) scr_jb_write(buffer, gap->data, gap->len);
6329
6604
  }
6330
6605
 
6606
+ /* These primitive values cannot invoke toJSON or mutate a containing frame.
6607
+ * BigInts, functions and every reference kind keep the protocol walk. */
6608
+ static bool scr_json_plain_value(const ScrDyn *value) {
6609
+ switch (value->kind) {
6610
+ case SCR_DYN_NULL: case SCR_DYN_UNDEF: case SCR_DYN_SYMBOL:
6611
+ case SCR_DYN_BOOL: case SCR_DYN_NUM: case SCR_DYN_STR: return true;
6612
+ default: return false;
6613
+ }
6614
+ }
6615
+
6616
+ /* Prove the whole immediate container before writing any of it. Without a
6617
+ * replacer or executable children, entries cannot change and borrowed names
6618
+ * need neither a key snapshot nor repeated lookups. Numeric property names
6619
+ * keep the ordinary sorted-key walk; sparse/accessor arrays keep Get. This
6620
+ * proof is repeated per call, so later mutations never leave a stale fact. */
6621
+ static bool scr_json_plain_container(const ScrDyn *value) {
6622
+ if (value->kind == SCR_DYN_ARR) {
6623
+ if (value->v.arr.presence || value->v.arr.properties) return false;
6624
+ for (size_t i = 0; i < value->v.arr.len; i++)
6625
+ if (!scr_json_plain_value(value->v.arr.items[i])) return false;
6626
+ return true;
6627
+ }
6628
+ if (value->kind != SCR_DYN_OBJ) return false;
6629
+ for (size_t i = 0; i < value->v.obj.len; i++) {
6630
+ const ScrDynEntry *entry = &value->v.obj.entries[i];
6631
+ if (!entry->enumerable) continue;
6632
+ double index;
6633
+ if (entry->accessor || !scr_json_plain_value(entry->value) ||
6634
+ scr_dyn_key_is_index(entry->key, entry->key_len, &index)) return false;
6635
+ }
6636
+ return true;
6637
+ }
6638
+
6639
+ static bool scr_json_plain_write(ScrJsonBuf *buffer, const ScrDyn *value,
6640
+ const ScrStr *gap, size_t depth) {
6641
+ bool array = value->kind == SCR_DYN_ARR;
6642
+ size_t length = array ? value->v.arr.len : value->v.obj.len;
6643
+ scr_jb_putc(buffer, array ? '[' : '{');
6644
+ bool first = true;
6645
+ for (size_t i = 0; i < length; i++) {
6646
+ const ScrDynEntry *entry = array ? NULL : &value->v.obj.entries[i];
6647
+ if (entry && !entry->enumerable) continue;
6648
+ const ScrDyn *child = array ? value->v.arr.items[i] : entry->value;
6649
+ bool omitted = scr_json_omitted(child);
6650
+ if (!array && omitted) continue;
6651
+ if (!omitted && !scr_json_callback_depth(depth + 1)) return false;
6652
+ if (!first) scr_jb_putc(buffer, ',');
6653
+ first = false;
6654
+ scr_json_gap(buffer, gap, depth + 1);
6655
+ if (entry) {
6656
+ scr_jb_put_json_span(buffer, entry->key, entry->key_len);
6657
+ scr_jb_putc(buffer, ':');
6658
+ if (gap->len) scr_jb_putc(buffer, ' ');
6659
+ }
6660
+ if (omitted) scr_jb_puts(buffer, "null");
6661
+ else scr_dyn_json_write(buffer, child);
6662
+ }
6663
+ if (!first) scr_json_gap(buffer, gap, depth);
6664
+ scr_jb_putc(buffer, array ? ']' : '}');
6665
+ return true;
6666
+ }
6667
+
6331
6668
  static bool scr_json_replaced_write(ScrJsonBuf *buffer, const ScrDyn *value,
6332
6669
  const ScrDyn *replacer, const ScrStr *gap, size_t depth) {
6333
6670
  if (!scr_json_callback_depth(depth)) return false;
6334
6671
  ScrDyn *view = value->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(value) : NULL;
6335
6672
  const ScrDyn *observed = view ? view : value;
6336
6673
  if (scr_exc_pending()) { scr_dyn_release(view); return false; }
6674
+ if (!view && replacer->kind == SCR_DYN_UNDEF && scr_json_plain_container(value))
6675
+ return scr_json_plain_write(buffer, value, gap, depth);
6337
6676
  if (observed->kind != SCR_DYN_OBJ && observed->kind != SCR_DYN_ARR && observed->kind != SCR_DYN_BYTES) {
6338
6677
  /* Engine objects need an engine callback bridge, not an opaque JSON
6339
6678
  * splice: that would silently skip all their children. */
@@ -6824,9 +7163,8 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_
6824
7163
  continue;
6825
7164
  }
6826
7165
  if (current->kind != SCR_DYN_OBJ) continue;
6827
- for (size_t i = 0; i < current->v.obj.len; i++) {
6828
- ScrDynEntry *entry = &current->v.obj.entries[i];
6829
- if (entry->key_len != key_len || memcmp(entry->key, key, key_len) != 0) continue;
7166
+ ScrDynEntry *entry = scr_dyn_find_entry(current, key, key_len);
7167
+ if (entry) {
6830
7168
  if (!entry->accessor) return scr_dyn_retain(entry->value);
6831
7169
  if (!entry->getter) return scr_dyn_retain(scr_dyn_undefined());
6832
7170
  ScrDyn *getter = scr_dyn_retain(entry->getter);
@@ -7094,15 +7432,9 @@ bool scr_dyn_reflect_set(ScrDyn *target, ScrDyn *raw_key, ScrDyn *value, ScrDyn
7094
7432
 
7095
7433
  /* The array-index test (ECMA: a canonical numeric string < 2^32-1). */
7096
7434
  static bool scr_dyn_key_is_index(const char *key, size_t len, double *out) {
7097
- if (len == 0 || len > 10) return false;
7098
- if (key[0] == '0' && len > 1) return false;
7099
- double v = 0;
7100
- for (size_t i = 0; i < len; i++) {
7101
- if (key[i] < '0' || key[i] > '9') return false;
7102
- v = v * 10 + (key[i] - '0');
7103
- }
7104
- if (v > 4294967294.0) return false;
7105
- *out = v;
7435
+ uint32_t index;
7436
+ if (!scr_key_array_index(key, len, &index)) return false;
7437
+ *out = index;
7106
7438
  return true;
7107
7439
  }
7108
7440
 
@@ -7120,34 +7452,31 @@ static ScrDyn *scr_dyn_objwalk_key(const char *key, size_t key_len) {
7120
7452
  ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v) {
7121
7453
  size_t n = v->v.obj.len;
7122
7454
  ScrDyn *keys = scr_dyn_new_arr();
7123
- bool *is_index = malloc(n ? n * sizeof *is_index : 1);
7124
- double *idx = malloc(n ? n * sizeof *idx : 1);
7125
- if (!is_index || !idx) scr_trap("scriptc: out of memory\n");
7126
- size_t index_count = 0;
7455
+ ScrKeyIndex *indices = NULL;
7456
+ size_t count = 0;
7127
7457
  for (size_t i = 0; i < n; i++) {
7128
- const ScrDynEntry *e = &v->v.obj.entries[i];
7129
- is_index[i] = scr_dyn_key_is_index(e->key, e->key_len, &idx[i]);
7130
- if (is_index[i]) index_count++;
7131
- }
7132
- double last = -1;
7133
- for (size_t done = 0; done < index_count; done++) {
7134
- size_t best = (size_t)-1;
7135
- for (size_t i = 0; i < n; i++) {
7136
- if (!is_index[i] || idx[i] <= last) continue;
7137
- if (best == (size_t)-1 || idx[i] < idx[best]) best = i;
7138
- }
7139
- if (best == (size_t)-1) break;
7140
- last = idx[best];
7141
- scr_dyn_arr_push(keys, scr_dyn_objwalk_key(v->v.obj.entries[best].key,
7142
- v->v.obj.entries[best].key_len));
7143
- }
7458
+ const ScrDynEntry *entry = &v->v.obj.entries[i];
7459
+ uint32_t index;
7460
+ if (!scr_key_array_index(entry->key, entry->key_len, &index)) continue;
7461
+ if (!indices) {
7462
+ if (n > SIZE_MAX / sizeof *indices) scr_json_oom();
7463
+ indices = malloc(n * sizeof *indices);
7464
+ if (!indices) scr_json_oom();
7465
+ }
7466
+ indices[count++] = (ScrKeyIndex){index, i};
7467
+ }
7468
+ scr_key_index_sort(indices, count);
7469
+ for (size_t i = 0; i < count; i++) {
7470
+ const ScrDynEntry *entry = &v->v.obj.entries[indices[i].entry];
7471
+ scr_dyn_arr_push(keys, scr_dyn_objwalk_key(entry->key, entry->key_len));
7472
+ }
7473
+ free(indices);
7144
7474
  for (size_t i = 0; i < n; i++) {
7145
- if (is_index[i]) continue;
7146
- scr_dyn_arr_push(keys, scr_dyn_objwalk_key(v->v.obj.entries[i].key,
7147
- v->v.obj.entries[i].key_len));
7475
+ const ScrDynEntry *entry = &v->v.obj.entries[i];
7476
+ uint32_t index;
7477
+ if (!scr_key_array_index(entry->key, entry->key_len, &index))
7478
+ scr_dyn_arr_push(keys, scr_dyn_objwalk_key(entry->key, entry->key_len));
7148
7479
  }
7149
- free(is_index);
7150
- free(idx);
7151
7480
  return keys;
7152
7481
  }
7153
7482