@scriptc/runtime 0.0.8 → 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
  }
@@ -424,6 +430,107 @@ void scr_dyn_arr_push(ScrDyn *arr, ScrDyn *item) {
424
430
  arr->v.arr.items[arr->v.arr.len++] = item; /* ownership moves in */
425
431
  }
426
432
 
433
+ /* Spread completion for a runtime-arity argument list (`f(...xs)` in the
434
+ * checked-dynamic tier): JS's spread over the DOM's iterable kinds —
435
+ * arrays element-by-element (retained), strings by code POINT (the string
436
+ * iterator; astral chars arrive unsplit), bytes by byte; every other kind
437
+ * throws V8's exact SPREAD-CALL TypeError (catchable, pending — callers
438
+ * check): nullish sources spell the spread expression (`what`) — "v is
439
+ * not iterable (cannot read property undefined)" — and everything else is
440
+ * the generic "Spread syntax requires ...iterable[Symbol.iterator] to be
441
+ * a function". Borrows src. */
442
+ void scr_dyn_arr_push_spread(ScrDyn *arr, const ScrDyn *src, const char *what) {
443
+ if (src->kind == SCR_DYN_ARR) {
444
+ for (size_t i = 0; i < src->v.arr.len; i++) {
445
+ scr_dyn_arr_push(arr, scr_dyn_retain(src->v.arr.items[i]));
446
+ }
447
+ return;
448
+ }
449
+ if (src->kind == SCR_DYN_BYTES) {
450
+ for (size_t i = 0; i < src->v.bytes->len; i++) {
451
+ scr_dyn_arr_push(arr, scr_dyn_new_num((double)src->v.bytes->data[i]));
452
+ }
453
+ return;
454
+ }
455
+ if (src->kind == SCR_DYN_STR) {
456
+ double len = scr_str_utf16_len(src->v.str);
457
+ for (double at = 0; at < len;) {
458
+ ScrStr *cp = scr_str_cp_at(src->v.str, at);
459
+ at += scr_str_utf16_len(cp);
460
+ scr_dyn_arr_push(arr, scr_dyn_new_str(cp));
461
+ scr_str_release(cp);
462
+ }
463
+ return;
464
+ }
465
+ if (src->kind == SCR_DYN_UNDEF || src->kind == SCR_DYN_NULL) {
466
+ ScrJsonBuf b;
467
+ scr_jb_init(&b);
468
+ scr_jb_puts(&b, what);
469
+ scr_jb_puts(&b, " is not iterable (cannot read property ");
470
+ scr_jb_puts(&b, src->kind == SCR_DYN_UNDEF ? "undefined" : "null");
471
+ scr_jb_puts(&b, ")");
472
+ scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
473
+ return;
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
+ }
481
+ static const char msg[] = "Spread syntax requires ...iterable[Symbol.iterator] to be a function";
482
+ scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
483
+ }
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
+
427
534
  /* Takes ownership of key (malloc'd) and value. Duplicate keys: the LATER
428
535
  * value wins (like JS JSON.parse) — the old value is released and the new
429
536
  * key buffer freed (the surviving entry keeps its original, equal key). */
@@ -481,6 +588,11 @@ ScrDyn *scr_dyn_new_str(ScrStr *s) {
481
588
 
482
589
  ScrDyn *scr_dyn_new_arr(void) { return scr_dyn_alloc(SCR_DYN_ARR); }
483
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
+ }
484
596
 
485
597
  ScrDyn *scr_dyn_new_bytes_copy(const ScrBytes *b) {
486
598
  ScrDyn *d = scr_dyn_alloc(SCR_DYN_BYTES);
@@ -540,6 +652,15 @@ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, con
540
652
  * compiled per signature). Args borrowed; result owned (+1) or NULL with
541
653
  * the exception pending. */
542
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
+ }
543
664
  if (d->kind != SCR_DYN_FUNC) {
544
665
  ScrJsonBuf b;
545
666
  scr_jb_init(&b);
@@ -551,6 +672,13 @@ ScrDyn *scr_dyn_call(const ScrDyn *d, ScrDyn *const *args, size_t argc, const ch
551
672
  return d->v.fn.thunk(d->v.fn.clo, args, argc);
552
673
  }
553
674
 
675
+ /* scr_dyn_call over a DOM ARRAY's elements — the spread-application form
676
+ * (`f(...args)` after the emitted argument array is built). Borrows both;
677
+ * result owned (+1), or NULL with the exception pending. */
678
+ ScrDyn *scr_dyn_apply(const ScrDyn *d, const ScrDyn *args, const char *what) {
679
+ return scr_dyn_call(d, args->v.arr.items, args->v.arr.len, what);
680
+ }
681
+
554
682
  /* ── native handles in the DOM (SCR_DYN_HANDLE) ───────────────────────
555
683
  * Per-tag ops stamped by the owning units at main() (scr_http_dyn_install
556
684
  * / scr_net_dyn_install — the scr_net_install hook story), so this
@@ -582,6 +710,160 @@ const ScrDynHandleOps *scr_dyn_handle_ops_of(const ScrDyn *d) {
582
710
  return scr_dyn_handle_ops(d->v.handle.tag);
583
711
  }
584
712
 
713
+ /* errors.js's determineSpecificType over a DOM value — the "Received
714
+ * ..." tail of Node's ERR_INVALID_ARG_TYPE messages. Renders into buf
715
+ * when the shape needs a payload; returns the text either way. Lives
716
+ * beside the DOM core (not the gated handle unit) because the always-
717
+ * linked argument validators (bytes, fs) render through it too. */
718
+ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) {
719
+ const char *d = detail;
720
+ switch (cb->kind) {
721
+ case SCR_DYN_NULL: d = "null"; break;
722
+ case SCR_DYN_UNDEF: d = "undefined"; break;
723
+ case SCR_DYN_OBJ: d = "an instance of Object"; break;
724
+ case SCR_DYN_ARR: d = "an instance of Array"; break;
725
+ case SCR_DYN_BYTES: d = "an instance of Uint8Array"; break;
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;
731
+ case SCR_DYN_HANDLE:
732
+ snprintf(detail, cap, "an instance of %s", scr_dyn_handle_cls(cb));
733
+ break;
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;
740
+ case SCR_DYN_BOOL:
741
+ snprintf(detail, cap, "type boolean (%s)", cb->v.b ? "true" : "false");
742
+ break;
743
+ case SCR_DYN_NUM: {
744
+ char num[32];
745
+ size_t n = scr_f64_to_str(cb->v.num, num);
746
+ snprintf(detail, cap, "type number (%.*s)", (int)n, num);
747
+ break;
748
+ }
749
+ case SCR_DYN_STR: {
750
+ const ScrStr *sv = cb->v.str;
751
+ char insp[32];
752
+ size_t n = 0;
753
+ insp[n++] = '\'';
754
+ for (size_t i = 0; i < sv->len && n < 28; i++) insp[n++] = sv->data[i];
755
+ if (sv->len + 2 > 28) {
756
+ n = 25;
757
+ memcpy(insp + n, "...", 3);
758
+ n += 3;
759
+ } else {
760
+ insp[n++] = '\'';
761
+ }
762
+ snprintf(detail, cap, "type string (%.*s)", (int)n, insp);
763
+ break;
764
+ }
765
+ default: d = "an instance of Object"; break;
766
+ }
767
+ return d;
768
+ }
769
+
770
+ /* Node's ERR_INVALID_ARG_TYPE thrower ("The \"chunk\" argument must be
771
+ * of type string or an instance of Buffer or Uint8Array. Received type
772
+ * number (5)") — the handle dispatchers' and argument validators'
773
+ * per-arg gates. `expected` is the full "of type ..."/"an instance of
774
+ * ..." clause. */
775
+ /* The compiler-resolved ERR_INVALID_ARG_TYPE throw with a RUNTIME-
776
+ * rendered Received tail (error.argTypeThrow — the always-throwing
777
+ * lowered arms whose offending value is not a literal). Borrows all
778
+ * three; always throws catchably. */
779
+ void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const ScrDyn *got) {
780
+ scr_dyn_arg_type_fail(argname->data, expected->data, got);
781
+ }
782
+
783
+ void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrDyn *got) {
784
+ char detail[64];
785
+ const char *d = scr_dyn_specific_type(got, detail, sizeof detail);
786
+ char msg[224];
787
+ int len = snprintf(msg, sizeof msg,
788
+ "The \"%s\" argument must be %s. Received %s", argname, expected, d);
789
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_TYPE");
790
+ }
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
+
585
867
  static void scr_dyn_handle_release(void *h, ScrDynHandleTag tag) {
586
868
  scr_dyn_handle_ops(tag)->release(h);
587
869
  }
@@ -604,6 +886,78 @@ ScrDyn *scr_dyn_alloc_promise(void (*release_fn)(ScrPromise *p)) {
604
886
  return scr_dyn_alloc(SCR_DYN_PROMISE);
605
887
  }
606
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
+
607
961
  void *scr_dyn_handle_unbox(const ScrDyn *d, ScrDynHandleTag tag, const ScrDynPath *path, const char *want) {
608
962
  if (d->kind != SCR_DYN_HANDLE || d->v.handle.tag != tag) {
609
963
  scr_dyn_check_fail(path, want, d);
@@ -727,6 +1081,10 @@ bool scr_dyn_truthy(const ScrDyn *d) {
727
1081
  case SCR_DYN_FUNC:
728
1082
  case SCR_DYN_HANDLE:
729
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);
730
1088
  default: return false; /* undefined, null */
731
1089
  }
732
1090
  }
@@ -735,6 +1093,10 @@ bool scr_dyn_truthy(const ScrDyn *d) {
735
1093
  * null answers "object" — JS's oldest wart, preserved. */
736
1094
  ScrStr *scr_dyn_typeof(const ScrDyn *d) {
737
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);
738
1100
  switch (d->kind) {
739
1101
  case SCR_DYN_UNDEF: s = "undefined"; break;
740
1102
  case SCR_DYN_NULL:
@@ -826,6 +1188,17 @@ static bool scr_dyn_json_write(ScrJsonBuf *b, const ScrDyn *d) {
826
1188
  /* No own enumerable properties — Node stringifies a promise as {}. */
827
1189
  scr_jb_puts(b, "{}");
828
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
+ }
829
1202
  case SCR_DYN_HANDLE:
830
1203
  default: {
831
1204
  const char *msg = "JSON.stringify of a runtime handle is not supported yet";
@@ -1043,6 +1416,14 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
1043
1416
  static const char f[] = "function () { [native code] }";
1044
1417
  return scr_str_new(f, sizeof f - 1);
1045
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
+ }
1046
1427
  case SCR_DYN_UNDEF:
1047
1428
  case SCR_DYN_NULL:
1048
1429
  default: {
@@ -1055,6 +1436,23 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
1055
1436
  }
1056
1437
  }
1057
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
+
1058
1456
  /* JS String() over the DOM kind — the WebIDL ToString the web globals
1059
1457
  * (atob/btoa, DOMException's name resolution) run on their arguments:
1060
1458
  * the unit kinds RENDER ("null"/"undefined") where the .toString() twin
@@ -1066,6 +1464,39 @@ ScrStr *scr_dyn_string_coerce(const ScrDyn *d) {
1066
1464
  return scr_dyn_to_string(d, NULL);
1067
1465
  }
1068
1466
 
1467
+ /* JS ToString over a DOM value WITH the object protocol (the WHATWG
1468
+ * USVString conversions — URLSearchParams names/values): an OBJ whose
1469
+ * own 'toString' member is callable is invoked with zero arguments (its
1470
+ * throw propagates, catchably); a non-primitive answer falls through to
1471
+ * 'valueOf' (ToPrimitive's string hint); exhaustion is the spec's
1472
+ * "Cannot convert object to primitive value" TypeError. Every other
1473
+ * kind matches scr_dyn_string_coerce (units RENDER — ToString(null) is
1474
+ * "null"). Borrows; +1, or NULL with the exception pending. */
1475
+ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
1476
+ if (d->kind == SCR_DYN_OBJ) {
1477
+ static const char *const hint[2] = { "toString", "valueOf" };
1478
+ for (int i = 0; i < 2; i++) {
1479
+ ScrDyn *m = scr_dyn_obj_get(d, hint[i], strlen(hint[i])); /* borrowed */
1480
+ if (!m || m->kind != SCR_DYN_FUNC) continue;
1481
+ ScrDyn *r = scr_dyn_call(m, NULL, 0, hint[i]);
1482
+ if (!r) return NULL; /* the method threw — pending */
1483
+ if (r->kind == SCR_DYN_OBJ || r->kind == SCR_DYN_ARR ||
1484
+ r->kind == SCR_DYN_FUNC || r->kind == SCR_DYN_HANDLE ||
1485
+ r->kind == SCR_DYN_PROMISE) {
1486
+ scr_dyn_release(r); /* non-primitive answer: try the next method */
1487
+ continue;
1488
+ }
1489
+ ScrStr *s = scr_dyn_string_coerce(r);
1490
+ scr_dyn_release(r);
1491
+ return s;
1492
+ }
1493
+ static const char msg[] = "Cannot convert object to primitive value";
1494
+ scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
1495
+ return NULL;
1496
+ }
1497
+ return scr_dyn_string_coerce(d);
1498
+ }
1499
+
1069
1500
  /* The checked-dynamic keyed WRITE (`h.k = v` on a dyn receiver): OBJ sets
1070
1501
  * the member (later writes win, insertion order — JS); undefined/null
1071
1502
  * throws Node's "Cannot set properties of ..."; every other kind throws
@@ -1102,6 +1533,13 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
1102
1533
  scr_dyn_handle_key_set(recv, key, value);
1103
1534
  return;
1104
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
+ }
1105
1543
  ScrJsonBuf b;
1106
1544
  scr_jb_init(&b);
1107
1545
  if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) {
@@ -1115,6 +1553,22 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
1115
1553
  for (size_t i = 0; i < key->len; i++) scr_jb_putc(&b, key->data[i]);
1116
1554
  scr_jb_puts(&b, "' on ");
1117
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
+ }
1118
1572
  }
1119
1573
  scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
1120
1574
  }
@@ -1176,12 +1630,28 @@ void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) {
1176
1630
  scr_jb_puts(b, "null"); /* the buffer never surfaces: the pending throw wins */
1177
1631
  return;
1178
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
+ }
1179
1648
  case SCR_DYN_OBJ: {
1180
1649
  scr_jb_putc(b, '{');
1181
1650
  bool first = true;
1182
1651
  for (size_t i = 0; i < d->v.obj.len; i++) {
1183
1652
  const ScrDynEntry *e = &d->v.obj.entries[i];
1184
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 */
1185
1655
  if (!first) scr_jb_putc(b, ',');
1186
1656
  first = false;
1187
1657
  /* Keys escape exactly like string values (put_json_str quotes). */
@@ -1230,6 +1700,9 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) {
1230
1700
  case SCR_DYN_FUNC: return "function";
1231
1701
  case SCR_DYN_HANDLE: return scr_dyn_handle_cls(d); /* "got IncomingMessage" */
1232
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. */
1233
1706
  }
1234
1707
  return "unknown";
1235
1708
  }
@@ -1784,6 +2257,12 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b) {
1784
2257
  case SCR_DYN_PROMISE:
1785
2258
  /* And the PROMISE: one promise crossing twice is one JS value. */
1786
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);
1787
2266
  default: return a == b;
1788
2267
  }
1789
2268
  }
@@ -1820,6 +2299,12 @@ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) {
1820
2299
 
1821
2300
  void scr_sc_validate_options(const ScrDyn *options) {
1822
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
+ }
1823
2308
  if (options->kind != SCR_DYN_OBJ) {
1824
2309
  static const char msg[] =
1825
2310
  "Failed to execute 'structuredClone': Options cannot be converted to a dictionary";
@@ -1898,6 +2383,12 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up) {
1898
2383
  }
1899
2384
  return out;
1900
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;
1901
2392
  case SCR_DYN_FUNC:
1902
2393
  case SCR_DYN_HANDLE:
1903
2394
  default: {
@@ -1942,6 +2433,11 @@ ScrDyn *scr_structured_clone_missing(void) {
1942
2433
  }
1943
2434
 
1944
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). */
1945
2441
  for (size_t i = 0; i < scr_errdyn_n; i++) {
1946
2442
  if (scr_errdyn_cache[i].dyn == d) {
1947
2443
  const ScrVt *vt = scr_errdyn_cache[i].err->vt;
@@ -2009,6 +2505,12 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
2009
2505
  scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
2010
2506
  return NULL;
2011
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
+ }
2012
2514
  ScrDyn *out = scr_dyn_new_arr();
2013
2515
  if (v->kind == SCR_DYN_OBJ) {
2014
2516
  size_t n = v->v.obj.len;
@@ -2110,21 +2612,150 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
2110
2612
 
2111
2613
  ScrDyn *scr_dyn_obj_keys(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWALK_KEYS); }
2112
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
+
2113
2654
  /* Object.assign over DOM values: copies `src`'s own members onto `target`
2114
2655
  * (last write wins) and answers the target retained (+1). Nullish
2115
2656
  * receivers throw Node's ToObject TypeError; nullish sources copy
2116
- * 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. */
2117
2659
  ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) {
2118
2660
  if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) {
2119
2661
  const char *m = "Cannot convert undefined or null to object";
2120
2662
  scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m));
2121
2663
  return NULL;
2122
2664
  }
2123
- if (target->kind == SCR_DYN_OBJ && src->kind == SCR_DYN_OBJ) {
2124
- for (size_t i = 0; i < src->v.obj.len; i++) {
2125
- scr_dyn_obj_set(target, src->v.obj.entries[i].key,
2126
- src->v.obj.entries[i].key_len,
2127
- 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);
2128
2759
  }
2129
2760
  }
2130
2761
  return scr_dyn_retain(target);
@@ -2136,6 +2767,12 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) {
2136
2767
  scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m));
2137
2768
  return false;
2138
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
+ }
2139
2776
  if (v->kind == SCR_DYN_OBJ) {
2140
2777
  return scr_dyn_obj_get(v, key->data, key->len) != NULL;
2141
2778
  }