@scriptc/runtime 0.0.9 → 0.0.10

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/src/scr_json.c CHANGED
@@ -328,6 +328,7 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
328
328
  d->rc = 1;
329
329
  d->kind = kind;
330
330
  d->buffer = false;
331
+ d->null_proto = false;
331
332
  if (kind == SCR_DYN_ARR) {
332
333
  d->v.arr.len = 0; /* cap/items preserved from the node's last life */
333
334
  } else if (kind == SCR_DYN_OBJ) {
@@ -381,6 +382,11 @@ void scr_dyn_release(ScrDyn *d) {
381
382
  * scr_json.c without the fiber machinery) never references it. */
382
383
  scr_dyn_promise_release_fn(d->v.promise);
383
384
  break;
385
+ case SCR_DYN_JSVAL:
386
+ /* Installed by scr_dyn_alloc_jsval (the gated constructor is the
387
+ * only producer) — same story as the promise arm. */
388
+ scr_dyn_jsval_ops()->release(d->v.jsval.cell);
389
+ break;
384
390
  default:
385
391
  break; /* null/bool/num have no children */
386
392
  }
@@ -466,10 +472,65 @@ void scr_dyn_arr_push_spread(ScrDyn *arr, const ScrDyn *src, const char *what) {
466
472
  scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
467
473
  return;
468
474
  }
475
+ if (src->kind == SCR_DYN_JSVAL) {
476
+ /* An engine array IS iterable — the generic not-iterable TypeError
477
+ * would be a wrong claim. Loud fence (lane dyn-routing-ops). */
478
+ scr_dyn_isl_fence(src, "spread");
479
+ return;
480
+ }
469
481
  static const char msg[] = "Spread syntax requires ...iterable[Symbol.iterator] to be a function";
470
482
  scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
471
483
  }
472
484
 
485
+ /* Destructuring pack over a DOM source (`const [a, b] = d`, a destructured
486
+ * dyn callback param): the spread walk's iterable kinds collect into a
487
+ * FRESH array — arrays element-by-element (retained), strings by code
488
+ * point, bytes by byte — and every other kind throws V8's DESTRUCTURING
489
+ * TypeError: `msg` verbatim when non-empty (the compile-time spelling —
490
+ * "v is not iterable" for identifier sources, "f is not a function or its
491
+ * return value is not iterable" for identifier-callee calls), else the
492
+ * runtime kind wording ("number 5 is not iterable (cannot read property
493
+ * Symbol(Symbol.iterator))"; objects and functions carry no value text,
494
+ * undefined no kind prefix, null V8's "object null"). Borrows both; +1 or
495
+ * NULL with the TypeError pending. */
496
+ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) {
497
+ if (src->kind == SCR_DYN_ARR || src->kind == SCR_DYN_BYTES || src->kind == SCR_DYN_STR) {
498
+ ScrDyn *out = scr_dyn_new_arr();
499
+ scr_dyn_arr_push_spread(out, src, ""); /* iterable kinds never consult `what` */
500
+ return out;
501
+ }
502
+ /* An engine-held value may well BE iterable — claiming "not iterable"
503
+ * would be a wrong answer. The routing lane owns iteration; until it
504
+ * arms this, the island fence names the operation loudly. */
505
+ if (src->kind == SCR_DYN_JSVAL) {
506
+ scr_dyn_isl_fence(src, "destructuring");
507
+ return NULL;
508
+ }
509
+ if (msg != NULL && msg->len > 0) {
510
+ scr_throw_error(SCR_ERR_TYPE, scr_str_new(msg->data, msg->len));
511
+ return NULL;
512
+ }
513
+ ScrJsonBuf b;
514
+ scr_jb_init(&b);
515
+ switch (src->kind) {
516
+ case SCR_DYN_UNDEF: scr_jb_puts(&b, "undefined"); break;
517
+ case SCR_DYN_NULL: scr_jb_puts(&b, "object null"); break;
518
+ case SCR_DYN_BOOL: scr_jb_puts(&b, src->v.b ? "boolean true" : "boolean false"); break;
519
+ case SCR_DYN_NUM: {
520
+ char buf[32];
521
+ scr_jb_puts(&b, "number ");
522
+ size_t n = scr_f64_to_str(src->v.num, buf);
523
+ scr_jb_write(&b, buf, n);
524
+ break;
525
+ }
526
+ case SCR_DYN_FUNC: scr_jb_puts(&b, "function"); break;
527
+ default: scr_jb_puts(&b, "object"); break;
528
+ }
529
+ scr_jb_puts(&b, " is not iterable (cannot read property Symbol(Symbol.iterator))");
530
+ scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
531
+ return NULL;
532
+ }
533
+
473
534
  /* Takes ownership of key (malloc'd) and value. Duplicate keys: the LATER
474
535
  * value wins (like JS JSON.parse) — the old value is released and the new
475
536
  * key buffer freed (the surviving entry keeps its original, equal key). */
@@ -527,6 +588,11 @@ ScrDyn *scr_dyn_new_str(ScrStr *s) {
527
588
 
528
589
  ScrDyn *scr_dyn_new_arr(void) { return scr_dyn_alloc(SCR_DYN_ARR); }
529
590
  ScrDyn *scr_dyn_new_obj(void) { return scr_dyn_alloc(SCR_DYN_OBJ); }
591
+ ScrDyn *scr_dyn_new_obj_null_proto(void) {
592
+ ScrDyn *d = scr_dyn_alloc(SCR_DYN_OBJ);
593
+ d->null_proto = true;
594
+ return d;
595
+ }
530
596
 
531
597
  ScrDyn *scr_dyn_new_bytes_copy(const ScrBytes *b) {
532
598
  ScrDyn *d = scr_dyn_alloc(SCR_DYN_BYTES);
@@ -586,6 +652,15 @@ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, con
586
652
  * compiled per signature). Args borrowed; result owned (+1) or NULL with
587
653
  * the exception pending. */
588
654
  ScrDyn *scr_dyn_call(const ScrDyn *d, ScrDyn *const *args, size_t argc, const char *what) {
655
+ if (d->kind == SCR_DYN_JSVAL) {
656
+ /* An ENGINE callee: the call routes through scr_jsval_call with the
657
+ * uniform argument conversion (wrapped cells by reference, DOM data
658
+ * deep-copied, FUNC boxes through the host shim); a non-callable
659
+ * engine value throws the ENGINE's own TypeError, bridged catchably.
660
+ * `what` is unused — the engine's message names the failure. */
661
+ (void)what;
662
+ return scr_dyn_jsval_ops()->call(d->v.jsval.cell, args, argc);
663
+ }
589
664
  if (d->kind != SCR_DYN_FUNC) {
590
665
  ScrJsonBuf b;
591
666
  scr_jb_init(&b);
@@ -648,11 +723,20 @@ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) {
648
723
  case SCR_DYN_OBJ: d = "an instance of Object"; break;
649
724
  case SCR_DYN_ARR: d = "an instance of Array"; break;
650
725
  case SCR_DYN_BYTES: d = "an instance of Uint8Array"; break;
651
- case SCR_DYN_FUNC: d = "function"; break; /* callers usually return before this */
726
+ case SCR_DYN_FUNC:
727
+ /* determineSpecificType: `function ${value.name}` — anonymous
728
+ * functions keep Node's trailing space. */
729
+ snprintf(detail, cap, "function %s", cb->v.fn.name != NULL ? cb->v.fn.name : "");
730
+ break;
652
731
  case SCR_DYN_HANDLE:
653
732
  snprintf(detail, cap, "an instance of %s", scr_dyn_handle_cls(cb));
654
733
  break;
655
734
  case SCR_DYN_PROMISE: d = "an instance of Promise"; break;
735
+ case SCR_DYN_JSVAL:
736
+ /* Engine-held: only objects/arrays/functions survive wrap-time scalar
737
+ * normalization — pick by the engine's typeof. */
738
+ d = scr_dyn_isl_typeof_is(cb, "function") ? "function" : "an instance of Object";
739
+ break;
656
740
  case SCR_DYN_BOOL:
657
741
  snprintf(detail, cap, "type boolean (%s)", cb->v.b ? "true" : "false");
658
742
  break;
@@ -705,6 +789,81 @@ void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrD
705
789
  scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_TYPE");
706
790
  }
707
791
 
792
+ /* The property flavor of the same ladder — Node renders option-bag
793
+ * members as "The \"options.x\" property must be ..." (errors.js keys the
794
+ * wording on the name, but every property-path caller here knows it is
795
+ * one). Same runtime-rendered Received tail; always throws catchably. */
796
+ void scr_dyn_prop_type_fail(const char *name, const char *expected, const ScrDyn *got) {
797
+ char detail[64];
798
+ const char *d = scr_dyn_specific_type(got, detail, sizeof detail);
799
+ char msg[224];
800
+ int len = snprintf(msg, sizeof msg,
801
+ "The \"%s\" property must be %s. Received %s", name, expected, d);
802
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_TYPE");
803
+ }
804
+
805
+ /* The compiler-resolved property-typed throw (error.propTypeThrow —
806
+ * argTypeThrow's option-bag sibling). Borrows all three; always throws. */
807
+ void scr_throw_prop_type(const ScrStr *name, const ScrStr *expected, const ScrDyn *got) {
808
+ scr_dyn_prop_type_fail(name->data, expected->data, got);
809
+ }
810
+
811
+ /* ERR_INVALID_ARG_VALUE's "Received" tail — util.inspect where ARG_TYPE
812
+ * renders determineSpecificType: strings quote, scalars print plain.
813
+ * Deep shapes render their bracket sketch (enough for the validators'
814
+ * ladders; nothing observable pins the deep forms). */
815
+ const char *scr_dyn_inspect_lite(const ScrDyn *v, char *buf, size_t cap) {
816
+ switch (v->kind) {
817
+ case SCR_DYN_NULL: return "null";
818
+ case SCR_DYN_UNDEF: return "undefined";
819
+ case SCR_DYN_BOOL: return v->v.b ? "true" : "false";
820
+ case SCR_DYN_NUM: {
821
+ scr_f64_to_str(v->v.num, buf);
822
+ return buf;
823
+ }
824
+ case SCR_DYN_STR: {
825
+ const ScrStr *s = v->v.str;
826
+ size_t n = 0;
827
+ buf[n++] = '\'';
828
+ for (size_t i = 0; i < s->len && n + 5 < cap; i++) buf[n++] = s->data[i];
829
+ if (s->len + 2 + 5 > cap) {
830
+ memcpy(buf + n, "...", 3);
831
+ n += 3;
832
+ }
833
+ buf[n++] = '\'';
834
+ buf[n] = 0;
835
+ return buf;
836
+ }
837
+ case SCR_DYN_ARR: return "[ ... ]";
838
+ case SCR_DYN_OBJ: return "{ ... }";
839
+ case SCR_DYN_BYTES: return "<Buffer ...>";
840
+ default: return "[object]";
841
+ }
842
+ }
843
+
844
+ /* Node's ERR_INVALID_ARG_VALUE thrower: "The <argument|property> '<name>'
845
+ * <reason>. Received <inspected>" — the argument/property choice follows
846
+ * errors.js (a dotted name is a property path). `reason` defaults to
847
+ * "is invalid" when NULL. TypeError, like Node's default. */
848
+ void scr_dyn_arg_value_fail(const char *name, const char *reason, const ScrDyn *got) {
849
+ char insp[64];
850
+ const char *d = scr_dyn_inspect_lite(got, insp, sizeof insp);
851
+ char msg[256];
852
+ int len = snprintf(msg, sizeof msg, "The %s '%s' %s. Received %s",
853
+ strchr(name, '.') != NULL ? "property" : "argument", name,
854
+ reason != NULL ? reason : "is invalid", d);
855
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_VALUE");
856
+ }
857
+
858
+ /* The deferred JS lowering fence, thrown from a ladder's post-validation
859
+ * tail: the compiler renders the message (the statement fence's own text,
860
+ * "[SC2020 at file:line]" included) and the ladder throws it verbatim
861
+ * AFTER its Node-order validations pass — Node's validation errors come
862
+ * first, the honest refuse second. Borrowed; always throws catchably. */
863
+ void scr_throw_lowering_fence(const ScrStr *msg) {
864
+ scr_throw_error_msg_code(SCR_ERR_ERROR, msg->data, msg->len, "SC2020");
865
+ }
866
+
708
867
  static void scr_dyn_handle_release(void *h, ScrDynHandleTag tag) {
709
868
  scr_dyn_handle_ops(tag)->release(h);
710
869
  }
@@ -727,6 +886,78 @@ ScrDyn *scr_dyn_alloc_promise(void (*release_fn)(ScrPromise *p)) {
727
886
  return scr_dyn_alloc(SCR_DYN_PROMISE);
728
887
  }
729
888
 
889
+ /* ── island values in the DOM (SCR_DYN_JSVAL) ─────────────────────────
890
+ * The gated constructor (scr_dyn_from_jsval, scr_island.c) builds through
891
+ * this allocator view and installs the engine-routing ops — the
892
+ * scr_dyn_alloc_promise story: a dynamic-free link never references
893
+ * engine symbols, and JSVAL nodes exist only after an install. */
894
+ static const ScrDynJsvalOps *scr_dynjs_ops = NULL;
895
+
896
+ ScrDyn *scr_dyn_alloc_jsval(ScrJsval *cell, const ScrDynJsvalOps *ops) {
897
+ scr_dynjs_ops = ops;
898
+ ScrDyn *d = scr_dyn_alloc(SCR_DYN_JSVAL);
899
+ d->v.jsval.cell = cell; /* ownership moves in */
900
+ return d;
901
+ }
902
+
903
+ const ScrDynJsvalOps *scr_dyn_jsval_ops(void) {
904
+ if (!scr_dynjs_ops) {
905
+ scr_trap("scriptc: internal error: dyn jsval ops not installed\n");
906
+ }
907
+ return scr_dynjs_ops;
908
+ }
909
+
910
+ bool scr_dyn_isl_typeof_is(const ScrDyn *d, const char *name) {
911
+ if (d->kind != SCR_DYN_JSVAL) return false;
912
+ ScrStr *t = scr_dyn_jsval_ops()->type_of(d->v.jsval.cell);
913
+ size_t n = strlen(name);
914
+ bool r = t->len == n && memcmp(t->data, name, n) == 0;
915
+ scr_str_release(t);
916
+ return r;
917
+ }
918
+
919
+ bool scr_dyn_isl_is_array(const ScrDyn *d) {
920
+ return d->kind == SCR_DYN_JSVAL && scr_dyn_jsval_ops()->is_array(d->v.jsval.cell);
921
+ }
922
+
923
+ bool scr_dyn_isl_is_error(const ScrDyn *d) {
924
+ return d->kind == SCR_DYN_JSVAL && scr_dyn_jsval_ops()->is_error(d->v.jsval.cell);
925
+ }
926
+
927
+ bool scr_dyn_isl_fence(const ScrDyn *d, const char *what) {
928
+ if (d->kind != SCR_DYN_JSVAL) return false;
929
+ ScrJsonBuf b;
930
+ scr_jb_init(&b);
931
+ scr_jb_puts(&b, what);
932
+ scr_jb_puts(&b, " on an island value held in 'unknown' is not supported yet");
933
+ scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
934
+ return false;
935
+ }
936
+
937
+ ScrDyn *scr_dyn_isl_key_get(const ScrDyn *d, const ScrStr *k) {
938
+ /* The emitted keyed read's JSVAL arm: o[k] runs in the ENGINE (getters
939
+ * included, their throws bridged) and the result wraps back scalar-
940
+ * normalized — the retired `.length -> fence` row (and before that,
941
+ * the fence box's silent `.length -> 0`). */
942
+ return scr_dyn_jsval_ops()->key_get(d->v.jsval.cell, k);
943
+ }
944
+
945
+ bool scr_dyn_is_nullish(const ScrDyn *d) {
946
+ if (d->kind == SCR_DYN_UNDEF || d->kind == SCR_DYN_NULL) return true;
947
+ /* JSVAL defensively routes to the engine's own test — the wrap
948
+ * constructor scalar-normalizes engine null/undefined away, so this
949
+ * arm answers false unless a producer bypassed it. */
950
+ if (d->kind == SCR_DYN_JSVAL) return scr_dyn_jsval_ops()->is_nullish(d->v.jsval.cell);
951
+ return false;
952
+ }
953
+
954
+ void scr_dyn_isl_tostr_buf(ScrJsonBuf *b, const ScrDyn *d) {
955
+ ScrStr *s = scr_dyn_jsval_ops()->to_str(d->v.jsval.cell);
956
+ if (!s) return; /* bridged — the exception is pending, append nothing */
957
+ for (size_t i = 0; i < s->len; i++) scr_jb_putc(b, s->data[i]);
958
+ scr_str_release(s);
959
+ }
960
+
730
961
  void *scr_dyn_handle_unbox(const ScrDyn *d, ScrDynHandleTag tag, const ScrDynPath *path, const char *want) {
731
962
  if (d->kind != SCR_DYN_HANDLE || d->v.handle.tag != tag) {
732
963
  scr_dyn_check_fail(path, want, d);
@@ -850,6 +1081,10 @@ bool scr_dyn_truthy(const ScrDyn *d) {
850
1081
  case SCR_DYN_FUNC:
851
1082
  case SCR_DYN_HANDLE:
852
1083
  case SCR_DYN_PROMISE: return true;
1084
+ case SCR_DYN_JSVAL:
1085
+ /* Route to the engine's ToBoolean: objects/arrays/functions are
1086
+ * true, but the symbol/bigint edge (0n is falsy) needs the engine. */
1087
+ return scr_dyn_jsval_ops()->truthy(d->v.jsval.cell);
853
1088
  default: return false; /* undefined, null */
854
1089
  }
855
1090
  }
@@ -858,6 +1093,10 @@ bool scr_dyn_truthy(const ScrDyn *d) {
858
1093
  * null answers "object" — JS's oldest wart, preserved. */
859
1094
  ScrStr *scr_dyn_typeof(const ScrDyn *d) {
860
1095
  const char *s;
1096
+ /* An island value answers the ENGINE's typeof — "object" for the
1097
+ * wrapped objects/arrays, "function" for engine functions (row 1 of
1098
+ * the jsval→DOM op table; scalars normalized away at wrap time). */
1099
+ if (d->kind == SCR_DYN_JSVAL) return scr_dyn_jsval_ops()->type_of(d->v.jsval.cell);
861
1100
  switch (d->kind) {
862
1101
  case SCR_DYN_UNDEF: s = "undefined"; break;
863
1102
  case SCR_DYN_NULL:
@@ -949,6 +1188,17 @@ static bool scr_dyn_json_write(ScrJsonBuf *b, const ScrDyn *d) {
949
1188
  /* No own enumerable properties — Node stringifies a promise as {}. */
950
1189
  scr_jb_puts(b, "{}");
951
1190
  return true;
1191
+ case SCR_DYN_JSVAL: {
1192
+ /* The ENGINE's own JSON.stringify text splices in (toJSON protocols,
1193
+ * cycle TypeErrors — the engine's, bridged catchably). An engine
1194
+ * FUNCTION is absent under stringify, like the DOM's FUNC kind. */
1195
+ if (scr_dyn_isl_typeof_is(d, "function")) return false;
1196
+ ScrStr *j = scr_dyn_jsval_ops()->to_json(d->v.jsval.cell);
1197
+ if (!j) return true; /* pending exception; caller checks */
1198
+ scr_jb_write(b, j->data, j->len);
1199
+ scr_str_release(j);
1200
+ return true;
1201
+ }
952
1202
  case SCR_DYN_HANDLE:
953
1203
  default: {
954
1204
  const char *msg = "JSON.stringify of a runtime handle is not supported yet";
@@ -1166,6 +1416,14 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
1166
1416
  static const char f[] = "function () { [native code] }";
1167
1417
  return scr_str_new(f, sizeof f - 1);
1168
1418
  }
1419
+ case SCR_DYN_JSVAL: {
1420
+ /* The engine's own ToString (row 2 of the jsval→DOM op table): the
1421
+ * real prototype chain runs — user toString included, its throw
1422
+ * bridging. A bridged failure follows this function's existing
1423
+ * throw shape (pending exception + the empty-string dummy). */
1424
+ ScrStr *s = scr_dyn_jsval_ops()->to_str(d->v.jsval.cell);
1425
+ return s ? s : scr_str_new("", 0);
1426
+ }
1169
1427
  case SCR_DYN_UNDEF:
1170
1428
  case SCR_DYN_NULL:
1171
1429
  default: {
@@ -1178,6 +1436,23 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
1178
1436
  }
1179
1437
  }
1180
1438
 
1439
+ /* The METHOD-CALL spelling `d.toString(enc?)` — scr_dyn_to_string with
1440
+ * the one receiver whose prototype LACKS the method carved out: a
1441
+ * null-prototype dictionary (Object.create(null)) has no toString at
1442
+ * all, so Node throws "<spelling> is not a function" where every other
1443
+ * OBJ answers "[object Object]". `what` carries the source spelling. */
1444
+ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) {
1445
+ if (d->kind == SCR_DYN_OBJ && d->null_proto) {
1446
+ ScrJsonBuf b;
1447
+ scr_jb_init(&b);
1448
+ for (size_t i = 0; i < what->len; i++) scr_jb_putc(&b, what->data[i]);
1449
+ scr_jb_puts(&b, " is not a function");
1450
+ scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
1451
+ return scr_str_new("", 0);
1452
+ }
1453
+ return scr_dyn_to_string(d, enc);
1454
+ }
1455
+
1181
1456
  /* JS String() over the DOM kind — the WebIDL ToString the web globals
1182
1457
  * (atob/btoa, DOMException's name resolution) run on their arguments:
1183
1458
  * the unit kinds RENDER ("null"/"undefined") where the .toString() twin
@@ -1258,6 +1533,13 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
1258
1533
  scr_dyn_handle_key_set(recv, key, value);
1259
1534
  return;
1260
1535
  }
1536
+ if (recv->kind == SCR_DYN_JSVAL) {
1537
+ /* The write lands on the REAL engine object (aliasing preserved —
1538
+ * island-side readers see it); the value crosses through the uniform
1539
+ * from_dyn conversion, and engine refusals bridge catchably. */
1540
+ scr_dyn_jsval_ops()->key_set(recv->v.jsval.cell, key, value);
1541
+ return;
1542
+ }
1261
1543
  ScrJsonBuf b;
1262
1544
  scr_jb_init(&b);
1263
1545
  if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) {
@@ -1271,6 +1553,22 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
1271
1553
  for (size_t i = 0; i < key->len; i++) scr_jb_putc(&b, key->data[i]);
1272
1554
  scr_jb_puts(&b, "' on ");
1273
1555
  scr_jb_puts(&b, scr_dyn_kind_name(recv));
1556
+ /* V8 quotes the primitive's own rendering after the kind — "on number
1557
+ * '5'", "on string 'abc'", "on boolean 'true'". Other kinds stop at
1558
+ * the kind word. */
1559
+ if (recv->kind == SCR_DYN_NUM) {
1560
+ char buf[32];
1561
+ size_t n = scr_f64_to_str(recv->v.num, buf);
1562
+ scr_jb_puts(&b, " '");
1563
+ scr_jb_write(&b, buf, n);
1564
+ scr_jb_putc(&b, '\'');
1565
+ } else if (recv->kind == SCR_DYN_STR) {
1566
+ scr_jb_puts(&b, " '");
1567
+ scr_jb_write(&b, recv->v.str->data, recv->v.str->len);
1568
+ scr_jb_putc(&b, '\'');
1569
+ } else if (recv->kind == SCR_DYN_BOOL) {
1570
+ scr_jb_puts(&b, recv->v.b ? " 'true'" : " 'false'");
1571
+ }
1274
1572
  }
1275
1573
  scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
1276
1574
  }
@@ -1332,12 +1630,28 @@ void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) {
1332
1630
  scr_jb_puts(b, "null"); /* the buffer never surfaces: the pending throw wins */
1333
1631
  return;
1334
1632
  }
1633
+ case SCR_DYN_JSVAL: {
1634
+ /* The ENGINE's own JSON.stringify text splices in (toJSON protocols,
1635
+ * cycle TypeErrors — all the engine's, bridged catchably). An engine
1636
+ * FUNCTION serializes like the DOM's FUNC kind (dropped from objects
1637
+ * by the member loop below; null defensively elsewhere). */
1638
+ if (scr_dyn_isl_typeof_is(d, "function")) {
1639
+ scr_jb_puts(b, "null");
1640
+ return;
1641
+ }
1642
+ ScrStr *j = scr_dyn_jsval_ops()->to_json(d->v.jsval.cell);
1643
+ if (!j) return; /* bridged — the pending throw wins */
1644
+ for (size_t i = 0; i < j->len; i++) scr_jb_putc(b, j->data[i]);
1645
+ scr_str_release(j);
1646
+ return;
1647
+ }
1335
1648
  case SCR_DYN_OBJ: {
1336
1649
  scr_jb_putc(b, '{');
1337
1650
  bool first = true;
1338
1651
  for (size_t i = 0; i < d->v.obj.len; i++) {
1339
1652
  const ScrDynEntry *e = &d->v.obj.entries[i];
1340
1653
  if (e->value->kind == SCR_DYN_UNDEF || e->value->kind == SCR_DYN_FUNC) continue; /* dropped, like Node */
1654
+ if (e->value->kind == SCR_DYN_JSVAL && scr_dyn_isl_typeof_is(e->value, "function")) continue; /* engine functions drop too */
1341
1655
  if (!first) scr_jb_putc(b, ',');
1342
1656
  first = false;
1343
1657
  /* Keys escape exactly like string values (put_json_str quotes). */
@@ -1386,6 +1700,9 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) {
1386
1700
  case SCR_DYN_FUNC: return "function";
1387
1701
  case SCR_DYN_HANDLE: return scr_dyn_handle_cls(d); /* "got IncomingMessage" */
1388
1702
  case SCR_DYN_PROMISE: return "Promise"; /* "got Promise" */
1703
+ case SCR_DYN_JSVAL: return "an island value"; /* "got an island value" — validated
1704
+ * extraction of engine-held values has no armed route yet (lane
1705
+ * dom-jsval-long-tail); the failure names the world honestly. */
1389
1706
  }
1390
1707
  return "unknown";
1391
1708
  }
@@ -1940,6 +2257,12 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b) {
1940
2257
  case SCR_DYN_PROMISE:
1941
2258
  /* And the PROMISE: one promise crossing twice is one JS value. */
1942
2259
  return a->v.promise == b->v.promise;
2260
+ case SCR_DYN_JSVAL:
2261
+ /* Identity is the ENGINE VALUE, not the box or even the cell: two
2262
+ * wraps of one engine value compare ===-equal (the engine's own
2263
+ * strict equality answers). Mixed kinds already answered false above
2264
+ * — a DOM copy is a different object, which is Node's answer too. */
2265
+ return a == b || scr_dyn_jsval_ops()->strict_eq(a->v.jsval.cell, b->v.jsval.cell);
1943
2266
  default: return a == b;
1944
2267
  }
1945
2268
  }
@@ -1976,6 +2299,12 @@ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) {
1976
2299
 
1977
2300
  void scr_sc_validate_options(const ScrDyn *options) {
1978
2301
  if (options == NULL || options->kind == SCR_DYN_UNDEF || options->kind == SCR_DYN_NULL) return;
2302
+ /* An engine-held options bag IS a dictionary to Node — the "cannot be
2303
+ * converted" TypeError would be a wrong claim. Loud fence. */
2304
+ if (options->kind == SCR_DYN_JSVAL) {
2305
+ scr_dyn_isl_fence(options, "structuredClone options");
2306
+ return;
2307
+ }
1979
2308
  if (options->kind != SCR_DYN_OBJ) {
1980
2309
  static const char msg[] =
1981
2310
  "Failed to execute 'structuredClone': Options cannot be converted to a dictionary";
@@ -2054,6 +2383,12 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up) {
2054
2383
  }
2055
2384
  return out;
2056
2385
  }
2386
+ case SCR_DYN_JSVAL:
2387
+ /* Node CLONES a plain engine object — the DataCloneError default
2388
+ * below would be a wrong claim, and fabricating a shape would be a
2389
+ * silent wrong answer. Loud fence (lane dom-jsval-long-tail). */
2390
+ scr_dyn_isl_fence(v, "structuredClone");
2391
+ return NULL;
2057
2392
  case SCR_DYN_FUNC:
2058
2393
  case SCR_DYN_HANDLE:
2059
2394
  default: {
@@ -2098,6 +2433,11 @@ ScrDyn *scr_structured_clone_missing(void) {
2098
2433
  }
2099
2434
 
2100
2435
  bool scr_dyn_err_instanceof(const ScrDyn *d, double kind) {
2436
+ /* A JSVAL node never came from a runtime ScrError, so the cache miss
2437
+ * below answers false — the documented contract ("a DOM value that
2438
+ * never came from an error answers false"). An ENGINE TypeError held
2439
+ * in 'unknown' thus answers false where Node answers true: covered by
2440
+ * lane dom-jsval-long-tail (needs the engine's class instanceof). */
2101
2441
  for (size_t i = 0; i < scr_errdyn_n; i++) {
2102
2442
  if (scr_errdyn_cache[i].dyn == d) {
2103
2443
  const ScrVt *vt = scr_errdyn_cache[i].err->vt;
@@ -2165,6 +2505,12 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
2165
2505
  scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
2166
2506
  return NULL;
2167
2507
  }
2508
+ if (v->kind == SCR_DYN_JSVAL) {
2509
+ /* The ENGINE walks its own object (own-key order, getters running,
2510
+ * Object.entries' pairs) and the results come back as a NATIVE DOM
2511
+ * array — keys are DOM strings, values wrap per element. */
2512
+ return scr_dyn_jsval_ops()->obj_walk(v->v.jsval.cell, (int)mode);
2513
+ }
2168
2514
  ScrDyn *out = scr_dyn_new_arr();
2169
2515
  if (v->kind == SCR_DYN_OBJ) {
2170
2516
  size_t n = v->v.obj.len;
@@ -2266,21 +2612,150 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
2266
2612
 
2267
2613
  ScrDyn *scr_dyn_obj_keys(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWALK_KEYS); }
2268
2614
 
2615
+ /* One source's own enumerable members onto an OBJ target — the
2616
+ * CopyDataProperties walk Object.assign runs per source. OBJ sources copy
2617
+ * their members directly (last write wins); the index-keyed kinds
2618
+ * (arrays, strings, bytes) ride the entries walk, so the copied key set
2619
+ * is EXACTLY what Object.keys answers for that kind (string sources per
2620
+ * UTF-16 code unit, like Node's String exotic object); nullish sources
2621
+ * copy nothing (Node skips them) and the scalar/function/handle kinds
2622
+ * have no own enumerable string keys. Non-OBJ targets copy nothing (the
2623
+ * existing two-arg stance — a DOM array target has no property table). */
2624
+ static void scr_dyn_assign_from(ScrDyn *target, const ScrDyn *src) {
2625
+ if (target->kind != SCR_DYN_OBJ) return;
2626
+ if (src->kind == SCR_DYN_UNDEF || src->kind == SCR_DYN_NULL) return;
2627
+ if (src->kind == SCR_DYN_OBJ) {
2628
+ for (size_t i = 0; i < src->v.obj.len; i++) {
2629
+ scr_dyn_obj_set(target, src->v.obj.entries[i].key,
2630
+ src->v.obj.entries[i].key_len,
2631
+ scr_dyn_retain(src->v.obj.entries[i].value));
2632
+ }
2633
+ return;
2634
+ }
2635
+ if (src->kind != SCR_DYN_ARR && src->kind != SCR_DYN_STR &&
2636
+ src->kind != SCR_DYN_BYTES) {
2637
+ return;
2638
+ }
2639
+ ScrDyn *pairs = scr_dyn_obj_entries(src); /* +1; never throws here */
2640
+ if (pairs == NULL) return;
2641
+ if (pairs->kind == SCR_DYN_ARR) {
2642
+ for (size_t i = 0; i < pairs->v.arr.len; i++) {
2643
+ const ScrDyn *pair = pairs->v.arr.items[i];
2644
+ if (pair->kind != SCR_DYN_ARR || pair->v.arr.len != 2) continue;
2645
+ const ScrDyn *k = pair->v.arr.items[0];
2646
+ if (k->kind != SCR_DYN_STR) continue;
2647
+ scr_dyn_obj_set(target, k->v.str->data, k->v.str->len,
2648
+ scr_dyn_retain(pair->v.arr.items[1]));
2649
+ }
2650
+ }
2651
+ scr_dyn_release(pairs);
2652
+ }
2653
+
2269
2654
  /* Object.assign over DOM values: copies `src`'s own members onto `target`
2270
2655
  * (last write wins) and answers the target retained (+1). Nullish
2271
2656
  * receivers throw Node's ToObject TypeError; nullish sources copy
2272
- * nothing; non-object sources copy nothing DOM-representable. */
2657
+ * nothing; index-keyed sources (arrays/strings/bytes) copy their index
2658
+ * keys like Node; the remaining kinds have no own enumerable keys. */
2273
2659
  ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) {
2274
2660
  if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) {
2275
2661
  const char *m = "Cannot convert undefined or null to object";
2276
2662
  scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m));
2277
2663
  return NULL;
2278
2664
  }
2279
- if (target->kind == SCR_DYN_OBJ && src->kind == SCR_DYN_OBJ) {
2280
- for (size_t i = 0; i < src->v.obj.len; i++) {
2281
- scr_dyn_obj_set(target, src->v.obj.entries[i].key,
2282
- src->v.obj.entries[i].key_len,
2283
- scr_dyn_retain(src->v.obj.entries[i].value));
2665
+ if (target->kind == SCR_DYN_JSVAL) {
2666
+ /* An ENGINE target: the copy runs in the engine (Object.assign's own
2667
+ * semantics — setters fire, own-enumerable order); the source enters
2668
+ * per the uniform conversion (a wrapped source spreads by reference,
2669
+ * DOM data as the usual member deep copy). */
2670
+ if (!scr_dyn_jsval_ops()->assign(target->v.jsval.cell, src)) return NULL;
2671
+ return scr_dyn_retain(target);
2672
+ }
2673
+ if (src->kind == SCR_DYN_JSVAL) {
2674
+ /* A wrapped SOURCE onto a DOM target: the engine lists its own
2675
+ * [key, value] pairs (getters running) and each lands as a DOM
2676
+ * member — values wrap per element, scalars normalized. */
2677
+ if (target->kind == SCR_DYN_OBJ) {
2678
+ ScrDyn *entries = scr_dyn_jsval_ops()->obj_walk(src->v.jsval.cell, 2);
2679
+ if (!entries) return NULL;
2680
+ for (size_t i = 0; i < entries->v.arr.len; i++) {
2681
+ const ScrDyn *pair = entries->v.arr.items[i];
2682
+ const ScrDyn *k = pair->v.arr.items[0];
2683
+ scr_dyn_obj_set(target, k->v.str->data, k->v.str->len,
2684
+ scr_dyn_retain(pair->v.arr.items[1]));
2685
+ }
2686
+ scr_dyn_release(entries);
2687
+ }
2688
+ return scr_dyn_retain(target);
2689
+ }
2690
+ scr_dyn_assign_from(target, src);
2691
+ return scr_dyn_retain(target);
2692
+ }
2693
+
2694
+ /* Variadic Object.assign's argument pack (the `Object.assign({},
2695
+ * ...arr.map(f), tail)` shape): the compiler builds one fresh DOM array
2696
+ * of sources — plain arguments push borrowed (+1 in), spread arguments
2697
+ * flatten through the spread-call walk (scr_dyn_arr_push_spread's V8
2698
+ * TypeError texts, `what` spelling the spread expression for the nullish
2699
+ * form) — so every source evaluates and flattens BEFORE any copying,
2700
+ * exactly JS's ArgumentListEvaluation. */
2701
+ void scr_dyn_pack_push(ScrDyn *pack, ScrDyn *v) {
2702
+ scr_dyn_arr_push(pack, scr_dyn_retain(v));
2703
+ }
2704
+
2705
+ void scr_dyn_pack_push_spread(ScrDyn *pack, const ScrDyn *src, const ScrStr *what) {
2706
+ scr_dyn_arr_push_spread(pack, src, what->data);
2707
+ }
2708
+
2709
+ /* The ITERATED-path spread completion: V8 only takes the optimized
2710
+ * apply-path texts (scr_dyn_arr_push_spread's — the expression spelled
2711
+ * for nullish sources) when the spread is the SINGLE LAST argument; a spread
2712
+ * followed by more arguments, or one of several spreads, drives the real
2713
+ * iterator protocol, whose failure text describes the VALUE instead —
2714
+ * "undefined", "object null", "number 5", "boolean true", "function",
2715
+ * bare "object" — + " is not iterable (cannot read property
2716
+ * Symbol(Symbol.iterator))". The compiler picks the variant by the
2717
+ * spread's syntactic position. MAY THROW (pending). Borrows src. */
2718
+ void scr_dyn_pack_push_spread_iter(ScrDyn *pack, const ScrDyn *src) {
2719
+ if (src->kind == SCR_DYN_ARR || src->kind == SCR_DYN_BYTES ||
2720
+ src->kind == SCR_DYN_STR) {
2721
+ scr_dyn_arr_push_spread(pack, src, ""); /* iterable kinds never throw */
2722
+ return;
2723
+ }
2724
+ ScrJsonBuf b;
2725
+ scr_jb_init(&b);
2726
+ switch (src->kind) {
2727
+ case SCR_DYN_UNDEF: scr_jb_puts(&b, "undefined"); break;
2728
+ case SCR_DYN_NULL: scr_jb_puts(&b, "object null"); break;
2729
+ case SCR_DYN_NUM: {
2730
+ scr_jb_puts(&b, "number ");
2731
+ ScrStr *s = scr_f64_to_scrstr(src->v.num);
2732
+ for (size_t i = 0; i < s->len; i++) scr_jb_putc(&b, s->data[i]);
2733
+ scr_str_release(s);
2734
+ break;
2735
+ }
2736
+ case SCR_DYN_BOOL: scr_jb_puts(&b, src->v.b ? "boolean true" : "boolean false"); break;
2737
+ case SCR_DYN_FUNC: scr_jb_puts(&b, "function"); break;
2738
+ default: scr_jb_puts(&b, "object"); break; /* OBJ/HANDLE/PROMISE */
2739
+ }
2740
+ scr_jb_puts(&b, " is not iterable (cannot read property Symbol(Symbol.iterator))");
2741
+ scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
2742
+ }
2743
+
2744
+ /* Object.assign(target, ...sources) over the flattened pack: the nullish
2745
+ * ToObject TypeError first (Node throws before looking at sources), then
2746
+ * each source's own-member copy left to right, answering the target
2747
+ * retained (+1) — identity, like JS. */
2748
+ ScrDyn *scr_dyn_assign_all(ScrDyn *target, const ScrDyn *sources) {
2749
+ if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) {
2750
+ const char *m = "Cannot convert undefined or null to object";
2751
+ scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m));
2752
+ return NULL;
2753
+ }
2754
+ if (sources->kind == SCR_DYN_ARR) {
2755
+ for (size_t i = 0; i < sources->v.arr.len; i++) {
2756
+ ScrDyn *r = scr_dyn_assign(target, sources->v.arr.items[i]);
2757
+ if (!r) return NULL;
2758
+ scr_dyn_release(r);
2284
2759
  }
2285
2760
  }
2286
2761
  return scr_dyn_retain(target);
@@ -2292,6 +2767,12 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) {
2292
2767
  scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m));
2293
2768
  return false;
2294
2769
  }
2770
+ /* Engine-held: the ENGINE's own Object.hasOwn answers (a bridged
2771
+ * surprise leaves the exception pending and answers false — callers
2772
+ * check pending like every fallible dyn op). */
2773
+ if (v->kind == SCR_DYN_JSVAL) {
2774
+ return scr_dyn_jsval_ops()->has_own(v->v.jsval.cell, key) == 1;
2775
+ }
2295
2776
  if (v->kind == SCR_DYN_OBJ) {
2296
2777
  return scr_dyn_obj_get(v, key->data, key->len) != NULL;
2297
2778
  }