jz 0.8.0 → 0.9.0

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 (73) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +27 -27
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6178
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +318 -43
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +1032 -145
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +428 -93
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +82 -11
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +1055 -70
  45. package/src/compile/index.js +277 -29
  46. package/src/compile/infer.js +42 -4
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/loop-recurrence.js +167 -0
  49. package/src/compile/narrow.js +555 -18
  50. package/src/compile/peel-stencil.js +5 -0
  51. package/src/compile/plan/index.js +45 -3
  52. package/src/compile/plan/inline.js +8 -1
  53. package/src/compile/plan/literals.js +39 -0
  54. package/src/compile/plan/scope.js +430 -23
  55. package/src/compile/program-facts.js +732 -38
  56. package/src/ctx.js +64 -9
  57. package/src/helper-counters.js +7 -1
  58. package/src/ir.js +113 -5
  59. package/src/kind-traits.js +66 -3
  60. package/src/kind.js +84 -12
  61. package/src/op-policy.js +7 -4
  62. package/src/optimize/index.js +1179 -704
  63. package/src/optimize/recurse.js +2 -2
  64. package/src/optimize/vectorize.js +982 -67
  65. package/src/prepare/index.js +809 -67
  66. package/src/prepare/math-kernel.js +331 -0
  67. package/src/prepare/pre-eval.js +714 -0
  68. package/src/snapshot.js +194 -0
  69. package/src/type.js +1170 -56
  70. package/src/wat/assemble.js +403 -65
  71. package/src/wat/codegen.js +74 -14
  72. package/transform.js +113 -4
  73. package/wasi.js +3 -0
package/module/json.js CHANGED
@@ -14,7 +14,8 @@ import { valTypeOf } from '../src/kind.js'
14
14
  import { T } from '../src/ast.js'
15
15
  import { VAL } from '../src/reps.js'
16
16
  import { err, inc, PTR, LAYOUT, declGlobal } from '../src/ctx.js'
17
- import { strHashLiteral } from './collection.js'
17
+ import { i64Hex } from '../layout.js'
18
+ import { strHashLiteral, heapResetWat } from './collection.js'
18
19
 
19
20
  function jsonConstString(ctx, expr) {
20
21
  if (Array.isArray(expr) && expr[0] === 'str' && typeof expr[1] === 'string') return expr[1]
@@ -142,12 +143,22 @@ export default (ctx) => {
142
143
  __jindent: ['__jput'],
143
144
  __json_val: ['__ptr_type', '__len', '__ptr_offset', '__jput', '__jindent', '__jput_num', '__jput_str', '__json_enter', '__json_leave', '__json_hash', '__json_obj'],
144
145
  __json_hash: ['__ptr_offset', '__jput', '__jindent', '__jput_str', '__json_omit', '__json_enter', '__json_leave', '__json_val', '__coll_order'],
145
- __json_obj: ['__ptr_offset', '__ptr_aux', '__len', '__jput', '__jindent', '__jput_str', '__json_omit', '__json_enter', '__json_leave', '__json_val', '__coll_order'],
146
- __jput_num: ['__ftoa'],
147
- __jput_str: ['__char_at', '__str_byteLen'],
146
+ // Durable-receiver global-table merge (see __json_obj's body) pulls in
147
+ // __ihash_get_local/__is_nullish only when collection.js's dyn-props
148
+ // machinery is actually part of this build (mirrors array.js's
149
+ // needsArrayDynMove-gated deps thunks) — a program that never writes a
150
+ // dynamic prop anywhere never loads collection.js.
151
+ __json_obj: () => [...(ctx.schema.dateSid != null ? ['__date_to_iso_string'] : []),
152
+ '__ptr_offset', '__ptr_aux', '__len', '__jput', '__jindent', '__jput_str', '__json_omit', '__json_enter', '__json_leave', '__json_val', '__coll_order',
153
+ ...(ctx.scope.globals.has('__dyn_props') ? ['__ihash_get_local', '__is_nullish'] : [])],
154
+ // Chain edges ($__jput_num → $__jput_str → $__jput): each body CALLS the
155
+ // next stage; without the explicit edge they ride the auto-dep scan, which
156
+ // silently yields nothing under self-host (test/selfhost-includes.js).
157
+ __jput_num: ['__ftoa', '__jput_str'],
158
+ __jput_str: ['__char_at', '__str_byteLen', '__jput'],
148
159
  __jp: ['__jp_val', '__jp_str', '__jp_num', '__jp_arr', '__jp_obj', '__sso_char', '__ptr_aux', '__ptr_type', '__ptr_offset', '__str_byteLen'],
149
160
  __jp_val: ['__jp_str', '__jp_num', '__jp_arr', '__jp_obj'],
150
- __jp_str: ['__sso_char', '__char_at', '__str_byteLen', '__hex4', '__ishex', '__utf8_enc'],
161
+ __jp_str: ['__sso_char', '__char_at', '__str_byteLen', '__hex4', '__ishex', '__utf8_enc', '__sso_norm'],
151
162
  __hex4: ['__hex1'],
152
163
  __jp_num: ['__pow10'],
153
164
  __jp_arr: ['__jp_val'],
@@ -187,6 +198,12 @@ export default (ctx) => {
187
198
  return mkPtrIR(PTR.OBJECT, 0, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(0)]])
188
199
  }
189
200
  const schemaId = ctx.schema.register(keys)
201
+ // Extern-write belt: these slot stores bypass the write censuses (they
202
+ // are IR, not AST `{}` literals). plan's hazard scan (jsonParseKeysets)
203
+ // normally kind-safe-marks this sid earlier; a sid it MISSED gets a
204
+ // null-kinds entry so every reader belt fails closed on it.
205
+ const hzLive = ctx.schema.slotWriteHazards
206
+ if (hzLive && !hzLive.kindSafeSids.has(schemaId)) hzLive.kindSafeSids.set(schemaId, null)
190
207
  const t = tempI32('jobj')
191
208
  const body = [
192
209
  ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(keys.length)]]],
@@ -475,13 +492,35 @@ export default (ctx) => {
475
492
  // Schema name table: global $__schema_tbl → array of f64 pointers.
476
493
  // schema_tbl[schemaId * 8] = f64 pointer to jz Array of key name strings.
477
494
  // Object props are sequential f64 at ptr_offset, indexed same as schema.
478
- ctx.core.stdlib['__json_obj'] = `(func $__json_obj (param $val i64)
495
+ // Thunked (not a plain template string) so heapResetWat()/the __dyn_props
496
+ // presence check below read the FINAL declaration state — see collection.js's
497
+ // heapResetWat comment for why (module load order isn't otherwise settled
498
+ // at the time this string would eagerly evaluate).
499
+ ctx.core.stdlib['__json_obj'] = () => `(func $__json_obj (param $val i64)
479
500
  (local $off i32) (local $sid i32) (local $keys i32) (local $nkeys i32)
480
501
  (local $i i32) (local $koff i32) (local $first i32) (local $pv i64)
481
- (local $props i64) (local $poff i32) (local $pcap i32) (local $dn i32) (local $slot i32) (local $ord i32)
482
- (local $j i32) (local $skip i32)
502
+ (local $props i64) (local $slot i32) (local $j i32) (local $skip i32)
503
+ ;; Two dyn-prop sources for a DURABLE receiver — see collection.js's
504
+ ;; heapResetWat and module/object.js's emitEnumerateObject for the full
505
+ ;; rationale: G(lobal) holds runtime/post-init keys, S(idecar) holds
506
+ ;; init-time keys. An EPHEMERAL receiver only ever populates S.
507
+ (local $poffG i32) (local $pcapG i32) (local $dnG i32) (local $ordG i32)
508
+ (local $poffS i32) (local $pcapS i32) (local $dnS i32) (local $ordS i32)
483
509
  (local.set $off (call $__ptr_offset (local.get $val)))
484
510
  (local.set $sid (call $__ptr_aux (local.get $val)))
511
+ ${ctx.schema.dateSid != null ? `
512
+ ;; Branded Date (module/date.js schema): serialize as its toJSON — the ISO
513
+ ;; string, or null for an invalid date — matching host JSON.stringify(date).
514
+ (if (i32.eq (local.get $sid) (i32.const ${ctx.schema.dateSid}))
515
+ (then
516
+ (if (f64.ne (f64.load (local.get $off)) (f64.load (local.get $off)))
517
+ (then
518
+ (call $__jput (i32.const 110)) (call $__jput (i32.const 117))
519
+ (call $__jput (i32.const 108)) (call $__jput (i32.const 108)) (return)))
520
+ (call $__jput (i32.const 34))
521
+ (call $__jput_str (i64.reinterpret_f64 (call $__date_to_iso_string (f64.load (local.get $off)))))
522
+ (call $__jput (i32.const 34))
523
+ (return)))` : ''}
485
524
  ;; Schema keys: schema_tbl + sid*8. Dyn-only programs (empty {} + computed
486
525
  ;; o[k]=v) never allocate __schema_tbl, leaving it 0 — guard the read so the
487
526
  ;; dyn-prop walk below still runs.
@@ -513,41 +552,73 @@ export default (ctx) => {
513
552
  (call $__json_val (local.get $pv))))
514
553
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
515
554
  (br $l)))
516
- ;; Dynamic (off-schema) properties: heap OBJECTs carry a HASH propsPtr at
517
- ;; off-16 (set by __dyn_set). Walk it in insertion order via __coll_order —
518
- ;; schema-only enumeration would drop computed props (e.g. {} then o.a=1).
519
- ;; Skip propsPtr entries whose key already appears in the schema: object
520
- ;; literals with needsDynShadow(target)=true shadow-write each schema key
521
- ;; into propsPtr so dyn-key reads can resolve via hash lookup. That mirror
522
- ;; is a runtime acceleration, not an enumeration entitywithout dedup,
523
- ;; JSON output emits each schema key twice. Schema-key interns equal the
524
- ;; keys we shadow-write (same compile-time string literal), so i64.eq matches.
555
+ ;; Dynamic (off-schema) properties: heap OBJECTs carry a HASH propsPtr
556
+ ;; either at off-16 (populated by an init-time write, or by any write at
557
+ ;; all on an EPHEMERAL receiver one allocated after the post-init
558
+ ;; high-water mark, per the durable-receiver policy) or in the global
559
+ ;; __dyn_props table keyed by offset (populated by a RUNTIME/post-init
560
+ ;; write on a DURABLE receiver; see collection.js's heapResetWat for the
561
+ ;; full policy). A durable receiver can carry BOTHgather each into its
562
+ ;; own (poff/pcap/dn) pair; G(lobal) and S(idecar) below. Static-segment
563
+ ;; objects (off < __heap_start) have no header at all — guard both reads
564
+ ;; on off >= __heap_start so neither ever reads neighbor static data.
525
565
  (if (i32.ge_u (local.get $off) (global.get $__heap_start))
526
566
  (then
527
- (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
528
- (if (i32.eq
529
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
530
- (i32.const ${PTR.HASH}))
567
+ ;; mask bit0 durable words may carry the runtime-shadowed marker
568
+ ;; (collection.js __dyn_set); unmasked, the resolved sidecar off is odd.
569
+ (local.set $props (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
570
+ (if (i32.eq
571
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
572
+ (i32.const ${PTR.HASH}))
573
+ (then
574
+ (local.set $poffS (call $__ptr_offset (local.get $props)))
575
+ (local.set $pcapS (i32.load (i32.sub (local.get $poffS) (i32.const 4))))
576
+ (local.set $dnS (i32.load (i32.sub (local.get $poffS) (i32.const 8))))))))${ctx.scope.globals.has('__dyn_props') ? `
577
+ ;; Global (runtime keys on a durable receiver) — independent of the sidecar
578
+ ;; gate above: STATIC-SEGMENT receivers (off < __heap_start, no header) also
579
+ ;; store their dyn writes here, and the probe is keyed by offset alone.
580
+ (if (i32.lt_u (local.get $off) ${heapResetWat()})
581
+ (then
582
+ (if (f64.ne (global.get $__dyn_props) (f64.const 0))
531
583
  (then
532
- (local.set $poff (call $__ptr_offset (local.get $props)))
533
- (local.set $pcap (i32.load (i32.sub (local.get $poff) (i32.const 4))))
534
- (local.set $dn (i32.load (i32.sub (local.get $poff) (i32.const 8))))
535
- (local.set $ord (call $__coll_order (local.get $poff) (local.get $pcap) (i32.const 24)))
584
+ (local.set $props (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props)) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
585
+ (if (i32.eqz (call $__is_nullish (local.get $props)))
586
+ (then
587
+ (local.set $poffG (call $__ptr_offset (local.get $props)))
588
+ (local.set $pcapG (i32.load (i32.sub (local.get $poffG) (i32.const 4))))
589
+ (local.set $dnG (i32.load (i32.sub (local.get $poffG) (i32.const 8))))))))))` : ''}
590
+ ;; Walk in insertion order via __coll_order — schema-only enumeration
591
+ ;; would drop computed props (e.g. {} then o.a=1). Skip entries whose key
592
+ ;; already appears in the schema: object literals with
593
+ ;; needsDynShadow(target)=true shadow-write each schema key into propsPtr
594
+ ;; so dyn-key reads can resolve via hash lookup. That mirror is a runtime
595
+ ;; acceleration, not an enumeration entity — without dedup, JSON output
596
+ ;; emits each schema key twice. Schema-key interns equal the keys we
597
+ ;; shadow-write (same compile-time string literal), so i64.eq matches.
598
+ ;; G walks first (schema-dedup only); S walks second (schema-dedup AND
599
+ ;; G-dedup, so a key present in both — reassigned at runtime after being
600
+ ;; set at init — emits once, from the authoritative G copy).
601
+ (if (i32.ne (local.get $poffG) (i32.const 0))
602
+ (then (local.set $ordG (call $__coll_order (local.get $poffG) (local.get $pcapG) (i32.const 24)))))
603
+ (if (i32.ne (local.get $poffS) (i32.const 0))
604
+ (then (local.set $ordS (call $__coll_order (local.get $poffS) (local.get $pcapS) (i32.const 24)))))
605
+ (if (i32.ne (local.get $dnG) (i32.const 0))
606
+ (then
536
607
  (local.set $i (i32.const 0))
537
- (block $dd (loop $dl
538
- (br_if $dd (i32.ge_s (local.get $i) (local.get $dn)))
539
- (local.set $slot (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))))
608
+ (block $gd (loop $gl
609
+ (br_if $gd (i32.ge_s (local.get $i) (local.get $dnG)))
610
+ (local.set $slot (i32.load (i32.add (local.get $ordG) (i32.shl (local.get $i) (i32.const 2)))))
540
611
  (local.set $pv (i64.load (i32.add (local.get $slot) (i32.const 16))))
541
612
  (local.set $skip (i32.const 0))
542
613
  (local.set $j (i32.const 0))
543
- (block $sd (loop $sl
544
- (br_if $sd (i32.ge_s (local.get $j) (local.get $nkeys)))
614
+ (block $gsd (loop $gsl
615
+ (br_if $gsd (i32.ge_s (local.get $j) (local.get $nkeys)))
545
616
  (if (i64.eq
546
617
  (i64.load (i32.add (local.get $slot) (i32.const 8)))
547
618
  (i64.load (i32.add (local.get $koff) (i32.shl (local.get $j) (i32.const 3)))))
548
- (then (local.set $skip (i32.const 1)) (br $sd)))
619
+ (then (local.set $skip (i32.const 1)) (br $gsd)))
549
620
  (local.set $j (i32.add (local.get $j) (i32.const 1)))
550
- (br $sl)))
621
+ (br $gsl)))
551
622
  (if (i32.and (i32.eqz (local.get $skip)) (i32.eqz (call $__json_omit (local.get $pv))))
552
623
  (then
553
624
  (if (i32.eqz (local.get $first)) (then (call $__jput (i32.const 44))))
@@ -560,7 +631,48 @@ export default (ctx) => {
560
631
  (if (global.get $__jgaplen) (then (call $__jput (i32.const 32))))
561
632
  (call $__json_val (local.get $pv))))
562
633
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
563
- (br $dl)))))))
634
+ (br $gl)))))
635
+ (if (i32.ne (local.get $dnS) (i32.const 0))
636
+ (then
637
+ (local.set $i (i32.const 0))
638
+ (block $sd (loop $sl
639
+ (br_if $sd (i32.ge_s (local.get $i) (local.get $dnS)))
640
+ (local.set $slot (i32.load (i32.add (local.get $ordS) (i32.shl (local.get $i) (i32.const 2)))))
641
+ (local.set $pv (i64.load (i32.add (local.get $slot) (i32.const 16))))
642
+ (local.set $skip (i32.const 0))
643
+ (local.set $j (i32.const 0))
644
+ (block $ssd (loop $ssl
645
+ (br_if $ssd (i32.ge_s (local.get $j) (local.get $nkeys)))
646
+ (if (i64.eq
647
+ (i64.load (i32.add (local.get $slot) (i32.const 8)))
648
+ (i64.load (i32.add (local.get $koff) (i32.shl (local.get $j) (i32.const 3)))))
649
+ (then (local.set $skip (i32.const 1)) (br $ssd)))
650
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
651
+ (br $ssl)))
652
+ (if (i32.eqz (local.get $skip))
653
+ (then
654
+ (local.set $j (i32.const 0))
655
+ (block $sgd (loop $sgl
656
+ (br_if $sgd (i32.ge_s (local.get $j) (local.get $dnG)))
657
+ (if (i64.eq
658
+ (i64.load (i32.add (local.get $slot) (i32.const 8)))
659
+ (i64.load (i32.add (i32.load (i32.add (local.get $ordG) (i32.shl (local.get $j) (i32.const 2)))) (i32.const 8))))
660
+ (then (local.set $skip (i32.const 1)) (br $sgd)))
661
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
662
+ (br $sgl)))))
663
+ (if (i32.and (i32.eqz (local.get $skip)) (i32.eqz (call $__json_omit (local.get $pv))))
664
+ (then
665
+ (if (i32.eqz (local.get $first)) (then (call $__jput (i32.const 44))))
666
+ (local.set $first (i32.const 0))
667
+ (call $__jindent)
668
+ (call $__jput (i32.const 34))
669
+ (call $__jput_str (i64.load (i32.add (local.get $slot) (i32.const 8))))
670
+ (call $__jput (i32.const 34))
671
+ (call $__jput (i32.const 58))
672
+ (if (global.get $__jgaplen) (then (call $__jput (i32.const 32))))
673
+ (call $__json_val (local.get $pv))))
674
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
675
+ (br $sl)))))
564
676
  (global.set $__jdepth (i32.sub (global.get $__jdepth) (i32.const 1)))
565
677
  (if (i32.eqz (local.get $first)) (then (call $__jindent)))
566
678
  (call $__jput (i32.const 125))
@@ -585,10 +697,14 @@ export default (ctx) => {
585
697
  declGlobal('__jpstr', 'i32') // input string offset
586
698
  declGlobal('__jplen', 'i32') // input length
587
699
  declGlobal('__jppos', 'i32') // current parse position
588
- // Side-channel hash for the most-recently-parsed string. __jp_str folds an
589
- // FNV-1a pass into its scan loop; __jp_obj forwards it to __hash_set_local_h
590
- // and skips the redundant __str_hash call inside the generic insert. 0 is a
591
- // sentinel meaning "string had escapes recompute via __str_hash".
700
+ // Side-channel hash for the most-recently-parsed string. __jp_str folds a
701
+ // byte-FNV-1a pass into its scan loop; __jp_obj mixes it straight into the
702
+ // schema-cache's key-SEQUENCE hash ($hh, __jp_schema_get's probe key) an
703
+ // independent hash from __str_hash, so it need not agree bit-for-bit with the
704
+ // runtime string hash (a schema-cache hit is verified by direct i64 key-array
705
+ // comparison, not by hash equality). 0 is a sentinel meaning "string had
706
+ // escapes"; it still mixes into $hh (a false schema-cache collision is caught
707
+ // by the verify step, never silently wrong).
592
708
  declGlobal('__jp_keyh', 'i32')
593
709
  // Sticky syntax-error flag. Set by any parser sub-routine on malformed input;
594
710
  // checked once by __jp after the top-level value, which throws if it is set.
@@ -636,9 +752,10 @@ export default (ctx) => {
636
752
 
637
753
  // Parse string (after opening " consumed). Single-pass scan that folds three
638
754
  // concerns into one byte loop: simplicity flag (no escapes / no high-bit),
639
- // SSO byte packing for ≤4-char ASCII keys, and FNV-1a hash. The hash is
640
- // stashed in $__jp_keyh so __jp_obj can use the prehashed insert and skip
641
- // a redundant __str_hash call.
755
+ // SSO byte packing for ≤4-char ASCII keys, and byte-FNV-1a hash. The hash is
756
+ // stashed in $__jp_keyh so __jp_obj can mix it into the schema-cache sequence
757
+ // hash without re-scanning the key bytes (see the $__jp_keyh declaration above
758
+ // for why it need not match __str_hash's SSO-mix output).
642
759
  // Hex nibble: '0'-'9' / 'a'-'f' / 'A'-'F' → 0..15; anything else → 0 (lenient).
643
760
  ctx.core.stdlib['__hex1'] = `(func $__hex1 (param $c i32) (result i32)
644
761
  (if (i32.le_u (i32.sub (local.get $c) (i32.const 48)) (i32.const 9))
@@ -724,8 +841,11 @@ export default (ctx) => {
724
841
  ;; Loop exited on the closing quote (34) or EOF (-1); the latter means an
725
842
  ;; unterminated string literal.
726
843
  (if (i32.eq (local.get $ch) (i32.const -1)) (then (global.set $__jp_err (i32.const 1))))
727
- ;; Stash hash. 0/1 bumped to 2 to match __str_hash convention; escape strings
728
- ;; (simple==0) get sentinel 0 so __jp_obj falls back to non-prehashed insert.
844
+ ;; Stash hash. 0/1 bumped to 2 to match __str_hash's clamp convention (kept for
845
+ ;; sentinel consistency, though __jp_obj's consumer the schema sequence hash —
846
+ ;; doesn't itself need agreement with __str_hash). Escape strings (simple==0) get
847
+ ;; sentinel 0, which still mixes into $hh (harmless: __jp_schema_get verifies by
848
+ ;; key-array compare, not hash equality).
729
849
  (global.set $__jp_keyh
730
850
  (if (result i32) (local.get $simple)
731
851
  (then (if (result i32) (i32.le_s (local.get $h) (i32.const 1))
@@ -738,13 +858,14 @@ export default (ctx) => {
738
858
  (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (local.get $len) (i32.const 10))) (local.get $sso)))))
739
859
  ;; Simple STRING fast path: no escapes, len > 4 — bulk memcpy from parse buffer,
740
860
  ;; skip rewind + per-byte escape-decode loop. Hits 5+ char keys without escapes.
861
+ ;; 5-6 char ASCII results route through __sso_norm (string-module invariant).
741
862
  (if (local.get $simple)
742
863
  (then
743
864
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
744
865
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
745
866
  (i32.store (i32.sub (local.get $off) (i32.const 4)) (local.get $len))
746
867
  (memory.copy (local.get $off) (i32.add (global.get $__jpstr) (local.get $start)) (local.get $len))
747
- (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))))
868
+ (return (call $__sso_norm (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off))))))
748
869
  ;; Copy chars to new string (handles escapes inline)
749
870
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
750
871
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -795,7 +916,8 @@ export default (ctx) => {
795
916
  ${ADV(1)} ;; skip closing "
796
917
  ;; Store actual length in header
797
918
  (i32.store (i32.sub (local.get $off) (i32.const 4)) (local.get $len))
798
- (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
919
+ ;; escape-decoded result may be short ASCII ("\\n" → 1 char) → normalize (invariant)
920
+ (call $__sso_norm (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off))))`
799
921
 
800
922
  // Parse number
801
923
  ctx.core.stdlib['__jp_num'] = `(func $__jp_num (result f64)
@@ -949,8 +1071,8 @@ export default (ctx) => {
949
1071
  (i32.store (i32.add (local.get $entry) (i32.const 4)) (local.get $sid)))
950
1072
  (local.get $sid))`
951
1073
 
952
- // Parse object → OBJECT (schema-tagged, slot-based) when key sequence has a
953
- // cached/registerable shape; falls back to HASH only on extreme key counts.
1074
+ // Parse object → OBJECT (schema-tagged, slot-based), always, via the runtime
1075
+ // schema cache (no HASH fallback in this build every key sequence gets a sid).
954
1076
  // Builds a transient (key, val) buffer during parse, then resolves a sid via
955
1077
  // the runtime schema cache, allocs an OBJECT, and copies values into slots.
956
1078
  // Walk-side `obj.prop` accesses then route through the OBJECT fast path
@@ -1090,7 +1212,7 @@ export default (ctx) => {
1090
1212
  if (cached) return cached
1091
1213
 
1092
1214
  const name = `__jp_shape_${ctx.runtime.jsonShapeParsers.size}`
1093
- const locals = new Map([['len', 'i32'], ['buf', 'i32'], ['i', 'i32'], ['ch', 'i32']])
1215
+ const locals = new Map([['len', 'i32'], ['buf', 'i32'], ['i', 'i32'], ['ch', 'i32'], ['kp', 'i32']])
1094
1216
  let uniq = 0
1095
1217
  const local = (p, t) => {
1096
1218
  const n = `${p}${uniq++}`
@@ -1100,7 +1222,38 @@ export default (ctx) => {
1100
1222
  const fail = `(return (call $__jp (local.get $str)))`
1101
1223
  const expect = (byte) => `(if (i32.ne ${PEEK} (i32.const ${byte})) (then ${fail}))
1102
1224
  ${ADV(1)}`
1103
- const expectText = (text) => [...text].map(c => expect(c.charCodeAt(0))).join('\n ')
1225
+ // SWAR text match: a known ≥2-byte ASCII run (schema keys, null/true/false)
1226
+ // compares as 8/4/2/1-byte chunks against packed little-endian constants —
1227
+ // one load + compare + branch per CHUNK instead of per character, and ONE
1228
+ // pos advance. In-bounds by construction: the entry allocates len+8 and
1229
+ // stores an 8-byte 0xFF sentinel, so any chunk starting inside the text
1230
+ // ends within the buffer, and a chunk overlapping the sentinel simply
1231
+ // fails the compare (0xFF matches no ASCII) into the generic-reparse fail,
1232
+ // exactly like the per-byte path. ASCII-only (a multi-byte char's UTF-8
1233
+ // bytes would need encoding here) — non-ASCII keys keep per-byte expects.
1234
+ const expectText = (text) => {
1235
+ const bytes = [...text].map(c => c.charCodeAt(0))
1236
+ if (bytes.length < 2 || bytes.some(b => b > 127))
1237
+ return [...text].map(c => expect(c.charCodeAt(0))).join('\n ')
1238
+ const le = (arr) => arr.reduce((a, b, k) => a | (BigInt(b) << BigInt(8 * k)), 0n)
1239
+ const at = (i) => (i ? `offset=${i} ` : '') + '(local.get $kp)'
1240
+ const out = [`(local.set $kp (i32.add (global.get $__jpstr) (global.get $__jppos)))`]
1241
+ let i = 0
1242
+ for (; bytes.length - i >= 8; i += 8)
1243
+ out.push(`(if (i64.ne (i64.load ${at(i)}) (i64.const ${i64Hex(le(bytes.slice(i, i + 8)))})) (then ${fail}))`)
1244
+ if (bytes.length - i >= 4) {
1245
+ out.push(`(if (i32.ne (i32.load ${at(i)}) (i32.const ${Number(le(bytes.slice(i, i + 4)))})) (then ${fail}))`)
1246
+ i += 4
1247
+ }
1248
+ if (bytes.length - i >= 2) {
1249
+ out.push(`(if (i32.ne (i32.load16_u ${at(i)}) (i32.const ${Number(le(bytes.slice(i, i + 2)))})) (then ${fail}))`)
1250
+ i += 2
1251
+ }
1252
+ if (i < bytes.length)
1253
+ out.push(`(if (i32.ne (i32.load8_u ${at(i)}) (i32.const ${bytes[i]})) (then ${fail}))`)
1254
+ out.push(ADV(bytes.length))
1255
+ return out.join('\n ')
1256
+ }
1104
1257
  // Forward-declared with `let` (assigned below) so `parse` captures a boxed
1105
1258
  // cell, not a not-yet-initialized `const` binding — the self-host kernel
1106
1259
  // miscompiles the latter capture in this deeply-nested mutually-recursive
@@ -1123,6 +1276,10 @@ export default (ctx) => {
1123
1276
  const obj = local('obj', 'i32')
1124
1277
  const val = local('val', 'f64')
1125
1278
  const sid = ctx.schema.register(keys)
1279
+ // Extern-write belt (see emitJsonConstValue): a shaped-parser sid the
1280
+ // plan hook missed fails closed via a null-kinds entry.
1281
+ const hzLive = ctx.schema.slotWriteHazards
1282
+ if (hzLive && !hzLive.kindSafeSids.has(sid)) hzLive.kindSafeSids.set(sid, null)
1126
1283
  let body = `${WS()}
1127
1284
  ${expect(123)}
1128
1285
  (local.set $${obj} (call $__alloc_hdr (i32.const 0) (i32.const ${Math.max(1, keys.length)})))`
@@ -1292,6 +1449,10 @@ ${localDecls}
1292
1449
  }
1293
1450
  const folded = foldStringify(x, replacer, space)
1294
1451
  if (folded !== undefined) return folded
1452
+ // A real replacer that the const-fold could not absorb would be silently
1453
+ // IGNORED by the runtime walker — a wrong result, not a slow one. Reject.
1454
+ if (!noReplacer)
1455
+ err('JSON.stringify with a runtime replacer is not supported — the constant-foldable case works; otherwise filter/map the value before stringifying')
1295
1456
  // Scalar boolean: the working-rep is the 0/1 number carrier, so the runtime
1296
1457
  // tag-walker would emit "0"/"1". A boolean's JSON is the bare word
1297
1458
  // true/false — exactly bool. Guard on no replacer: a replacer function may
@@ -1335,12 +1496,72 @@ ${localDecls}
1335
1496
  if (sp === NOT_LIT) return undefined
1336
1497
  }
1337
1498
 
1499
+ // Array replacer = a per-level property whitelist (ES SerializeJSONObject
1500
+ // step: PropertyList). Applied by a hand-rolled pre-filter, NOT the host
1501
+ // stringify's replacer param — the kernel's stringify drops replacers, so
1502
+ // in-kernel folds would silently ignore the filter (host≠kernel).
1503
+ let picked = val
1504
+ if (rep !== undefined && rep != null) {
1505
+ const keys = rep.map(String)
1506
+ const pick = (v) => {
1507
+ if (Array.isArray(v)) return v.map(pick)
1508
+ if (v !== null && typeof v === 'object') {
1509
+ const o = {}
1510
+ for (const k of keys) if (k in v) o[k] = pick(v[k])
1511
+ return o
1512
+ }
1513
+ return v
1514
+ }
1515
+ picked = pick(val)
1516
+ }
1338
1517
  let result
1339
- try { result = JSON.stringify(val, rep, sp) }
1518
+ try { result = JSON.stringify(picked, null, sp) }
1340
1519
  catch { return undefined }
1341
1520
  return result === undefined ? undefExpr() : asF64(emit(['str', result]))
1342
1521
  }
1343
1522
 
1523
+ // Plan-time mirror of the JSON.parse emit dispatch below: resolve the same
1524
+ // const/shape sources and return `{keys, kinds}` for every object key-set
1525
+ // the parser could WRITE into a compile-time schema (const fold or shaped
1526
+ // parser). The generic runtime parser needs no entry — it mints sids from
1527
+ // $__schema_next, seeded past the compile-time table (assemble.js), so it
1528
+ // can never alias a censused sid. `kinds` are the SAMPLE's per-slot VAL
1529
+ // kinds — trustworthy because both emit paths are shape-guarded: the const
1530
+ // fold bakes exactly these values, and the shaped parser falls back to the
1531
+ // generic parser (disjoint sids) on ANY text/structure divergence, so an
1532
+ // object carrying the sid always matches the sample's kinds. A null kind
1533
+ // (sample `null` value) poisons just that slot. Consumed by plan's
1534
+ // collectSlotWriteHazards BEFORE any census consumer bakes a decision.
1535
+ const JSON_KIND = (v) => v == null ? null
1536
+ : typeof v === 'number' ? VAL.NUMBER
1537
+ : typeof v === 'string' ? VAL.STRING
1538
+ : typeof v === 'boolean' ? VAL.BOOL
1539
+ : Array.isArray(v) ? VAL.ARRAY
1540
+ : VAL.OBJECT
1541
+ ctx.schema.jsonParseKeysets = (x) => {
1542
+ if (x === undefined || (Array.isArray(x) && x[0] == null && typeof x[1] !== 'string')) return []
1543
+ const out = []
1544
+ const collect = (v) => {
1545
+ if (v == null || typeof v !== 'object') return
1546
+ if (Array.isArray(v)) { for (const e of v) collect(e); return }
1547
+ const keys = Object.keys(v)
1548
+ if (keys.length) out.push({ keys, kinds: keys.map(k => JSON_KIND(v[k])) })
1549
+ for (const k of keys) collect(v[k])
1550
+ }
1551
+ const src = jsonConstString(ctx, x)
1552
+ if (src != null) {
1553
+ try { collect(JSON.parse(src)); return out } catch { /* emitter falls through the same way */ }
1554
+ }
1555
+ const shapeSrcs = jsonShapeStrings(ctx, x)
1556
+ if (shapeSrcs) {
1557
+ try {
1558
+ const parsed = shapeSrcs.map(s => JSON.parse(s))
1559
+ if (parsed.every(v => sameJsonShape(parsed[0], v))) collect(parsed[0])
1560
+ } catch { /* generic parser — disjoint runtime sids */ }
1561
+ }
1562
+ return out
1563
+ }
1564
+
1344
1565
  ctx.core.emit['JSON.parse'] = (x) => {
1345
1566
  // A non-string primitive literal argument is coerced via ToString, then
1346
1567
  // parsed: JSON.parse(0) → "0", JSON.parse(null) → "null", JSON.parse() →