watr 4.3.1 → 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.1",
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": {
@@ -46,7 +46,7 @@
46
46
  "test": "node test",
47
47
  "test:repl": "npx playwright test",
48
48
  "types": "npx tsc src/*.js watr.js --allowJs --declaration --emitDeclarationOnly --declarationMap --outDir types",
49
- "prepublishOnly": "npm run build && npm run types",
49
+ "prepublishOnly": "npm test && npm run build && npm run types",
50
50
  "prepare": "git submodule update --init --recursive 2>/dev/null || true"
51
51
  },
52
52
  "repository": {
package/src/encode.js CHANGED
@@ -106,8 +106,9 @@ i32.parse = n => {
106
106
  */
107
107
  export function i64(n, buffer = []) {
108
108
  if (typeof n === 'string') n = i64.parse(n)
109
+ else if (typeof n === 'number') n = BigInt(n)
109
110
  // Normalize unsigned to signed: values > MAX_INT64 become negative
110
- else if (typeof n === 'bigint' && n > 0x7fffffffffffffffn) {
111
+ if (typeof n === 'bigint' && n > 0x7fffffffffffffffn) {
111
112
  n = n - 0x10000000000000000n
112
113
  }
113
114
 
@@ -122,35 +123,31 @@ export function i64(n, buffer = []) {
122
123
  }
123
124
  return buffer
124
125
  }
126
+ const _buf = new ArrayBuffer(8)
127
+ const _u8 = new Uint8Array(_buf), _i32 = new Int32Array(_buf), _f32 = new Float32Array(_buf), _f64 = new Float64Array(_buf), _i64 = new BigInt64Array(_buf)
128
+
125
129
  i64.parse = n => {
126
130
  n = cleanInt(n)
127
131
  n = n[0] === '-' ? -BigInt(n.slice(1)) : BigInt(n) // can be -0x123
128
132
  if (n < -0x8000000000000000n || n > 0xffffffffffffffffn) err(`i64 constant out of range`)
129
- byteView.setBigInt64(0, n)
130
- return byteView.getBigInt64(0)
133
+ _i64[0] = n
134
+ return _i64[0]
131
135
  }
132
136
 
133
- const byteView = new DataView(new Float64Array(1).buffer)
134
-
135
137
  const F32_SIGN = 0x80000000, F32_NAN = 0x7f800000
136
138
  export function f32(input, value, idx) {
137
139
  if (typeof input === 'string' && ~(idx = input.indexOf('nan:'))) {
138
140
  value = i32.parse(input.slice(idx + 4))
139
141
  value |= F32_NAN
140
142
  if (input[0] === '-') value |= F32_SIGN
141
- byteView.setInt32(0, value)
143
+ _i32[0] = value
142
144
  }
143
145
  else {
144
146
  value = typeof input === 'string' ? f32.parse(input) : input
145
- byteView.setFloat32(0, value);
147
+ _f32[0] = value
146
148
  }
147
149
 
148
- return [
149
- byteView.getUint8(3),
150
- byteView.getUint8(2),
151
- byteView.getUint8(1),
152
- byteView.getUint8(0)
153
- ];
150
+ return [_u8[0], _u8[1], _u8[2], _u8[3]]
154
151
  }
155
152
 
156
153
  const F64_SIGN = 0x8000000000000000n, F64_NAN = 0x7ff0000000000000n
@@ -159,23 +156,14 @@ export function f64(input, value, idx) {
159
156
  value = i64.parse(input.slice(idx + 4))
160
157
  value |= F64_NAN
161
158
  if (input[0] === '-') value |= F64_SIGN
162
- byteView.setBigInt64(0, value)
159
+ _i64[0] = value
163
160
  }
164
161
  else {
165
162
  value = typeof input === 'string' ? f64.parse(input) : input
166
- byteView.setFloat64(0, value);
163
+ _f64[0] = value
167
164
  }
168
165
 
169
- return ([
170
- byteView.getUint8(7),
171
- byteView.getUint8(6),
172
- byteView.getUint8(5),
173
- byteView.getUint8(4),
174
- byteView.getUint8(3),
175
- byteView.getUint8(2),
176
- byteView.getUint8(1),
177
- byteView.getUint8(0)
178
- ]);
166
+ return [_u8[0], _u8[1], _u8[2], _u8[3], _u8[4], _u8[5], _u8[6], _u8[7]]
179
167
  }
180
168
 
181
169
  f64.parse = (input, max=Number.MAX_VALUE) => {
@@ -219,3 +207,10 @@ f64.parse = (input, max=Number.MAX_VALUE) => {
219
207
  }
220
208
 
221
209
  f32.parse = input => f64.parse(input, 3.4028234663852886e+38)
210
+
211
+ export const v128 = (input) => {
212
+ let n = typeof input === 'string' ? BigInt(input.replaceAll('_', '')) : BigInt(input)
213
+ let arr = new Uint8Array(16)
214
+ for (let i = 0; i < 16; i++) arr[i] = Number(n & 0xffn), n >>= 8n
215
+ return [...arr]
216
+ }
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
  /**
@@ -804,7 +807,7 @@ const localReuse = (ast) => {
804
807
  if (!Array.isArray(sub)) continue
805
808
 
806
809
  if (sub[0] === 'local') {
807
- localDecls.push({ idx: i, node: sub })
810
+ localDecls.push({ node: sub, idx: i })
808
811
  // (local $name type) or (local type)
809
812
  if (typeof sub[1] === 'string' && sub[1][0] === '$') {
810
813
  localTypes.set(sub[1], sub[2])
@@ -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
@@ -55,4 +55,5 @@ export namespace i8 { }
55
55
  */
56
56
  export function i16(n: number | string, buffer?: number[]): number[];
57
57
  export namespace i16 { }
58
+ export function v128(input: any): any[];
58
59
  //# sourceMappingURL=encode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/encode.js"],"names":[],"mappings":"AA6CA;;;;;;GAMG;AACH,6BAHW,MAAM,GACJ,MAAM,EAAE,CAapB;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;IAQD,4BAIC;;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAmBpB;;IACD,4BAMC;;AAKD,gEAkBC;;IAmED,mCAA6D;;AAhE7D,gEAsBC;;IAED,iDAsCC;;AA3MM,wBAJI,MAAM,GAAC,MAAM,GAAC,MAAM,GAAC,IAAI,WACzB,MAAM,EAAE,GACN,MAAM,EAAE,CA8BpB;AAsBD;;;;;;GAMG;AACH,sBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AApBD;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB"}
1
+ {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/encode.js"],"names":[],"mappings":"AA6CA;;;;;;GAMG;AACH,6BAHW,MAAM,GACJ,MAAM,EAAE,CAapB;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;IAQD,4BAIC;;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAoBpB;;IAID,4BAMC;;AAGD,gEAaC;;IA0DD,mCAA6D;;AAvD7D,gEAaC;;IAED,iDAsCC;;AA/LM,wBAJI,MAAM,GAAC,MAAM,GAAC,MAAM,GAAC,IAAI,WACzB,MAAM,EAAE,GACN,MAAM,EAAE,CA8BpB;AAsBD;;;;;;GAMG;AACH,sBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AApBD;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AA6HM,wCAKN"}
@@ -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