jz 0.1.0 → 0.2.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.
package/module/json.js CHANGED
@@ -8,8 +8,10 @@
8
8
  * @module json
9
9
  */
10
10
 
11
- import { emit, typed, asF64, T, temp, nullExpr, allocPtr, slotAddr } from '../src/compile.js'
12
- import { err, inc, PTR } from '../src/ctx.js'
11
+ import { typed, asF64, asI64, temp, tempI32, nullExpr, allocPtr, slotAddr, mkPtrIR, extractF64Bits, appendStaticSlots, NULL_WAT } from '../src/ir.js'
12
+ import { emit } from '../src/emit.js'
13
+ import { T } from '../src/analyze.js'
14
+ import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
13
15
  import { strHashLiteral } from './collection.js'
14
16
 
15
17
  function jsonConstString(ctx, expr) {
@@ -19,6 +21,18 @@ function jsonConstString(ctx, expr) {
19
21
  return null
20
22
  }
21
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
+
22
36
  function hashCapFor(n) {
23
37
  let cap = 8
24
38
  const need = Math.max(1, Math.ceil(n * 4 / 3))
@@ -34,15 +48,30 @@ export default (ctx) => {
34
48
  __json_obj: ['__ptr_offset', '__ptr_aux', '__len', '__jput', '__jput_str', '__json_val'],
35
49
  __jput_num: ['__ftoa'],
36
50
  __jput_str: ['__char_at', '__str_byteLen'],
37
- __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'],
38
53
  __jp_str: ['__sso_char', '__char_at', '__str_byteLen'],
39
54
  __jp_num: ['__pow10'],
40
55
  __jp_arr: ['__jp_val'],
41
- __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'],
42
58
  })
43
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.
44
73
  function emitJsonConstValue(v) {
45
- if (v == null) return asF64(emit(nullExpr))
74
+ if (v == null) return nullExpr()
46
75
  if (typeof v === 'number') return asF64(emit(v))
47
76
  if (typeof v === 'string') return asF64(emit(['str', v]))
48
77
  if (typeof v === 'boolean') return asF64(emit(v ? 1 : 0))
@@ -55,15 +84,19 @@ export default (ctx) => {
55
84
  }
56
85
  if (typeof v === 'object') {
57
86
  const keys = Object.keys(v)
58
- const obj = allocPtr({ type: PTR.HASH, len: 0, cap: hashCapFor(keys.length), stride: 24, tag: 'jhash' })
59
- const h = temp('jhash')
60
- const body = [obj.init, ['local.set', `$${h}`, obj.ptr]]
61
- for (const k of keys) {
62
- body.push(['local.set', `$${h}`,
63
- ['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]])
64
90
  }
65
- body.push(['local.get', `$${h}`])
66
- 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}`]))
67
100
  return typed(['block', ['result', 'f64'], ...body], 'f64')
68
101
  }
69
102
  return asF64(emit(nullExpr))
@@ -92,8 +125,8 @@ export default (ctx) => {
92
125
  (i32.store8 (i32.add (global.get $__jbuf) (global.get $__jpos)) (local.get $b))
93
126
  (global.set $__jpos (i32.add (global.get $__jpos) (i32.const 1))))`
94
127
 
95
- // __jput_str(ptr: f64) — append string chars (without quotes) to buffer
96
- 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)
97
130
  (local $len i32) (local $i i32) (local $ch i32)
98
131
  (local.set $len (call $__str_byteLen (local.get $ptr)))
99
132
  (local.set $i (i32.const 0))
@@ -118,19 +151,20 @@ export default (ctx) => {
118
151
 
119
152
  // __jput_num(val: f64) — convert number to string, append bytes to buffer
120
153
  ctx.core.stdlib['__jput_num'] = `(func $__jput_num (param $val f64)
121
- (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)))))`
122
155
 
123
- // __json_val(val: f64) — stringify any value, append to buffer
124
- ctx.core.stdlib['__json_val'] = `(func $__json_val (param $val f64)
125
- (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)))
126
160
  ;; Number (not NaN) — but Infinity must be null per JSON spec
127
- (if (f64.eq (local.get $val) (local.get $val))
161
+ (if (f64.eq (local.get $f) (local.get $f))
128
162
  (then
129
- (if (f64.eq (f64.abs (local.get $val)) (f64.const inf))
163
+ (if (f64.eq (f64.abs (local.get $f)) (f64.const inf))
130
164
  (then
131
165
  (call $__jput (i32.const 110)) (call $__jput (i32.const 117))
132
166
  (call $__jput (i32.const 108)) (call $__jput (i32.const 108)) (return)))
133
- (call $__jput_num (local.get $val)) (return)))
167
+ (call $__jput_num (local.get $f)) (return)))
134
168
  ;; NaN-boxed pointer
135
169
  (local.set $type (call $__ptr_type (local.get $val)))
136
170
  ;; Plain NaN (type=0) → null
@@ -139,8 +173,7 @@ export default (ctx) => {
139
173
  (call $__jput (i32.const 110)) (call $__jput (i32.const 117))
140
174
  (call $__jput (i32.const 108)) (call $__jput (i32.const 108)) (return)))
141
175
  ;; String
142
- (if (i32.or (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
143
- (i32.eq (local.get $type) (i32.const ${PTR.SSO})))
176
+ (if (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
144
177
  (then
145
178
  (call $__jput (i32.const 34))
146
179
  (call $__jput_str (local.get $val))
@@ -155,7 +188,7 @@ export default (ctx) => {
155
188
  (block $d (loop $l
156
189
  (br_if $d (i32.ge_s (local.get $i) (local.get $len)))
157
190
  (if (local.get $i) (then (call $__jput (i32.const 44)))) ;; ,
158
- (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)))))
159
192
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
160
193
  (br $l)))
161
194
  (call $__jput (i32.const 93)) ;; ]
@@ -171,9 +204,9 @@ export default (ctx) => {
171
204
  (call $__jput (i32.const 110)) (call $__jput (i32.const 117))
172
205
  (call $__jput (i32.const 108)) (call $__jput (i32.const 108)))`
173
206
 
174
- // __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,...}
175
208
  // Slot layout: 24 bytes each — [hash:f64][key:f64][val:f64]. Empty slots have hash==0.
176
- ctx.core.stdlib['__json_hash'] = `(func $__json_hash (param $val f64)
209
+ ctx.core.stdlib['__json_hash'] = `(func $__json_hash (param $val i64)
177
210
  (local $off i32) (local $cap i32) (local $i i32) (local $slot i32) (local $first i32)
178
211
  (local.set $off (call $__ptr_offset (local.get $val)))
179
212
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
@@ -188,10 +221,10 @@ export default (ctx) => {
188
221
  (then (call $__jput (i32.const 44))))
189
222
  (local.set $first (i32.const 0))
190
223
  (call $__jput (i32.const 34))
191
- (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))))
192
225
  (call $__jput (i32.const 34))
193
226
  (call $__jput (i32.const 58))
194
- (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))))))
195
228
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
196
229
  (br $l)))
197
230
  (call $__jput (i32.const 125)))`
@@ -200,32 +233,32 @@ export default (ctx) => {
200
233
  // Schema name table: global $__schema_tbl → array of f64 pointers.
201
234
  // schema_tbl[schemaId * 8] = f64 pointer to jz Array of key name strings.
202
235
  // Object props are sequential f64 at ptr_offset, indexed same as schema.
203
- ctx.core.stdlib['__json_obj'] = `(func $__json_obj (param $val f64)
236
+ ctx.core.stdlib['__json_obj'] = `(func $__json_obj (param $val i64)
204
237
  (local $off i32) (local $sid i32) (local $keys i32) (local $nkeys i32)
205
238
  (local $i i32) (local $koff i32)
206
239
  (local.set $off (call $__ptr_offset (local.get $val)))
207
240
  (local.set $sid (call $__ptr_aux (local.get $val)))
208
241
  ;; Load keys array from schema table: schema_tbl + sid * 8
209
242
  (local.set $keys (call $__ptr_offset
210
- (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))))))
211
244
  (local.set $nkeys (call $__len
212
- (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))))))
213
246
  (local.set $koff (local.get $keys))
214
247
  (call $__jput (i32.const 123))
215
248
  (block $d (loop $l
216
249
  (br_if $d (i32.ge_s (local.get $i) (local.get $nkeys)))
217
250
  (if (local.get $i) (then (call $__jput (i32.const 44))))
218
251
  (call $__jput (i32.const 34))
219
- (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)))))
220
253
  (call $__jput (i32.const 34))
221
254
  (call $__jput (i32.const 58))
222
- (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)))))
223
256
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
224
257
  (br $l)))
225
258
  (call $__jput (i32.const 125)))`
226
259
 
227
- // __stringify(val: f64) → f64 (NaN-boxed string)
228
- 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)
229
262
  ;; Reset output buffer
230
263
  (global.set $__jbuf (call $__alloc (i32.const 256)))
231
264
  (global.set $__jpos (i32.const 0))
@@ -239,62 +272,92 @@ export default (ctx) => {
239
272
  ctx.scope.globals.set('__jpstr', '(global $__jpstr (mut i32) (i32.const 0))') // input string offset
240
273
  ctx.scope.globals.set('__jplen', '(global $__jplen (mut i32) (i32.const 0))') // input length
241
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))')
242
291
 
243
292
  // Sentinel-driven peek: __jp copies input to a scratch buffer with 0xFF bytes
244
293
  // appended past the end. i32.load8_s sign-extends, so the sentinel reads as -1
245
- // — exactly the EOF value all callers already test for. Bounds check and
246
- // function-call overhead both gone; ~50 calls/parse char in well-formed JSON.
247
- ctx.core.stdlib['__jp_peek'] = `(func $__jp_peek (result i32)
248
- (i32.load8_s (i32.add (global.get $__jpstr) (global.get $__jppos))))`
249
-
250
- ctx.core.stdlib['__jp_adv'] = `(func $__jp_adv (param $n i32)
251
- (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})))`
252
299
 
253
- ctx.core.stdlib['__jp_ws'] = `(func $__jp_ws
254
- (local $ch i32)
255
- (block $d (loop $l
256
- (local.set $ch (call $__jp_peek))
257
- (br_if $d (i32.and (i32.ne (local.get $ch) (i32.const 32))
258
- (i32.and (i32.ne (local.get $ch) (i32.const 9))
259
- (i32.and (i32.ne (local.get $ch) (i32.const 10))
260
- (i32.ne (local.get $ch) (i32.const 13))))))
261
- (call $__jp_adv (i32.const 1))
262
- (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
+ }
263
316
 
264
- // Parse string (after opening " consumed). Two-phase: scan to closing quote
265
- // tracking whether all chars are simple ASCII (no escapes, no high-bit), then
266
- // 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.
267
322
  ctx.core.stdlib['__jp_str'] = `(func $__jp_str (result f64)
268
- (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)
269
324
  (local.set $start (global.get $__jppos))
270
325
  (local.set $simple (i32.const 1))
326
+ (local.set $h (i32.const 0x811c9dc5))
271
327
  (block $d (loop $l
272
- (local.set $ch (call $__jp_peek))
328
+ (local.set $ch ${PEEK})
273
329
  (br_if $d (i32.eq (local.get $ch) (i32.const 34)))
274
330
  (br_if $d (i32.eq (local.get $ch) (i32.const -1)))
275
331
  ;; Mark non-simple: escape (\\=92) or non-ASCII (load8_s gives <0 for byte≥128).
276
332
  (if (i32.or (i32.eq (local.get $ch) (i32.const 92)) (i32.lt_s (local.get $ch) (i32.const 0)))
277
333
  (then (local.set $simple (i32.const 0))))
278
334
  (if (i32.eq (local.get $ch) (i32.const 92))
279
- (then (call $__jp_adv (i32.const 2)))
280
- (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)}))
281
348
  (br $l)))
282
- (local.set $len (i32.sub (global.get $__jppos) (local.get $start)))
283
- (call $__jp_adv (i32.const 1)) ;; skip "
284
- ;; SSO fast path: ≤4 ASCII chars, no escapes — pack bytes into the offset slot,
285
- ;; 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.
286
358
  (if (i32.and (local.get $simple) (i32.le_u (local.get $len) (i32.const 4)))
287
359
  (then
288
- (local.set $i (i32.const 0))
289
- (block $sd (loop $sl
290
- (br_if $sd (i32.ge_s (local.get $i) (local.get $len)))
291
- (local.set $sso
292
- (i32.or (local.get $sso)
293
- (i32.shl (i32.load8_u (i32.add (i32.add (global.get $__jpstr) (local.get $start)) (local.get $i)))
294
- (i32.shl (local.get $i) (i32.const 3)))))
295
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
296
- (br $sl)))
297
- (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)))))
298
361
  ;; Simple STRING fast path: no escapes, len > 4 — bulk memcpy from parse buffer,
299
362
  ;; skip rewind + per-byte escape-decode loop. Hits 5+ char keys without escapes.
300
363
  (if (local.get $simple)
@@ -311,25 +374,25 @@ export default (ctx) => {
311
374
  (global.set $__jppos (local.get $start)) ;; rewind to re-scan
312
375
  (local.set $len (i32.const 0)) ;; actual output length
313
376
  (block $d2 (loop $l2
314
- (local.set $ch (call $__jp_peek))
377
+ (local.set $ch ${PEEK})
315
378
  (br_if $d2 (i32.eq (local.get $ch) (i32.const 34)))
316
379
  (br_if $d2 (i32.eq (local.get $ch) (i32.const -1)))
317
380
  (if (i32.eq (local.get $ch) (i32.const 92))
318
381
  (then
319
- (call $__jp_adv (i32.const 1))
320
- (local.set $ch (call $__jp_peek))
321
- (call $__jp_adv (i32.const 1))
382
+ ${ADV(1)}
383
+ (local.set $ch ${PEEK})
384
+ ${ADV(1)}
322
385
  ;; Decode escape: n→10 t→9 r→13 b→8 f→12, else literal
323
386
  (if (i32.eq (local.get $ch) (i32.const 110)) (then (local.set $ch (i32.const 10))))
324
387
  (if (i32.eq (local.get $ch) (i32.const 116)) (then (local.set $ch (i32.const 9))))
325
388
  (if (i32.eq (local.get $ch) (i32.const 114)) (then (local.set $ch (i32.const 13))))
326
389
  (if (i32.eq (local.get $ch) (i32.const 98)) (then (local.set $ch (i32.const 8))))
327
390
  (if (i32.eq (local.get $ch) (i32.const 102)) (then (local.set $ch (i32.const 12)))))
328
- (else (call $__jp_adv (i32.const 1))))
391
+ (else ${ADV(1)}))
329
392
  (i32.store8 (i32.add (local.get $off) (local.get $len)) (local.get $ch))
330
393
  (local.set $len (i32.add (local.get $len) (i32.const 1)))
331
394
  (br $l2)))
332
- (call $__jp_adv (i32.const 1)) ;; skip closing "
395
+ ${ADV(1)} ;; skip closing "
333
396
  ;; Store actual length in header
334
397
  (i32.store (i32.sub (local.get $off) (i32.const 4)) (local.get $len))
335
398
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
@@ -338,37 +401,37 @@ export default (ctx) => {
338
401
  ctx.core.stdlib['__jp_num'] = `(func $__jp_num (result f64)
339
402
  (local $neg i32) (local $val f64) (local $scale f64) (local $ch i32)
340
403
  (local $exp i32) (local $expNeg i32)
341
- (if (i32.eq (call $__jp_peek) (i32.const 45))
342
- (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)}))
343
406
  (block $d (loop $l
344
- (local.set $ch (call $__jp_peek))
407
+ (local.set $ch ${PEEK})
345
408
  (br_if $d (i32.or (i32.lt_s (local.get $ch) (i32.const 48)) (i32.gt_s (local.get $ch) (i32.const 57))))
346
409
  (local.set $val (f64.add (f64.mul (local.get $val) (f64.const 10))
347
410
  (f64.convert_i32_s (i32.sub (local.get $ch) (i32.const 48)))))
348
- (call $__jp_adv (i32.const 1)) (br $l)))
349
- (if (i32.eq (call $__jp_peek) (i32.const 46))
411
+ ${ADV(1)} (br $l)))
412
+ (if (i32.eq ${PEEK} (i32.const 46))
350
413
  (then
351
- (call $__jp_adv (i32.const 1))
414
+ ${ADV(1)}
352
415
  (local.set $scale (f64.const 0.1))
353
416
  (block $fd (loop $fl
354
- (local.set $ch (call $__jp_peek))
417
+ (local.set $ch ${PEEK})
355
418
  (br_if $fd (i32.or (i32.lt_s (local.get $ch) (i32.const 48)) (i32.gt_s (local.get $ch) (i32.const 57))))
356
419
  (local.set $val (f64.add (local.get $val)
357
420
  (f64.mul (local.get $scale) (f64.convert_i32_s (i32.sub (local.get $ch) (i32.const 48))))))
358
421
  (local.set $scale (f64.mul (local.get $scale) (f64.const 0.1)))
359
- (call $__jp_adv (i32.const 1)) (br $fl)))))
360
- (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)))
361
424
  (then
362
- (call $__jp_adv (i32.const 1))
363
- (if (i32.eq (call $__jp_peek) (i32.const 45))
364
- (then (local.set $expNeg (i32.const 1)) (call $__jp_adv (i32.const 1)))
365
- (else (if (i32.eq (call $__jp_peek) (i32.const 43))
366
- (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)}))))
367
430
  (block $ed (loop $el
368
- (local.set $ch (call $__jp_peek))
431
+ (local.set $ch ${PEEK})
369
432
  (br_if $ed (i32.or (i32.lt_s (local.get $ch) (i32.const 48)) (i32.gt_s (local.get $ch) (i32.const 57))))
370
433
  (local.set $exp (i32.add (i32.mul (local.get $exp) (i32.const 10)) (i32.sub (local.get $ch) (i32.const 48))))
371
- (call $__jp_adv (i32.const 1)) (br $el)))
434
+ ${ADV(1)} (br $el)))
372
435
  (if (local.get $expNeg) (then (local.set $exp (i32.sub (i32.const 0) (local.get $exp)))))
373
436
  (local.set $val (f64.mul (local.get $val) (call $__pow10
374
437
  (if (result i32) (i32.lt_s (local.get $exp) (i32.const 0))
@@ -383,14 +446,14 @@ export default (ctx) => {
383
446
  (local.set $cap (i32.const 8))
384
447
  (local.set $ptr (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $cap) (i32.const 3)))))
385
448
  (local.set $ptr (i32.add (local.get $ptr) (i32.const 8)))
386
- (call $__jp_ws)
387
- (if (i32.eq (call $__jp_peek) (i32.const 93))
388
- (then (call $__jp_adv (i32.const 1))
449
+ ${WS()}
450
+ (if (i32.eq ${PEEK} (i32.const 93))
451
+ (then ${ADV(1)}
389
452
  (i32.store (i32.sub (local.get $ptr) (i32.const 8)) (i32.const 0))
390
453
  (i32.store (i32.sub (local.get $ptr) (i32.const 4)) (local.get $cap))
391
454
  (return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $ptr)))))
392
455
  (block $d (loop $l
393
- (call $__jp_ws)
456
+ ${WS()}
394
457
  ;; Grow if needed
395
458
  (if (i32.ge_s (local.get $len) (local.get $cap))
396
459
  (then
@@ -401,75 +464,350 @@ export default (ctx) => {
401
464
  (local.set $ptr (local.get $new))))
402
465
  (f64.store (i32.add (local.get $ptr) (i32.shl (local.get $len) (i32.const 3))) (call $__jp_val))
403
466
  (local.set $len (i32.add (local.get $len) (i32.const 1)))
404
- (call $__jp_ws)
405
- (local.set $ch (call $__jp_peek))
467
+ ${WS()}
468
+ (local.set $ch ${PEEK})
406
469
  (br_if $d (i32.eq (local.get $ch) (i32.const 93)))
407
- (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)}))
408
471
  (br $l)))
409
- (call $__jp_adv (i32.const 1))
472
+ ${ADV(1)}
410
473
  (i32.store (i32.sub (local.get $ptr) (i32.const 8)) (local.get $len))
411
474
  (i32.store (i32.sub (local.get $ptr) (i32.const 4)) (local.get $cap))
412
475
  (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $ptr)))`
413
476
 
414
- // 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.
415
554
  ctx.core.stdlib['__jp_obj'] = `(func $__jp_obj (result f64)
416
- (local $obj f64) (local $key f64) (local $ch i32)
417
- (local.set $obj (call $__hash_new))
418
- (call $__jp_ws)
419
- (if (i32.eq (call $__jp_peek) (i32.const 125))
420
- (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))))))
421
569
  (block $d (loop $l
422
- (call $__jp_ws)
423
- (if (i32.eq (call $__jp_peek) (i32.const 34))
424
- (then (call $__jp_adv (i32.const 1))))
425
- (local.set $key (call $__jp_str))
426
- (call $__jp_ws)
427
- (if (i32.eq (call $__jp_peek) (i32.const 58))
428
- (then (call $__jp_adv (i32.const 1))))
429
- (call $__jp_ws)
430
- (local.set $obj (call $__hash_set_local (local.get $obj) (local.get $key) (call $__jp_val)))
431
- (call $__jp_ws)
432
- (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})
433
598
  (br_if $d (i32.eq (local.get $ch) (i32.const 125)))
434
- (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)}))
435
600
  (br $l)))
436
- (call $__jp_adv (i32.const 1))
437
- (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)))`
438
617
 
439
618
  // Main value dispatcher
440
619
  ctx.core.stdlib['__jp_val'] = `(func $__jp_val (result f64)
441
620
  (local $ch i32)
442
- (call $__jp_ws)
443
- (local.set $ch (call $__jp_peek))
621
+ ${WS()}
622
+ (local.set $ch ${PEEK})
444
623
  (if (i32.eq (local.get $ch) (i32.const 34))
445
- (then (call $__jp_adv (i32.const 1)) (return (call $__jp_str))))
624
+ (then ${ADV(1)} (return (call $__jp_str))))
446
625
  (if (i32.eq (local.get $ch) (i32.const 91))
447
- (then (call $__jp_adv (i32.const 1)) (return (call $__jp_arr))))
626
+ (then ${ADV(1)} (return (call $__jp_arr))))
448
627
  (if (i32.eq (local.get $ch) (i32.const 123))
449
- (then (call $__jp_adv (i32.const 1)) (return (call $__jp_obj))))
628
+ (then ${ADV(1)} (return (call $__jp_obj))))
450
629
  (if (i32.or (i32.and (i32.ge_s (local.get $ch) (i32.const 48)) (i32.le_s (local.get $ch) (i32.const 57)))
451
630
  (i32.eq (local.get $ch) (i32.const 45)))
452
631
  (then (return (call $__jp_num))))
453
632
  (if (i32.eq (local.get $ch) (i32.const 116))
454
- (then (call $__jp_adv (i32.const 4)) (return (f64.const 1))))
633
+ (then ${ADV(4)} (return (f64.const 1))))
455
634
  (if (i32.eq (local.get $ch) (i32.const 102))
456
- (then (call $__jp_adv (i32.const 5)) (return (f64.const 0))))
635
+ (then ${ADV(5)} (return (f64.const 0))))
457
636
  (if (i32.eq (local.get $ch) (i32.const 110))
458
- (then (call $__jp_adv (i32.const 4)) (return (f64.const 0))))
459
- (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
+ }
460
798
 
461
799
  // Entry point — copies input to a scratch buffer with 0xFF sentinel padding
462
800
  // past the end so __jp_peek can omit its bounds check. Pad is 8 bytes so any
463
801
  // overshoot from speculative peek/adv on malformed input still hits sentinel,
464
802
  // not unallocated memory.
465
- ctx.core.stdlib['__jp'] = `(func $__jp (param $str f64) (result f64)
803
+ ctx.core.stdlib['__jp'] = `(func $__jp (param $str i64) (result f64)
466
804
  (local $len i32) (local $buf i32) (local $i i32)
467
805
  (local.set $len (call $__str_byteLen (local.get $str)))
468
806
  (local.set $buf (call $__alloc (i32.add (local.get $len) (i32.const 8))))
469
807
  ;; Pre-fill 8 sentinel bytes at end (writes overlapping a 64-bit slot).
470
808
  (i64.store (i32.add (local.get $buf) (local.get $len)) (i64.const -1))
471
- ;; SSO: byte-by-byte via __sso_char; STRING: bulk memcpy from string offset.
472
- (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}))
473
811
  (then
474
812
  (local.set $i (i32.const 0))
475
813
  (block $d (loop $l
@@ -489,7 +827,7 @@ export default (ctx) => {
489
827
 
490
828
  ctx.core.emit['JSON.stringify'] = (x) => {
491
829
  inc('__stringify')
492
- return typed(['call', '$__stringify', asF64(emit(x))], 'f64')
830
+ return typed(['call', '$__stringify', asI64(emit(x))], 'f64')
493
831
  }
494
832
 
495
833
  ctx.core.emit['JSON.parse'] = (x) => {
@@ -498,7 +836,16 @@ export default (ctx) => {
498
836
  try { return emitJsonConstValue(JSON.parse(src)) }
499
837
  catch { /* fall through to runtime parser for invalid JSON so runtime behavior stays unchanged */ }
500
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
+ }
501
848
  inc('__jp')
502
- return typed(['call', '$__jp', asF64(emit(x))], 'f64')
849
+ return typed(['call', '$__jp', asI64(emit(x))], 'f64')
503
850
  }
504
851
  }