jz 0.3.0 → 0.4.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/object.js CHANGED
@@ -32,7 +32,7 @@ export default (ctx) => {
32
32
  const merged = target ? ctx.schema.resolve(target) : null
33
33
  const schemaId = merged ? ctx.schema.idOf(target) : 0
34
34
  const cap = merged ? Math.max(1, merged.length) : 1
35
- return mkPtrIR(PTR.OBJECT, schemaId, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', cap], ['i32.const', 8]])
35
+ return mkPtrIR(PTR.OBJECT, schemaId, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', cap]])
36
36
  }
37
37
 
38
38
  // Flatten comma-grouped props: [',', p1, p2] → [p1, p2]
@@ -82,7 +82,7 @@ export default (ctx) => {
82
82
  }
83
83
 
84
84
  const body = [
85
- ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
85
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]],
86
86
  ]
87
87
  for (let i = 0; i < values.length; i++)
88
88
  body.push(['f64.store', slotAddr(t, i), asF64(emit(values[i]))])
@@ -150,8 +150,9 @@ export default (ctx) => {
150
150
  }
151
151
 
152
152
  ctx.core.emit['Object.entries'] = (obj) => {
153
+ if (isHashTyped(obj)) return emitHashEntries(obj)
153
154
  const schema = resolveSchema(obj)
154
- if (!schema) err('Object.entries requires object with known schema')
155
+ if (!schema) return emitRuntimeEntries(obj)
155
156
  const va = asF64(emit(obj))
156
157
  const n = schema.length
157
158
  const t = temp('oe'), pair = tempI32('op'), base = tempI32('eb')
@@ -160,7 +161,7 @@ export default (ctx) => {
160
161
  ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
161
162
  for (let i = 0; i < n; i++) {
162
163
  body.push(
163
- ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2], ['i32.const', 8]]],
164
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
164
165
  ['f64.store', slotAddr(pair, 0), emit(['str', schema[i]])],
165
166
  ['f64.store', slotAddr(pair, 1), ['f64.load', slotAddr(base, i)]],
166
167
  ['f64.store', slotAddr(out.local, i), mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])])
@@ -185,7 +186,7 @@ export default (ctx) => {
185
186
  updateRep(target, { schemaId })
186
187
  const t = tempI32('bx'), s = temp('bs')
187
188
  const body = [
188
- ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, boxedSchema.length)], ['i32.const', 8]]],
189
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, boxedSchema.length)]]],
189
190
  ['f64.store', ['local.get', `$${t}`], asF64(emit(target))],
190
191
  ]
191
192
  const sBase = tempI32('sb')
@@ -317,7 +318,7 @@ export default (ctx) => {
317
318
  const srcBase = tempI32('cb')
318
319
  const body = [
319
320
  ['local.set', `$${s}`, asF64(emit(proto))],
320
- ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, n)], ['i32.const', 8]]],
321
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, n)]]],
321
322
  ['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]],
322
323
  ]
323
324
  // Copy all properties from proto
@@ -378,7 +379,7 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
378
379
  const ptr = temp('objp')
379
380
  const src = tempI32('osp')
380
381
 
381
- const body = [['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]]]
382
+ const body = [['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]]]
382
383
 
383
384
  // Process props in order — later props override earlier (JS semantics)
384
385
  let srcF
@@ -462,6 +463,13 @@ function emitHashValues(obj) {
462
463
  hashValuesFromTemp(t)], 'f64')
463
464
  }
464
465
 
466
+ function emitHashEntries(obj) {
467
+ const t = temp('he')
468
+ return typed(['block', ['result', 'f64'],
469
+ ['local.set', `$${t}`, asF64(emit(obj))],
470
+ hashEntriesFromTemp(t)], 'f64')
471
+ }
472
+
465
473
  // Inline body of the HASH walk against an already-bound f64 local. Shared by
466
474
  // the static-HASH path and the runtime-dispatch path so both produce the same
467
475
  // IR shape from the same source — only difference is whether they enter from
@@ -520,6 +528,37 @@ function hashValuesFromTemp(t) {
520
528
  out.ptr]
521
529
  }
522
530
 
531
+ function hashEntriesFromTemp(t) {
532
+ inc('__ptr_offset', '__cap', '__len', '__alloc_hdr')
533
+ const off = tempI32('heo'), cap = tempI32('hec'), n = tempI32('hen')
534
+ const i = tempI32('hei'), o = tempI32('hej'), slot = tempI32('hes'), pair = tempI32('hep')
535
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'hea' })
536
+ const id = ctx.func.uniq++
537
+ return ['block', ['result', 'f64'],
538
+ ['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
539
+ out.init,
540
+ ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
541
+ ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
542
+ ['local.set', `$${i}`, ['i32.const', 0]],
543
+ ['local.set', `$${o}`, ['i32.const', 0]],
544
+ ['block', `$ebrk${id}`, ['loop', `$eloop${id}`,
545
+ ['br_if', `$ebrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
546
+ ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
547
+ ['i32.mul', ['local.get', `$${i}`], ['i32.const', 24]]]],
548
+ ['if', ['f64.ne', ['f64.load', ['local.get', `$${slot}`]], ['f64.const', 0]],
549
+ ['then',
550
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
551
+ ['f64.store', ['local.get', `$${pair}`],
552
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]],
553
+ ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
554
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 16]]]],
555
+ elemStore(out.local, o, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
556
+ ['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]]]],
557
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
558
+ ['br', `$eloop${id}`]]],
559
+ out.ptr]
560
+ }
561
+
523
562
  // Type-unknown receiver: bind the value, branch on ptr-type. HASH walks the
524
563
  // probe table; OBJECT loads the schema's key array (registered statically at
525
564
  // compile time or lazily at runtime by JSON.parse via __jp_schema_get); other
@@ -568,6 +607,26 @@ function emitRuntimeValues(obj) {
568
607
  ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
569
608
  }
570
609
 
610
+ function emitRuntimeEntries(obj) {
611
+ inc('__ptr_type')
612
+ if (!ctx.scope.globals.has('__schema_tbl'))
613
+ ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
614
+ const t = temp('re'), tt = tempI32('ret')
615
+ const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'ree' })
616
+ return typed(['block', ['result', 'f64'],
617
+ ['local.set', `$${t}`, asF64(emit(obj))],
618
+ ['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
619
+ ['if', ['result', 'f64'],
620
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
621
+ ['then', hashEntriesFromTemp(t)],
622
+ ['else', ['if', ['result', 'f64'],
623
+ ['i32.and',
624
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
625
+ ['i32.ne', ['global.get', '$__schema_tbl'], ['i32.const', 0]]],
626
+ ['then', objectEntriesFromTemp(t)],
627
+ ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
628
+ }
629
+
571
630
  // Schema-keyed Object.keys: copy the schema's keys array (a jz Array of
572
631
  // STRINGs registered in __schema_tbl[sid]) into a fresh ARRAY so callers can
573
632
  // mutate without aliasing the schema substrate.
@@ -584,7 +643,7 @@ function objectKeysFromTemp(t) {
584
643
  ['i64.const', LAYOUT.OFFSET_MASK]]]],
585
644
  ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
586
645
  ['local.set', `$${out}`, ['call', '$__alloc_hdr',
587
- ['local.get', `$${n}`], ['local.get', `$${n}`], ['i32.const', 8]]],
646
+ ['local.get', `$${n}`], ['local.get', `$${n}`]]],
588
647
  ['local.set', `$${i}`, ['i32.const', 0]],
589
648
  ['block', `$kbrk${id}`, ['loop', `$kloop${id}`,
590
649
  ['br_if', `$kbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
@@ -612,7 +671,7 @@ function objectValuesFromTemp(t) {
612
671
  ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
613
672
  ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
614
673
  ['local.set', `$${out}`, ['call', '$__alloc_hdr',
615
- ['local.get', `$${n}`], ['local.get', `$${n}`], ['i32.const', 8]]],
674
+ ['local.get', `$${n}`], ['local.get', `$${n}`]]],
616
675
  ['local.set', `$${i}`, ['i32.const', 0]],
617
676
  ['block', `$ovbrk${id}`, ['loop', `$ovloop${id}`,
618
677
  ['br_if', `$ovbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
@@ -624,3 +683,39 @@ function objectValuesFromTemp(t) {
624
683
  ['br', `$ovloop${id}`]]],
625
684
  mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
626
685
  }
686
+
687
+ function objectEntriesFromTemp(t) {
688
+ inc('__alloc_hdr', '__ptr_offset')
689
+ const sid = tempI32('oes'), src = tempI32('oesrc'), n = tempI32('oen')
690
+ const base = tempI32('oebase'), out = tempI32('oeo'), i = tempI32('oei'), pair = tempI32('oep')
691
+ const id = ctx.func.uniq++
692
+ return ['block', ['result', 'f64'],
693
+ ['local.set', `$${sid}`, ['i32.wrap_i64', ['i64.and',
694
+ ['i64.shr_u', ['i64.reinterpret_f64', ['local.get', `$${t}`]], ['i64.const', LAYOUT.AUX_SHIFT]],
695
+ ['i64.const', LAYOUT.AUX_MASK]]]],
696
+ ['local.set', `$${src}`, ['i32.wrap_i64', ['i64.and',
697
+ ['i64.load', ['i32.add', ['global.get', '$__schema_tbl'], ['i32.shl', ['local.get', `$${sid}`], ['i32.const', 3]]]],
698
+ ['i64.const', LAYOUT.OFFSET_MASK]]]],
699
+ ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
700
+ ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
701
+ ['local.set', `$${out}`, ['call', '$__alloc_hdr',
702
+ ['local.get', `$${n}`], ['local.get', `$${n}`]]],
703
+ ['local.set', `$${i}`, ['i32.const', 0]],
704
+ ['block', `$oebrk${id}`, ['loop', `$oeloop${id}`,
705
+ ['br_if', `$oebrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
706
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
707
+ ['i64.store',
708
+ ['local.get', `$${pair}`],
709
+ ['i64.load',
710
+ ['i32.add', ['local.get', `$${src}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
711
+ ['f64.store',
712
+ ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
713
+ ['f64.load',
714
+ ['i32.add', ['local.get', `$${base}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
715
+ ['f64.store',
716
+ ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]],
717
+ mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])],
718
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
719
+ ['br', `$oeloop${id}`]]],
720
+ mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
721
+ }
package/module/string.js CHANGED
@@ -752,12 +752,27 @@ export default (ctx) => {
752
752
  (br $next)))
753
753
  (f64.reinterpret_i64 (local.get $result)))`
754
754
 
755
+ // Empty separator: per JS spec, split into individual byte-chars
756
+ // ("abc".split("") -> ["a","b","c"], "".split("") -> []). Without this
757
+ // guard the main loop advances by plen=0 and spins forever.
755
758
  ctx.core.stdlib['__str_split'] = `(func $__str_split (param $str i64) (param $sep i64) (result f64)
756
759
  (local $slen i32) (local $plen i32) (local $count i32)
757
760
  (local $i i32) (local $j i32) (local $match i32)
758
761
  (local $arr i32) (local $piece_start i32) (local $piece_idx i32)
759
762
  (local.set $slen (call $__str_byteLen (local.get $str)))
760
763
  (local.set $plen (call $__str_byteLen (local.get $sep)))
764
+ (if (i32.eqz (local.get $plen)) (then
765
+ (local.set $arr (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $slen) (i32.const 3)))))
766
+ (i32.store (local.get $arr) (local.get $slen))
767
+ (i32.store (i32.add (local.get $arr) (i32.const 4)) (local.get $slen))
768
+ (local.set $arr (i32.add (local.get $arr) (i32.const 8)))
769
+ (block $de (loop $le
770
+ (br_if $de (i32.ge_s (local.get $i) (local.get $slen)))
771
+ (f64.store (i32.add (local.get $arr) (i32.shl (local.get $i) (i32.const 3)))
772
+ (call $__str_slice (local.get $str) (local.get $i) (i32.add (local.get $i) (i32.const 1))))
773
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
774
+ (br $le)))
775
+ (return (call $__mkptr (i32.const 1) (i32.const 0) (local.get $arr)))))
761
776
  (local.set $count (i32.const 1))
762
777
  (local.set $i (i32.const 0))
763
778
  (block $d1 (loop $l1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jz",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Modern functional JS compiling to WASM",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -24,8 +24,11 @@
24
24
  "test262": "node test/test262.js",
25
25
  "test262:builtins": "node test/test262-builtins.js",
26
26
  "bench": "node bench/bench.mjs",
27
+ "bench:size": "node scripts/bench-size.mjs",
27
28
  "bench:compile": "node scripts/bench-compile.mjs",
28
- "test:bench-pin": "node test/bench-pin.js"
29
+ "test:bench-pin": "node test/bench-pin.js",
30
+ "test:all": "npm test && npm run test:bench-pin",
31
+ "prepublishOnly": "npm run test:all"
29
32
  },
30
33
  "repository": {
31
34
  "type": "git",
@@ -38,8 +41,8 @@
38
41
  "author": "Dmitry Iv",
39
42
  "license": "MIT",
40
43
  "dependencies": {
41
- "subscript": "^10.4.7",
42
- "watr": "^4.5.3"
44
+ "subscript": "^10.4.8",
45
+ "watr": "^4.6.5"
43
46
  },
44
47
  "keywords": [
45
48
  "javascript",
package/src/analyze.js CHANGED
@@ -37,9 +37,9 @@ export const STMT_OPS = new Set([';', 'let', 'const', 'return', 'if', 'for', 'fo
37
37
  export const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
38
38
 
39
39
  /** Distinguish a function block body `{ ... }` from an expression-bodied object literal `({a:1})`.
40
- * Both share the `'{}'` op tag; blocks have a non-`':'` first child (object literals start with key:val pairs). */
40
+ * Both share the `'{}'` op tag; blocks start with statement ops, while object literals start with `:` or `...`. */
41
41
  export const isBlockBody = (body) =>
42
- Array.isArray(body) && body[0] === '{}' && body[1]?.[0] !== ':'
42
+ Array.isArray(body) && body[0] === '{}' && (body.length === 1 || STMT_OPS.has(body[1]?.[0]))
43
43
 
44
44
  /** Extract integer value from AST literal node. Returns null if not a 32-bit integer. */
45
45
  export function intLiteralValue(expr) {
package/src/assemble.js CHANGED
@@ -14,6 +14,25 @@
14
14
 
15
15
  import { parse as parseWat } from 'watr'
16
16
  import { ctx, inc, resolveIncludes, PTR, LAYOUT } from './ctx.js'
17
+
18
+ // Stdlib WAT templates are fixed text (or feature-keyed text from a factory) —
19
+ // `parseWat` of the same string always yields the same tree. Parsing is the
20
+ // dominant cost when a program pulls heavy stdlib (Math pow/sqrt, JSON, regex):
21
+ // it re-tokenizes ~KB of text every compile. Parse once per distinct resolved
22
+ // string, then hand out a deep clone (downstream passes mutate nodes in place).
23
+ // Module-level on purpose: the cache persists across compile() calls.
24
+ const stdlibParseCache = new Map() // resolved WAT string → pristine parsed tree
25
+ const cloneTemplate = (node) => {
26
+ if (!Array.isArray(node)) return node
27
+ const copy = node.map(cloneTemplate)
28
+ if (node.loc != null) copy.loc = node.loc
29
+ return copy
30
+ }
31
+ const parseTemplate = (str) => {
32
+ let tmpl = stdlibParseCache.get(str)
33
+ if (tmpl === undefined) stdlibParseCache.set(str, tmpl = parseWat(str))
34
+ return cloneTemplate(tmpl)
35
+ }
17
36
  import { T, VAL, analyzeValTypes } from './analyze.js'
18
37
  import { optimizeFunc, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from './optimize.js'
19
38
  import { emit } from './emit.js'
@@ -36,7 +55,7 @@ const heapSetIR = value => ctx.memory.shared
36
55
  : ['global.set', '$__heap', value]
37
56
 
38
57
  const ARENA_SAFE_CALLS = new Set([
39
- '$__alloc', '$__alloc_hdr', '$__mkptr',
58
+ '$__alloc', '$__alloc_hdr', '$__alloc_hdr_n', '$__mkptr',
40
59
  '$__ptr_offset', '$__ptr_type', '$__ptr_aux',
41
60
  '$__len', '$__cap', '$__typed_shift', '$__typed_data',
42
61
  ])
@@ -60,7 +79,7 @@ function applyArenaRewind(func, fn, safeCallees) {
60
79
  }
61
80
  if (op === 'call') {
62
81
  const name = node[1]
63
- if (name === '$__alloc' || name === '$__alloc_hdr') hasAlloc = true
82
+ if (name === '$__alloc' || name === '$__alloc_hdr' || name === '$__alloc_hdr_n') hasAlloc = true
64
83
  if (!(safeCallees ?? ARENA_SAFE_CALLS).has(name)) {
65
84
  unsafe = true
66
85
  return
@@ -154,7 +173,7 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
154
173
  for (const [name, { schemaId, schema }] of ctx.schema.autoBox) {
155
174
  inc('__alloc_hdr', '__mkptr')
156
175
  boxInit.push(
157
- ['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
176
+ ['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]],
158
177
  ['f64.store', ['local.get', `$${bt}`],
159
178
  ctx.func.names.has(name) ? ['f64.const', 0] : ['global.get', `$${name}`]],
160
179
  ...schema.slice(1).map((_, i) =>
@@ -195,7 +214,7 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
195
214
  const keys = ctx.schema.list[s]
196
215
  const n = keys.length
197
216
  schemaInit.push(
198
- ['local.set', `$${sarr}`, ['call', '$__alloc_hdr', ['i32.const', n], ['i32.const', n], ['i32.const', 8]]])
217
+ ['local.set', `$${sarr}`, ['call', '$__alloc_hdr', ['i32.const', n], ['i32.const', n]]])
199
218
  for (let k = 0; k < n; k++)
200
219
  schemaInit.push(
201
220
  ['f64.store', ['i32.add', ['local.get', `$${sarr}`], ['i32.const', k * 8]],
@@ -384,7 +403,7 @@ export function pullStdlib(sec) {
384
403
  if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
385
404
  else sec.memory.push(['memory', ['export', '"memory"'], pages])
386
405
  if (ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
387
- sec.funcs.push(...ctx.core._allocRawFuncs.map(s => parseWat(s)))
406
+ sec.funcs.push(...ctx.core._allocRawFuncs.map(parseTemplate))
388
407
  }
389
408
 
390
409
  const stdlibStr = (name) => {
@@ -394,14 +413,14 @@ export function pullStdlib(sec) {
394
413
  ctx.core.extImports ??= new Set()
395
414
  for (const name of Object.keys(ctx.core.stdlib)) {
396
415
  if (name.startsWith('__ext_') && ctx.core.includes.has(name)) {
397
- const parsed = parseWat(stdlibStr(name))
416
+ const parsed = parseTemplate(stdlibStr(name))
398
417
  sec.extStdlib.push(parsed[0] === "module" ? parsed[1] : parsed)
399
418
  ctx.core.extImports.add(name)
400
419
  ctx.core.includes.delete(name)
401
420
  }
402
421
  }
403
422
  for (const n of ctx.core.includes) if (!ctx.core.stdlib[n]) console.error("MISSING stdlib:", n)
404
- sec.stdlib.push(...[...ctx.core.includes].map(n => parseWat(stdlibStr(n))))
423
+ sec.stdlib.push(...[...ctx.core.includes].map(n => parseTemplate(stdlibStr(n))))
405
424
  }
406
425
 
407
426
  export function syncImports(sec) {
@@ -449,7 +468,11 @@ export function optimizeModule(sec) {
449
468
  }
450
469
  }
451
470
  if (!cfg || cfg.hoistConstantPool !== false)
452
- hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => ctx.scope.globals.set(name, wat))
471
+ hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => {
472
+ ctx.scope.globals.set(name, wat)
473
+ const m = wat.match(/\(global\s+\$?\S+\s+(?:\(mut\s+)?(i32|i64|f64|f32)/)
474
+ if (m) ctx.scope.globalTypes.set(name, m[1])
475
+ })
453
476
 
454
477
  // Second promoteGlobals pass disabled: promoting hoistConstantPool's __fc*
455
478
  // globals regressed the watr perf micro-pin (WASM compile time increased).
package/src/codegen.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * AST → jz source codegen.
3
+ *
4
+ * Pretty-prints a jzify-transformed AST back to jz source text. CLI-only
5
+ * (`jz jzify file.js` → `file.jz`); the compile path consumes the AST directly
6
+ * and never round-trips through source.
7
+ *
8
+ * @module codegen
9
+ */
10
+
11
+ const INDENT = ' '
12
+ const prec = { '=': 1, '+=': 1, '-=': 1, '*=': 1, '/=': 1, '%=': 1, '&=': 1, '|=': 1, '^=': 1, '>>=': 1, '<<=': 1, '>>>=': 1, '||=': 1, '&&=': 1,
13
+ '??': 2, '||': 3, '&&': 4, '|': 5, '^': 6, '&': 7, '===': 8, '!==': 8, '==': 8, '!=': 8,
14
+ '<': 9, '>': 9, '<=': 9, '>=': 9, '<<': 10, '>>': 10, '>>>': 10,
15
+ '+': 11, '-': 11, '*': 12, '/': 12, '%': 12, '**': 13 }
16
+
17
+ /** Wrap statement in { } if not already a block */
18
+ function wrapBlock(node, depth) {
19
+ if (Array.isArray(node) && node[0] === '{}') return codegen(node, depth)
20
+ return '{ ' + codegen(node, depth) + '; }'
21
+ }
22
+
23
+ /** Generate jz source from AST. Enforces semicolons. */
24
+ export function codegen(node, depth = 0) {
25
+ if (node == null) return ''
26
+ if (typeof node === 'number') return String(node)
27
+ if (typeof node === 'bigint') return node + 'n'
28
+ if (typeof node === 'string') return node
29
+ if (!Array.isArray(node)) return String(node)
30
+
31
+ const [op, ...a] = node
32
+ const ind = INDENT.repeat(depth), ind1 = INDENT.repeat(depth + 1)
33
+
34
+ // Literal: [, value]
35
+ if (op == null) return typeof a[0] === 'string' ? JSON.stringify(a[0]) : a[0] == null ? 'null' : String(a[0]) + (typeof a[0] === 'bigint' ? 'n' : '')
36
+
37
+ // Statements
38
+ if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
39
+ if (op === '{}') {
40
+ // Discriminate object literal / destructuring pattern from block.
41
+ // Object: `:` key-value, `,` of object-pattern items (id / `:` / `...` / `= default`),
42
+ // lone string shorthand. Empty `{}` outputs the same string either way.
43
+ const body = a[0]
44
+ const isObjItem = (n) => typeof n === 'string' ||
45
+ (Array.isArray(n) && (n[0] === ':' || n[0] === '...' || n[0] === 'as' ||
46
+ (n[0] === '=' && typeof n[1] === 'string')))
47
+ const isObj = body == null ? false
48
+ : typeof body === 'string' ? true
49
+ : Array.isArray(body) && (body[0] === ':' || body[0] === '...' || body[0] === 'as' ||
50
+ (body[0] === ',' && body.slice(1).every(isObjItem)))
51
+ if (isObj) {
52
+ if (typeof body === 'string') return '{ ' + body + ' }'
53
+ if (body[0] === ',') return '{ ' + body.slice(1).map(x => codegen(x)).join(', ') + ' }'
54
+ return '{ ' + codegen(body) + ' }'
55
+ }
56
+ // Block: body is null, a single statement, or [';', ...stmts]
57
+ const stmts = body == null ? [] : (Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body])
58
+ const rendered = stmts.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
59
+ return '{\n' + ind1 + rendered + (rendered ? ';' : '') + '\n' + ind + '}'
60
+ }
61
+
62
+ // Declarations
63
+ if (op === 'let' || op === 'const') return op + ' ' + a.map(d => codegen(d, depth)).join(', ')
64
+ if (op === 'export') { const inner = codegen(a[0], depth); return inner ? 'export ' + inner : '' }
65
+ if (op === 'default') return 'default ' + codegen(a[0], depth)
66
+
67
+ // Control flow
68
+ if (op === 'if') {
69
+ const cond = codegen(a[0]), then = wrapBlock(a[1], depth)
70
+ return a[2] != null
71
+ ? 'if (' + cond + ') ' + then + ' else ' + wrapBlock(a[2], depth)
72
+ : 'if (' + cond + ') ' + then
73
+ }
74
+ if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
75
+ if (op === 'for') {
76
+ if (a.length === 2) { // ['for', head, body] — subscript shape
77
+ const [head, body] = a
78
+ if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
79
+ return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + wrapBlock(body, depth)
80
+ // ['let'/'const', ['in'/'of', name, obj]] — subscript wraps var→let around in/of
81
+ if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
82
+ return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' + wrapBlock(body, depth)
83
+ // C-style head [';', init, cond, update] is positional — empty slots are valid,
84
+ // must not flow through the generic `;` joiner (which adds newlines + a trailing `;`).
85
+ if (Array.isArray(head) && head[0] === ';')
86
+ return 'for (' + (head[1] == null ? '' : codegen(head[1])) + '; ' + (head[2] == null ? '' : codegen(head[2])) + '; ' + (head[3] == null ? '' : codegen(head[3])) + ') ' + wrapBlock(body, depth)
87
+ return 'for (' + codegen(head) + ') ' + wrapBlock(body, depth)
88
+ }
89
+ return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + wrapBlock(a[3], depth)
90
+ }
91
+ if (op === 'return') return 'return ' + codegen(a[0])
92
+ if (op === 'throw') return 'throw ' + codegen(a[0])
93
+ if (op === 'break') return 'break'
94
+ if (op === 'continue') return 'continue'
95
+ // catch with optional binding: ['catch', tryBlock, catchBody] or ['catch', tryBlock, paramName, catchBody]
96
+ if (op === 'catch') {
97
+ if (a.length === 3) return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
98
+ return 'try ' + codegen(a[0], depth) + ' catch ' + codegen(a[1], depth)
99
+ }
100
+
101
+ // Arrow
102
+ if (op === '=>') {
103
+ // Params: already wrapped in () by parser, or bare name
104
+ const p = a[0]
105
+ const params = Array.isArray(p) && p[0] === '()' ? codegen(p) : '(' + codegen(p) + ')'
106
+ const body = a[1]
107
+ const isBlock = Array.isArray(body) && (body[0] === '{}' || body[0] === ';' || body[0] === 'return')
108
+ const bodyStr = Array.isArray(body) && body[0] !== '{}' && isBlock
109
+ ? '{ ' + codegen(body, depth) + '; }'
110
+ : codegen(body, depth)
111
+ return params + ' => ' + bodyStr
112
+ }
113
+
114
+ // Grouping parens / function call
115
+ if (op === '()') {
116
+ if (a.length === 1) return '(' + (a[0] == null ? '' : codegen(a[0])) + ')'
117
+ return codegen(a[0]) + '(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
118
+ }
119
+
120
+ // Property access
121
+ if (op === '.') return codegen(a[0]) + '.' + a[1]
122
+ if (op === '?.') return codegen(a[0]) + '?.' + a[1]
123
+ if (op === '?.[]') return codegen(a[0]) + '?.[' + codegen(a[1]) + ']'
124
+ if (op === '?.()') return codegen(a[0]) + '?.(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
125
+ if (op === '[]') {
126
+ // Array literal: ['[]', body] (length 2 → a.length 1). body may be null (empty),
127
+ // a single element, or a [',', ...items] sequence.
128
+ if (a.length === 1) {
129
+ if (a[0] == null) return '[]'
130
+ const body = a[0]
131
+ if (Array.isArray(body) && body[0] === ',') return '[' + body.slice(1).map(x => codegen(x)).join(', ') + ']'
132
+ return '[' + codegen(body) + ']'
133
+ }
134
+ // Subscript: ['[]', obj, idx]
135
+ return codegen(a[0]) + '[' + codegen(a[1]) + ']'
136
+ }
137
+ if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
138
+ if (op === 'str') return JSON.stringify(a[0])
139
+ if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
140
+
141
+ // Comma
142
+ if (op === ',') return a.map(x => codegen(x)).join(', ')
143
+ // Template literal: alternating string/expr parts. String parts are [null, "str"], expr parts are AST nodes.
144
+ if (op === '`') return '`' + a.map(p => {
145
+ if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') return p[1].replace(/[`\\$]/g, c => '\\' + c)
146
+ return '${' + codegen(p) + '}'
147
+ }).join('') + '`'
148
+
149
+ // Spread
150
+ if (op === '...') return '...' + codegen(a[0])
151
+
152
+ // Import / export rename
153
+ if (op === 'import') return 'import ' + codegen(a[0])
154
+ if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
155
+ if (op === 'as') return codegen(a[0]) + ' as ' + codegen(a[1])
156
+
157
+ // Unary prefix
158
+ if (a.length === 1) {
159
+ if (op === '++' || op === '--') return a[0] == null ? op : op + codegen(a[0])
160
+ if (op === 'typeof') return 'typeof ' + codegen(a[0])
161
+ if (op === 'u-') return '-' + codegen(a[0])
162
+ if (op === 'u+') return '+' + codegen(a[0])
163
+ return op + codegen(a[0])
164
+ }
165
+
166
+ // Postfix
167
+ if (a.length === 2 && a[1] === null) return codegen(a[0]) + op
168
+
169
+ // Binary
170
+ if (a.length === 2 && prec[op]) return codegen(a[0]) + ' ' + op + ' ' + codegen(a[1])
171
+
172
+ // Ternary
173
+ if (op === '?' || op === '?:') return codegen(a[0]) + ' ? ' + codegen(a[1]) + ' : ' + codegen(a[2])
174
+
175
+ // Fallback
176
+ return op + '(' + a.map(x => codegen(x)).join(', ') + ')'
177
+ }
package/src/compile.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  T, VAL, analyzeValTypes, analyzeIntCertain, analyzeLocals,
32
32
  analyzePtrUnboxable, typedElemAux, invalidateLocalsCache,
33
33
  analyzeBoxedCaptures, updateRep, inferStringParams,
34
+ isBlockBody,
34
35
  } from './analyze.js'
35
36
  import { optimizeFunc, treeshake } from './optimize.js'
36
37
  import { emit, emitter, emitFlat, emitBody } from './emit.js'
@@ -138,6 +139,17 @@ const tcoTailRewrite = (ir, resultType) => {
138
139
  return ir
139
140
  }
140
141
 
142
+ const ensureThrowRuntime = (sec) => {
143
+ if (!ctx.runtime.throws) return
144
+
145
+ if (!ctx.scope.globals.has('__jz_last_err_bits'))
146
+ ctx.scope.globals.set('__jz_last_err_bits', '(global $__jz_last_err_bits (mut i64) (i64.const 0))')
147
+ if (!sec.tags.some(t => Array.isArray(t) && t[0] === 'tag' && t[1] === '$__jz_err'))
148
+ sec.tags.push(['tag', '$__jz_err', ['param', 'f64']])
149
+ if (!sec.tags.some(t => Array.isArray(t) && t[0] === 'export' && t[1] === '"__jz_last_err_bits"'))
150
+ sec.tags.push(['export', '"__jz_last_err_bits"', ['global', '$__jz_last_err_bits']])
151
+ }
152
+
141
153
  // === Module compilation ===
142
154
 
143
155
  const cloneRepMap = map => map ? new Map([...map].map(([k, v]) => [k, { ...v }])) : null
@@ -158,7 +170,7 @@ function analyzeFuncForEmit(func, programFacts) {
158
170
  const { name, body, sig } = func
159
171
  enterFunc(func)
160
172
 
161
- const block = Array.isArray(body) && body[0] === '{}' && body[1]?.[0] !== ':'
173
+ const block = isBlockBody(body)
162
174
  ctx.func.boxed = new Map()
163
175
  ctx.func.repByLocal = null
164
176
  ctx.types.typedElem = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : null
@@ -375,7 +387,8 @@ function emitFunc(func, funcFacts, programFacts) {
375
387
  *
376
388
  * Result rebox cases (then reinterpret to i64 at the boundary):
377
389
  * - sig.ptrKind != null → mkPtrIR(ptrKind, ptrAux ?? 0, callIR)
378
- * - sig.results[0] = i32 → f64.convert_i32_s(callIR)
390
+ * - sig.results[0] = i32 → f64.convert_i32_s(callIR), or `_u` when
391
+ * sig.unsignedResult (preserves `(x >>> 0)` ∈ [0, 2³²))
379
392
  * - sig.results[0] = f64 → callIR (some params narrowed but result stayed f64)
380
393
  */
381
394
  function synthesizeBoundaryWrappers() {
@@ -408,7 +421,7 @@ function synthesizeBoundaryWrappers() {
408
421
  const ptrType = valKindToPtr(sig.ptrKind)
409
422
  body = mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR)
410
423
  } else if (sig.results[0] === 'i32') {
411
- body = ['f64.convert_i32_s', callIR]
424
+ body = [sig.unsignedResult ? 'f64.convert_i32_u' : 'f64.convert_i32_s', callIR]
412
425
  } else {
413
426
  body = callIR
414
427
  }
@@ -485,7 +498,7 @@ function emitClosureBody(cb) {
485
498
  }
486
499
 
487
500
  // Emit body
488
- const block = Array.isArray(cb.body) && cb.body[0] === '{}' && cb.body[1]?.[0] !== ':'
501
+ const block = isBlockBody(cb.body)
489
502
  let bodyIR
490
503
  if (block) {
491
504
  invalidateLocalsCache(cb.body)
@@ -612,7 +625,7 @@ function emitClosureBody(cb) {
612
625
  ['then', ['local.set', `$${restLen}`, ['i32.const', restSlots]]]])
613
626
  fn.push(['local.set', `$${restOff}`,
614
627
  ['call', '$__alloc_hdr',
615
- ['local.get', `$${restLen}`], ['local.get', `$${restLen}`], ['i32.const', 8]]])
628
+ ['local.get', `$${restLen}`], ['local.get', `$${restLen}`]]])
616
629
  for (let i = 0; i < restSlots; i++) {
617
630
  fn.push(['if', ['i32.gt_s', ['local.get', `$${restLen}`], ['i32.const', i]],
618
631
  ['then', ['f64.store',
@@ -775,12 +788,6 @@ export default function compile(ast, profiler) {
775
788
 
776
789
  // Memory section deferred — emitted after resolveIncludes() when __alloc is needed
777
790
 
778
- if (ctx.runtime.throws) {
779
- ctx.scope.globals.set('__jz_last_err_bits', '(global $__jz_last_err_bits (mut i64) (i64.const 0))')
780
- sec.tags.push(['tag', '$__jz_err', ['param', 'f64']])
781
- sec.tags.push(['export', '"__jz_last_err_bits"', ['global', '$__jz_last_err_bits']])
782
- }
783
-
784
791
  if (ctx.closure.table?.length)
785
792
  sec.table.push(['table', ['export', '"__jz_table"'], ctx.closure.table.length, 'funcref'])
786
793
 
@@ -829,6 +836,8 @@ export default function compile(ast, profiler) {
829
836
 
830
837
  optimizeModule(sec)
831
838
 
839
+ ensureThrowRuntime(sec)
840
+
832
841
  // Populate globals (after __start — const folding may update declarations)
833
842
  sec.globals.push(...[...ctx.scope.globals.values()].filter(g => g).map(g => parseWat(g)))
834
843
 
@@ -918,7 +927,7 @@ export default function compile(ast, profiler) {
918
927
  const { callCount } = treeshake(
919
928
  [{ arr: sec.stdlib }, { arr: sec.funcs }, { arr: sec.start }],
920
929
  [...sec.start, ...sec.elem, ...sec.customs, ...sec.extStdlib, ...sec.imports],
921
- { removeDead: !optCfg || optCfg.treeshake !== false }
930
+ { removeDead: !optCfg || optCfg.treeshake !== false, globals: sec.globals }
922
931
  )
923
932
 
924
933
  // Reorder non-import funcs by call count: hot callees get low LEB128 indices.