jz 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/date.js CHANGED
@@ -10,15 +10,15 @@
10
10
  */
11
11
 
12
12
  import { typed, asF64, toNumF64, allocPtr, temp } from '../src/ir.js'
13
- import { emit } from '../src/emit.js'
13
+ import { emit, deps } from '../src/bridge.js'
14
14
  import { inc, PTR } from '../src/ctx.js'
15
- import { VAL } from '../src/analyze.js'
15
+ import { VAL } from '../src/reps.js'
16
16
 
17
17
  const MS_PER_DAY = 86400000
18
18
  const MAX_TIME = 8640000000000000
19
19
 
20
20
  export default (ctx) => {
21
- Object.assign(ctx.core.stdlibDeps, {
21
+ deps({
22
22
  __date_days_from_year: [],
23
23
  __date_make_day: ['__date_days_from_year'],
24
24
  __date_make_time: [],
@@ -686,6 +686,18 @@ export default (ctx) => {
686
686
  ctx.core.emit['.getDay'] = ctx.core.emit['.getUTCDay']
687
687
  ctx.core.emit[`.${VAL.DATE}:getDay`] = ctx.core.emit[`.${VAL.DATE}:getUTCDay`]
688
688
 
689
+ ctx.core.emit['.getHours'] = ctx.core.emit['.getUTCHours']
690
+ ctx.core.emit[`.${VAL.DATE}:getHours`] = ctx.core.emit[`.${VAL.DATE}:getUTCHours`]
691
+
692
+ ctx.core.emit['.getMinutes'] = ctx.core.emit['.getUTCMinutes']
693
+ ctx.core.emit[`.${VAL.DATE}:getMinutes`] = ctx.core.emit[`.${VAL.DATE}:getUTCMinutes`]
694
+
695
+ ctx.core.emit['.getSeconds'] = ctx.core.emit['.getUTCSeconds']
696
+ ctx.core.emit[`.${VAL.DATE}:getSeconds`] = ctx.core.emit[`.${VAL.DATE}:getUTCSeconds`]
697
+
698
+ ctx.core.emit['.getMilliseconds'] = ctx.core.emit['.getUTCMilliseconds']
699
+ ctx.core.emit[`.${VAL.DATE}:getMilliseconds`] = ctx.core.emit[`.${VAL.DATE}:getUTCMilliseconds`]
700
+
689
701
  // ── UTC setter emit handlers ──────────────────────────────────────────────
690
702
 
691
703
  const emitDatePtr = (dateExpr) =>
@@ -10,11 +10,41 @@
10
10
  * @module fn
11
11
  */
12
12
 
13
- import { typed, asF64, asI32, mkPtrIR, temp, tempI32, MAX_CLOSURE_ARITY, UNDEF_NAN } from '../src/ir.js'
14
- import { emit } from '../src/emit.js'
15
- import { isReassigned } from '../src/analyze.js'
16
- import { T, lookupValType, repOf, findFreeVars } from '../src/analyze.js'
17
- import { PTR, LAYOUT, inc, err } from '../src/ctx.js'
13
+ import { typed, asF64, asI32, mkPtrIR, temp, tempI32, MAX_CLOSURE_ARITY, UNDEF_NAN, appendStaticSlots } from '../src/ir.js'
14
+ import { emit } from '../src/bridge.js'
15
+ import { isReassigned } from '../src/ast.js'
16
+ import { findFreeVars } from '../src/compile/analyze.js'
17
+ import { T } from '../src/ast.js'
18
+ import { lookupValType, repOf, VAL } from '../src/reps.js'
19
+ import { valTypeOf } from '../src/kind.js'
20
+ import { PTR, LAYOUT, inc, err, declGlobal } from '../src/ctx.js'
21
+
22
+ // A closure whose every return value is provably a plain number lets each caller
23
+ // skip the `__to_num` result coercion — the dominant per-sample cost in closure-heavy
24
+ // numeric kernels (floatbeats etc.). The check is structural at make time: arithmetic /
25
+ // bitwise / `|0` / math.* returns resolve via `valTypeOf`, as do chained numeric-closure
26
+ // calls (`g(x)=>…; f()=>g(…)` — `g`'s entry is already set when the outer `f` is made,
27
+ // since decls emit in order). An unprovable shape (polymorphic `a[i]`, a value-returning
28
+ // fall-through, a bare local that could collide with a parent binding) stays boxed.
29
+ const closureReturnExprs = (body) => {
30
+ if (!Array.isArray(body) || body[0] !== '{}') return [body] // expression-bodied arrow
31
+ const stmts = Array.isArray(body[1]) && body[1][0] === ';' ? body[1].slice(1) : [body[1]]
32
+ const last = stmts[stmts.length - 1]
33
+ if (!Array.isArray(last) || last[0] !== 'return') return null // may fall off the end with undefined
34
+ const rets = []
35
+ let ok = true
36
+ const walk = n => {
37
+ if (!ok || !Array.isArray(n) || n[0] === '=>') return // don't descend into nested closures
38
+ if (n[0] === 'return') { if (n.length < 2) ok = false; else rets.push(n[1]); return }
39
+ for (let i = 1; i < n.length; i++) walk(n[i])
40
+ }
41
+ for (const s of stmts) walk(s)
42
+ return ok ? rets : null
43
+ }
44
+ const returnsPlainNumber = (body) => {
45
+ const rets = closureReturnExprs(body)
46
+ return !!rets && rets.length > 0 && rets.every(e => valTypeOf(e) === VAL.NUMBER)
47
+ }
18
48
 
19
49
  const intConstExpr = (node) => {
20
50
  if (typeof node === 'number' && Number.isInteger(node)) return node
@@ -107,6 +137,11 @@ export default (ctx) => {
107
137
  // call_indirect on the captured pointer). Gated on isReassigned over the inner body
108
138
  // so a local rewrite of the captured name disables propagation.
109
139
  const captureDirectClosures = new Map()
140
+ // Propagate the parent's `nullable` mark: a capture whose parent binding can
141
+ // hold null/undefined (e.g. `let x = null` later assigned a number) must keep
142
+ // that fact inside the body, or the body's own write facts (val = NUMBER)
143
+ // would let `x == null` fold to a constant false and skip the guard.
144
+ const captureNullables = new Set()
110
145
  for (const name of envCaptures) {
111
146
  const vt = lookupValType(name)
112
147
  if (vt != null) captureValTypes.set(name, vt)
@@ -117,6 +152,7 @@ export default (ctx) => {
117
152
  const bodyName = ctx.func.directClosures?.get(name)
118
153
  if (bodyName && !isReassigned(body, name)) captureDirectClosures.set(name, bodyName)
119
154
  if (repOf(name)?.intCertain === true) captureIntCertain.add(name)
155
+ if (repOf(name)?.nullable) captureNullables.add(name)
120
156
  }
121
157
 
122
158
  const schemaNames = ctx.schema.vars?.size ? new Set(ctx.schema.vars.keys()) : null
@@ -134,17 +170,23 @@ export default (ctx) => {
134
170
  // All closures use uniform convention: (env: f64, args_array: f64) → f64
135
171
  // The body unpacks individual params from the args array
136
172
  const boxedCaptures = envCaptures.filter(c => ctx.func.boxed?.has(c))
173
+ // i32-narrowed cells travel with the capture: the closure body must access
174
+ // the shared cell at the same width the owner does (see funcFacts.cellTypes).
175
+ const cellI32Captures = boxedCaptures.filter(c => ctx.func.cellTypes?.has(c))
137
176
  const bodyFn = { name: fnName, params, body, captures: envCaptures, arity: 1,
138
177
  ...(restParam && { rest: restParam }),
139
178
  ...(defaults && { defaults }),
140
179
  ...(boxedCaptures.length && { boxed: new Set(boxedCaptures) }),
180
+ ...(cellI32Captures.length && { cellI32: new Set(cellI32Captures) }),
141
181
  ...(captureIntConsts.size && { intConsts: captureIntConsts }),
142
182
  ...(captureIntCertain.size && { intCertain: captureIntCertain }),
183
+ ...(captureNullables.size && { nullables: captureNullables }),
143
184
  ...(captureValTypes.size && { valTypes: captureValTypes }),
144
185
  ...(captureSchemaVars.size && { schemaVars: captureSchemaVars }),
145
186
  ...(captureTypedElems.size && { typedElems: captureTypedElems }),
146
187
  ...(captureDirectClosures.size && { directClosures: captureDirectClosures }) }
147
188
  ctx.closure.bodies.push(bodyFn)
189
+ if (returnsPlainNumber(body)) (ctx.closure.numericReturn ||= new Set()).add(fnName)
148
190
 
149
191
  const tableIdx = addToTable(fnName)
150
192
 
@@ -160,6 +202,27 @@ export default (ctx) => {
160
202
  return ir
161
203
  }
162
204
 
205
+ if (body._nonEscaping && ctx.transform.optimize) {
206
+ // Allocate environmental slots in the static data segment
207
+ const staticOff = appendStaticSlots(new Array(envCaptures.length).fill('0x0000000000000000'))
208
+ const block = []
209
+ // Store captured values in env: boxed cells as raw i32 in low 4 bytes, others as f64.
210
+ // Avoids i32↔f64 roundtrip; body loads via i32.load/f64.load using the same branch.
211
+ for (let i = 0; i < envCaptures.length; i++) {
212
+ const addr = ['i32.const', staticOff + i * 8]
213
+ if (ctx.func.boxed?.has(envCaptures[i]))
214
+ block.push(['i32.store', addr, ['local.get', `$${ctx.func.boxed.get(envCaptures[i])}`]])
215
+ else
216
+ block.push(['f64.store', addr, asF64(emit(envCaptures[i]))])
217
+ }
218
+ block.push(mkPtrIR(PTR.CLOSURE, tableIdx, ['i32.const', staticOff]))
219
+
220
+ const ir = typed(['block', ['result', 'f64'], ...block], 'f64')
221
+ ir.closureBodyName = fnName
222
+ ir.closureFuncIdx = tableIdx
223
+ return ir
224
+ }
225
+
163
226
  const t = tempI32('env')
164
227
 
165
228
  const block = [
@@ -195,7 +258,10 @@ export default (ctx) => {
195
258
 
196
259
  if (prebuiltArray) {
197
260
  // Spread path: decode array into inline slots. Slots beyond array len padded with UNDEF.
198
- // Rest-param closures receive up to (MAX - fixedParams) spread elements (overflow lost).
261
+ // The full args array offset is published in $__closure_spill so a rest-param
262
+ // callee can recover elements beyond width W (the W inline slots hold args[0..W-1];
263
+ // the rest reads args[W..argc-1] straight from the spill array). Unbounded arity.
264
+ declGlobal('__closure_spill', 'i32')
199
265
  const arrT = tempI32('sa')
200
266
  const lenL = tempI32('sl')
201
267
  const setup = [
@@ -220,6 +286,7 @@ export default (ctx) => {
220
286
  return typed(['block', ['result', 'f64'],
221
287
  ...setup,
222
288
  ['local.set', `$${t}`, asF64(closureExpr)],
289
+ ['global.set', '$__closure_spill', ['local.get', `$${arrT}`]],
223
290
  ['call_indirect', ['type', '$ftN'],
224
291
  ['local.get', `$${t}`],
225
292
  ['local.get', `$${lenL}`],
package/module/index.js CHANGED
@@ -13,4 +13,5 @@ import json from './json.js'
13
13
  import regex from './regex.js'
14
14
  import timer from './timer.js'
15
15
  import date from './date.js'
16
- export { math, core, array, object, string, number, fn, typedarray, collection, symbol, console, json, regex, timer, date }
16
+ import simd from './simd.js'
17
+ export { math, core, array, object, string, number, fn, typedarray, collection, symbol, console, json, regex, timer, date, simd }
package/module/json.js CHANGED
@@ -8,10 +8,12 @@
8
8
  * @module json
9
9
  */
10
10
 
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
- import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
11
+ import { typed, asF64, asI64, toStrI64, temp, tempI32, nullExpr, undefExpr, allocPtr, slotAddr, mkPtrIR, extractF64Bits, appendStaticSlots, NULL_WAT, UNDEF_NAN, UNDEF_WAT, FALSE_NAN, TRUE_NAN, FALSE_IR, TRUE_IR } from '../src/ir.js'
12
+ import { emit, bool, deps } from '../src/bridge.js'
13
+ import { valTypeOf } from '../src/kind.js'
14
+ import { T } from '../src/ast.js'
15
+ import { VAL } from '../src/reps.js'
16
+ import { err, inc, PTR, LAYOUT, declGlobal } from '../src/ctx.js'
15
17
  import { strHashLiteral } from './collection.js'
16
18
 
17
19
  function jsonConstString(ctx, expr) {
@@ -58,6 +60,60 @@ function literalValue(node) {
58
60
  return NOT_LIT
59
61
  }
60
62
 
63
+ // A BigInt has no JSON representation: `JSON.stringify` of a bigint — anywhere in
64
+ // the value graph — is a TypeError (ES JSON.stringify → SerializeJSONProperty).
65
+ // Detects it in a resolved literal value so the caller can emit the throw rather
66
+ // than handing the bigint to the host `JSON.stringify` (which crashes the fold).
67
+ const literalHasBigInt = (v) =>
68
+ typeof v === 'bigint' ||
69
+ (Array.isArray(v) ? v.some(literalHasBigInt)
70
+ : !!v && typeof v === 'object' && Object.values(v).some(literalHasBigInt))
71
+
72
+ // Fold a literal AST node directly to its compact JSON string (no replacer /
73
+ // no space). Unlike the value-level `literalValue` + `JSON.stringify` path, this
74
+ // renders the self-host kernel boolean marker `['bool', 1|0]` as true/false:
75
+ // the value path can't, because jz carries a boolean as a 0/1 *number*, so the
76
+ // kernel's compile-time `JSON.stringify` of the folded value emits "1"/"0", not
77
+ // "true"/"false" (native folds via real JS booleans and is unaffected). Scalars
78
+ // delegate to the value path for exact number/string formatting; anything not
79
+ // cleanly renderable (undefined, holes, function/symbol) returns NOT_LIT so the
80
+ // caller falls back. Returns a JSON string or NOT_LIT.
81
+ function foldJsonStr(node) {
82
+ if (!Array.isArray(node)) return NOT_LIT
83
+ const op = node[0]
84
+ // `['bool', carrier]` — prepare wraps the 0/1 carrier as a number-literal node
85
+ // `[, 0|1]` on the kernel leg (bare number in native), so unwrap before testing.
86
+ if (op === 'bool') {
87
+ const c = Array.isArray(node[1]) ? node[1][1] : node[1]
88
+ return c ? 'true' : 'false'
89
+ }
90
+ if (op === '[') {
91
+ const parts = []
92
+ for (let i = 1; i < node.length; i++) {
93
+ const s = foldJsonStr(node[i])
94
+ if (s === NOT_LIT) return NOT_LIT
95
+ parts.push(s)
96
+ }
97
+ return '[' + parts.join(',') + ']'
98
+ }
99
+ if (op === '{}') {
100
+ const parts = []
101
+ for (let i = 1; i < node.length; i++) {
102
+ const e = node[i]
103
+ if (!Array.isArray(e) || e[0] !== ':' || (typeof e[1] !== 'string' && typeof e[1] !== 'number')) return NOT_LIT
104
+ const s = foldJsonStr(e[2])
105
+ if (s === NOT_LIT) return NOT_LIT
106
+ parts.push(JSON.stringify(String(e[1])) + ':' + s)
107
+ }
108
+ return '{' + parts.join(',') + '}'
109
+ }
110
+ // Scalar (number / string / null / u-): exact formatting via the value path.
111
+ const v = literalValue(node)
112
+ if (v === NOT_LIT || typeof v === 'bigint') return NOT_LIT // bigint ⇒ TypeError, not a literal string
113
+ const s = JSON.stringify(v)
114
+ return s === undefined ? NOT_LIT : s // undefined/function/symbol — defer
115
+ }
116
+
61
117
  function jsonShapeString(ctx, expr) {
62
118
  if (typeof expr === 'string') return ctx.scope.shapeStrs?.get(expr) ?? null
63
119
  return null
@@ -78,15 +134,15 @@ function hashCapFor(n) {
78
134
  }
79
135
 
80
136
  export default (ctx) => {
81
- Object.assign(ctx.core.stdlibDeps, {
137
+ deps({
82
138
  __stringify: ['__json_val', '__json_setgap', '__json_omit', '__jput', '__jput_str', '__jput_num', '__mkstr'],
83
139
  __json_setgap: ['__alloc', '__ptr_type', '__str_byteLen', '__char_at'],
84
140
  __json_omit: ['__ptr_type'],
85
141
  __json_enter: ['__alloc'],
86
142
  __jindent: ['__jput'],
87
143
  __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'],
144
+ __json_hash: ['__ptr_offset', '__jput', '__jindent', '__jput_str', '__json_omit', '__json_enter', '__json_leave', '__json_val', '__coll_order'],
145
+ __json_obj: ['__ptr_offset', '__ptr_aux', '__len', '__jput', '__jindent', '__jput_str', '__json_omit', '__json_enter', '__json_leave', '__json_val', '__coll_order'],
90
146
  __jput_num: ['__ftoa'],
91
147
  __jput_str: ['__char_at', '__str_byteLen'],
92
148
  __jp: ['__jp_val', '__jp_str', '__jp_num', '__jp_arr', '__jp_obj', '__sso_char', '__ptr_aux', '__ptr_type', '__ptr_offset', '__str_byteLen'],
@@ -116,7 +172,7 @@ export default (ctx) => {
116
172
  if (v == null) return nullExpr()
117
173
  if (typeof v === 'number') return asF64(emit(v))
118
174
  if (typeof v === 'string') return asF64(emit(['str', v]))
119
- if (typeof v === 'boolean') return asF64(emit(v ? 1 : 0))
175
+ if (typeof v === 'boolean') return typed((v ? TRUE_IR : FALSE_IR).slice(), 'f64')
120
176
  if (Array.isArray(v)) {
121
177
  const a = allocPtr({ type: PTR.ARRAY, len: v.length, cap: Math.max(v.length, 4), tag: 'jarr' })
122
178
  const body = [a.init]
@@ -150,23 +206,23 @@ export default (ctx) => {
150
206
  // Scratch buffer approach: __json_buf is a growable output buffer.
151
207
  // Functions append bytes to it, __json_pos tracks current write position.
152
208
 
153
- ctx.scope.globals.set('__jbuf', '(global $__jbuf (mut i32) (i32.const 0))')
154
- ctx.scope.globals.set('__jpos', '(global $__jpos (mut i32) (i32.const 0))')
155
- ctx.scope.globals.set('__jcap', '(global $__jcap (mut i32) (i32.const 0))')
156
- ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
209
+ declGlobal('__jbuf', 'i32')
210
+ declGlobal('__jpos', 'i32')
211
+ declGlobal('__jcap', 'i32')
212
+ declGlobal('__schema_tbl', 'i32')
157
213
  // Pretty-print state for the `space` argument. $__jgap points at the gap
158
214
  // string bytes ($__jgaplen of them); $__jdepth is the live nesting depth.
159
215
  // $__jgaplen == 0 ⇒ compact mode — every indent emission below is gated on
160
216
  // 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))')
217
+ declGlobal('__jgap', 'i32')
218
+ declGlobal('__jgaplen', 'i32')
219
+ declGlobal('__jdepth', 'i32')
164
220
  // Cycle-detection stack: the i64 values of the containers currently open on
165
221
  // the recursion path. JSON.stringify of a structure that points back at an
166
222
  // ancestor must throw a TypeError. $__jstack is a lazily-allocated buffer of
167
223
  // 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))')
224
+ declGlobal('__jstack', 'i32')
225
+ declGlobal('__jsp', 'i32')
170
226
 
171
227
  // __jput(byte: i32) — append one byte to output buffer
172
228
  ctx.core.stdlib['__jput'] = `(func $__jput (param $b i32)
@@ -322,6 +378,16 @@ export default (ctx) => {
322
378
  (call $__jput_num (local.get $f)) (return)))
323
379
  ;; NaN-boxed pointer
324
380
  (local.set $type (call $__ptr_type (local.get $val)))
381
+ ;; Boolean atoms: FALSE_NAN / TRUE_NAN → "false" / "true"
382
+ (if (i64.eq (local.get $val) (i64.const ${FALSE_NAN}))
383
+ (then
384
+ (call $__jput (i32.const 102)) (call $__jput (i32.const 97))
385
+ (call $__jput (i32.const 108)) (call $__jput (i32.const 115))
386
+ (call $__jput (i32.const 101)) (return)))
387
+ (if (i64.eq (local.get $val) (i64.const ${TRUE_NAN}))
388
+ (then
389
+ (call $__jput (i32.const 116)) (call $__jput (i32.const 114))
390
+ (call $__jput (i32.const 117)) (call $__jput (i32.const 101)) (return)))
325
391
  ;; Plain NaN (type=0) → null
326
392
  (if (i32.eqz (local.get $type))
327
393
  (then
@@ -369,34 +435,35 @@ export default (ctx) => {
369
435
  (call $__jput (i32.const 110)) (call $__jput (i32.const 117))
370
436
  (call $__jput (i32.const 108)) (call $__jput (i32.const 108)))`
371
437
 
372
- // __json_hash(val: i64) — stringify HASH/MAP: iterate slots, emit {"key":val,...}
373
- // Slot layout: 24 bytes each — [hash:f64][key:f64][val:f64]. Empty slots have hash==0.
438
+ // __json_hash(val: i64) — stringify HASH/MAP: emit {"key":val,...} in insertion
439
+ // order. Slot layout: 24 bytes each — [hash:f64][key:f64][val:f64]. __coll_order
440
+ // returns the n live slot offsets sorted by packed seq, matching the JS spec.
374
441
  ctx.core.stdlib['__json_hash'] = `(func $__json_hash (param $val i64)
375
- (local $off i32) (local $cap i32) (local $i i32) (local $slot i32) (local $first i32) (local $pv i64)
442
+ (local $off i32) (local $cap i32) (local $n i32) (local $i i32) (local $slot i32) (local $ord i32) (local $first i32) (local $pv i64)
376
443
  (local.set $off (call $__ptr_offset (local.get $val)))
377
444
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
445
+ (local.set $n (i32.load (i32.sub (local.get $off) (i32.const 8))))
446
+ (local.set $ord (call $__coll_order (local.get $off) (local.get $cap) (i32.const 24)))
378
447
  (local.set $first (i32.const 1))
379
448
  (call $__json_enter (local.get $val))
380
449
  (call $__jput (i32.const 123))
381
450
  (global.set $__jdepth (i32.add (global.get $__jdepth) (i32.const 1)))
382
451
  (block $d (loop $l
383
- (br_if $d (i32.ge_s (local.get $i) (local.get $cap)))
384
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const 24))))
385
- (if (f64.ne (f64.load (local.get $slot)) (f64.const 0))
452
+ (br_if $d (i32.ge_s (local.get $i) (local.get $n)))
453
+ (local.set $slot (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))))
454
+ (local.set $pv (i64.load (i32.add (local.get $slot) (i32.const 16))))
455
+ (if (i32.eqz (call $__json_omit (local.get $pv)))
386
456
  (then
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))))))
457
+ (if (i32.eqz (local.get $first))
458
+ (then (call $__jput (i32.const 44))))
459
+ (local.set $first (i32.const 0))
460
+ (call $__jindent)
461
+ (call $__jput (i32.const 34))
462
+ (call $__jput_str (i64.load (i32.add (local.get $slot) (i32.const 8))))
463
+ (call $__jput (i32.const 34))
464
+ (call $__jput (i32.const 58))
465
+ (if (global.get $__jgaplen) (then (call $__jput (i32.const 32))))
466
+ (call $__json_val (local.get $pv))))
400
467
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
401
468
  (br $l)))
402
469
  (global.set $__jdepth (i32.sub (global.get $__jdepth) (i32.const 1)))
@@ -411,14 +478,21 @@ export default (ctx) => {
411
478
  ctx.core.stdlib['__json_obj'] = `(func $__json_obj (param $val i64)
412
479
  (local $off i32) (local $sid i32) (local $keys i32) (local $nkeys i32)
413
480
  (local $i i32) (local $koff i32) (local $first i32) (local $pv i64)
481
+ (local $props i64) (local $poff i32) (local $pcap i32) (local $dn i32) (local $slot i32) (local $ord i32)
482
+ (local $j i32) (local $skip i32)
414
483
  (local.set $off (call $__ptr_offset (local.get $val)))
415
484
  (local.set $sid (call $__ptr_aux (local.get $val)))
416
- ;; Load keys array from schema table: schema_tbl + sid * 8
417
- (local.set $keys (call $__ptr_offset
418
- (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
419
- (local.set $nkeys (call $__len
420
- (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
421
- (local.set $koff (local.get $keys))
485
+ ;; Schema keys: schema_tbl + sid*8. Dyn-only programs (empty {} + computed
486
+ ;; o[k]=v) never allocate __schema_tbl, leaving it 0 — guard the read so the
487
+ ;; dyn-prop walk below still runs.
488
+ (local.set $nkeys (i32.const 0))
489
+ (local.set $koff (i32.const 0))
490
+ (if (i32.ne (global.get $__schema_tbl) (i32.const 0))
491
+ (then
492
+ (local.set $koff (call $__ptr_offset
493
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
494
+ (local.set $nkeys (call $__len
495
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))))
422
496
  (local.set $first (i32.const 1))
423
497
  (call $__json_enter (local.get $val))
424
498
  (call $__jput (i32.const 123))
@@ -439,6 +513,54 @@ export default (ctx) => {
439
513
  (call $__json_val (local.get $pv))))
440
514
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
441
515
  (br $l)))
516
+ ;; Dynamic (off-schema) properties: heap OBJECTs carry a HASH propsPtr at
517
+ ;; off-16 (set by __dyn_set). Walk it in insertion order via __coll_order —
518
+ ;; schema-only enumeration would drop computed props (e.g. {} then o.a=1).
519
+ ;; Skip propsPtr entries whose key already appears in the schema: object
520
+ ;; literals with needsDynShadow(target)=true shadow-write each schema key
521
+ ;; into propsPtr so dyn-key reads can resolve via hash lookup. That mirror
522
+ ;; is a runtime acceleration, not an enumeration entity — without dedup,
523
+ ;; JSON output emits each schema key twice. Schema-key interns equal the
524
+ ;; keys we shadow-write (same compile-time string literal), so i64.eq matches.
525
+ (if (i32.ge_u (local.get $off) (global.get $__heap_start))
526
+ (then
527
+ (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
528
+ (if (i32.eq
529
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
530
+ (i32.const ${PTR.HASH}))
531
+ (then
532
+ (local.set $poff (call $__ptr_offset (local.get $props)))
533
+ (local.set $pcap (i32.load (i32.sub (local.get $poff) (i32.const 4))))
534
+ (local.set $dn (i32.load (i32.sub (local.get $poff) (i32.const 8))))
535
+ (local.set $ord (call $__coll_order (local.get $poff) (local.get $pcap) (i32.const 24)))
536
+ (local.set $i (i32.const 0))
537
+ (block $dd (loop $dl
538
+ (br_if $dd (i32.ge_s (local.get $i) (local.get $dn)))
539
+ (local.set $slot (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))))
540
+ (local.set $pv (i64.load (i32.add (local.get $slot) (i32.const 16))))
541
+ (local.set $skip (i32.const 0))
542
+ (local.set $j (i32.const 0))
543
+ (block $sd (loop $sl
544
+ (br_if $sd (i32.ge_s (local.get $j) (local.get $nkeys)))
545
+ (if (i64.eq
546
+ (i64.load (i32.add (local.get $slot) (i32.const 8)))
547
+ (i64.load (i32.add (local.get $koff) (i32.shl (local.get $j) (i32.const 3)))))
548
+ (then (local.set $skip (i32.const 1)) (br $sd)))
549
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
550
+ (br $sl)))
551
+ (if (i32.and (i32.eqz (local.get $skip)) (i32.eqz (call $__json_omit (local.get $pv))))
552
+ (then
553
+ (if (i32.eqz (local.get $first)) (then (call $__jput (i32.const 44))))
554
+ (local.set $first (i32.const 0))
555
+ (call $__jindent)
556
+ (call $__jput (i32.const 34))
557
+ (call $__jput_str (i64.load (i32.add (local.get $slot) (i32.const 8))))
558
+ (call $__jput (i32.const 34))
559
+ (call $__jput (i32.const 58))
560
+ (if (global.get $__jgaplen) (then (call $__jput (i32.const 32))))
561
+ (call $__json_val (local.get $pv))))
562
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
563
+ (br $dl)))))))
442
564
  (global.set $__jdepth (i32.sub (global.get $__jdepth) (i32.const 1)))
443
565
  (if (i32.eqz (local.get $first)) (then (call $__jindent)))
444
566
  (call $__jput (i32.const 125))
@@ -460,19 +582,19 @@ export default (ctx) => {
460
582
 
461
583
  // === JSON.parse ===
462
584
 
463
- ctx.scope.globals.set('__jpstr', '(global $__jpstr (mut i32) (i32.const 0))') // input string offset
464
- ctx.scope.globals.set('__jplen', '(global $__jplen (mut i32) (i32.const 0))') // input length
465
- ctx.scope.globals.set('__jppos', '(global $__jppos (mut i32) (i32.const 0))') // current parse position
585
+ declGlobal('__jpstr', 'i32') // input string offset
586
+ declGlobal('__jplen', 'i32') // input length
587
+ declGlobal('__jppos', 'i32') // current parse position
466
588
  // Side-channel hash for the most-recently-parsed string. __jp_str folds an
467
589
  // FNV-1a pass into its scan loop; __jp_obj forwards it to __hash_set_local_h
468
590
  // and skips the redundant __str_hash call inside the generic insert. 0 is a
469
591
  // sentinel meaning "string had escapes — recompute via __str_hash".
470
- ctx.scope.globals.set('__jp_keyh', '(global $__jp_keyh (mut i32) (i32.const 0))')
592
+ declGlobal('__jp_keyh', 'i32')
471
593
  // Sticky syntax-error flag. Set by any parser sub-routine on malformed input;
472
594
  // checked once by __jp after the top-level value, which throws if it is set.
473
595
  // Sticky (never cleared mid-parse) so recursive descent need not thread an
474
596
  // 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))')
597
+ declGlobal('__jp_err', 'i32')
476
598
  // Runtime schema infrastructure. __schema_next points at the first free slot
477
599
  // in $__schema_tbl reserved for runtime registration; compile.js initializes
478
600
  // it to ctx.schema.list.length when __jp_obj is included. The schema cache
@@ -482,8 +604,8 @@ export default (ctx) => {
482
604
  // every __hash_set_local). Cache slot layout: i32 hash, i32 sid (8 bytes).
483
605
  // Hash 0 = empty slot; we bump <=1 to 2 like __str_hash to avoid sentinel
484
606
  // collision with valid hashes.
485
- ctx.scope.globals.set('__schema_next', '(global $__schema_next (mut i32) (i32.const 0))')
486
- ctx.scope.globals.set('__schema_cache', '(global $__schema_cache (mut i32) (i32.const 0))')
607
+ declGlobal('__schema_next', 'i32')
608
+ declGlobal('__schema_cache', 'i32')
487
609
 
488
610
  // Sentinel-driven peek: __jp copies input to a scratch buffer with 0xFF bytes
489
611
  // appended past the end. i32.load8_s sign-extends, so the sentinel reads as -1
@@ -905,9 +1027,17 @@ export default (ctx) => {
905
1027
  // word[0]; AND-check word[1..] against the bytes after the parse position.
906
1028
  // Past-end reads land in the 0xFF sentinel pad, which matches nothing.
907
1029
  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})`)
1030
+ // Index with charCodeAt rather than `[...word]` spread: jz (self-host) does not
1031
+ // spread a string into a char array, so the spread form yields an empty test set
1032
+ // and the literal match folds to a constant-false `(if 0 )` — breaking
1033
+ // JSON.parse of true/false/null once jz compiles its own parser. The first char
1034
+ // is already matched by the caller's `ch` switch, so compare offsets 1..len-1.
1035
+ let acc = ''
1036
+ for (let i = 1; i < word.length; i++) {
1037
+ const t = `(i32.eq (i32.load8_u (i32.add (global.get $__jpstr) (i32.add (global.get $__jppos) (i32.const ${i})))) (i32.const ${word.charCodeAt(i)}))`
1038
+ acc = acc ? `(i32.and ${acc} ${t})` : t
1039
+ }
1040
+ return acc
911
1041
  }
912
1042
  const litCase = (ch, word, valIR, adv) => `(if (i32.eq (local.get $ch) (i32.const ${ch}))
913
1043
  (then (if ${litMatch(word)}
@@ -928,8 +1058,8 @@ export default (ctx) => {
928
1058
  (if (i32.or (i32.and (i32.ge_s (local.get $ch) (i32.const 48)) (i32.le_s (local.get $ch) (i32.const 57)))
929
1059
  (i32.eq (local.get $ch) (i32.const 45)))
930
1060
  (then (return (call $__jp_num))))
931
- ${litCase(116, 'true', '(f64.const 1)', 4)}
932
- ${litCase(102, 'false', '(f64.const 0)', 5)}
1061
+ ${litCase(116, 'true', `(f64.const nan:${TRUE_NAN})`, 4)}
1062
+ ${litCase(102, 'false', `(f64.const nan:${FALSE_NAN})`, 5)}
933
1063
  ${litCase(110, 'null', NULL_WAT, 4)}
934
1064
  ;; No production matched — malformed JSON value.
935
1065
  (global.set $__jp_err (i32.const 1))
@@ -971,18 +1101,24 @@ export default (ctx) => {
971
1101
  const expect = (byte) => `(if (i32.ne ${PEEK} (i32.const ${byte})) (then ${fail}))
972
1102
  ${ADV(1)}`
973
1103
  const expectText = (text) => [...text].map(c => expect(c.charCodeAt(0))).join('\n ')
1104
+ // Forward-declared with `let` (assigned below) so `parse` captures a boxed
1105
+ // cell, not a not-yet-initialized `const` binding — the self-host kernel
1106
+ // miscompiles the latter capture in this deeply-nested mutually-recursive
1107
+ // context (parse ends up calling a garbage `parseObject`, emitting a corrupt
1108
+ // node into the shaped parser).
1109
+ let parseObject, parseArray
974
1110
  const parse = (v, out) => {
975
1111
  if (v == null) return `${expectText('null')}
976
1112
  (local.set $${out} ${NULL_WAT})`
977
1113
  if (typeof v === 'boolean') return `${expectText(v ? 'true' : 'false')}
978
- (local.set $${out} (f64.const ${v ? 1 : 0}))`
1114
+ (local.set $${out} (f64.const nan:${v ? TRUE_NAN : FALSE_NAN}))`
979
1115
  if (typeof v === 'number') return `(local.set $${out} (call $__jp_num))`
980
1116
  if (typeof v === 'string') return `${expect(34)}
981
1117
  (local.set $${out} (call $__jp_str))`
982
1118
  if (Array.isArray(v)) return parseArray(v[0], out)
983
1119
  return parseObject(v, out)
984
1120
  }
985
- const parseObject = (v, out) => {
1121
+ parseObject = (v, out) => {
986
1122
  const keys = Object.keys(v)
987
1123
  const obj = local('obj', 'i32')
988
1124
  const val = local('val', 'f64')
@@ -1010,7 +1146,7 @@ export default (ctx) => {
1010
1146
  return `${body}
1011
1147
  (local.set $${out} (call $__mkptr (i32.const ${PTR.OBJECT}) (i32.const ${sid}) (local.get $${obj})))`
1012
1148
  }
1013
- const parseArray = (elem, out) => {
1149
+ parseArray = (elem, out) => {
1014
1150
  const ptr = local('arr', 'i32')
1015
1151
  const len = local('alen', 'i32')
1016
1152
  const cap = local('acap', 'i32')
@@ -1139,13 +1275,28 @@ ${localDecls}
1139
1275
  // the runtime call entirely. The runtime `__stringify` path (which ignores a
1140
1276
  // replacer) handles every non-constant case unchanged.
1141
1277
  ctx.core.emit['JSON.stringify'] = (x, replacer, space) => {
1278
+ // An explicit `null`/`undefined` replacer is spec-equivalent to none; only a
1279
+ // real array/function replacer changes the result. (foldStringify normalizes
1280
+ // the same way via literalValue.) Used by both peepholes below.
1281
+ const noReplacer = replacer == null || literalValue(replacer) == null
1282
+ // BigInt has no JSON form — spec throws a TypeError. Emit the throw for the
1283
+ // statically-known cases (a top-level bigint expr, or a literal tree holding
1284
+ // one); the host fold can't render a bigint either. A function/array replacer
1285
+ // may rewrite the value, so defer to the fold/runtime path when one is present.
1286
+ if (noReplacer) {
1287
+ const lv = literalValue(x)
1288
+ if (valTypeOf(x) === VAL.BIGINT || (lv !== NOT_LIT && literalHasBigInt(lv))) {
1289
+ ctx.runtime.throws = true
1290
+ return typed(['block', ['result', 'f64'], ['throw', '$__jz_err', ['f64.const', 0]]], 'f64')
1291
+ }
1292
+ }
1142
1293
  const folded = foldStringify(x, replacer, space)
1143
1294
  if (folded !== undefined) return folded
1144
1295
  // Scalar boolean: the working-rep is the 0/1 number carrier, so the runtime
1145
1296
  // 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)
1297
+ // true/false — exactly bool. Guard on no replacer: a replacer function may
1298
+ // rewrite the top-level value, in which case fall to runtime.
1299
+ if (noReplacer && valTypeOf(x) === VAL.BOOL) return bool(x)
1149
1300
  inc('__stringify')
1150
1301
  const spaceIR = asI64(space == null ? undefExpr() : emit(space))
1151
1302
  return typed(['call', '$__stringify', asI64(emit(x)), spaceIR], 'f64')
@@ -1153,6 +1304,12 @@ ${localDecls}
1153
1304
 
1154
1305
  // Returns folded IR, or `undefined` when any argument is non-constant.
1155
1306
  function foldStringify(x, replacer, space) {
1307
+ // No replacer / no space: try the bool-aware AST string fold first so a
1308
+ // literal boolean renders as true/false on the self-host leg (see foldJsonStr).
1309
+ if (replacer == null && space == null) {
1310
+ const s = foldJsonStr(x)
1311
+ if (s !== NOT_LIT) return asF64(emit(['str', s]))
1312
+ }
1156
1313
  const val = literalValue(x)
1157
1314
  if (val === NOT_LIT) return undefined
1158
1315
 
@@ -1210,6 +1367,14 @@ ${localDecls}
1210
1367
  } catch { /* fall through to generic runtime parser */ }
1211
1368
  }
1212
1369
  inc('__jp')
1213
- return typed(['call', '$__jp', asI64(emit(x))], 'f64')
1370
+ const value = temp('jp_arg')
1371
+ const input = valTypeOf(x) === VAL.BOOL
1372
+ ? asI64(bool(x))
1373
+ : valTypeOf(x) === VAL.STRING
1374
+ ? ['i64.reinterpret_f64', ['local.get', `$${value}`]]
1375
+ : toStrI64(null, typed(['local.get', `$${value}`], 'f64'))
1376
+ return typed(['block', ['result', 'f64'],
1377
+ ['local.set', `$${value}`, asF64(emit(x))],
1378
+ ['call', '$__jp', input]], 'f64')
1214
1379
  }
1215
1380
  }