jz 0.3.1 → 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/README.md +277 -258
- package/cli.js +10 -52
- package/index.js +181 -29
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +195 -76
- package/module/collection.js +249 -20
- package/module/console.js +1 -1
- package/module/core.js +267 -119
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +367 -61
- package/module/math.js +568 -128
- package/module/number.js +390 -227
- package/module/object.js +352 -39
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +555 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +12 -6
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1872 -487
- package/src/assemble.js +58 -20
- package/src/autoload.js +13 -1
- package/src/codegen.js +177 -0
- package/src/compile.js +462 -186
- package/src/ctx.js +85 -18
- package/src/emit.js +668 -197
- package/src/infer.js +548 -0
- package/src/ir.js +302 -27
- package/src/jzify.js +898 -259
- package/src/narrow.js +455 -246
- package/src/optimize.js +263 -99
- package/src/plan.js +713 -35
- package/src/prepare.js +818 -293
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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',
|
|
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(['
|
|
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
|
-
//
|
|
183
|
+
// __jindent — emit 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.
|
|
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
|
-
|
|
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
|
-
(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
252
|
-
(call $
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
(
|
|
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
|
-
;;
|
|
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.
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
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 spaces — ends 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.
|
|
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
|
-
|
|
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))
|
|
@@ -582,10 +806,10 @@ export default (ctx) => {
|
|
|
582
806
|
;; miss: register new schema.
|
|
583
807
|
(local.set $sid (global.get $__schema_next))
|
|
584
808
|
(global.set $__schema_next (i32.add (local.get $sid) (i32.const 1)))
|
|
585
|
-
;; Allocate jz Array of n keys. __alloc_hdr(len, cap
|
|
586
|
-
;;
|
|
809
|
+
;; Allocate jz Array of n keys. __alloc_hdr(len, cap) returns base of
|
|
810
|
+
;; slot region with len@-8 and cap@-4. The schema dispatch arm reads
|
|
587
811
|
;; nkeys from -8, so len must equal cap=n.
|
|
588
|
-
(local.set $karr (call $__alloc_hdr (local.get $n) (local.get $n)
|
|
812
|
+
(local.set $karr (call $__alloc_hdr (local.get $n) (local.get $n)))
|
|
589
813
|
(local.set $i (i32.const 0))
|
|
590
814
|
(block $cd (loop $cl
|
|
591
815
|
(br_if $cd (i32.ge_s (local.get $i) (local.get $n)))
|
|
@@ -623,7 +847,7 @@ export default (ctx) => {
|
|
|
623
847
|
(then ${ADV(1)}
|
|
624
848
|
(local.set $sid (call $__jp_schema_get (local.get $kbuf) (i32.const 0) (local.get $hh)))
|
|
625
849
|
(return (call $__mkptr (i32.const ${PTR.OBJECT}) (local.get $sid)
|
|
626
|
-
(call $__alloc_hdr (i32.const 0) (i32.const 1)
|
|
850
|
+
(call $__alloc_hdr (i32.const 0) (i32.const 1))))))
|
|
627
851
|
(block $d (loop $l
|
|
628
852
|
${WS()}
|
|
629
853
|
(if (i32.eq ${PEEK} (i32.const 34))
|
|
@@ -654,14 +878,18 @@ 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
|
-
|
|
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).
|
|
661
889
|
(local.set $sid (call $__jp_schema_get (local.get $kbuf) (local.get $kn) (local.get $hh)))
|
|
662
890
|
;; Allocate OBJECT slot region: kn × 8 bytes, with header (size at -8,
|
|
663
891
|
;; cap at -4) matching the static-fold path's emitJsonConstValue layout.
|
|
664
|
-
(local.set $obj (call $__alloc_hdr (i32.const 0) (local.get $kn)
|
|
892
|
+
(local.set $obj (call $__alloc_hdr (i32.const 0) (local.get $kn)))
|
|
665
893
|
;; Copy values into OBJECT slots.
|
|
666
894
|
(local.set $i (i32.const 0))
|
|
667
895
|
(block $vd (loop $vl
|
|
@@ -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
|
-
(
|
|
691
|
-
|
|
692
|
-
(
|
|
693
|
-
|
|
694
|
-
(
|
|
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) {
|
|
@@ -749,7 +989,7 @@ export default (ctx) => {
|
|
|
749
989
|
const sid = ctx.schema.register(keys)
|
|
750
990
|
let body = `${WS()}
|
|
751
991
|
${expect(123)}
|
|
752
|
-
(local.set $${obj} (call $__alloc_hdr (i32.const 0) (i32.const ${Math.max(1, keys.length)})
|
|
992
|
+
(local.set $${obj} (call $__alloc_hdr (i32.const 0) (i32.const ${Math.max(1, keys.length)})))`
|
|
753
993
|
keys.forEach((k, i) => {
|
|
754
994
|
body += `
|
|
755
995
|
${WS()}
|
|
@@ -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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|