watr 4.3.3 → 4.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watr",
3
- "version": "4.3.3",
3
+ "version": "4.3.4",
4
4
  "description": "Light & fast WAT compiler – WebAssembly Text to binary, parse, print, transform",
5
5
  "main": "watr.js",
6
6
  "bin": {
package/src/optimize.js CHANGED
@@ -285,6 +285,9 @@ const treeshake = (ast) => {
285
285
 
286
286
  // ==================== CONSTANT FOLDING ====================
287
287
 
288
+ /** IEEE 754 roundTiesToEven (bankers' rounding) */
289
+ const roundEven = (x) => x - Math.floor(x) !== 0.5 ? Math.round(x) : 2 * Math.round(x / 2)
290
+
288
291
  /** Operators that can be constant-folded */
289
292
  const FOLDABLE = {
290
293
  // i32
@@ -358,7 +361,7 @@ const FOLDABLE = {
358
361
  'f32.ceil': (a) => Math.fround(Math.ceil(a)),
359
362
  'f32.floor': (a) => Math.fround(Math.floor(a)),
360
363
  'f32.trunc': (a) => Math.fround(Math.trunc(a)),
361
- 'f32.nearest': (a) => Math.fround(Math.round(a)),
364
+ 'f32.nearest': (a) => Math.fround(roundEven(a)),
362
365
 
363
366
  'f64.add': (a, b) => a + b,
364
367
  'f64.sub': (a, b) => a - b,
@@ -370,7 +373,7 @@ const FOLDABLE = {
370
373
  'f64.ceil': (a) => Math.ceil(a),
371
374
  'f64.floor': (a) => Math.floor(a),
372
375
  'f64.trunc': (a) => Math.trunc(a),
373
- 'f64.nearest': (a) => Math.round(a),
376
+ 'f64.nearest': roundEven,
374
377
  }
375
378
 
376
379
  /**
@@ -842,82 +845,127 @@ const localReuse = (ast) => {
842
845
  return result
843
846
  }
844
847
 
845
- // ==================== CONSTANT PROPAGATION ====================
848
+ // ==================== PROPAGATION & LOCAL ELIMINATION ====================
849
+
850
+ /** Check if expression is pure (no side effects, no memory ops) */
851
+ const isPure = (node) => {
852
+ if (!Array.isArray(node)) return true
853
+ const op = node[0]
854
+ if (typeof op !== 'string') return false
855
+ if (op === 'call' || op === 'call_indirect' || op === 'return_call' || op === 'return_call_indirect') return false
856
+ if (op.includes('.store') || op.includes('.load') || op.includes('memory.')) return false
857
+ if (op === 'global.set') return false
858
+ for (let i = 1; i < node.length; i++) if (Array.isArray(node[i]) && !isPure(node[i])) return false
859
+ return true
860
+ }
861
+
862
+ /** Count all local.get/set/tee occurrences in one walk */
863
+ const countLocalUses = (node) => {
864
+ const counts = new Map()
865
+ const ensure = name => { if (!counts.has(name)) counts.set(name, { gets: 0, sets: 0, tees: 0 }); return counts.get(name) }
866
+ walk(node, n => {
867
+ if (!Array.isArray(n) || n.length < 2 || typeof n[1] !== 'string') return
868
+ if (n[0] === 'local.get') ensure(n[1]).gets++
869
+ else if (n[0] === 'local.set') ensure(n[1]).sets++
870
+ else if (n[0] === 'local.tee') ensure(n[1]).tees++
871
+ })
872
+ return counts
873
+ }
874
+
875
+ /** Can this tracked value be substituted for a local.get? */
876
+ const canSubst = (k) => getConst(k.val) || (k.pure && k.singleUse)
877
+
878
+ /** Try substitute local.get nodes with known values */
879
+ const substGets = (node, known) => walkPost(node, n => {
880
+ if (!Array.isArray(n) || n[0] !== 'local.get' || n.length !== 2) return
881
+ const k = typeof n[1] === 'string' && known.get(n[1])
882
+ if (k && canSubst(k)) return clone(k.val)
883
+ })
846
884
 
847
885
  /**
848
- * Propagate constant values through local variables.
849
- * When a local is set to a constant and not modified before use, replace the get with the constant.
850
- * @param {Array} ast
851
- * @returns {Array}
886
+ * Propagate values through locals and eliminate single-use/dead locals.
887
+ * Constants propagate to all uses; pure single-use exprs inline into get site.
888
+ * Multi-pass with batch counting for convergence.
852
889
  */
853
890
  const propagate = (ast) => {
854
891
  const result = clone(ast)
855
892
 
856
- walk(result, (node) => {
857
- if (!Array.isArray(node) || node[0] !== 'func') return
893
+ walk(result, (funcNode) => {
894
+ if (!Array.isArray(funcNode) || funcNode[0] !== 'func') return
858
895
 
859
- // Track which locals have known constant values
860
- // This is a simple single-pass analysis within straight-line code
861
- const constLocals = new Map() // $name const node
896
+ const params = new Set()
897
+ for (const sub of funcNode)
898
+ if (Array.isArray(sub) && sub[0] === 'param' && typeof sub[1] === 'string') params.add(sub[1])
862
899
 
863
- // Process function body in order
864
- const processBlock = (block, startIdx = 1) => {
865
- for (let i = startIdx; i < block.length; i++) {
866
- const instr = block[i]
867
- if (!Array.isArray(instr)) continue
900
+ for (let pass = 0; pass < 4; pass++) {
901
+ let changed = false
902
+ const uses = countLocalUses(funcNode)
903
+ const getUses = name => uses.get(name) || { gets: 0, sets: 0, tees: 0 }
904
+ const known = new Map()
868
905
 
906
+ for (let i = 1; i < funcNode.length; i++) {
907
+ const instr = funcNode[i]
908
+ if (!Array.isArray(instr)) continue
869
909
  const op = instr[0]
870
910
 
871
- // local.set $x (const) remember constant
872
- if (op === 'local.set' && instr.length === 3) {
873
- const local = instr[1]
874
- const val = instr[2]
875
- const c = getConst(val)
876
- if (c && typeof local === 'string') {
877
- constLocals.set(local, val)
878
- } else if (typeof local === 'string') {
879
- constLocals.delete(local) // invalidate if set to non-const
880
- }
881
- }
882
- // local.tee also sets
883
- else if (op === 'local.tee' && instr.length === 3) {
884
- const local = instr[1]
885
- const val = instr[2]
886
- const c = getConst(val)
887
- if (c && typeof local === 'string') {
888
- constLocals.set(local, val)
889
- } else if (typeof local === 'string') {
890
- constLocals.delete(local)
891
- }
911
+ if (op === 'param' || op === 'result' || op === 'local' || op === 'type' || op === 'export') continue
912
+
913
+ // Track local.set values
914
+ if (op === 'local.set' && instr.length === 3 && typeof instr[1] === 'string') {
915
+ substGets(instr[2], known) // substitute known values in RHS
916
+ const u = getUses(instr[1])
917
+ known.set(instr[1], {
918
+ val: instr[2], pure: isPure(instr[2]),
919
+ singleUse: u.gets <= 1 && u.sets <= 1 && u.tees === 0
920
+ })
921
+ continue
892
922
  }
893
- // local.get $x → replace with const if known
894
- else if (op === 'local.get' && instr.length === 2) {
895
- const local = instr[1]
896
- if (typeof local === 'string' && constLocals.has(local)) {
897
- const constVal = constLocals.get(local)
898
- // Replace in place
899
- instr.length = 0
900
- instr.push(...clone(constVal))
923
+
924
+ // Invalidate at control-flow boundaries
925
+ if (op === 'block' || op === 'loop' || op === 'if') known.clear()
926
+ // Calls only invalidate non-constant tracked values
927
+ if (op === 'call' || op === 'call_indirect' || op === 'return_call' || op === 'return_call_indirect')
928
+ for (const [k, v] of known) if (!getConst(v.val)) known.delete(k)
929
+
930
+ // Substitute: standalone local.get (walkPost can't replace root)
931
+ if (op === 'local.get' && instr.length === 2 && typeof instr[1] === 'string') {
932
+ const k = known.get(instr[1])
933
+ if (k && canSubst(k)) {
934
+ const r = clone(k.val)
935
+ instr.length = 0; instr.push(...(Array.isArray(r) ? r : [r]))
936
+ changed = true; continue
901
937
  }
902
938
  }
903
- // Control flow invalidates all knowledge (conservative)
904
- else if (op === 'block' || op === 'loop' || op === 'if' || op === 'call' || op === 'call_indirect') {
905
- constLocals.clear()
939
+
940
+ // Substitute nested local.gets (skip control-flow nodes locals may be reassigned inside)
941
+ if (op !== 'block' && op !== 'loop' && op !== 'if') {
942
+ const prev = JSON.stringify(instr)
943
+ substGets(instr, known)
944
+ if (JSON.stringify(instr) !== prev) changed = true
906
945
  }
946
+ }
907
947
 
908
- // Recursively process nested expressions that might have local.get
909
- walkPost(instr, (n) => {
910
- if (!Array.isArray(n) || n[0] !== 'local.get' || n.length !== 2) return
911
- const local = n[1]
912
- if (typeof local === 'string' && constLocals.has(local)) {
913
- const constVal = constLocals.get(local)
914
- return clone(constVal)
915
- }
916
- })
948
+ // Remove dead stores + unused local decls in one reverse pass
949
+ const postUses = countLocalUses(funcNode)
950
+ const pu = name => postUses.get(name) || { gets: 0, sets: 0, tees: 0 }
951
+ for (let i = funcNode.length - 1; i >= 1; i--) {
952
+ const sub = funcNode[i]
953
+ if (!Array.isArray(sub)) continue
954
+ const name = typeof sub[1] === 'string' ? sub[1] : null
955
+ if (!name || params.has(name)) continue
956
+ const u = pu(name)
957
+ // Dead store: set but never read, pure RHS
958
+ if (sub[0] === 'local.set' && u.gets === 0 && u.tees === 0 && isPure(sub[2])) {
959
+ funcNode.splice(i, 1); changed = true
960
+ }
961
+ // Unused local declaration
962
+ else if (sub[0] === 'local' && name[0] === '$' && u.gets === 0 && u.sets === 0 && u.tees === 0) {
963
+ funcNode.splice(i, 1); changed = true
964
+ }
917
965
  }
918
- }
919
966
 
920
- processBlock(node)
967
+ if (!changed) break
968
+ }
921
969
  })
922
970
 
923
971
  return result
@@ -56,12 +56,11 @@ export function strength(ast: any[]): any[];
56
56
  */
57
57
  export function branch(ast: any[]): any[];
58
58
  /**
59
- * Propagate constant values through local variables.
60
- * When a local is set to a constant and not modified before use, replace the get with the constant.
61
- * @param {Array} ast
62
- * @returns {Array}
59
+ * Propagate values through locals and eliminate single-use/dead locals.
60
+ * Constants propagate to all uses; pure single-use exprs inline into get site.
61
+ * Multi-pass with batch counting for convergence.
63
62
  */
64
- export function propagate(ast: any[]): any[];
63
+ export function propagate(ast: any): any;
65
64
  /**
66
65
  * Inline tiny functions (single expression, no locals, no params or simple params).
67
66
  * @param {Array} ast
@@ -1 +1 @@
1
- {"version":3,"file":"optimize.d.ts","sourceRoot":"","sources":["../../src/optimize.js"],"names":[],"mappings":"AAioCA;;;;;;;;;;;GAWG;AACH,sCATW,QAAM,MAAM,SACZ,OAAO,GAAC,MAAM,MAAO,SAwB/B;AAtkCD;;;;;GAKG;AACH,6CA8LC;AAyHD;;;;GAIG;AACH,wCAmCC;AAwQD;;;;GAIG;AACH,4CAuBC;AA+CD;;;;;GAKG;AACH,8CAqDC;AApTD;;;;GAIG;AACH,4CASC;AAID;;;;GAIG;AACH,4CA6DC;AAID;;;;GAIG;AACH,0CA0EC;AAiJD;;;;;GAKG;AACH,6CAuEC;AAID;;;;GAIG;AACH,0CAwFC;AAn+BD;;;;GAIG;AACH,gCAHW,OAAO,GAAC,MAAM,MAAO,OAe/B"}
1
+ {"version":3,"file":"optimize.d.ts","sourceRoot":"","sources":["../../src/optimize.js"],"names":[],"mappings":"AAirCA;;;;;;;;;;;GAWG;AACH,sCATW,QAAM,MAAM,SACZ,OAAO,GAAC,MAAM,MAAO,SAwB/B;AAtnCD;;;;;GAKG;AACH,6CA8LC;AA4HD;;;;GAIG;AACH,wCAmCC;AAwQD;;;;GAIG;AACH,4CAuBC;AA+CD;;;;;GAKG;AACH,8CAqDC;AApTD;;;;GAIG;AACH,4CASC;AAID;;;;GAIG;AACH,4CA6DC;AAID;;;;GAIG;AACH,0CA0EC;AAoLD;;;;GAIG;AACH,yCAkFC;AAID;;;;GAIG;AACH,0CAwFC;AAnhCD;;;;GAIG;AACH,gCAHW,OAAO,GAAC,MAAM,MAAO,OAe/B"}
package/watr.js CHANGED
@@ -152,7 +152,7 @@ function genImports(imports) {
152
152
  function compile(source, ...values) {
153
153
  // Options object as last argument (non-template call)
154
154
  let opts = {}
155
- if (!Array.isArray(source) && values.length && typeof values[values.length - 1] === 'object' && values[values.length - 1] !== null && !(values[values.length - 1] instanceof Uint8Array)) {
155
+ if (!Array.isArray(source) && values.length && typeof values[values.length - 1] === 'object' && values[values.length - 1] !== null && !values[values.length - 1].byteLength) {
156
156
  opts = values.pop()
157
157
  }
158
158
 
@@ -189,7 +189,7 @@ function compile(source, ...values) {
189
189
  return parsed
190
190
  }
191
191
  // Uint8Array → convert to plain array for flat() compatibility
192
- if (value instanceof Uint8Array) return [...value]
192
+ if (value.byteLength !== undefined) return [...value]
193
193
  return value
194
194
  }
195
195
  return node