jz 0.1.1 → 0.2.1

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/module/json.js CHANGED
@@ -8,10 +8,10 @@
8
8
  * @module json
9
9
  */
10
10
 
11
- import { typed, asF64, temp, nullExpr, allocPtr, slotAddr } from '../src/ir.js'
11
+ import { typed, asF64, asI64, temp, tempI32, nullExpr, allocPtr, slotAddr, mkPtrIR, extractF64Bits, appendStaticSlots, NULL_WAT } from '../src/ir.js'
12
12
  import { emit } from '../src/emit.js'
13
13
  import { T } from '../src/analyze.js'
14
- import { err, inc, PTR } from '../src/ctx.js'
14
+ import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
15
15
  import { strHashLiteral } from './collection.js'
16
16
 
17
17
  function jsonConstString(ctx, expr) {
@@ -21,6 +21,18 @@ function jsonConstString(ctx, expr) {
21
21
  return null
22
22
  }
23
23
 
24
+ function jsonShapeString(ctx, expr) {
25
+ if (typeof expr === 'string') return ctx.scope.shapeStrs?.get(expr) ?? null
26
+ return null
27
+ }
28
+
29
+ function jsonShapeStrings(ctx, expr) {
30
+ const single = jsonShapeString(ctx, expr)
31
+ if (single != null) return [single]
32
+ if (Array.isArray(expr) && expr[0] === '[]' && typeof expr[1] === 'string') return ctx.scope.shapeStrArrays?.get(expr[1]) ?? null
33
+ return null
34
+ }
35
+
24
36
  function hashCapFor(n) {
25
37
  let cap = 8
26
38
  const need = Math.max(1, Math.ceil(n * 4 / 3))
@@ -36,15 +48,30 @@ export default (ctx) => {
36
48
  __json_obj: ['__ptr_offset', '__ptr_aux', '__len', '__jput', '__jput_str', '__json_val'],
37
49
  __jput_num: ['__ftoa'],
38
50
  __jput_str: ['__char_at', '__str_byteLen'],
39
- __jp: ['__jp_val', '__jp_str', '__jp_num', '__jp_arr', '__jp_obj', '__jp_peek', '__jp_adv', '__jp_ws'],
51
+ __jp: ['__jp_val', '__jp_str', '__jp_num', '__jp_arr', '__jp_obj', '__sso_char', '__ptr_aux', '__ptr_type', '__ptr_offset', '__str_byteLen'],
52
+ __jp_val: ['__jp_str', '__jp_num', '__jp_arr', '__jp_obj'],
40
53
  __jp_str: ['__sso_char', '__char_at', '__str_byteLen'],
41
54
  __jp_num: ['__pow10'],
42
55
  __jp_arr: ['__jp_val'],
43
- __jp_obj: ['__jp_val', '__hash_new', '__hash_set_local'],
56
+ __jp_obj: ['__jp_val', '__jp_str', '__jp_schema_get', '__alloc_hdr', '__mkptr'],
57
+ __jp_schema_get: ['__alloc', '__alloc_hdr', '__mkptr'],
44
58
  })
45
59
 
60
+ // Emit a compile-time-known JSON value tree.
61
+ //
62
+ // Objects → fixed-shape OBJECT (schema-tagged, slot-based). Property reads
63
+ // on the receiving binding compile to direct f64.load at the slot offset
64
+ // (no hash probe, no key-string compare). Per-iter cost ≈ alloc + N stores
65
+ // where N is the schema length, vs HASH's alloc + N hash_set_local_h calls.
66
+ //
67
+ // Arrays → ARRAY pointer with f64 element slots, same as before.
68
+ //
69
+ // For pure-numeric/literal trees (no nested objects with computed values),
70
+ // the {...} static-data fast path in module/object.js would apply if we
71
+ // routed through the same recognizer; for now we always alloc fresh per
72
+ // call to preserve `JSON.parse(SRC); a.x = 7; b.x === original` semantics.
46
73
  function emitJsonConstValue(v) {
47
- if (v == null) return asF64(emit(nullExpr))
74
+ if (v == null) return nullExpr()
48
75
  if (typeof v === 'number') return asF64(emit(v))
49
76
  if (typeof v === 'string') return asF64(emit(['str', v]))
50
77
  if (typeof v === 'boolean') return asF64(emit(v ? 1 : 0))
@@ -57,15 +84,19 @@ export default (ctx) => {
57
84
  }
58
85
  if (typeof v === 'object') {
59
86
  const keys = Object.keys(v)
60
- const obj = allocPtr({ type: PTR.HASH, len: 0, cap: hashCapFor(keys.length), stride: 24, tag: 'jhash' })
61
- const h = temp('jhash')
62
- const body = [obj.init, ['local.set', `$${h}`, obj.ptr]]
63
- for (const k of keys) {
64
- body.push(['local.set', `$${h}`,
65
- ['call', '$__hash_set_local_h', ['local.get', `$${h}`], asF64(emit(['str', k])), ['i32.const', strHashLiteral(k)], emitJsonConstValue(v[k])]])
87
+ // Empty object: minimal OBJECT with no slots.
88
+ if (keys.length === 0) {
89
+ return mkPtrIR(PTR.OBJECT, 0, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', 1], ['i32.const', 8]])
66
90
  }
67
- body.push(['local.get', `$${h}`])
68
- if (keys.length) inc('__hash_set_local_h')
91
+ const schemaId = ctx.schema.register(keys)
92
+ const t = tempI32('jobj')
93
+ const body = [
94
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', keys.length], ['i32.const', 8]]],
95
+ ]
96
+ for (let i = 0; i < keys.length; i++) {
97
+ body.push(['f64.store', slotAddr(t, i), asF64(emitJsonConstValue(v[keys[i]]))])
98
+ }
99
+ body.push(mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${t}`]))
69
100
  return typed(['block', ['result', 'f64'], ...body], 'f64')
70
101
  }
71
102
  return asF64(emit(nullExpr))
@@ -94,8 +125,8 @@ export default (ctx) => {
94
125
  (i32.store8 (i32.add (global.get $__jbuf) (global.get $__jpos)) (local.get $b))
95
126
  (global.set $__jpos (i32.add (global.get $__jpos) (i32.const 1))))`
96
127
 
97
- // __jput_str(ptr: f64) — append string chars (without quotes) to buffer
98
- ctx.core.stdlib['__jput_str'] = `(func $__jput_str (param $ptr f64)
128
+ // __jput_str(ptr: i64) — append string chars (without quotes) to buffer
129
+ ctx.core.stdlib['__jput_str'] = `(func $__jput_str (param $ptr i64)
99
130
  (local $len i32) (local $i i32) (local $ch i32)
100
131
  (local.set $len (call $__str_byteLen (local.get $ptr)))
101
132
  (local.set $i (i32.const 0))
@@ -120,19 +151,20 @@ export default (ctx) => {
120
151
 
121
152
  // __jput_num(val: f64) — convert number to string, append bytes to buffer
122
153
  ctx.core.stdlib['__jput_num'] = `(func $__jput_num (param $val f64)
123
- (call $__jput_str (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0))))`
154
+ (call $__jput_str (i64.reinterpret_f64 (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0)))))`
124
155
 
125
- // __json_val(val: f64) — stringify any value, append to buffer
126
- ctx.core.stdlib['__json_val'] = `(func $__json_val (param $val f64)
127
- (local $type i32) (local $len i32) (local $i i32) (local $off i32)
156
+ // __json_val(val: i64) — stringify any value, append to buffer
157
+ ctx.core.stdlib['__json_val'] = `(func $__json_val (param $val i64)
158
+ (local $type i32) (local $len i32) (local $i i32) (local $off i32) (local $f f64)
159
+ (local.set $f (f64.reinterpret_i64 (local.get $val)))
128
160
  ;; Number (not NaN) — but Infinity must be null per JSON spec
129
- (if (f64.eq (local.get $val) (local.get $val))
161
+ (if (f64.eq (local.get $f) (local.get $f))
130
162
  (then
131
- (if (f64.eq (f64.abs (local.get $val)) (f64.const inf))
163
+ (if (f64.eq (f64.abs (local.get $f)) (f64.const inf))
132
164
  (then
133
165
  (call $__jput (i32.const 110)) (call $__jput (i32.const 117))
134
166
  (call $__jput (i32.const 108)) (call $__jput (i32.const 108)) (return)))
135
- (call $__jput_num (local.get $val)) (return)))
167
+ (call $__jput_num (local.get $f)) (return)))
136
168
  ;; NaN-boxed pointer
137
169
  (local.set $type (call $__ptr_type (local.get $val)))
138
170
  ;; Plain NaN (type=0) → null
@@ -141,8 +173,7 @@ export default (ctx) => {
141
173
  (call $__jput (i32.const 110)) (call $__jput (i32.const 117))
142
174
  (call $__jput (i32.const 108)) (call $__jput (i32.const 108)) (return)))
143
175
  ;; String
144
- (if (i32.or (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
145
- (i32.eq (local.get $type) (i32.const ${PTR.SSO})))
176
+ (if (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
146
177
  (then
147
178
  (call $__jput (i32.const 34))
148
179
  (call $__jput_str (local.get $val))
@@ -157,7 +188,7 @@ export default (ctx) => {
157
188
  (block $d (loop $l
158
189
  (br_if $d (i32.ge_s (local.get $i) (local.get $len)))
159
190
  (if (local.get $i) (then (call $__jput (i32.const 44)))) ;; ,
160
- (call $__json_val (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
191
+ (call $__json_val (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
161
192
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
162
193
  (br $l)))
163
194
  (call $__jput (i32.const 93)) ;; ]
@@ -173,9 +204,9 @@ export default (ctx) => {
173
204
  (call $__jput (i32.const 110)) (call $__jput (i32.const 117))
174
205
  (call $__jput (i32.const 108)) (call $__jput (i32.const 108)))`
175
206
 
176
- // __json_hash(val: f64) — stringify HASH/MAP: iterate slots, emit {"key":val,...}
207
+ // __json_hash(val: i64) — stringify HASH/MAP: iterate slots, emit {"key":val,...}
177
208
  // Slot layout: 24 bytes each — [hash:f64][key:f64][val:f64]. Empty slots have hash==0.
178
- ctx.core.stdlib['__json_hash'] = `(func $__json_hash (param $val f64)
209
+ ctx.core.stdlib['__json_hash'] = `(func $__json_hash (param $val i64)
179
210
  (local $off i32) (local $cap i32) (local $i i32) (local $slot i32) (local $first i32)
180
211
  (local.set $off (call $__ptr_offset (local.get $val)))
181
212
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
@@ -190,10 +221,10 @@ export default (ctx) => {
190
221
  (then (call $__jput (i32.const 44))))
191
222
  (local.set $first (i32.const 0))
192
223
  (call $__jput (i32.const 34))
193
- (call $__jput_str (f64.load (i32.add (local.get $slot) (i32.const 8))))
224
+ (call $__jput_str (i64.load (i32.add (local.get $slot) (i32.const 8))))
194
225
  (call $__jput (i32.const 34))
195
226
  (call $__jput (i32.const 58))
196
- (call $__json_val (f64.load (i32.add (local.get $slot) (i32.const 16))))))
227
+ (call $__json_val (i64.load (i32.add (local.get $slot) (i32.const 16))))))
197
228
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
198
229
  (br $l)))
199
230
  (call $__jput (i32.const 125)))`
@@ -202,32 +233,32 @@ export default (ctx) => {
202
233
  // Schema name table: global $__schema_tbl → array of f64 pointers.
203
234
  // schema_tbl[schemaId * 8] = f64 pointer to jz Array of key name strings.
204
235
  // Object props are sequential f64 at ptr_offset, indexed same as schema.
205
- ctx.core.stdlib['__json_obj'] = `(func $__json_obj (param $val f64)
236
+ ctx.core.stdlib['__json_obj'] = `(func $__json_obj (param $val i64)
206
237
  (local $off i32) (local $sid i32) (local $keys i32) (local $nkeys i32)
207
238
  (local $i i32) (local $koff i32)
208
239
  (local.set $off (call $__ptr_offset (local.get $val)))
209
240
  (local.set $sid (call $__ptr_aux (local.get $val)))
210
241
  ;; Load keys array from schema table: schema_tbl + sid * 8
211
242
  (local.set $keys (call $__ptr_offset
212
- (f64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
243
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
213
244
  (local.set $nkeys (call $__len
214
- (f64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
245
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
215
246
  (local.set $koff (local.get $keys))
216
247
  (call $__jput (i32.const 123))
217
248
  (block $d (loop $l
218
249
  (br_if $d (i32.ge_s (local.get $i) (local.get $nkeys)))
219
250
  (if (local.get $i) (then (call $__jput (i32.const 44))))
220
251
  (call $__jput (i32.const 34))
221
- (call $__jput_str (f64.load (i32.add (local.get $koff) (i32.shl (local.get $i) (i32.const 3)))))
252
+ (call $__jput_str (i64.load (i32.add (local.get $koff) (i32.shl (local.get $i) (i32.const 3)))))
222
253
  (call $__jput (i32.const 34))
223
254
  (call $__jput (i32.const 58))
224
- (call $__json_val (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
255
+ (call $__json_val (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
225
256
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
226
257
  (br $l)))
227
258
  (call $__jput (i32.const 125)))`
228
259
 
229
- // __stringify(val: f64) → f64 (NaN-boxed string)
230
- ctx.core.stdlib['__stringify'] = `(func $__stringify (param $val f64) (result f64)
260
+ // __stringify(val: i64) → f64 (NaN-boxed string)
261
+ ctx.core.stdlib['__stringify'] = `(func $__stringify (param $val i64) (result f64)
231
262
  ;; Reset output buffer
232
263
  (global.set $__jbuf (call $__alloc (i32.const 256)))
233
264
  (global.set $__jpos (i32.const 0))
@@ -241,62 +272,92 @@ export default (ctx) => {
241
272
  ctx.scope.globals.set('__jpstr', '(global $__jpstr (mut i32) (i32.const 0))') // input string offset
242
273
  ctx.scope.globals.set('__jplen', '(global $__jplen (mut i32) (i32.const 0))') // input length
243
274
  ctx.scope.globals.set('__jppos', '(global $__jppos (mut i32) (i32.const 0))') // current parse position
275
+ // Side-channel hash for the most-recently-parsed string. __jp_str folds an
276
+ // FNV-1a pass into its scan loop; __jp_obj forwards it to __hash_set_local_h
277
+ // and skips the redundant __str_hash call inside the generic insert. 0 is a
278
+ // sentinel meaning "string had escapes — recompute via __str_hash".
279
+ ctx.scope.globals.set('__jp_keyh', '(global $__jp_keyh (mut i32) (i32.const 0))')
280
+ // Runtime schema infrastructure. __schema_next points at the first free slot
281
+ // in $__schema_tbl reserved for runtime registration; compile.js initializes
282
+ // it to ctx.schema.list.length when __jp_obj is included. The schema cache
283
+ // is a 64-entry open-addressed hash on key-sequence FNV — repeated parses of
284
+ // the same shape reuse a previously-registered sid, so __jp_obj allocates a
285
+ // fresh-shape OBJECT once and converts to slot stores thereafter (skipping
286
+ // every __hash_set_local). Cache slot layout: i32 hash, i32 sid (8 bytes).
287
+ // Hash 0 = empty slot; we bump <=1 to 2 like __str_hash to avoid sentinel
288
+ // collision with valid hashes.
289
+ ctx.scope.globals.set('__schema_next', '(global $__schema_next (mut i32) (i32.const 0))')
290
+ ctx.scope.globals.set('__schema_cache', '(global $__schema_cache (mut i32) (i32.const 0))')
244
291
 
245
292
  // Sentinel-driven peek: __jp copies input to a scratch buffer with 0xFF bytes
246
293
  // appended past the end. i32.load8_s sign-extends, so the sentinel reads as -1
247
- // — exactly the EOF value all callers already test for. Bounds check and
248
- // function-call overhead both gone; ~50 calls/parse char in well-formed JSON.
249
- ctx.core.stdlib['__jp_peek'] = `(func $__jp_peek (result i32)
250
- (i32.load8_s (i32.add (global.get $__jpstr) (global.get $__jppos))))`
251
-
252
- ctx.core.stdlib['__jp_adv'] = `(func $__jp_adv (param $n i32)
253
- (global.set $__jppos (i32.add (global.get $__jppos) (local.get $n))))`
294
+ // — exactly the EOF value all callers already test for. Inlined into every
295
+ // parser body via PEEK/ADV string templates; the per-char function-call
296
+ // overhead (~50 calls/char in well-formed JSON) was the dominant cost.
297
+ const PEEK = `(i32.load8_s (i32.add (global.get $__jpstr) (global.get $__jppos)))`
298
+ const ADV = (n) => `(global.set $__jppos (i32.add (global.get $__jppos) (i32.const ${n})))`
254
299
 
255
- ctx.core.stdlib['__jp_ws'] = `(func $__jp_ws
256
- (local $ch i32)
257
- (block $d (loop $l
258
- (local.set $ch (call $__jp_peek))
259
- (br_if $d (i32.and (i32.ne (local.get $ch) (i32.const 32))
260
- (i32.and (i32.ne (local.get $ch) (i32.const 9))
261
- (i32.and (i32.ne (local.get $ch) (i32.const 10))
262
- (i32.ne (local.get $ch) (i32.const 13))))))
263
- (call $__jp_adv (i32.const 1))
264
- (br $l))))`
300
+ // Whitespace skip — inlined at every call site as a tight loop. Compact
301
+ // JSON often has zero whitespace between tokens, so the dominant case is
302
+ // a single peek + break. WS chars (9/10/13/32) all fit in [0..32]; we
303
+ // exit on anything > 32 unsigned. The sentinel byte (PEEK returns -1
304
+ // sign-extended) is 0xFFFFFFFF unsigned > 32 — so the same check
305
+ // handles EOF without a separate guard. Other control chars in [0..8],
306
+ // [11..12], [14..31] would be falsely consumed as WS, but those aren't
307
+ // valid in well-formed JSON anyway.
308
+ let WS_ID = 0
309
+ const WS = () => {
310
+ const id = WS_ID++
311
+ return `(block $jpws_d${id} (loop $jpws_l${id}
312
+ (br_if $jpws_d${id} (i32.gt_u ${PEEK} (i32.const 32)))
313
+ ${ADV(1)}
314
+ (br $jpws_l${id})))`
315
+ }
265
316
 
266
- // Parse string (after opening " consumed). Two-phase: scan to closing quote
267
- // tracking whether all chars are simple ASCII (no escapes, no high-bit), then
268
- // either pack into SSO (≤4 simple chars) or heap-alloc + escape-decode.
317
+ // Parse string (after opening " consumed). Single-pass scan that folds three
318
+ // concerns into one byte loop: simplicity flag (no escapes / no high-bit),
319
+ // SSO byte packing for ≤4-char ASCII keys, and FNV-1a hash. The hash is
320
+ // stashed in $__jp_keyh so __jp_obj can use the prehashed insert and skip
321
+ // a redundant __str_hash call.
269
322
  ctx.core.stdlib['__jp_str'] = `(func $__jp_str (result f64)
270
- (local $start i32) (local $ch i32) (local $len i32) (local $off i32) (local $i i32) (local $simple i32) (local $sso i32)
323
+ (local $start i32) (local $ch i32) (local $len i32) (local $off i32) (local $i i32) (local $simple i32) (local $sso i32) (local $h i32)
271
324
  (local.set $start (global.get $__jppos))
272
325
  (local.set $simple (i32.const 1))
326
+ (local.set $h (i32.const 0x811c9dc5))
273
327
  (block $d (loop $l
274
- (local.set $ch (call $__jp_peek))
328
+ (local.set $ch ${PEEK})
275
329
  (br_if $d (i32.eq (local.get $ch) (i32.const 34)))
276
330
  (br_if $d (i32.eq (local.get $ch) (i32.const -1)))
277
331
  ;; Mark non-simple: escape (\\=92) or non-ASCII (load8_s gives <0 for byte≥128).
278
332
  (if (i32.or (i32.eq (local.get $ch) (i32.const 92)) (i32.lt_s (local.get $ch) (i32.const 0)))
279
333
  (then (local.set $simple (i32.const 0))))
280
334
  (if (i32.eq (local.get $ch) (i32.const 92))
281
- (then (call $__jp_adv (i32.const 2)))
282
- (else (call $__jp_adv (i32.const 1))))
335
+ (then
336
+ (local.set $len (i32.add (local.get $len) (i32.const 1)))
337
+ ${ADV(2)})
338
+ (else
339
+ ;; Pack first 4 bytes into SSO slot (used only when len ≤ 4).
340
+ (if (i32.lt_u (local.get $len) (i32.const 4))
341
+ (then (local.set $sso
342
+ (i32.or (local.get $sso)
343
+ (i32.shl (i32.and (local.get $ch) (i32.const 0xFF))
344
+ (i32.shl (local.get $len) (i32.const 3)))))))
345
+ (local.set $h (i32.mul (i32.xor (local.get $h) (i32.and (local.get $ch) (i32.const 0xFF))) (i32.const 0x01000193)))
346
+ (local.set $len (i32.add (local.get $len) (i32.const 1)))
347
+ ${ADV(1)}))
283
348
  (br $l)))
284
- (local.set $len (i32.sub (global.get $__jppos) (local.get $start)))
285
- (call $__jp_adv (i32.const 1)) ;; skip "
286
- ;; SSO fast path: ≤4 ASCII chars, no escapes — pack bytes into the offset slot,
287
- ;; skip alloc + memcopy entirely. The dominant case for object keys (id/kind/meta/bias).
349
+ ;; Stash hash. 0/1 bumped to 2 to match __str_hash convention; escape strings
350
+ ;; (simple==0) get sentinel 0 so __jp_obj falls back to non-prehashed insert.
351
+ (global.set $__jp_keyh
352
+ (if (result i32) (local.get $simple)
353
+ (then (if (result i32) (i32.le_s (local.get $h) (i32.const 1))
354
+ (then (i32.add (local.get $h) (i32.const 2))) (else (local.get $h))))
355
+ (else (i32.const 0))))
356
+ ${ADV(1)} ;; skip "
357
+ ;; SSO fast path: ≤4 ASCII chars, no escapes — bytes already packed inline.
288
358
  (if (i32.and (local.get $simple) (i32.le_u (local.get $len) (i32.const 4)))
289
359
  (then
290
- (local.set $i (i32.const 0))
291
- (block $sd (loop $sl
292
- (br_if $sd (i32.ge_s (local.get $i) (local.get $len)))
293
- (local.set $sso
294
- (i32.or (local.get $sso)
295
- (i32.shl (i32.load8_u (i32.add (i32.add (global.get $__jpstr) (local.get $start)) (local.get $i)))
296
- (i32.shl (local.get $i) (i32.const 3)))))
297
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
298
- (br $sl)))
299
- (return (call $__mkptr (i32.const ${PTR.SSO}) (local.get $len) (local.get $sso)))))
360
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $len)) (local.get $sso)))))
300
361
  ;; Simple STRING fast path: no escapes, len > 4 — bulk memcpy from parse buffer,
301
362
  ;; skip rewind + per-byte escape-decode loop. Hits 5+ char keys without escapes.
302
363
  (if (local.get $simple)
@@ -313,25 +374,25 @@ export default (ctx) => {
313
374
  (global.set $__jppos (local.get $start)) ;; rewind to re-scan
314
375
  (local.set $len (i32.const 0)) ;; actual output length
315
376
  (block $d2 (loop $l2
316
- (local.set $ch (call $__jp_peek))
377
+ (local.set $ch ${PEEK})
317
378
  (br_if $d2 (i32.eq (local.get $ch) (i32.const 34)))
318
379
  (br_if $d2 (i32.eq (local.get $ch) (i32.const -1)))
319
380
  (if (i32.eq (local.get $ch) (i32.const 92))
320
381
  (then
321
- (call $__jp_adv (i32.const 1))
322
- (local.set $ch (call $__jp_peek))
323
- (call $__jp_adv (i32.const 1))
382
+ ${ADV(1)}
383
+ (local.set $ch ${PEEK})
384
+ ${ADV(1)}
324
385
  ;; Decode escape: n→10 t→9 r→13 b→8 f→12, else literal
325
386
  (if (i32.eq (local.get $ch) (i32.const 110)) (then (local.set $ch (i32.const 10))))
326
387
  (if (i32.eq (local.get $ch) (i32.const 116)) (then (local.set $ch (i32.const 9))))
327
388
  (if (i32.eq (local.get $ch) (i32.const 114)) (then (local.set $ch (i32.const 13))))
328
389
  (if (i32.eq (local.get $ch) (i32.const 98)) (then (local.set $ch (i32.const 8))))
329
390
  (if (i32.eq (local.get $ch) (i32.const 102)) (then (local.set $ch (i32.const 12)))))
330
- (else (call $__jp_adv (i32.const 1))))
391
+ (else ${ADV(1)}))
331
392
  (i32.store8 (i32.add (local.get $off) (local.get $len)) (local.get $ch))
332
393
  (local.set $len (i32.add (local.get $len) (i32.const 1)))
333
394
  (br $l2)))
334
- (call $__jp_adv (i32.const 1)) ;; skip closing "
395
+ ${ADV(1)} ;; skip closing "
335
396
  ;; Store actual length in header
336
397
  (i32.store (i32.sub (local.get $off) (i32.const 4)) (local.get $len))
337
398
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
@@ -340,37 +401,37 @@ export default (ctx) => {
340
401
  ctx.core.stdlib['__jp_num'] = `(func $__jp_num (result f64)
341
402
  (local $neg i32) (local $val f64) (local $scale f64) (local $ch i32)
342
403
  (local $exp i32) (local $expNeg i32)
343
- (if (i32.eq (call $__jp_peek) (i32.const 45))
344
- (then (local.set $neg (i32.const 1)) (call $__jp_adv (i32.const 1))))
404
+ (if (i32.eq ${PEEK} (i32.const 45))
405
+ (then (local.set $neg (i32.const 1)) ${ADV(1)}))
345
406
  (block $d (loop $l
346
- (local.set $ch (call $__jp_peek))
407
+ (local.set $ch ${PEEK})
347
408
  (br_if $d (i32.or (i32.lt_s (local.get $ch) (i32.const 48)) (i32.gt_s (local.get $ch) (i32.const 57))))
348
409
  (local.set $val (f64.add (f64.mul (local.get $val) (f64.const 10))
349
410
  (f64.convert_i32_s (i32.sub (local.get $ch) (i32.const 48)))))
350
- (call $__jp_adv (i32.const 1)) (br $l)))
351
- (if (i32.eq (call $__jp_peek) (i32.const 46))
411
+ ${ADV(1)} (br $l)))
412
+ (if (i32.eq ${PEEK} (i32.const 46))
352
413
  (then
353
- (call $__jp_adv (i32.const 1))
414
+ ${ADV(1)}
354
415
  (local.set $scale (f64.const 0.1))
355
416
  (block $fd (loop $fl
356
- (local.set $ch (call $__jp_peek))
417
+ (local.set $ch ${PEEK})
357
418
  (br_if $fd (i32.or (i32.lt_s (local.get $ch) (i32.const 48)) (i32.gt_s (local.get $ch) (i32.const 57))))
358
419
  (local.set $val (f64.add (local.get $val)
359
420
  (f64.mul (local.get $scale) (f64.convert_i32_s (i32.sub (local.get $ch) (i32.const 48))))))
360
421
  (local.set $scale (f64.mul (local.get $scale) (f64.const 0.1)))
361
- (call $__jp_adv (i32.const 1)) (br $fl)))))
362
- (if (i32.or (i32.eq (call $__jp_peek) (i32.const 101)) (i32.eq (call $__jp_peek) (i32.const 69)))
422
+ ${ADV(1)} (br $fl)))))
423
+ (if (i32.or (i32.eq ${PEEK} (i32.const 101)) (i32.eq ${PEEK} (i32.const 69)))
363
424
  (then
364
- (call $__jp_adv (i32.const 1))
365
- (if (i32.eq (call $__jp_peek) (i32.const 45))
366
- (then (local.set $expNeg (i32.const 1)) (call $__jp_adv (i32.const 1)))
367
- (else (if (i32.eq (call $__jp_peek) (i32.const 43))
368
- (then (call $__jp_adv (i32.const 1))))))
425
+ ${ADV(1)}
426
+ (if (i32.eq ${PEEK} (i32.const 45))
427
+ (then (local.set $expNeg (i32.const 1)) ${ADV(1)})
428
+ (else (if (i32.eq ${PEEK} (i32.const 43))
429
+ (then ${ADV(1)}))))
369
430
  (block $ed (loop $el
370
- (local.set $ch (call $__jp_peek))
431
+ (local.set $ch ${PEEK})
371
432
  (br_if $ed (i32.or (i32.lt_s (local.get $ch) (i32.const 48)) (i32.gt_s (local.get $ch) (i32.const 57))))
372
433
  (local.set $exp (i32.add (i32.mul (local.get $exp) (i32.const 10)) (i32.sub (local.get $ch) (i32.const 48))))
373
- (call $__jp_adv (i32.const 1)) (br $el)))
434
+ ${ADV(1)} (br $el)))
374
435
  (if (local.get $expNeg) (then (local.set $exp (i32.sub (i32.const 0) (local.get $exp)))))
375
436
  (local.set $val (f64.mul (local.get $val) (call $__pow10
376
437
  (if (result i32) (i32.lt_s (local.get $exp) (i32.const 0))
@@ -385,14 +446,14 @@ export default (ctx) => {
385
446
  (local.set $cap (i32.const 8))
386
447
  (local.set $ptr (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $cap) (i32.const 3)))))
387
448
  (local.set $ptr (i32.add (local.get $ptr) (i32.const 8)))
388
- (call $__jp_ws)
389
- (if (i32.eq (call $__jp_peek) (i32.const 93))
390
- (then (call $__jp_adv (i32.const 1))
449
+ ${WS()}
450
+ (if (i32.eq ${PEEK} (i32.const 93))
451
+ (then ${ADV(1)}
391
452
  (i32.store (i32.sub (local.get $ptr) (i32.const 8)) (i32.const 0))
392
453
  (i32.store (i32.sub (local.get $ptr) (i32.const 4)) (local.get $cap))
393
454
  (return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $ptr)))))
394
455
  (block $d (loop $l
395
- (call $__jp_ws)
456
+ ${WS()}
396
457
  ;; Grow if needed
397
458
  (if (i32.ge_s (local.get $len) (local.get $cap))
398
459
  (then
@@ -403,75 +464,350 @@ export default (ctx) => {
403
464
  (local.set $ptr (local.get $new))))
404
465
  (f64.store (i32.add (local.get $ptr) (i32.shl (local.get $len) (i32.const 3))) (call $__jp_val))
405
466
  (local.set $len (i32.add (local.get $len) (i32.const 1)))
406
- (call $__jp_ws)
407
- (local.set $ch (call $__jp_peek))
467
+ ${WS()}
468
+ (local.set $ch ${PEEK})
408
469
  (br_if $d (i32.eq (local.get $ch) (i32.const 93)))
409
- (if (i32.eq (local.get $ch) (i32.const 44)) (then (call $__jp_adv (i32.const 1))))
470
+ (if (i32.eq (local.get $ch) (i32.const 44)) (then ${ADV(1)}))
410
471
  (br $l)))
411
- (call $__jp_adv (i32.const 1))
472
+ ${ADV(1)}
412
473
  (i32.store (i32.sub (local.get $ptr) (i32.const 8)) (local.get $len))
413
474
  (i32.store (i32.sub (local.get $ptr) (i32.const 4)) (local.get $cap))
414
475
  (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $ptr)))`
415
476
 
416
- // Parse object HASH (dynamic string-keyed object)
477
+ // Schema cache lookup/register. Cache is a 64-entry open-addressed table
478
+ // keyed by FNV of (key1_hash, key2_hash, ..., n). On hit, sid is reused
479
+ // and the OBJECT is allocated with that schemaId so subsequent property
480
+ // accesses go through the slot fast path. On miss, register a new schema
481
+ // by allocating a jz Array of key STRINGs and storing it in $__schema_tbl
482
+ // at the next free slot. Allocated lazily on first call.
483
+ //
484
+ // kbuf layout: 16 bytes per entry — [key:i64][val:i64]. n entries at $kbuf.
485
+ // Returns sid (i32). Caller materializes OBJECT with given sid + values.
486
+ ctx.core.stdlib['__jp_schema_get'] = `(func $__jp_schema_get (param $kbuf i32) (param $n i32) (param $hh i32) (result i32)
487
+ (local $cache i32) (local $idx i32) (local $entry i32) (local $eh i32) (local $sid i32)
488
+ (local $karr i32) (local $karr_off i32) (local $i i32) (local $tries i32)
489
+ (local.set $cache (global.get $__schema_cache))
490
+ ;; Lazy-init cache: 64 entries × 8 bytes = 512 bytes, zero-filled by alloc.
491
+ (if (i32.eqz (local.get $cache))
492
+ (then
493
+ (local.set $cache (call $__alloc (i32.const 512)))
494
+ (global.set $__schema_cache (local.get $cache))))
495
+ (local.set $idx (i32.and (local.get $hh) (i32.const 63)))
496
+ (block $found (block $miss (loop $probe
497
+ (local.set $entry (i32.add (local.get $cache) (i32.shl (local.get $idx) (i32.const 3))))
498
+ (local.set $eh (i32.load (local.get $entry)))
499
+ (br_if $miss (i32.eqz (local.get $eh)))
500
+ (if (i32.eq (local.get $eh) (local.get $hh))
501
+ (then
502
+ (local.set $sid (i32.load (i32.add (local.get $entry) (i32.const 4))))
503
+ ;; Verify by comparing key i64s against schema_tbl[sid]'s key array.
504
+ (local.set $karr (i32.wrap_i64 (i64.and
505
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))
506
+ (i64.const ${LAYOUT.OFFSET_MASK}))))
507
+ (if (i32.eq (i32.load (i32.sub (local.get $karr) (i32.const 8))) (local.get $n))
508
+ (then
509
+ (local.set $i (i32.const 0))
510
+ (block $eq (block $neq (loop $cmp
511
+ (br_if $eq (i32.ge_s (local.get $i) (local.get $n)))
512
+ (br_if $neq (i64.ne
513
+ (i64.load (i32.add (local.get $karr) (i32.shl (local.get $i) (i32.const 3))))
514
+ (i64.load (i32.add (local.get $kbuf) (i32.shl (local.get $i) (i32.const 4))))))
515
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
516
+ (br $cmp)))
517
+ (br $found)))))
518
+ ;; Hash collision or length mismatch — keep probing.
519
+ )
520
+ (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
521
+ (br_if $miss (i32.ge_s (local.get $tries) (i32.const 64)))
522
+ (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.const 63)))
523
+ (br $probe)))
524
+ ;; miss: register new schema.
525
+ (local.set $sid (global.get $__schema_next))
526
+ (global.set $__schema_next (i32.add (local.get $sid) (i32.const 1)))
527
+ ;; Allocate jz Array of n keys. __alloc_hdr(len, cap, stride) returns base
528
+ ;; of slot region with len@-8 and cap@-4. The schema dispatch arm reads
529
+ ;; nkeys from -8, so len must equal cap=n.
530
+ (local.set $karr (call $__alloc_hdr (local.get $n) (local.get $n) (i32.const 8)))
531
+ (local.set $i (i32.const 0))
532
+ (block $cd (loop $cl
533
+ (br_if $cd (i32.ge_s (local.get $i) (local.get $n)))
534
+ (i64.store
535
+ (i32.add (local.get $karr) (i32.shl (local.get $i) (i32.const 3)))
536
+ (i64.load (i32.add (local.get $kbuf) (i32.shl (local.get $i) (i32.const 4)))))
537
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
538
+ (br $cl)))
539
+ ;; Store ARRAY ptr in schema table at sid.
540
+ (i64.store
541
+ (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3)))
542
+ (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $karr))))
543
+ ;; Insert into cache at probe position.
544
+ (i32.store (local.get $entry) (local.get $hh))
545
+ (i32.store (i32.add (local.get $entry) (i32.const 4)) (local.get $sid)))
546
+ (local.get $sid))`
547
+
548
+ // Parse object → OBJECT (schema-tagged, slot-based) when key sequence has a
549
+ // cached/registerable shape; falls back to HASH only on extreme key counts.
550
+ // Builds a transient (key, val) buffer during parse, then resolves a sid via
551
+ // the runtime schema cache, allocs an OBJECT, and copies values into slots.
552
+ // Walk-side `obj.prop` accesses then route through the OBJECT fast path
553
+ // (slot load) instead of the dispatcher → __hash_get_local chain.
417
554
  ctx.core.stdlib['__jp_obj'] = `(func $__jp_obj (result f64)
418
- (local $obj f64) (local $key f64) (local $ch i32)
419
- (local.set $obj (call $__hash_new))
420
- (call $__jp_ws)
421
- (if (i32.eq (call $__jp_peek) (i32.const 125))
422
- (then (call $__jp_adv (i32.const 1)) (return (local.get $obj))))
555
+ (local $kbuf i32) (local $kn i32) (local $kcap i32) (local $hh i32)
556
+ (local $key i64) (local $val i64) (local $h i32) (local $ch i32)
557
+ (local $sid i32) (local $obj i32) (local $i i32) (local $newbuf i32)
558
+ (local.set $kcap (i32.const 8))
559
+ (local.set $kbuf (call $__alloc (i32.shl (local.get $kcap) (i32.const 4))))
560
+ (local.set $hh (i32.const 0x811c9dc5))
561
+ ${WS()}
562
+ ;; Empty object — alloc an empty OBJECT with sid 0 (schema slot 0 may be
563
+ ;; empty/unused; downstream Object.keys handles 0-length names array).
564
+ (if (i32.eq ${PEEK} (i32.const 125))
565
+ (then ${ADV(1)}
566
+ (local.set $sid (call $__jp_schema_get (local.get $kbuf) (i32.const 0) (local.get $hh)))
567
+ (return (call $__mkptr (i32.const ${PTR.OBJECT}) (local.get $sid)
568
+ (call $__alloc_hdr (i32.const 0) (i32.const 1) (i32.const 8))))))
423
569
  (block $d (loop $l
424
- (call $__jp_ws)
425
- (if (i32.eq (call $__jp_peek) (i32.const 34))
426
- (then (call $__jp_adv (i32.const 1))))
427
- (local.set $key (call $__jp_str))
428
- (call $__jp_ws)
429
- (if (i32.eq (call $__jp_peek) (i32.const 58))
430
- (then (call $__jp_adv (i32.const 1))))
431
- (call $__jp_ws)
432
- (local.set $obj (call $__hash_set_local (local.get $obj) (local.get $key) (call $__jp_val)))
433
- (call $__jp_ws)
434
- (local.set $ch (call $__jp_peek))
570
+ ${WS()}
571
+ (if (i32.eq ${PEEK} (i32.const 34))
572
+ (then ${ADV(1)}))
573
+ (local.set $key (i64.reinterpret_f64 (call $__jp_str)))
574
+ (local.set $h (global.get $__jp_keyh))
575
+ ;; Mix key hash into running sequence hash. Escape-bearing keys (h=0)
576
+ ;; still mix; identical key sequences differing only by escapes will
577
+ ;; collide here, but the verify-step in __jp_schema_get rejects via
578
+ ;; i64.ne on the actual key bytes.
579
+ (local.set $hh (i32.mul (i32.xor (local.get $hh) (local.get $h)) (i32.const 0x01000193)))
580
+ ${WS()}
581
+ (if (i32.eq ${PEEK} (i32.const 58))
582
+ (then ${ADV(1)}))
583
+ ${WS()}
584
+ (local.set $val (i64.reinterpret_f64 (call $__jp_val)))
585
+ ;; Grow kbuf if at capacity.
586
+ (if (i32.ge_s (local.get $kn) (local.get $kcap))
587
+ (then
588
+ (local.set $kcap (i32.shl (local.get $kcap) (i32.const 1)))
589
+ (local.set $newbuf (call $__alloc (i32.shl (local.get $kcap) (i32.const 4))))
590
+ (memory.copy (local.get $newbuf) (local.get $kbuf) (i32.shl (local.get $kn) (i32.const 4)))
591
+ (local.set $kbuf (local.get $newbuf))))
592
+ ;; Append (key, val).
593
+ (i64.store (i32.add (local.get $kbuf) (i32.shl (local.get $kn) (i32.const 4))) (local.get $key))
594
+ (i64.store (i32.add (local.get $kbuf) (i32.add (i32.shl (local.get $kn) (i32.const 4)) (i32.const 8))) (local.get $val))
595
+ (local.set $kn (i32.add (local.get $kn) (i32.const 1)))
596
+ ${WS()}
597
+ (local.set $ch ${PEEK})
435
598
  (br_if $d (i32.eq (local.get $ch) (i32.const 125)))
436
- (if (i32.eq (local.get $ch) (i32.const 44)) (then (call $__jp_adv (i32.const 1))))
599
+ (if (i32.eq (local.get $ch) (i32.const 44)) (then ${ADV(1)}))
437
600
  (br $l)))
438
- (call $__jp_adv (i32.const 1))
439
- (local.get $obj))`
601
+ ${ADV(1)}
602
+ ;; Resolve schema sid (cached or freshly registered).
603
+ (local.set $sid (call $__jp_schema_get (local.get $kbuf) (local.get $kn) (local.get $hh)))
604
+ ;; Allocate OBJECT slot region: kn × 8 bytes, with header (size at -8,
605
+ ;; cap at -4) matching the static-fold path's emitJsonConstValue layout.
606
+ (local.set $obj (call $__alloc_hdr (i32.const 0) (local.get $kn) (i32.const 8)))
607
+ ;; Copy values into OBJECT slots.
608
+ (local.set $i (i32.const 0))
609
+ (block $vd (loop $vl
610
+ (br_if $vd (i32.ge_s (local.get $i) (local.get $kn)))
611
+ (i64.store
612
+ (i32.add (local.get $obj) (i32.shl (local.get $i) (i32.const 3)))
613
+ (i64.load (i32.add (local.get $kbuf) (i32.add (i32.shl (local.get $i) (i32.const 4)) (i32.const 8)))))
614
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
615
+ (br $vl)))
616
+ (call $__mkptr (i32.const ${PTR.OBJECT}) (local.get $sid) (local.get $obj)))`
440
617
 
441
618
  // Main value dispatcher
442
619
  ctx.core.stdlib['__jp_val'] = `(func $__jp_val (result f64)
443
620
  (local $ch i32)
444
- (call $__jp_ws)
445
- (local.set $ch (call $__jp_peek))
621
+ ${WS()}
622
+ (local.set $ch ${PEEK})
446
623
  (if (i32.eq (local.get $ch) (i32.const 34))
447
- (then (call $__jp_adv (i32.const 1)) (return (call $__jp_str))))
624
+ (then ${ADV(1)} (return (call $__jp_str))))
448
625
  (if (i32.eq (local.get $ch) (i32.const 91))
449
- (then (call $__jp_adv (i32.const 1)) (return (call $__jp_arr))))
626
+ (then ${ADV(1)} (return (call $__jp_arr))))
450
627
  (if (i32.eq (local.get $ch) (i32.const 123))
451
- (then (call $__jp_adv (i32.const 1)) (return (call $__jp_obj))))
628
+ (then ${ADV(1)} (return (call $__jp_obj))))
452
629
  (if (i32.or (i32.and (i32.ge_s (local.get $ch) (i32.const 48)) (i32.le_s (local.get $ch) (i32.const 57)))
453
630
  (i32.eq (local.get $ch) (i32.const 45)))
454
631
  (then (return (call $__jp_num))))
455
632
  (if (i32.eq (local.get $ch) (i32.const 116))
456
- (then (call $__jp_adv (i32.const 4)) (return (f64.const 1))))
633
+ (then ${ADV(4)} (return (f64.const 1))))
457
634
  (if (i32.eq (local.get $ch) (i32.const 102))
458
- (then (call $__jp_adv (i32.const 5)) (return (f64.const 0))))
635
+ (then ${ADV(5)} (return (f64.const 0))))
459
636
  (if (i32.eq (local.get $ch) (i32.const 110))
460
- (then (call $__jp_adv (i32.const 4)) (return (f64.const 0))))
461
- (f64.const 0))`
637
+ (then ${ADV(4)} (return ${NULL_WAT})))
638
+ ${NULL_WAT})`
639
+
640
+ function canSpecializeJsonShape(v) {
641
+ if (v == null) return true
642
+ if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') return true
643
+ if (Array.isArray(v)) return v.length > 0 && v.every(x => sameJsonShape(v[0], x)) && canSpecializeJsonShape(v[0])
644
+ if (typeof v === 'object') return Object.keys(v).every(k => /^[\x20-\x21\x23-\x5b\x5d-\x7e]*$/.test(k) && canSpecializeJsonShape(v[k]))
645
+ return false
646
+ }
647
+
648
+ function sameJsonShape(a, b) {
649
+ if (a == null || b == null) return a == null && b == null
650
+ if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length > 0 && b.length > 0 && sameJsonShape(a[0], b[0])
651
+ if (typeof a !== typeof b) return false
652
+ if (typeof a !== 'object') return true
653
+ const ak = Object.keys(a), bk = Object.keys(b)
654
+ return ak.length === bk.length && ak.every((k, i) => k === bk[i] && sameJsonShape(a[k], b[k]))
655
+ }
656
+
657
+ function emitJsonShapeParser(parsed) {
658
+ if (!canSpecializeJsonShape(parsed)) return null
659
+ ctx.runtime.jsonShapeParsers ||= new Map()
660
+ const sig = JSON.stringify(shapeSignature(parsed))
661
+ const cached = ctx.runtime.jsonShapeParsers.get(sig)
662
+ if (cached) return cached
663
+
664
+ const name = `__jp_shape_${ctx.runtime.jsonShapeParsers.size}`
665
+ const locals = new Map([['len', 'i32'], ['buf', 'i32'], ['i', 'i32'], ['ch', 'i32']])
666
+ let uniq = 0
667
+ const local = (p, t) => {
668
+ const n = `${p}${uniq++}`
669
+ locals.set(n, t)
670
+ return n
671
+ }
672
+ const fail = `(return (call $__jp (local.get $str)))`
673
+ const expect = (byte) => `(if (i32.ne ${PEEK} (i32.const ${byte})) (then ${fail}))
674
+ ${ADV(1)}`
675
+ const expectText = (text) => [...text].map(c => expect(c.charCodeAt(0))).join('\n ')
676
+ const parse = (v, out) => {
677
+ if (v == null) return `${expectText('null')}
678
+ (local.set $${out} ${NULL_WAT})`
679
+ if (typeof v === 'boolean') return `${expectText(v ? 'true' : 'false')}
680
+ (local.set $${out} (f64.const ${v ? 1 : 0}))`
681
+ if (typeof v === 'number') return `(local.set $${out} (call $__jp_num))`
682
+ if (typeof v === 'string') return `${expect(34)}
683
+ (local.set $${out} (call $__jp_str))`
684
+ if (Array.isArray(v)) return parseArray(v[0], out)
685
+ return parseObject(v, out)
686
+ }
687
+ const parseObject = (v, out) => {
688
+ const keys = Object.keys(v)
689
+ const obj = local('obj', 'i32')
690
+ const val = local('val', 'f64')
691
+ const sid = ctx.schema.register(keys)
692
+ let body = `${WS()}
693
+ ${expect(123)}
694
+ (local.set $${obj} (call $__alloc_hdr (i32.const 0) (i32.const ${Math.max(1, keys.length)}) (i32.const 8)))`
695
+ keys.forEach((k, i) => {
696
+ body += `
697
+ ${WS()}
698
+ ${expect(34)}
699
+ ${expectText(k)}
700
+ ${expect(34)}
701
+ ${WS()}
702
+ ${expect(58)}
703
+ ${WS()}
704
+ ${parse(v[k], val)}
705
+ (f64.store (i32.add (local.get $${obj}) (i32.const ${i * 8})) (local.get $${val}))
706
+ ${WS()}
707
+ ${expect(i === keys.length - 1 ? 125 : 44)}`
708
+ })
709
+ if (keys.length === 0) body += `
710
+ ${WS()}
711
+ ${expect(125)}`
712
+ return `${body}
713
+ (local.set $${out} (call $__mkptr (i32.const ${PTR.OBJECT}) (i32.const ${sid}) (local.get $${obj})))`
714
+ }
715
+ const parseArray = (elem, out) => {
716
+ const ptr = local('arr', 'i32')
717
+ const len = local('alen', 'i32')
718
+ const cap = local('acap', 'i32')
719
+ const val = local('aval', 'f64')
720
+ const next = local('anew', 'i32')
721
+ const id = uniq++
722
+ return `${WS()}
723
+ ${expect(91)}
724
+ (local.set $${cap} (i32.const 8))
725
+ (local.set $${ptr} (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $${cap}) (i32.const 3)))))
726
+ (local.set $${ptr} (i32.add (local.get $${ptr}) (i32.const 8)))
727
+ ${WS()}
728
+ (if (i32.eq ${PEEK} (i32.const 93))
729
+ (then
730
+ ${ADV(1)}
731
+ (i32.store (i32.sub (local.get $${ptr}) (i32.const 8)) (i32.const 0))
732
+ (i32.store (i32.sub (local.get $${ptr}) (i32.const 4)) (local.get $${cap}))
733
+ (local.set $${out} (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $${ptr}))))
734
+ (else
735
+ (block $ad${id} (loop $al${id}
736
+ (if (i32.ge_s (local.get $${len}) (local.get $${cap}))
737
+ (then
738
+ (local.set $${cap} (i32.shl (local.get $${cap}) (i32.const 1)))
739
+ (local.set $${next} (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $${cap}) (i32.const 3)))))
740
+ (local.set $${next} (i32.add (local.get $${next}) (i32.const 8)))
741
+ (memory.copy (local.get $${next}) (local.get $${ptr}) (i32.shl (local.get $${len}) (i32.const 3)))
742
+ (local.set $${ptr} (local.get $${next}))))
743
+ ${parse(elem, val)}
744
+ (f64.store (i32.add (local.get $${ptr}) (i32.shl (local.get $${len}) (i32.const 3))) (local.get $${val}))
745
+ (local.set $${len} (i32.add (local.get $${len}) (i32.const 1)))
746
+ ${WS()}
747
+ (local.set $ch ${PEEK})
748
+ (br_if $ad${id} (i32.eq (local.get $ch) (i32.const 93)))
749
+ (if (i32.ne (local.get $ch) (i32.const 44)) (then ${fail}))
750
+ ${ADV(1)}
751
+ ${WS()}
752
+ (br $al${id})))
753
+ ${ADV(1)}
754
+ (i32.store (i32.sub (local.get $${ptr}) (i32.const 8)) (local.get $${len}))
755
+ (i32.store (i32.sub (local.get $${ptr}) (i32.const 4)) (local.get $${cap}))
756
+ (local.set $${out} (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $${ptr})))))`
757
+ }
758
+
759
+ const out = local('out', 'f64')
760
+ const body = `${parse(parsed, out)}
761
+ ${WS()}
762
+ (if (i32.ne ${PEEK} (i32.const -1)) (then ${fail}))
763
+ (local.get $${out})`
764
+ const localDecls = [...locals].map(([n, t]) => ` (local $${n} ${t})`).join('\n')
765
+ ctx.core.stdlib[name] = `(func $${name} (param $str i64) (result f64)
766
+ ${localDecls}
767
+ (local.set $len (call $__str_byteLen (local.get $str)))
768
+ (local.set $buf (call $__alloc (i32.add (local.get $len) (i32.const 8))))
769
+ (i64.store (i32.add (local.get $buf) (local.get $len)) (i64.const -1))
770
+ (if (i32.and (call $__ptr_aux (local.get $str)) (i32.const ${LAYOUT.SSO_BIT}))
771
+ (then
772
+ (local.set $i (i32.const 0))
773
+ (block $sd (loop $sl
774
+ (br_if $sd (i32.ge_s (local.get $i) (local.get $len)))
775
+ (i32.store8 (i32.add (local.get $buf) (local.get $i))
776
+ (call $__sso_char (local.get $str) (local.get $i)))
777
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
778
+ (br $sl))))
779
+ (else
780
+ (memory.copy (local.get $buf) (call $__ptr_offset (local.get $str)) (local.get $len))))
781
+ (global.set $__jpstr (local.get $buf))
782
+ (global.set $__jplen (local.get $len))
783
+ (global.set $__jppos (i32.const 0))
784
+ ${body})`
785
+ ctx.core.stdlibDeps[name] = ['__jp', '__jp_num', '__jp_str', '__str_byteLen', '__alloc', '__ptr_aux', '__sso_char', '__ptr_offset', '__alloc_hdr', '__mkptr']
786
+ ctx.runtime.jsonShapeParsers.set(sig, name)
787
+ return name
788
+ }
789
+
790
+ function shapeSignature(v) {
791
+ if (v == null) return null
792
+ if (typeof v === 'number') return 'number'
793
+ if (typeof v === 'string') return 'string'
794
+ if (typeof v === 'boolean') return 'boolean'
795
+ if (Array.isArray(v)) return ['array', shapeSignature(v[0])]
796
+ return ['object', Object.keys(v).map(k => [k, shapeSignature(v[k])])]
797
+ }
462
798
 
463
799
  // Entry point — copies input to a scratch buffer with 0xFF sentinel padding
464
800
  // past the end so __jp_peek can omit its bounds check. Pad is 8 bytes so any
465
801
  // overshoot from speculative peek/adv on malformed input still hits sentinel,
466
802
  // not unallocated memory.
467
- ctx.core.stdlib['__jp'] = `(func $__jp (param $str f64) (result f64)
803
+ ctx.core.stdlib['__jp'] = `(func $__jp (param $str i64) (result f64)
468
804
  (local $len i32) (local $buf i32) (local $i i32)
469
805
  (local.set $len (call $__str_byteLen (local.get $str)))
470
806
  (local.set $buf (call $__alloc (i32.add (local.get $len) (i32.const 8))))
471
807
  ;; Pre-fill 8 sentinel bytes at end (writes overlapping a 64-bit slot).
472
808
  (i64.store (i32.add (local.get $buf) (local.get $len)) (i64.const -1))
473
- ;; SSO: byte-by-byte via __sso_char; STRING: bulk memcpy from string offset.
474
- (if (i32.eq (call $__ptr_type (local.get $str)) (i32.const ${PTR.SSO}))
809
+ ;; SSO: byte-by-byte via __sso_char; heap STRING: bulk memcpy from string offset.
810
+ (if (i32.and (call $__ptr_aux (local.get $str)) (i32.const ${LAYOUT.SSO_BIT}))
475
811
  (then
476
812
  (local.set $i (i32.const 0))
477
813
  (block $d (loop $l
@@ -491,7 +827,7 @@ export default (ctx) => {
491
827
 
492
828
  ctx.core.emit['JSON.stringify'] = (x) => {
493
829
  inc('__stringify')
494
- return typed(['call', '$__stringify', asF64(emit(x))], 'f64')
830
+ return typed(['call', '$__stringify', asI64(emit(x))], 'f64')
495
831
  }
496
832
 
497
833
  ctx.core.emit['JSON.parse'] = (x) => {
@@ -500,7 +836,16 @@ export default (ctx) => {
500
836
  try { return emitJsonConstValue(JSON.parse(src)) }
501
837
  catch { /* fall through to runtime parser for invalid JSON so runtime behavior stays unchanged */ }
502
838
  }
839
+ const shapeSrcs = jsonShapeStrings(ctx, x)
840
+ if (shapeSrcs) {
841
+ try {
842
+ const parsed = shapeSrcs.map(src => JSON.parse(src))
843
+ if (!parsed.every(v => sameJsonShape(parsed[0], v))) throw new Error('mixed JSON shapes')
844
+ const fn = emitJsonShapeParser(parsed[0])
845
+ if (fn) { inc(fn); return typed(['call', `$${fn}`, asI64(emit(x))], 'f64') }
846
+ } catch { /* fall through to generic runtime parser */ }
847
+ }
503
848
  inc('__jp')
504
- return typed(['call', '$__jp', asF64(emit(x))], 'f64')
849
+ return typed(['call', '$__jp', asI64(emit(x))], 'f64')
505
850
  }
506
851
  }