jz 0.4.0 → 0.5.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,9 +8,9 @@
8
8
  * @module json
9
9
  */
10
10
 
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'
11
+ import { typed, asF64, asI64, temp, tempI32, nullExpr, undefExpr, allocPtr, slotAddr, mkPtrIR, extractF64Bits, appendStaticSlots, NULL_WAT, UNDEF_NAN, UNDEF_WAT } from '../src/ir.js'
12
+ import { emit, emitBoolStr } from '../src/emit.js'
13
+ import { T, valTypeOf, VAL } from '../src/analyze.js'
14
14
  import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
15
15
  import { strHashLiteral } from './collection.js'
16
16
 
@@ -21,6 +21,43 @@ function jsonConstString(ctx, expr) {
21
21
  return null
22
22
  }
23
23
 
24
+ // Sentinel distinct from any JS value `JSON.stringify` could hold (`undefined`
25
+ // is a legitimate literal — an array hole or a bare `undefined`).
26
+ const NOT_LIT = Symbol('not-literal')
27
+
28
+ // Evaluate a prepared AST node to its constant JS value, or NOT_LIT if any
29
+ // part is dynamic. Mirrors the literal grammar prepare produces: `[null, v]`
30
+ // for primitives, `['str', s]`, `['[' …]` arrays, `['{}' …]` objects.
31
+ function literalValue(node) {
32
+ if (node === undefined) return undefined // array hole
33
+ if (!Array.isArray(node)) return NOT_LIT
34
+ const op = node[0]
35
+ if (op == null) return node[1] // number | string | bool | null | undefined
36
+ if (op === 'str') return node[1]
37
+ if (op === 'u-') { const v = literalValue(node[1]); return v === NOT_LIT ? NOT_LIT : -v }
38
+ if (op === '[') {
39
+ const arr = []
40
+ for (let i = 1; i < node.length; i++) {
41
+ const v = literalValue(node[i])
42
+ if (v === NOT_LIT) return NOT_LIT
43
+ arr.push(v)
44
+ }
45
+ return arr
46
+ }
47
+ if (op === '{}') {
48
+ const obj = {}
49
+ for (let i = 1; i < node.length; i++) {
50
+ const e = node[i]
51
+ if (!Array.isArray(e) || e[0] !== ':' || (typeof e[1] !== 'string' && typeof e[1] !== 'number')) return NOT_LIT
52
+ const v = literalValue(e[2])
53
+ if (v === NOT_LIT) return NOT_LIT
54
+ obj[e[1]] = v
55
+ }
56
+ return obj
57
+ }
58
+ return NOT_LIT
59
+ }
60
+
24
61
  function jsonShapeString(ctx, expr) {
25
62
  if (typeof expr === 'string') return ctx.scope.shapeStrs?.get(expr) ?? null
26
63
  return null
@@ -42,15 +79,19 @@ function hashCapFor(n) {
42
79
 
43
80
  export default (ctx) => {
44
81
  Object.assign(ctx.core.stdlibDeps, {
45
- __stringify: ['__json_val', '__jput', '__jput_str', '__jput_num', '__mkstr'],
46
- __json_val: ['__ptr_type', '__len', '__ptr_offset', '__jput', '__jput_num', '__jput_str', '__json_hash', '__json_obj'],
47
- __json_hash: ['__ptr_offset', '__jput', '__jput_str', '__json_val'],
48
- __json_obj: ['__ptr_offset', '__ptr_aux', '__len', '__jput', '__jput_str', '__json_val'],
82
+ __stringify: ['__json_val', '__json_setgap', '__json_omit', '__jput', '__jput_str', '__jput_num', '__mkstr'],
83
+ __json_setgap: ['__alloc', '__ptr_type', '__str_byteLen', '__char_at'],
84
+ __json_omit: ['__ptr_type'],
85
+ __json_enter: ['__alloc'],
86
+ __jindent: ['__jput'],
87
+ __json_val: ['__ptr_type', '__len', '__ptr_offset', '__jput', '__jindent', '__jput_num', '__jput_str', '__json_enter', '__json_leave', '__json_hash', '__json_obj'],
88
+ __json_hash: ['__ptr_offset', '__jput', '__jindent', '__jput_str', '__json_omit', '__json_enter', '__json_leave', '__json_val'],
89
+ __json_obj: ['__ptr_offset', '__ptr_aux', '__len', '__jput', '__jindent', '__jput_str', '__json_omit', '__json_enter', '__json_leave', '__json_val'],
49
90
  __jput_num: ['__ftoa'],
50
91
  __jput_str: ['__char_at', '__str_byteLen'],
51
92
  __jp: ['__jp_val', '__jp_str', '__jp_num', '__jp_arr', '__jp_obj', '__sso_char', '__ptr_aux', '__ptr_type', '__ptr_offset', '__str_byteLen'],
52
93
  __jp_val: ['__jp_str', '__jp_num', '__jp_arr', '__jp_obj'],
53
- __jp_str: ['__sso_char', '__char_at', '__str_byteLen', '__hex4', '__utf8_enc'],
94
+ __jp_str: ['__sso_char', '__char_at', '__str_byteLen', '__hex4', '__ishex', '__utf8_enc'],
54
95
  __hex4: ['__hex1'],
55
96
  __jp_num: ['__pow10'],
56
97
  __jp_arr: ['__jp_val'],
@@ -87,15 +128,15 @@ export default (ctx) => {
87
128
  const keys = Object.keys(v)
88
129
  // Empty object: minimal OBJECT with no slots.
89
130
  if (keys.length === 0) {
90
- return mkPtrIR(PTR.OBJECT, 0, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', 1]])
131
+ return mkPtrIR(PTR.OBJECT, 0, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(0)]])
91
132
  }
92
133
  const schemaId = ctx.schema.register(keys)
93
134
  const t = tempI32('jobj')
94
135
  const body = [
95
- ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', keys.length]]],
136
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(keys.length)]]],
96
137
  ]
97
138
  for (let i = 0; i < keys.length; i++) {
98
- body.push(['f64.store', slotAddr(t, i), asF64(emitJsonConstValue(v[keys[i]]))])
139
+ body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], i, asF64(emitJsonConstValue(v[keys[i]]))))
99
140
  }
100
141
  body.push(mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${t}`]))
101
142
  return typed(['block', ['result', 'f64'], ...body], 'f64')
@@ -113,6 +154,19 @@ export default (ctx) => {
113
154
  ctx.scope.globals.set('__jpos', '(global $__jpos (mut i32) (i32.const 0))')
114
155
  ctx.scope.globals.set('__jcap', '(global $__jcap (mut i32) (i32.const 0))')
115
156
  ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
157
+ // Pretty-print state for the `space` argument. $__jgap points at the gap
158
+ // string bytes ($__jgaplen of them); $__jdepth is the live nesting depth.
159
+ // $__jgaplen == 0 ⇒ compact mode — every indent emission below is gated on
160
+ // it, so the no-space path stays byte-identical to the unindented output.
161
+ ctx.scope.globals.set('__jgap', '(global $__jgap (mut i32) (i32.const 0))')
162
+ ctx.scope.globals.set('__jgaplen', '(global $__jgaplen (mut i32) (i32.const 0))')
163
+ ctx.scope.globals.set('__jdepth', '(global $__jdepth (mut i32) (i32.const 0))')
164
+ // Cycle-detection stack: the i64 values of the containers currently open on
165
+ // the recursion path. JSON.stringify of a structure that points back at an
166
+ // ancestor must throw a TypeError. $__jstack is a lazily-allocated buffer of
167
+ // up to 256 entries; $__jsp is the live depth.
168
+ ctx.scope.globals.set('__jstack', '(global $__jstack (mut i32) (i32.const 0))')
169
+ ctx.scope.globals.set('__jsp', '(global $__jsp (mut i32) (i32.const 0))')
116
170
 
117
171
  // __jput(byte: i32) — append one byte to output buffer
118
172
  ctx.core.stdlib['__jput'] = `(func $__jput (param $b i32)
@@ -126,23 +180,88 @@ export default (ctx) => {
126
180
  (i32.store8 (i32.add (global.get $__jbuf) (global.get $__jpos)) (local.get $b))
127
181
  (global.set $__jpos (i32.add (global.get $__jpos) (i32.const 1))))`
128
182
 
129
- // __jput_str(ptr: i64) append string chars (without quotes) to buffer
183
+ // __jindentemit a newline followed by $__jdepth copies of the gap string.
184
+ // No-op in compact mode ($__jgaplen == 0), so callers can invoke it
185
+ // unconditionally and the unindented output stays exactly as before.
186
+ ctx.core.stdlib['__jindent'] = `(func $__jindent
187
+ (local $i i32) (local $j i32)
188
+ (if (i32.eqz (global.get $__jgaplen)) (then (return)))
189
+ (call $__jput (i32.const 10))
190
+ (block $d (loop $l
191
+ (br_if $d (i32.ge_s (local.get $i) (global.get $__jdepth)))
192
+ (local.set $j (i32.const 0))
193
+ (block $d2 (loop $l2
194
+ (br_if $d2 (i32.ge_s (local.get $j) (global.get $__jgaplen)))
195
+ (call $__jput (i32.load8_u (i32.add (global.get $__jgap) (local.get $j))))
196
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
197
+ (br $l2)))
198
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
199
+ (br $l))))`
200
+
201
+ // __json_setgap(space: i64) — derive the indentation gap from the third
202
+ // JSON.stringify argument. Number → min(10, ToInteger) spaces; String →
203
+ // first min(10) code units; anything else → compact. Resets $__jdepth.
204
+ ctx.core.stdlib['__json_setgap'] = `(func $__json_setgap (param $sp i64)
205
+ (local $f f64) (local $n i32) (local $i i32) (local $g i32)
206
+ (global.set $__jdepth (i32.const 0))
207
+ (global.set $__jgaplen (i32.const 0))
208
+ (local.set $f (f64.reinterpret_i64 (local.get $sp)))
209
+ (if (f64.eq (local.get $f) (local.get $f))
210
+ (then
211
+ (local.set $n (i32.trunc_sat_f64_s (local.get $f)))
212
+ (if (i32.gt_s (local.get $n) (i32.const 10)) (then (local.set $n (i32.const 10))))
213
+ (if (i32.lt_s (local.get $n) (i32.const 1)) (then (return)))
214
+ (local.set $g (call $__alloc (local.get $n)))
215
+ (block $d (loop $l
216
+ (br_if $d (i32.ge_s (local.get $i) (local.get $n)))
217
+ (i32.store8 (i32.add (local.get $g) (local.get $i)) (i32.const 32))
218
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
219
+ (br $l)))
220
+ (global.set $__jgap (local.get $g))
221
+ (global.set $__jgaplen (local.get $n))
222
+ (return)))
223
+ (if (i32.eq (call $__ptr_type (local.get $sp)) (i32.const ${PTR.STRING}))
224
+ (then
225
+ (local.set $n (call $__str_byteLen (local.get $sp)))
226
+ (if (i32.gt_s (local.get $n) (i32.const 10)) (then (local.set $n (i32.const 10))))
227
+ (if (i32.eqz (local.get $n)) (then (return)))
228
+ (local.set $g (call $__alloc (local.get $n)))
229
+ (block $d (loop $l
230
+ (br_if $d (i32.ge_s (local.get $i) (local.get $n)))
231
+ (i32.store8 (i32.add (local.get $g) (local.get $i))
232
+ (call $__char_at (local.get $sp) (local.get $i)))
233
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
234
+ (br $l)))
235
+ (global.set $__jgap (local.get $g))
236
+ (global.set $__jgaplen (local.get $n)))))`
237
+
238
+ // __jput_str(ptr: i64) — append string chars (without quotes) to buffer.
239
+ // Per QuoteJSONString: every code unit U+0000..U+001F must be escaped — the
240
+ // five with short forms (\b \t \n \f \r) plus \uXXXX for the rest.
130
241
  ctx.core.stdlib['__jput_str'] = `(func $__jput_str (param $ptr i64)
131
- (local $len i32) (local $i i32) (local $ch i32)
242
+ (local $len i32) (local $i i32) (local $ch i32) (local $n i32)
132
243
  (local.set $len (call $__str_byteLen (local.get $ptr)))
133
244
  (local.set $i (i32.const 0))
134
245
  (block $d (loop $l
135
246
  (br_if $d (i32.ge_s (local.get $i) (local.get $len)))
136
247
  (local.set $ch (call $__char_at (local.get $ptr) (local.get $i)))
137
248
  ;; Escape special JSON chars
138
- (if (i32.le_u (local.get $ch) (i32.const 13))
249
+ (if (i32.lt_u (local.get $ch) (i32.const 32))
139
250
  (then
140
251
  (if (i32.eq (local.get $ch) (i32.const 10)) (then (call $__jput (i32.const 92)) (call $__jput (i32.const 110)))
141
252
  (else (if (i32.eq (local.get $ch) (i32.const 13)) (then (call $__jput (i32.const 92)) (call $__jput (i32.const 114)))
142
253
  (else (if (i32.eq (local.get $ch) (i32.const 9)) (then (call $__jput (i32.const 92)) (call $__jput (i32.const 116)))
143
254
  (else (if (i32.eq (local.get $ch) (i32.const 8)) (then (call $__jput (i32.const 92)) (call $__jput (i32.const 98)))
144
255
  (else (if (i32.eq (local.get $ch) (i32.const 12)) (then (call $__jput (i32.const 92)) (call $__jput (i32.const 102)))
145
- (else (call $__jput (local.get $ch)))))))))))))
256
+ (else
257
+ ;; \\u00XX — control char with no short escape
258
+ (call $__jput (i32.const 92)) (call $__jput (i32.const 117))
259
+ (call $__jput (i32.const 48)) (call $__jput (i32.const 48))
260
+ (call $__jput (i32.add (i32.const 48) (i32.shr_u (local.get $ch) (i32.const 4))))
261
+ (local.set $n (i32.and (local.get $ch) (i32.const 15)))
262
+ (if (i32.ge_u (local.get $n) (i32.const 10))
263
+ (then (call $__jput (i32.add (local.get $n) (i32.const 87))))
264
+ (else (call $__jput (i32.add (local.get $n) (i32.const 48))))))))))))))))
146
265
  (else
147
266
  (if (i32.eq (local.get $ch) (i32.const 34)) (then (call $__jput (i32.const 92)) (call $__jput (i32.const 34)))
148
267
  (else (if (i32.eq (local.get $ch) (i32.const 92)) (then (call $__jput (i32.const 92)) (call $__jput (i32.const 92)))
@@ -154,6 +273,41 @@ export default (ctx) => {
154
273
  ctx.core.stdlib['__jput_num'] = `(func $__jput_num (param $val f64)
155
274
  (call $__jput_str (i64.reinterpret_f64 (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0)))))`
156
275
 
276
+ // __json_omit(val: i64) → i32 — 1 if the value serializes to nothing
277
+ // (undefined or a function/CLOSURE). Per the JSON spec such values are
278
+ // dropped from objects, rendered as null in arrays, and make a top-level
279
+ // JSON.stringify return undefined.
280
+ ctx.core.stdlib['__json_omit'] = `(func $__json_omit (param $val i64) (result i32)
281
+ (local $f f64)
282
+ (local.set $f (f64.reinterpret_i64 (local.get $val)))
283
+ (if (f64.eq (local.get $f) (local.get $f)) (then (return (i32.const 0))))
284
+ (if (i64.eq (local.get $val) (i64.const ${UNDEF_NAN})) (then (return (i32.const 1))))
285
+ (i32.eq (call $__ptr_type (local.get $val)) (i32.const ${PTR.CLOSURE})))`
286
+
287
+ // __json_enter(val: i64) — push a container onto the cycle stack, throwing a
288
+ // TypeError ($__jz_err) if it is already an open ancestor (a circular ref).
289
+ ctx.core.stdlib['__json_enter'] = `(func $__json_enter (param $val i64)
290
+ (local $i i32) (local $st i32)
291
+ (local.set $st (global.get $__jstack))
292
+ (if (i32.eqz (local.get $st))
293
+ (then
294
+ (local.set $st (call $__alloc (i32.const 2048)))
295
+ (global.set $__jstack (local.get $st))))
296
+ (block $d (loop $l
297
+ (br_if $d (i32.ge_s (local.get $i) (global.get $__jsp)))
298
+ (if (i64.eq (i64.load (i32.add (local.get $st) (i32.shl (local.get $i) (i32.const 3)))) (local.get $val))
299
+ (then (throw $__jz_err (f64.const 0))))
300
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
301
+ (br $l)))
302
+ ;; A structure deeper than the buffer is treated as non-serializable.
303
+ (if (i32.ge_s (global.get $__jsp) (i32.const 256)) (then (throw $__jz_err (f64.const 0))))
304
+ (i64.store (i32.add (local.get $st) (i32.shl (global.get $__jsp) (i32.const 3))) (local.get $val))
305
+ (global.set $__jsp (i32.add (global.get $__jsp) (i32.const 1))))`
306
+
307
+ // __json_leave — pop the most recent container off the cycle stack.
308
+ ctx.core.stdlib['__json_leave'] = `(func $__json_leave
309
+ (global.set $__jsp (i32.sub (global.get $__jsp) (i32.const 1))))`
310
+
157
311
  // __json_val(val: i64) — stringify any value, append to buffer
158
312
  ctx.core.stdlib['__json_val'] = `(func $__json_val (param $val i64)
159
313
  (local $type i32) (local $len i32) (local $i i32) (local $off i32) (local $f f64)
@@ -182,17 +336,27 @@ export default (ctx) => {
182
336
  ;; Array
183
337
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
184
338
  (then
339
+ (call $__json_enter (local.get $val))
185
340
  (call $__jput (i32.const 91)) ;; [
186
341
  (local.set $len (call $__len (local.get $val)))
187
342
  (local.set $off (call $__ptr_offset (local.get $val)))
188
343
  (local.set $i (i32.const 0))
344
+ ;; A non-empty array opens one indent level; an empty array stays compact.
345
+ (if (i32.gt_s (local.get $len) (i32.const 0))
346
+ (then (global.set $__jdepth (i32.add (global.get $__jdepth) (i32.const 1)))))
189
347
  (block $d (loop $l
190
348
  (br_if $d (i32.ge_s (local.get $i) (local.get $len)))
191
349
  (if (local.get $i) (then (call $__jput (i32.const 44)))) ;; ,
350
+ (call $__jindent)
192
351
  (call $__json_val (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
193
352
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
194
353
  (br $l)))
354
+ (if (i32.gt_s (local.get $len) (i32.const 0))
355
+ (then
356
+ (global.set $__jdepth (i32.sub (global.get $__jdepth) (i32.const 1)))
357
+ (call $__jindent)))
195
358
  (call $__jput (i32.const 93)) ;; ]
359
+ (call $__json_leave)
196
360
  (return)))
197
361
  ;; HASH/MAP — iterate entries: {"key":val,...}
198
362
  (if (i32.or (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
@@ -208,27 +372,37 @@ export default (ctx) => {
208
372
  // __json_hash(val: i64) — stringify HASH/MAP: iterate slots, emit {"key":val,...}
209
373
  // Slot layout: 24 bytes each — [hash:f64][key:f64][val:f64]. Empty slots have hash==0.
210
374
  ctx.core.stdlib['__json_hash'] = `(func $__json_hash (param $val i64)
211
- (local $off i32) (local $cap i32) (local $i i32) (local $slot i32) (local $first i32)
375
+ (local $off i32) (local $cap i32) (local $i i32) (local $slot i32) (local $first i32) (local $pv i64)
212
376
  (local.set $off (call $__ptr_offset (local.get $val)))
213
377
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
214
378
  (local.set $first (i32.const 1))
379
+ (call $__json_enter (local.get $val))
215
380
  (call $__jput (i32.const 123))
381
+ (global.set $__jdepth (i32.add (global.get $__jdepth) (i32.const 1)))
216
382
  (block $d (loop $l
217
383
  (br_if $d (i32.ge_s (local.get $i) (local.get $cap)))
218
384
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const 24))))
219
385
  (if (f64.ne (f64.load (local.get $slot)) (f64.const 0))
220
386
  (then
221
- (if (i32.eqz (local.get $first))
222
- (then (call $__jput (i32.const 44))))
223
- (local.set $first (i32.const 0))
224
- (call $__jput (i32.const 34))
225
- (call $__jput_str (i64.load (i32.add (local.get $slot) (i32.const 8))))
226
- (call $__jput (i32.const 34))
227
- (call $__jput (i32.const 58))
228
- (call $__json_val (i64.load (i32.add (local.get $slot) (i32.const 16))))))
387
+ (local.set $pv (i64.load (i32.add (local.get $slot) (i32.const 16))))
388
+ (if (i32.eqz (call $__json_omit (local.get $pv)))
389
+ (then
390
+ (if (i32.eqz (local.get $first))
391
+ (then (call $__jput (i32.const 44))))
392
+ (local.set $first (i32.const 0))
393
+ (call $__jindent)
394
+ (call $__jput (i32.const 34))
395
+ (call $__jput_str (i64.load (i32.add (local.get $slot) (i32.const 8))))
396
+ (call $__jput (i32.const 34))
397
+ (call $__jput (i32.const 58))
398
+ (if (global.get $__jgaplen) (then (call $__jput (i32.const 32))))
399
+ (call $__json_val (local.get $pv))))))
229
400
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
230
401
  (br $l)))
231
- (call $__jput (i32.const 125)))`
402
+ (global.set $__jdepth (i32.sub (global.get $__jdepth) (i32.const 1)))
403
+ (if (i32.eqz (local.get $first)) (then (call $__jindent)))
404
+ (call $__jput (i32.const 125))
405
+ (call $__json_leave))`
232
406
 
233
407
  // __json_obj(val: f64) — stringify OBJECT using runtime schema name table.
234
408
  // Schema name table: global $__schema_tbl → array of f64 pointers.
@@ -236,7 +410,7 @@ export default (ctx) => {
236
410
  // Object props are sequential f64 at ptr_offset, indexed same as schema.
237
411
  ctx.core.stdlib['__json_obj'] = `(func $__json_obj (param $val i64)
238
412
  (local $off i32) (local $sid i32) (local $keys i32) (local $nkeys i32)
239
- (local $i i32) (local $koff i32)
413
+ (local $i i32) (local $koff i32) (local $first i32) (local $pv i64)
240
414
  (local.set $off (call $__ptr_offset (local.get $val)))
241
415
  (local.set $sid (call $__ptr_aux (local.get $val)))
242
416
  ;; Load keys array from schema table: schema_tbl + sid * 8
@@ -245,25 +419,41 @@ export default (ctx) => {
245
419
  (local.set $nkeys (call $__len
246
420
  (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
247
421
  (local.set $koff (local.get $keys))
422
+ (local.set $first (i32.const 1))
423
+ (call $__json_enter (local.get $val))
248
424
  (call $__jput (i32.const 123))
425
+ (global.set $__jdepth (i32.add (global.get $__jdepth) (i32.const 1)))
249
426
  (block $d (loop $l
250
427
  (br_if $d (i32.ge_s (local.get $i) (local.get $nkeys)))
251
- (if (local.get $i) (then (call $__jput (i32.const 44))))
252
- (call $__jput (i32.const 34))
253
- (call $__jput_str (i64.load (i32.add (local.get $koff) (i32.shl (local.get $i) (i32.const 3)))))
254
- (call $__jput (i32.const 34))
255
- (call $__jput (i32.const 58))
256
- (call $__json_val (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
428
+ (local.set $pv (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
429
+ (if (i32.eqz (call $__json_omit (local.get $pv)))
430
+ (then
431
+ (if (i32.eqz (local.get $first)) (then (call $__jput (i32.const 44))))
432
+ (local.set $first (i32.const 0))
433
+ (call $__jindent)
434
+ (call $__jput (i32.const 34))
435
+ (call $__jput_str (i64.load (i32.add (local.get $koff) (i32.shl (local.get $i) (i32.const 3)))))
436
+ (call $__jput (i32.const 34))
437
+ (call $__jput (i32.const 58))
438
+ (if (global.get $__jgaplen) (then (call $__jput (i32.const 32))))
439
+ (call $__json_val (local.get $pv))))
257
440
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
258
441
  (br $l)))
259
- (call $__jput (i32.const 125)))`
442
+ (global.set $__jdepth (i32.sub (global.get $__jdepth) (i32.const 1)))
443
+ (if (i32.eqz (local.get $first)) (then (call $__jindent)))
444
+ (call $__jput (i32.const 125))
445
+ (call $__json_leave))`
260
446
 
261
- // __stringify(val: i64) → f64 (NaN-boxed string)
262
- ctx.core.stdlib['__stringify'] = `(func $__stringify (param $val i64) (result f64)
263
- ;; Reset output buffer
447
+ // __stringify(val: i64, space: i64) → f64 (NaN-boxed string)
448
+ ctx.core.stdlib['__stringify'] = `(func $__stringify (param $val i64) (param $space i64) (result f64)
449
+ ;; Top-level undefined / function serializes to nothing → return undefined.
450
+ (if (call $__json_omit (local.get $val)) (then (return ${UNDEF_WAT})))
451
+ ;; Reset output buffer + cycle stack
452
+ (global.set $__jsp (i32.const 0))
264
453
  (global.set $__jbuf (call $__alloc (i32.const 256)))
265
454
  (global.set $__jpos (i32.const 0))
266
455
  (global.set $__jcap (i32.const 256))
456
+ (call $__json_setgap (local.get $space))
267
457
  (call $__json_val (local.get $val))
268
458
  ;; Create string from buffer
269
459
  (call $__mkstr (global.get $__jbuf) (global.get $__jpos)))`
@@ -278,6 +468,11 @@ export default (ctx) => {
278
468
  // and skips the redundant __str_hash call inside the generic insert. 0 is a
279
469
  // sentinel meaning "string had escapes — recompute via __str_hash".
280
470
  ctx.scope.globals.set('__jp_keyh', '(global $__jp_keyh (mut i32) (i32.const 0))')
471
+ // Sticky syntax-error flag. Set by any parser sub-routine on malformed input;
472
+ // checked once by __jp after the top-level value, which throws if it is set.
473
+ // Sticky (never cleared mid-parse) so recursive descent need not thread an
474
+ // error result through every call — __jp resets it per invocation.
475
+ ctx.scope.globals.set('__jp_err', '(global $__jp_err (mut i32) (i32.const 0))')
281
476
  // Runtime schema infrastructure. __schema_next points at the first free slot
282
477
  // in $__schema_tbl reserved for runtime registration; compile.js initializes
283
478
  // it to ctx.schema.list.length when __jp_obj is included. The schema cache
@@ -300,17 +495,19 @@ export default (ctx) => {
300
495
 
301
496
  // Whitespace skip — inlined at every call site as a tight loop. Compact
302
497
  // JSON often has zero whitespace between tokens, so the dominant case is
303
- // a single peek + break. WS chars (9/10/13/32) all fit in [0..32]; we
304
- // exit on anything > 32 unsigned. The sentinel byte (PEEK returns -1
305
- // sign-extended) is 0xFFFFFFFF unsigned > 32so the same check
306
- // handles EOF without a separate guard. Other control chars in [0..8],
307
- // [11..12], [14..31] would be falsely consumed as WS, but those aren't
308
- // valid in well-formed JSON anyway.
498
+ // a single peek + break. Per the JSON grammar only tab (9), LF (10), CR
499
+ // (13) and space (32) are JSONWhitespace; every other byte including
500
+ // other control chars and non-ASCII Unicode spacesends the run, so a
501
+ // stray VT/FF or U+2028 surfaces to the value dispatcher as a syntax
502
+ // error. The sentinel byte (PEEK returns -1) matches none of the four and
503
+ // ends the run too, so EOF needs no separate guard.
309
504
  let WS_ID = 0
310
505
  const WS = () => {
311
506
  const id = WS_ID++
312
507
  return `(block $jpws_d${id} (loop $jpws_l${id}
313
- (br_if $jpws_d${id} (i32.gt_u ${PEEK} (i32.const 32)))
508
+ (br_if $jpws_d${id} (i32.eqz (i32.or
509
+ (i32.or (i32.eq ${PEEK} (i32.const 32)) (i32.eq ${PEEK} (i32.const 9)))
510
+ (i32.or (i32.eq ${PEEK} (i32.const 10)) (i32.eq ${PEEK} (i32.const 13))))))
314
511
  ${ADV(1)}
315
512
  (br $jpws_l${id})))`
316
513
  }
@@ -328,6 +525,21 @@ export default (ctx) => {
328
525
  (then (return (i32.sub (i32.or (local.get $c) (i32.const 0x20)) (i32.const 87)))))
329
526
  (i32.const 0))`
330
527
 
528
+ // Strict hex-digit predicate — 1 iff $c is [0-9A-Fa-f], else 0. Used to
529
+ // validate \uXXXX escapes (__hex1 itself is lenient and returns 0 for
530
+ // non-hex, which would silently accept `\u0X50`).
531
+ ctx.core.stdlib['__ishex'] = `(func $__ishex (param $c i32) (result i32)
532
+ (i32.or
533
+ (i32.le_u (i32.sub (local.get $c) (i32.const 48)) (i32.const 9))
534
+ (i32.le_u (i32.sub (i32.or (local.get $c) (i32.const 0x20)) (i32.const 97)) (i32.const 5))))`
535
+
536
+ // All four bytes at the current parse position are hex digits.
537
+ const HEX4_VALID = `(i32.and
538
+ (i32.and (call $__ishex (i32.load8_u (i32.add (global.get $__jpstr) (global.get $__jppos))))
539
+ (call $__ishex (i32.load8_u (i32.add (global.get $__jpstr) (i32.add (global.get $__jppos) (i32.const 1))))))
540
+ (i32.and (call $__ishex (i32.load8_u (i32.add (global.get $__jpstr) (i32.add (global.get $__jppos) (i32.const 2)))))
541
+ (call $__ishex (i32.load8_u (i32.add (global.get $__jpstr) (i32.add (global.get $__jppos) (i32.const 3)))))))`
542
+
331
543
  // Read 4 hex bytes at absolute address $p → 16-bit value.
332
544
  ctx.core.stdlib['__hex4'] = `(func $__hex4 (param $p i32) (result i32)
333
545
  (i32.or (i32.or (i32.or
@@ -366,6 +578,9 @@ export default (ctx) => {
366
578
  (local.set $ch ${PEEK})
367
579
  (br_if $d (i32.eq (local.get $ch) (i32.const 34)))
368
580
  (br_if $d (i32.eq (local.get $ch) (i32.const -1)))
581
+ ;; Unescaped control char (U+0000..U+001F) is not a valid JSONStringCharacter.
582
+ (if (i32.lt_u (local.get $ch) (i32.const 32))
583
+ (then (global.set $__jp_err (i32.const 1)) (br $d)))
369
584
  ;; Mark non-simple: escape (\\=92) or non-ASCII (load8_s gives <0 for byte≥128).
370
585
  (if (i32.or (i32.eq (local.get $ch) (i32.const 92)) (i32.lt_s (local.get $ch) (i32.const 0)))
371
586
  (then (local.set $simple (i32.const 0))))
@@ -384,6 +599,9 @@ export default (ctx) => {
384
599
  (local.set $len (i32.add (local.get $len) (i32.const 1)))
385
600
  ${ADV(1)}))
386
601
  (br $l)))
602
+ ;; Loop exited on the closing quote (34) or EOF (-1); the latter means an
603
+ ;; unterminated string literal.
604
+ (if (i32.eq (local.get $ch) (i32.const -1)) (then (global.set $__jp_err (i32.const 1))))
387
605
  ;; Stash hash. 0/1 bumped to 2 to match __str_hash convention; escape strings
388
606
  ;; (simple==0) get sentinel 0 so __jp_obj falls back to non-prehashed insert.
389
607
  (global.set $__jp_keyh
@@ -422,6 +640,7 @@ export default (ctx) => {
422
640
  ${ADV(1)}
423
641
  (if (i32.eq (local.get $ch) (i32.const 117)) ;; \\uXXXX
424
642
  (then
643
+ (if (i32.eqz ${HEX4_VALID}) (then (global.set $__jp_err (i32.const 1))))
425
644
  (local.set $cp (call $__hex4 (i32.add (global.get $__jpstr) (global.get $__jppos))))
426
645
  ${ADV(4)}
427
646
  ;; High surrogate immediately followed by \\uXXXX low surrogate → combine.
@@ -431,6 +650,7 @@ export default (ctx) => {
431
650
  (i32.eq (i32.load8_u (i32.add (global.get $__jpstr) (i32.add (global.get $__jppos) (i32.const 1)))) (i32.const 117))))
432
651
  (then
433
652
  ${ADV(2)}
653
+ (if (i32.eqz ${HEX4_VALID}) (then (global.set $__jp_err (i32.const 1))))
434
654
  (local.set $i (call $__hex4 (i32.add (global.get $__jpstr) (global.get $__jppos))))
435
655
  ${ADV(4)}
436
656
  (local.set $cp (i32.add (i32.const 0x10000)
@@ -525,7 +745,11 @@ export default (ctx) => {
525
745
  ${WS()}
526
746
  (local.set $ch ${PEEK})
527
747
  (br_if $d (i32.eq (local.get $ch) (i32.const 93)))
528
- (if (i32.eq (local.get $ch) (i32.const 44)) (then ${ADV(1)}))
748
+ ;; After an element only a comma (more) or close-bracket (done) is
749
+ ;; valid; anything else (incl. EOF) is a syntax error. Break to terminate.
750
+ (if (i32.eq (local.get $ch) (i32.const 44))
751
+ (then ${ADV(1)})
752
+ (else (global.set $__jp_err (i32.const 1)) (br $d)))
529
753
  (br $l)))
530
754
  ${ADV(1)}
531
755
  (i32.store (i32.sub (local.get $ptr) (i32.const 8)) (local.get $len))
@@ -654,7 +878,11 @@ export default (ctx) => {
654
878
  ${WS()}
655
879
  (local.set $ch ${PEEK})
656
880
  (br_if $d (i32.eq (local.get $ch) (i32.const 125)))
657
- (if (i32.eq (local.get $ch) (i32.const 44)) (then ${ADV(1)}))
881
+ ;; After a member only a comma (more) or close-brace (done) is
882
+ ;; valid; anything else (incl. EOF) is a syntax error. Break to terminate.
883
+ (if (i32.eq (local.get $ch) (i32.const 44))
884
+ (then ${ADV(1)})
885
+ (else (global.set $__jp_err (i32.const 1)) (br $d)))
658
886
  (br $l)))
659
887
  ${ADV(1)}
660
888
  ;; Resolve schema sid (cached or freshly registered).
@@ -673,6 +901,19 @@ export default (ctx) => {
673
901
  (br $vl)))
674
902
  (call $__mkptr (i32.const ${PTR.OBJECT}) (local.get $sid) (local.get $obj)))`
675
903
 
904
+ // Verify a bare keyword literal: the dispatch char is already known to be
905
+ // word[0]; AND-check word[1..] against the bytes after the parse position.
906
+ // Past-end reads land in the 0xFF sentinel pad, which matches nothing.
907
+ const litMatch = (word) => {
908
+ const tests = [...word].slice(1).map((c, i) =>
909
+ `(i32.eq (i32.load8_u (i32.add (global.get $__jpstr) (i32.add (global.get $__jppos) (i32.const ${i + 1})))) (i32.const ${c.charCodeAt(0)}))`)
910
+ return tests.reduce((a, b) => `(i32.and ${a} ${b})`)
911
+ }
912
+ const litCase = (ch, word, valIR, adv) => `(if (i32.eq (local.get $ch) (i32.const ${ch}))
913
+ (then (if ${litMatch(word)}
914
+ (then ${ADV(adv)} (return ${valIR}))
915
+ (else (global.set $__jp_err (i32.const 1)) (return ${NULL_WAT})))))`
916
+
676
917
  // Main value dispatcher
677
918
  ctx.core.stdlib['__jp_val'] = `(func $__jp_val (result f64)
678
919
  (local $ch i32)
@@ -687,12 +928,11 @@ export default (ctx) => {
687
928
  (if (i32.or (i32.and (i32.ge_s (local.get $ch) (i32.const 48)) (i32.le_s (local.get $ch) (i32.const 57)))
688
929
  (i32.eq (local.get $ch) (i32.const 45)))
689
930
  (then (return (call $__jp_num))))
690
- (if (i32.eq (local.get $ch) (i32.const 116))
691
- (then ${ADV(4)} (return (f64.const 1))))
692
- (if (i32.eq (local.get $ch) (i32.const 102))
693
- (then ${ADV(5)} (return (f64.const 0))))
694
- (if (i32.eq (local.get $ch) (i32.const 110))
695
- (then ${ADV(4)} (return ${NULL_WAT})))
931
+ ${litCase(116, 'true', '(f64.const 1)', 4)}
932
+ ${litCase(102, 'false', '(f64.const 0)', 5)}
933
+ ${litCase(110, 'null', NULL_WAT, 4)}
934
+ ;; No production matched — malformed JSON value.
935
+ (global.set $__jp_err (i32.const 1))
696
936
  ${NULL_WAT})`
697
937
 
698
938
  function canSpecializeJsonShape(v) {
@@ -859,7 +1099,7 @@ ${localDecls}
859
1099
  // overshoot from speculative peek/adv on malformed input still hits sentinel,
860
1100
  // not unallocated memory.
861
1101
  ctx.core.stdlib['__jp'] = `(func $__jp (param $str i64) (result f64)
862
- (local $len i32) (local $buf i32) (local $i i32)
1102
+ (local $len i32) (local $buf i32) (local $i i32) (local $r f64)
863
1103
  (local.set $len (call $__str_byteLen (local.get $str)))
864
1104
  (local.set $buf (call $__alloc (i32.add (local.get $len) (i32.const 8))))
865
1105
  ;; Pre-fill 8 sentinel bytes at end (writes overlapping a 64-bit slot).
@@ -879,21 +1119,87 @@ ${localDecls}
879
1119
  (global.set $__jpstr (local.get $buf))
880
1120
  (global.set $__jplen (local.get $len))
881
1121
  (global.set $__jppos (i32.const 0))
882
- (call $__jp_val))`
1122
+ (global.set $__jp_err (i32.const 0))
1123
+ (local.set $r (call $__jp_val))
1124
+ ;; Any non-whitespace byte after the top-level value is a syntax error.
1125
+ ${WS()}
1126
+ (if (i32.ne ${PEEK} (i32.const -1)) (then (global.set $__jp_err (i32.const 1))))
1127
+ (if (global.get $__jp_err) (then (throw $__jz_err (f64.const 0))))
1128
+ (local.get $r))`
883
1129
 
884
1130
  // === Emitters ===
885
1131
 
886
- ctx.core.emit['JSON.stringify'] = (x) => {
1132
+ // JSON.stringify(value [, replacer [, space ]]).
1133
+ //
1134
+ // Compile-time fold: when `value` is a fully-literal tree — and `replacer` /
1135
+ // `space` are likewise constant — the result is computed with the host
1136
+ // `JSON.stringify` and emitted as a single string constant. This is the
1137
+ // mirror of the `JSON.parse` const-fold: spec-exact (replacer-array filtering,
1138
+ // dedup, key order, indentation all come free from the host), and it removes
1139
+ // the runtime call entirely. The runtime `__stringify` path (which ignores a
1140
+ // replacer) handles every non-constant case unchanged.
1141
+ ctx.core.emit['JSON.stringify'] = (x, replacer, space) => {
1142
+ const folded = foldStringify(x, replacer, space)
1143
+ if (folded !== undefined) return folded
1144
+ // Scalar boolean: the working-rep is the 0/1 number carrier, so the runtime
1145
+ // tag-walker would emit "0"/"1". A boolean's JSON is the bare word
1146
+ // true/false — exactly emitBoolStr. Guard on `replacer == null`: a replacer
1147
+ // function may rewrite the top-level value, in which case fall to runtime.
1148
+ if (replacer == null && valTypeOf(x) === VAL.BOOL) return emitBoolStr(x)
887
1149
  inc('__stringify')
888
- return typed(['call', '$__stringify', asI64(emit(x))], 'f64')
1150
+ const spaceIR = asI64(space == null ? undefExpr() : emit(space))
1151
+ return typed(['call', '$__stringify', asI64(emit(x)), spaceIR], 'f64')
1152
+ }
1153
+
1154
+ // Returns folded IR, or `undefined` when any argument is non-constant.
1155
+ function foldStringify(x, replacer, space) {
1156
+ const val = literalValue(x)
1157
+ if (val === NOT_LIT) return undefined
1158
+
1159
+ let rep
1160
+ if (replacer != null) {
1161
+ const rv = literalValue(replacer)
1162
+ if (rv === NOT_LIT) {
1163
+ const arr = jsonShapeStrings(ctx, replacer) // const-bound string array
1164
+ if (arr == null) return undefined
1165
+ rep = arr
1166
+ } else if (rv == null) {
1167
+ rep = undefined // null / undefined replacer
1168
+ } else if (Array.isArray(rv)) {
1169
+ rep = rv
1170
+ } else {
1171
+ return undefined // function replacer — can't fold
1172
+ }
1173
+ }
1174
+
1175
+ let sp
1176
+ if (space != null) {
1177
+ sp = literalValue(space)
1178
+ if (sp === NOT_LIT) return undefined
1179
+ }
1180
+
1181
+ let result
1182
+ try { result = JSON.stringify(val, rep, sp) }
1183
+ catch { return undefined }
1184
+ return result === undefined ? undefExpr() : asF64(emit(['str', result]))
889
1185
  }
890
1186
 
891
1187
  ctx.core.emit['JSON.parse'] = (x) => {
1188
+ // A non-string primitive literal argument is coerced via ToString, then
1189
+ // parsed: JSON.parse(0) → "0", JSON.parse(null) → "null", JSON.parse() →
1190
+ // "undefined" (which parses to a SyntaxError, the spec-correct outcome).
1191
+ // Literals reach the emitter as `[null, value]` nodes.
1192
+ if (x === undefined) x = ['str', 'undefined']
1193
+ else if (Array.isArray(x) && x[0] == null && typeof x[1] !== 'string')
1194
+ x = ['str', x[1] == null ? 'null' : String(x[1])]
892
1195
  const src = jsonConstString(ctx, x)
893
1196
  if (src != null) {
894
1197
  try { return emitJsonConstValue(JSON.parse(src)) }
895
1198
  catch { /* fall through to runtime parser for invalid JSON so runtime behavior stays unchanged */ }
896
1199
  }
1200
+ // The runtime parser (and any shape parser that falls back to it) raises a
1201
+ // SyntaxError via $__jz_err on malformed input, so the throw tag must exist.
1202
+ ctx.runtime.throws = true
897
1203
  const shapeSrcs = jsonShapeStrings(ctx, x)
898
1204
  if (shapeSrcs) {
899
1205
  try {