jz 0.3.1 → 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/src/emit.js CHANGED
@@ -536,7 +536,7 @@ export function emitDecl(...inits) {
536
536
  ctx.func.locals.set(innerName, 'f64')
537
537
  result.push(
538
538
  ['local.set', `$${innerName}`, ['local.get', `$${name}`]],
539
- ['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
539
+ ['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]],
540
540
  ['f64.store', ['local.get', `$${bt}`], ['local.get', `$${name}`]],
541
541
  ...schema.slice(1).map((_, j) =>
542
542
  ['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (j + 1) * 8]], ['f64.const', 0]]),
@@ -1879,6 +1879,7 @@ export const emitter = {
1879
1879
  const callIR = typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0])
1880
1880
  if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
1881
1881
  if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
1882
+ if (func.sig.unsignedResult) callIR.unsigned = true
1882
1883
  return callIR
1883
1884
  }
1884
1885
  }
@@ -2282,6 +2283,7 @@ export const emitter = {
2282
2283
  arrayIR], func.sig.results[0])
2283
2284
  if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
2284
2285
  if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
2286
+ if (func.sig.unsignedResult) callIR.unsigned = true
2285
2287
  return callIR
2286
2288
  }
2287
2289
 
@@ -2302,6 +2304,9 @@ export const emitter = {
2302
2304
  const callIR = typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64')
2303
2305
  if (func?.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
2304
2306
  if (func?.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
2307
+ // Unsigned-uint32 result (every tail was `>>>`): consumer's asF64 must use
2308
+ // `f64.convert_i32_u` instead of `_s`, preserving the [0, 2^32) range.
2309
+ if (func?.sig.unsignedResult) callIR.unsigned = true
2305
2310
  return callIR
2306
2311
  }
2307
2312
 
package/src/fuse.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * AST-level fusion passes.
3
+ *
4
+ * Unlike src/optimize.js (which is a pure WAT IR→IR rewrite, post-emission),
5
+ * these rewrites need the *raw, pre-resolution* AST shape — bindings still named,
6
+ * arrow bodies still inline — so they run inside prepare(), before scope
7
+ * resolution and emit. They mutate the AST in place and always fire (cheap; the
8
+ * shape guards are strict enough that misfires are impossible).
9
+ *
10
+ * @module fuse
11
+ */
12
+
13
+ /** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
14
+ * into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
15
+ * intermediate array. Only fires on shapes where every use of `b` is a numeric `b[idx]` read or a
16
+ * `b.length` read, the arrow is pure with a single named param, and `b` is not referenced after the
17
+ * consumer for-loop. Preserves observable behavior because the arrow's pure-expression body has no
18
+ * order-dependent effects. */
19
+ export function fuseSparseMapReads(root) {
20
+ walkSparse(root)
21
+ }
22
+ function walkSparse(node) {
23
+ if (!Array.isArray(node)) return
24
+ for (let i = 1; i < node.length; i++) walkSparse(node[i])
25
+ if (node[0] === ';') tryFuseInBlock(node)
26
+ }
27
+ function tryFuseInBlock(seq) {
28
+ for (let i = 1; i < seq.length - 1; i++) {
29
+ const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
30
+ if (fused) {
31
+ seq.splice(i, 2, ...fused)
32
+ i-- // re-examine same position (chained fusions)
33
+ }
34
+ }
35
+ }
36
+ function tryFusePair(decl, forNode, seq, declIdx) {
37
+ if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
38
+ if (decl.length !== 2) return null // single binding only
39
+ const bind = decl[1]
40
+ if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
41
+ const NAME = bind[1], rhs = bind[2]
42
+ if (!Array.isArray(rhs) || rhs[0] !== '()') return null
43
+ const callee = rhs[1]
44
+ if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
45
+ const RECV = callee[1]
46
+ if (typeof RECV !== 'string' || RECV === NAME) return null
47
+ const arrow = rhs[2]
48
+ if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
49
+ // Single-name param only: `x => …` or `(x) => …`
50
+ const ap = arrow[1]
51
+ const PARAM = typeof ap === 'string' ? ap :
52
+ (Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
53
+ if (!PARAM || PARAM === NAME || PARAM === RECV) return null
54
+ // Body: single-expression arrow only (block bodies skipped — could extend later).
55
+ const aBody = arrow[2]
56
+ if (Array.isArray(aBody) && aBody[0] === '{}') return null
57
+ if (!isPureSparseArrowBody(aBody, PARAM)) return null
58
+ // For-loop: ['for', [';', initStmt, cond, inc], body]
59
+ if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
60
+ const head = forNode[1]
61
+ if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
62
+ const cond = head[2], forBody = forNode[2]
63
+ // Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
64
+ if (!hasOnlySparseUses(cond, NAME)) return null
65
+ if (!hasOnlySparseUses(forBody, NAME)) return null
66
+ if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
67
+ // `NAME` must not be read after the for-loop in the same block.
68
+ for (let k = declIdx + 2; k < seq.length; k++) {
69
+ if (refsName(seq[k], NAME)) return null
70
+ }
71
+ // RECV must not be reassigned inside the for-loop (would invalidate substitution).
72
+ if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
73
+ // PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
74
+ if (bindsName(forNode, PARAM)) return null
75
+ // Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
76
+ const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
77
+ const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
78
+ const newHead = [';', head[1], newCond, head[3]]
79
+ return [['for', newHead, newBody]]
80
+ }
81
+ function isPureSparseArrowBody(n, PARAM) {
82
+ if (typeof n === 'string') return true
83
+ if (!Array.isArray(n)) return true
84
+ const op = n[0]
85
+ // Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
86
+ if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
87
+ if (op === '=>') return false // nested closure is opaque
88
+ if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
89
+ if (op === '=') return false
90
+ for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
91
+ return true
92
+ }
93
+ function hasOnlySparseUses(n, NAME) {
94
+ if (typeof n === 'string') return n !== NAME
95
+ if (!Array.isArray(n)) return true
96
+ const op = n[0]
97
+ if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
98
+ if (op === '.' && n[1] === NAME) {
99
+ if (n[2] === 'length') return true
100
+ return false // any other property access on NAME is opaque
101
+ }
102
+ for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
103
+ return true
104
+ }
105
+ function hasAnyIndexedRead(n, NAME) {
106
+ if (!Array.isArray(n)) return false
107
+ if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
108
+ for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
109
+ return false
110
+ }
111
+ function refsName(n, NAME) {
112
+ if (typeof n === 'string') return n === NAME
113
+ if (!Array.isArray(n)) return false
114
+ for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
115
+ return false
116
+ }
117
+ function assignsName(n, NAME) {
118
+ if (!Array.isArray(n)) return false
119
+ const op = n[0]
120
+ if ((op === '=' || op === '++' || op === '--' ||
121
+ (typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
122
+ && n[1] === NAME) return true
123
+ for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
124
+ return false
125
+ }
126
+ function bindsName(n, NAME) {
127
+ if (!Array.isArray(n)) return false
128
+ const op = n[0]
129
+ if ((op === 'let' || op === 'const')) {
130
+ for (let i = 1; i < n.length; i++) {
131
+ const bind = n[i]
132
+ if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
133
+ }
134
+ }
135
+ if (op === '=>') {
136
+ const p = n[1]
137
+ if (p === NAME) return true
138
+ if (Array.isArray(p)) {
139
+ if (p[0] === '()' && p[1] === NAME) return true
140
+ // skip deeper destructuring forms — conservative
141
+ }
142
+ }
143
+ for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
144
+ return false
145
+ }
146
+ function substSparse(n, NAME, RECV, PARAM, arrowBody) {
147
+ if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
148
+ if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
149
+ if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
150
+ const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
151
+ return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
152
+ }
153
+ return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
154
+ }
155
+ function cloneAndBind(node, PARAM, replacement) {
156
+ if (node === PARAM) return replacement
157
+ if (!Array.isArray(node)) return node
158
+ return node.map((c, i) => i === 0 ? c : cloneAndBind(c, PARAM, replacement))
159
+ }
package/src/ir.js CHANGED
@@ -391,7 +391,8 @@ export function truthyIR(e) {
391
391
  }
392
392
  // Fresh pointer constructors never produce nullish. Treat as always truthy.
393
393
  if (e[0] === 'call' && typeof e[1] === 'string' &&
394
- (e[1].startsWith('$__mkptr') || e[1] === '$__alloc' || e[1].startsWith('$__alloc_hdr'))) {
394
+ (e[1].startsWith('$__mkptr') || e[1] === '$__alloc' ||
395
+ e[1] === '$__alloc_hdr' || e[1].startsWith('$__alloc_hdr_'))) {
395
396
  return typed(['i32.const', 1], 'i32')
396
397
  }
397
398
  // Pointer-typed local reads: value is never a plain number — truthy iff not nullish.
@@ -647,11 +648,17 @@ export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal) {
647
648
  * ptr — f64 IR expression: __mkptr(type, aux, local).
648
649
  * Caller emits init, fills via local, then uses ptr (or local for further work). */
649
650
  export function allocPtr({ type, aux = 0, len, cap, stride = 8, tag = 'ap' }) {
650
- inc('__alloc_hdr')
651
+ // stride=8 (f64 slots — Array/HASH/OBJECT) hits the specialized __alloc_hdr which
652
+ // hardcodes the multiply. Everything else (Set:16, Map probe:24, raw bytes:1) goes
653
+ // through the generic __alloc_hdr_n(len, cap, stride).
651
654
  const local = tempI32(tag)
652
655
  const irOf = v => typeof v === 'number' ? ['i32.const', v] : v
653
- const init = ['local.set', `$${local}`,
654
- ['call', '$__alloc_hdr', irOf(len), irOf(cap == null ? len : cap), ['i32.const', stride]]]
656
+ const args = [irOf(len), irOf(cap == null ? len : cap)]
657
+ let helper
658
+ if (stride === 8) helper = '__alloc_hdr'
659
+ else { helper = '__alloc_hdr_n'; args.push(['i32.const', stride]) }
660
+ inc(helper)
661
+ const init = ['local.set', `$${local}`, ['call', '$' + helper, ...args]]
655
662
  const ptr = mkPtrIR(type, aux, ['local.get', `$${local}`])
656
663
  return { local, init, ptr }
657
664
  }
package/src/jzify.js CHANGED
@@ -31,7 +31,7 @@ export default function jzify(ast) {
31
31
  const names = new Set()
32
32
  ast = hoistVars(ast, names)
33
33
  if (names.size) ast = prependDecls(ast, names)
34
- return foldStaticExportHelpers(transformScope(ast))
34
+ return foldStaticExportHelpers(canonicalizeObjectIdioms(transformScope(ast)))
35
35
  }
36
36
 
37
37
  /**
@@ -80,7 +80,10 @@ function hoistVars(node, names) {
80
80
  if (op === 'for') {
81
81
  const head = node[1]
82
82
  let h2
83
- if (Array.isArray(head) && head[0] === 'var' && Array.isArray(head[1]) &&
83
+ const normalizedHead = normalizeForDeclHead(head, names)
84
+ if (normalizedHead) {
85
+ h2 = normalizedHead
86
+ } else if (Array.isArray(head) && head[0] === 'var' && Array.isArray(head[1]) &&
84
87
  (head[1][0] === 'in' || head[1][0] === 'of') && typeof head[1][1] === 'string') {
85
88
  names.add(head[1][1])
86
89
  h2 = [head[1][0], head[1][1], hoistVars(head[1][2], names)]
@@ -137,6 +140,27 @@ function prependDecls(body, names) {
137
140
  return body == null ? decl : [';', decl, body]
138
141
  }
139
142
 
143
+ function normalizeForDeclHead(head, names) {
144
+ if (!Array.isArray(head) || (head[0] !== 'var' && head[0] !== 'let' && head[0] !== 'const') || head.length !== 2) return null
145
+ const kind = head[0]
146
+ const expr = head[1]
147
+ if (!Array.isArray(expr)) return null
148
+ if (expr.length >= 3 && Array.isArray(expr[1]) &&
149
+ (expr[1][0] === 'in' || expr[1][0] === 'of') && typeof expr[1][1] === 'string') {
150
+ const iter = expr[1]
151
+ return [iter[0], normalizeForDecl(kind, iter[1], names), hoistVars([expr[0], iter[2], ...expr.slice(2)], names)]
152
+ }
153
+ return null
154
+ }
155
+
156
+ function normalizeForDecl(kind, name, names) {
157
+ if (kind === 'var') {
158
+ names.add(name)
159
+ return name
160
+ }
161
+ return [kind, name]
162
+ }
163
+
140
164
  /** Convert a named function declaration to a hoisted const arrow */
141
165
  function hoistFnDecl(name, params, body) {
142
166
  const [p2, b2] = lowerArguments(params, body)
@@ -507,12 +531,16 @@ const handlers = {
507
531
  const clean = cases.map(c => {
508
532
  if (c[0] === 'case' && Array.isArray(c[2]) && c[2][0] === ';') {
509
533
  const body = c[2].slice(1).filter(s => typeof s !== 'number')
510
- return ['case', c[1], body.length === 1 ? body[0] : [';', ...body]]
534
+ const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
535
+ return ['case', c[1], stripped]
511
536
  }
512
537
  if (c[0] === 'default' && Array.isArray(c[1]) && c[1][0] === ';') {
513
538
  const body = c[1].slice(1).filter(s => s != null && typeof s !== 'number')
514
- return ['default', body.length === 1 ? body[0] : [';', ...body]]
539
+ const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
540
+ return ['default', stripped]
515
541
  }
542
+ if (c[0] === 'case') return ['case', c[1], stripTerminalSwitchBreak(c[2])]
543
+ if (c[0] === 'default') return ['default', stripTerminalSwitchBreak(c[1])]
516
544
  return c
517
545
  })
518
546
  return transformSwitch(disc, clean)
@@ -589,7 +617,10 @@ function transform(node) {
589
617
  const [op, ...args] = node
590
618
  if (op == null) return node
591
619
  const h = handlers[op]
592
- return (h && h(...args)) ?? (h ? [op, ...args.map(transform)] : [op, ...args.map(transform)])
620
+ // A handler that returns nullish (including no `return`) means "no rewrite at
621
+ // this node" — fall through to a generic recurse. `??` (not `||`) so handlers
622
+ // like `'==='` can legitimately return `0`.
623
+ return (h && h(...args)) ?? [op, ...args.map(transform)]
593
624
  }
594
625
 
595
626
  // Esbuild emits a small ESM helper:
@@ -713,12 +744,98 @@ function replaceStaticExportReads(node, rewrites) {
713
744
  return node.map((part, i) => i === 0 ? part : replaceStaticExportReads(part, rewrites))
714
745
  }
715
746
 
747
+ function canonicalizeObjectIdioms(node) {
748
+ if (node == null || typeof node !== 'object' || !Array.isArray(node)) return node
749
+
750
+ const out = node.map((part, i) => i === 0 ? part : canonicalizeObjectIdioms(part))
751
+
752
+ const hasOwnCall = objectHasOwnPropertyCall(out)
753
+ if (hasOwnCall) return ['()', ['.', hasOwnCall.obj, 'hasOwnProperty'], hasOwnCall.key]
754
+
755
+ if (out[0] === '&&') {
756
+ const leftCtor = constructorIsObject(out[1])
757
+ const rightKeys = objectKeysLengthZero(out[2])
758
+ if (leftCtor && rightKeys && astEqual(leftCtor.obj, rightKeys.obj)) return out[2]
759
+
760
+ const leftKeys = objectKeysLengthZero(out[1])
761
+ const rightCtor = constructorIsObject(out[2])
762
+ if (leftKeys && rightCtor && astEqual(leftKeys.obj, rightCtor.obj)) return out[1]
763
+ }
764
+
765
+ return out
766
+ }
767
+
768
+ function objectHasOwnPropertyCall(node) {
769
+ if (!Array.isArray(node) || node[0] !== '()') return null
770
+ const callee = node[1]
771
+ if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'call') return null
772
+ if (!Array.isArray(callee[1]) || callee[1][0] !== '.' || callee[1][1] !== 'Object' || callee[1][2] !== 'hasOwnProperty') return null
773
+ const args = callArgs(node.slice(2))
774
+ if (args.length < 2) return null
775
+ return { obj: args[0], key: args[1] }
776
+ }
777
+
778
+ function constructorIsObject(node) {
779
+ if (!Array.isArray(node) || (node[0] !== '===' && node[0] !== '==')) return null
780
+ const left = constructorReceiver(node[1])
781
+ if (left && node[2] === 'Object') return { obj: left }
782
+ const right = constructorReceiver(node[2])
783
+ if (right && node[1] === 'Object') return { obj: right }
784
+ return null
785
+ }
786
+
787
+ function constructorReceiver(node) {
788
+ return Array.isArray(node) && node[0] === '.' && node[2] === 'constructor' ? node[1] : null
789
+ }
790
+
791
+ function objectKeysLengthZero(node) {
792
+ if (!Array.isArray(node) || (node[0] !== '===' && node[0] !== '==')) return null
793
+ const left = objectKeysLengthReceiver(node[1])
794
+ if (left && isZeroLiteral(node[2])) return { obj: left }
795
+ const right = objectKeysLengthReceiver(node[2])
796
+ if (right && isZeroLiteral(node[1])) return { obj: right }
797
+ return null
798
+ }
799
+
800
+ function objectKeysLengthReceiver(node) {
801
+ if (!Array.isArray(node) || node[0] !== '.' || node[2] !== 'length') return null
802
+ const call = node[1]
803
+ if (!Array.isArray(call) || call[0] !== '()') return null
804
+ const callee = call[1]
805
+ if (!Array.isArray(callee) || callee[0] !== '.' || callee[1] !== 'Object' || callee[2] !== 'keys') return null
806
+ const args = callArgs(call.slice(2))
807
+ return args.length === 1 ? args[0] : null
808
+ }
809
+
810
+ function isZeroLiteral(node) {
811
+ return Array.isArray(node) && node[0] == null && node[1] === 0
812
+ }
813
+
814
+ function astEqual(a, b) {
815
+ return JSON.stringify(a) === JSON.stringify(b)
816
+ }
817
+
716
818
  function cloneAst(node) {
717
819
  if (node == null || typeof node !== 'object') return node
718
820
  if (!Array.isArray(node)) return node
719
821
  return node.map(cloneAst)
720
822
  }
721
823
 
824
+ function stripTerminalSwitchBreak(body) {
825
+ if (!Array.isArray(body)) return body
826
+ if (body[0] === 'break') return null
827
+ if (body[0] === '{}') {
828
+ const inner = stripTerminalSwitchBreak(body[1])
829
+ if (inner == null) return ['{}', [';']]
830
+ return ['{}', Array.isArray(inner) && inner[0] === ';' ? inner : [';', inner]]
831
+ }
832
+ if (body[0] !== ';') return body
833
+
834
+ const stmts = body.slice(1)
835
+ if (Array.isArray(stmts.at(-1)) && stmts.at(-1)[0] === 'break') stmts.pop()
836
+ return stmts.length === 0 ? null : stmts.length === 1 ? stmts[0] : [';', ...stmts]
837
+ }
838
+
722
839
  /** Transform switch statement to if/else chain. */
723
840
  let swIdx = 0
724
841
  function transformSwitch(discriminant, cases) {
@@ -742,173 +859,3 @@ function transformSwitch(discriminant, cases) {
742
859
  if (chain) stmts.push(chain)
743
860
  return [';', ...stmts]
744
861
  }
745
-
746
- // === AST → jz source codegen ===
747
-
748
- const INDENT = ' '
749
- const prec = { '=': 1, '+=': 1, '-=': 1, '*=': 1, '/=': 1, '%=': 1, '&=': 1, '|=': 1, '^=': 1, '>>=': 1, '<<=': 1, '>>>=': 1, '||=': 1, '&&=': 1,
750
- '??': 2, '||': 3, '&&': 4, '|': 5, '^': 6, '&': 7, '===': 8, '!==': 8, '==': 8, '!=': 8,
751
- '<': 9, '>': 9, '<=': 9, '>=': 9, '<<': 10, '>>': 10, '>>>': 10,
752
- '+': 11, '-': 11, '*': 12, '/': 12, '%': 12, '**': 13 }
753
-
754
- /** Wrap statement in { } if not already a block */
755
- function wrapBlock(node, depth) {
756
- if (Array.isArray(node) && node[0] === '{}') return codegen(node, depth)
757
- return '{ ' + codegen(node, depth) + '; }'
758
- }
759
-
760
- /** Generate jz source from AST. Enforces semicolons. */
761
- export function codegen(node, depth = 0) {
762
- if (node == null) return ''
763
- if (typeof node === 'number') return String(node)
764
- if (typeof node === 'bigint') return node + 'n'
765
- if (typeof node === 'string') return node
766
- if (!Array.isArray(node)) return String(node)
767
-
768
- const [op, ...a] = node
769
- const ind = INDENT.repeat(depth), ind1 = INDENT.repeat(depth + 1)
770
-
771
- // Literal: [, value]
772
- if (op == null) return typeof a[0] === 'string' ? JSON.stringify(a[0]) : a[0] == null ? 'null' : String(a[0]) + (typeof a[0] === 'bigint' ? 'n' : '')
773
-
774
- // Statements
775
- if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
776
- if (op === '{}') {
777
- // Discriminate object literal / destructuring pattern from block.
778
- // Object: `:` key-value, `,` of object-pattern items (id / `:` / `...` / `= default`),
779
- // lone string shorthand. Empty `{}` outputs the same string either way.
780
- const body = a[0]
781
- const isObjItem = (n) => typeof n === 'string' ||
782
- (Array.isArray(n) && (n[0] === ':' || n[0] === '...' || n[0] === 'as' ||
783
- (n[0] === '=' && typeof n[1] === 'string')))
784
- const isObj = body == null ? false
785
- : typeof body === 'string' ? true
786
- : Array.isArray(body) && (body[0] === ':' || body[0] === '...' || body[0] === 'as' ||
787
- (body[0] === ',' && body.slice(1).every(isObjItem)))
788
- if (isObj) {
789
- if (typeof body === 'string') return '{ ' + body + ' }'
790
- if (body[0] === ',') return '{ ' + body.slice(1).map(x => codegen(x)).join(', ') + ' }'
791
- return '{ ' + codegen(body) + ' }'
792
- }
793
- // Block: body is null, a single statement, or [';', ...stmts]
794
- const stmts = body == null ? [] : (Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body])
795
- const rendered = stmts.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
796
- return '{\n' + ind1 + rendered + (rendered ? ';' : '') + '\n' + ind + '}'
797
- }
798
-
799
- // Declarations
800
- if (op === 'let' || op === 'const') return op + ' ' + a.map(d => codegen(d, depth)).join(', ')
801
- if (op === 'export') { const inner = codegen(a[0], depth); return inner ? 'export ' + inner : '' }
802
- if (op === 'default') return 'default ' + codegen(a[0], depth)
803
-
804
- // Control flow
805
- if (op === 'if') {
806
- const cond = codegen(a[0]), then = wrapBlock(a[1], depth)
807
- return a[2] != null
808
- ? 'if (' + cond + ') ' + then + ' else ' + wrapBlock(a[2], depth)
809
- : 'if (' + cond + ') ' + then
810
- }
811
- if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
812
- if (op === 'for') {
813
- if (a.length === 2) { // ['for', head, body] — subscript shape
814
- const [head, body] = a
815
- if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
816
- return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + wrapBlock(body, depth)
817
- // ['let'/'const', ['in'/'of', name, obj]] — subscript wraps var→let around in/of
818
- if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
819
- return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' + wrapBlock(body, depth)
820
- // C-style head [';', init, cond, update] is positional — empty slots are valid,
821
- // must not flow through the generic `;` joiner (which adds newlines + a trailing `;`).
822
- if (Array.isArray(head) && head[0] === ';')
823
- return 'for (' + (head[1] == null ? '' : codegen(head[1])) + '; ' + (head[2] == null ? '' : codegen(head[2])) + '; ' + (head[3] == null ? '' : codegen(head[3])) + ') ' + wrapBlock(body, depth)
824
- return 'for (' + codegen(head) + ') ' + wrapBlock(body, depth)
825
- }
826
- return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + wrapBlock(a[3], depth)
827
- }
828
- if (op === 'return') return 'return ' + codegen(a[0])
829
- if (op === 'throw') return 'throw ' + codegen(a[0])
830
- if (op === 'break') return 'break'
831
- if (op === 'continue') return 'continue'
832
- // catch with optional binding: ['catch', tryBlock, catchBody] or ['catch', tryBlock, paramName, catchBody]
833
- if (op === 'catch') {
834
- if (a.length === 3) return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
835
- return 'try ' + codegen(a[0], depth) + ' catch ' + codegen(a[1], depth)
836
- }
837
-
838
- // Arrow
839
- if (op === '=>') {
840
- // Params: already wrapped in () by parser, or bare name
841
- const p = a[0]
842
- const params = Array.isArray(p) && p[0] === '()' ? codegen(p) : '(' + codegen(p) + ')'
843
- const body = a[1]
844
- const isBlock = Array.isArray(body) && (body[0] === '{}' || body[0] === ';' || body[0] === 'return')
845
- const bodyStr = Array.isArray(body) && body[0] !== '{}' && isBlock
846
- ? '{ ' + codegen(body, depth) + '; }'
847
- : codegen(body, depth)
848
- return params + ' => ' + bodyStr
849
- }
850
-
851
- // Grouping parens / function call
852
- if (op === '()') {
853
- if (a.length === 1) return '(' + (a[0] == null ? '' : codegen(a[0])) + ')'
854
- return codegen(a[0]) + '(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
855
- }
856
-
857
- // Property access
858
- if (op === '.') return codegen(a[0]) + '.' + a[1]
859
- if (op === '?.') return codegen(a[0]) + '?.' + a[1]
860
- if (op === '?.[]') return codegen(a[0]) + '?.[' + codegen(a[1]) + ']'
861
- if (op === '?.()') return codegen(a[0]) + '?.(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
862
- if (op === '[]') {
863
- // Array literal: ['[]', body] (length 2 → a.length 1). body may be null (empty),
864
- // a single element, or a [',', ...items] sequence.
865
- if (a.length === 1) {
866
- if (a[0] == null) return '[]'
867
- const body = a[0]
868
- if (Array.isArray(body) && body[0] === ',') return '[' + body.slice(1).map(x => codegen(x)).join(', ') + ']'
869
- return '[' + codegen(body) + ']'
870
- }
871
- // Subscript: ['[]', obj, idx]
872
- return codegen(a[0]) + '[' + codegen(a[1]) + ']'
873
- }
874
- if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
875
- if (op === 'str') return JSON.stringify(a[0])
876
- if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
877
-
878
- // Comma
879
- if (op === ',') return a.map(x => codegen(x)).join(', ')
880
- // Template literal: alternating string/expr parts. String parts are [null, "str"], expr parts are AST nodes.
881
- if (op === '`') return '`' + a.map(p => {
882
- if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') return p[1].replace(/[`\\$]/g, c => '\\' + c)
883
- return '${' + codegen(p) + '}'
884
- }).join('') + '`'
885
-
886
- // Spread
887
- if (op === '...') return '...' + codegen(a[0])
888
-
889
- // Import / export rename
890
- if (op === 'import') return 'import ' + codegen(a[0])
891
- if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
892
- if (op === 'as') return codegen(a[0]) + ' as ' + codegen(a[1])
893
-
894
- // Unary prefix
895
- if (a.length === 1) {
896
- if (op === '++' || op === '--') return a[0] == null ? op : op + codegen(a[0])
897
- if (op === 'typeof') return 'typeof ' + codegen(a[0])
898
- if (op === 'u-') return '-' + codegen(a[0])
899
- if (op === 'u+') return '+' + codegen(a[0])
900
- return op + codegen(a[0])
901
- }
902
-
903
- // Postfix
904
- if (a.length === 2 && a[1] === null) return codegen(a[0]) + op
905
-
906
- // Binary
907
- if (a.length === 2 && prec[op]) return codegen(a[0]) + ' ' + op + ' ' + codegen(a[1])
908
-
909
- // Ternary
910
- if (op === '?' || op === '?:') return codegen(a[0]) + ' ? ' + codegen(a[1]) + ' : ' + codegen(a[2])
911
-
912
- // Fallback
913
- return op + '(' + a.map(x => codegen(x)).join(', ') + ')'
914
- }
package/src/narrow.js CHANGED
@@ -192,11 +192,20 @@ function enrichCallerValTypesFromPointerParams(callerCtx) {
192
192
  function refreshCallerLocals(callerCtx) {
193
193
  for (const func of ctx.func.list) {
194
194
  if (!func.body || func.raw) continue
195
+ // Seed pointer-narrowed params' val-kind so analyzeBody recognises e.g.
196
+ // `n = arr.length` (arr a TYPED/BUFFER pointer param) as an i32 local — without
197
+ // this, post-G `refreshCallerLocals` still walks bodies with arr untyped, the
198
+ // length stays f64, and any callee taking that length never gets an i32 param
199
+ // (heapsort→siftDown's `end`). analyzeFuncForEmit re-seeds + re-invalidates at
200
+ // emit time, so this transient repByLocal doesn't leak past narrowing.
201
+ ctx.func.repByLocal = new Map()
202
+ for (const p of func.sig.params) if (p.ptrKind != null) ctx.func.repByLocal.set(p.name, { val: p.ptrKind })
195
203
  invalidateLocalsCache(func.body)
196
204
  const fresh = analyzeLocals(func.body)
197
205
  for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
198
206
  callerCtx.get(func).callerLocals = fresh
199
207
  }
208
+ ctx.func.repByLocal = null
200
209
  }
201
210
 
202
211
  function resetParamWasmFacts(paramReps) {
@@ -361,10 +370,24 @@ export default function narrowSignatures(programFacts, ast) {
361
370
  },
362
371
  },
363
372
  ])
373
+ // Transitive ctor/schema propagation down call chains. A naive single-pass
374
+ // mergeRule poisons a callee's param on the *first* sweep if the caller's own
375
+ // param (the very thing that supplies the ctor) hasn't been typed yet — and the
376
+ // poison is sticky, so later sweeps can't recover. Two-pass was the old patch;
377
+ // it still loses any chain deeper than `main→f→g→h` (e.g. heapsort's siftDown).
378
+ // Fix: iterate a *soft* merge — propagate known ctors, treat "can't tell yet"
379
+ // as skip (no poison) — to a fixpoint, then one *hard* validating sweep that
380
+ // poisons params whose call sites still can't be proven (genuinely-untyped args).
364
381
  const runArrElemFixpoint = (field, inferFn, elemsCtxMap) => {
365
- runCallsiteLattice([mergeRule(field, (arg, _k, state) =>
366
- inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field))
367
- )])
382
+ const infer = (arg, _k, state) => inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field))
383
+ let changed
384
+ const bump = (r, v) => { if (v == null || r[field] === null) return; const b = r[field]; mergeParamFact(r, field, v); if (r[field] !== b) changed = true }
385
+ const soft = {
386
+ missing(r, k, state) { const def = defaultArg(state, k); if (def != null) bump(r, infer(def, k, state)) },
387
+ apply(r, arg, k, state) { bump(r, infer(arg, k, state)) },
388
+ }
389
+ do { changed = false; runCallsiteLattice([soft]) } while (changed)
390
+ runCallsiteLattice([mergeRule(field, infer)])
368
391
  }
369
392
  const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArgArrElemSchema, phase.callerElems('arrElemSchemas'))
370
393
  const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArgArrElemValType, phase.callerElems('arrElemValTypes'))
@@ -423,18 +446,22 @@ export default function narrowSignatures(programFacts, ast) {
423
446
  if (isBlockBody(body) && hasBareReturn(body)) continue
424
447
  const exprs = returnExprs(body)
425
448
  if (!exprs.length) continue
426
- // Skip narrowing when any return-tail is `>>>` (unsigned uint32). Narrowing to i32
427
- // loses the unsigned interpretation: the wrapper rebox via `f64.convert_i32_s` would
428
- // sign-flip values with bit 31 set, breaking the canonical `(x >>> 0)` uint32 idiom.
429
- // A future pass could track sig.unsignedResult and emit `f64.convert_i32_u` instead.
430
- if (exprs.some(e => Array.isArray(e) && e[0] === '>>>')) continue
449
+ // Narrow result to i32 when every return-tail types as i32 (incl. bitwise ops, `|0`,
450
+ // and `>>>`). When ANY tail is `>>>` (unsigned uint32 idiom) we still narrow, but tag
451
+ // `sig.unsignedResult` so the call-site rebox uses `f64.convert_i32_u` preserving
452
+ // the [0, 2^32) range that `(x >>> 0)` carries through hash/CRC finalizers.
453
+ const anyUnsigned = exprs.some(e => Array.isArray(e) && e[0] === '>>>')
431
454
  const savedCurrent = ctx.func.current
432
455
  ctx.func.current = func.sig
433
456
  const locals = isBlockBody(body) ? analyzeLocals(body) : new Map()
434
457
  for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
435
458
  const allI32 = exprs.every(e => exprTypeWithCalls(e, locals) === 'i32')
436
459
  ctx.func.current = savedCurrent
437
- if (allI32) { func.sig.results = ['i32']; changed = true }
460
+ if (allI32) {
461
+ func.sig.results = ['i32']
462
+ if (anyUnsigned) func.sig.unsignedResult = true
463
+ changed = true
464
+ }
438
465
  }
439
466
  }
440
467