watr 1.4.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/package.json +14 -3
  2. package/readme.md +34 -78
  3. package/src/compile.js +156 -171
  4. package/src/const.js +19 -18
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "watr",
3
- "version": "1.4.1",
3
+ "version": "2.1.0",
4
4
  "description": "Ligth & fast WAT compiler",
5
5
  "main": "watr.js",
6
+ "exports": {
7
+ ".": "./watr.js",
8
+ "./parse": "./src/parse.js",
9
+ "./print": "./src/print.js",
10
+ "./compile": "./src/compile.js"
11
+ },
6
12
  "type": "module",
7
13
  "scripts": {
8
14
  "test": "node test"
@@ -16,11 +22,16 @@
16
22
  ],
17
23
  "keywords": [
18
24
  "wat",
25
+ "wasm",
26
+ "wat2wasm",
19
27
  "compiler",
20
- "wabt"
28
+ "wabt",
29
+ "pretty-print",
30
+ "webassembly",
31
+ "wasm-text"
21
32
  ],
22
33
  "author": "Dmitry Iv",
23
- "license": "ISC",
34
+ "license": "MIT",
24
35
  "bugs": {
25
36
  "url": "https://github.com/audio-lab/watr/issues"
26
37
  },
package/readme.md CHANGED
@@ -1,25 +1,16 @@
1
- # watr [![Test](https://github.com/audio-lab/watr/actions/workflows/test.js.yml/badge.svg)](https://github.com/audio-lab/watr/actions/workflows/test.js.yml)
1
+ # watr [![test](https://github.com/audio-lab/watr/actions/workflows/test.js.yml/badge.svg)](https://github.com/audio-lab/watr/actions/workflows/test.js.yml) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/watr/latest?color=brightgreen&label=gzip)](https://bundlephobia.com/package/watr)
2
2
 
3
- > Light & fast WAT compiler.
3
+ > Bare minimum wasm text compiler & formatter.
4
4
 
5
- Provides bare minimum WAT to WASM compilation.<br/>
6
- Useful as WASM API layer, eg. for hi-level languages or for dynamic (in-browser?) compilation.
7
- <!--, eg. [sonl](https://github.com/audio-lab/sonl). -->
5
+ Light & fast alternative for [wat2wasm](https://github.com/AssemblyScript/wabt.js), useful for hi-level languages or dynamic (in-browser) compilation.<br>
8
6
 
9
7
  <!-- See [REPL](https://audio-lab.github.io/watr/repl.html).-->
10
8
 
11
- <!--
12
- &nbsp; | watr | wat-compiler | wabt
13
- ---|---|---|---
14
- Size (gzipped) | 2.8kb | 6kb | 300kb
15
- Performance (op/s) | 45000 | 2500 | 3100
16
- -->
17
-
18
9
  &nbsp; | Size (gzipped) | Performance (op/s)
19
10
  ---|---|---
20
- watr | 3.8 kb | 1900
21
- [wat-compiler](https://github.com/stagas/wat-compiler) | 6 kb | 135
22
- [wabt](https://github.com/AssemblyScript/wabt.js) | 300 kb | 250
11
+ watr | 3.8 kb | 5950
12
+ [wat-compiler](https://github.com/stagas/wat-compiler) | 6 kb | 348
13
+ [wabt](https://github.com/AssemblyScript/wabt.js) | 300 kb | 574
23
14
 
24
15
  ## Usage
25
16
 
@@ -43,32 +34,17 @@ double(108) // 216
43
34
 
44
35
  ## API
45
36
 
46
- ### Parse
47
-
48
- Parser converts input Wasm text string to syntax tree.
49
-
50
- ```js
51
- import { parse } from 'watr
52
-
53
- parse(`(func (export "double") (param f64) (result f64) (f64.mul (local.get 0) (f64.const 2)))`)
54
-
55
- // [
56
- // 'func', ['export', '"double"'], ['param', 'f64'], ['result', 'f64'],
57
- // ['f64.mul', ['local.get', 0], ['f64.const', 2]]
58
- // ]
59
- ```
60
-
61
37
  ### Compile
62
38
 
63
- Compiles Wasm tree or text into wasm binary. Lightweight alternative to [wabt/wat2wasm](https://github.com/WebAssembly/wabt).
39
+ Compiles wasm text or syntax tree into wasm binary.
64
40
 
65
41
  ```js
66
42
  import { compile } from 'watr'
67
43
 
68
- const buffer = compile([
69
- 'func', ['export', '"double"'], ['param', 'f64'], ['result', 'f64'],
70
- ['f64.mul', ['local.get', 0], ['f64.const', 2]]
71
- ])
44
+ const buffer = compile(`(func (export "double")
45
+ (param f64) (result f64)
46
+ (f64.mul (local.get 0) (f64.const 2))
47
+ )`)
72
48
  const module = new WebAssembly.Module(buffer)
73
49
  const instance = new WebAssembly.Instance(module)
74
50
  const {double} = instance.exports
@@ -78,20 +54,20 @@ double(108) // 216
78
54
 
79
55
  ### Print
80
56
 
81
- Format input Wasm text string or tree into minified or pretty form.
57
+ Format input wasm text or syntax tree into minified or pretty form.
82
58
 
83
59
  ```js
84
60
  import { print } from 'watr'
85
61
 
86
- const tree = [
87
- 'func', ['export', '"double"'], ['param', 'f64'], ['result', 'f64'],
88
- ['f64.mul', ['local.get', 0], ['f64.const', 2]]
89
- ]
62
+ const src = `(func (export "double")
63
+ (param f64) (result f64)
64
+ (f64.mul (local.get 0) (f64.const 2))
65
+ )`
90
66
 
91
67
  // pretty-print (default)
92
- const str = print(tree, {
93
- indent: ' ', // indentation chars
94
- newline: '\n', // new line chars
68
+ print(src, {
69
+ indent: ' ',
70
+ newline: '\n',
95
71
  })
96
72
  // (func (export "double")
97
73
  // (param f64) (result f64)
@@ -100,53 +76,34 @@ const str = print(tree, {
100
76
  // (f64.const 2)))
101
77
 
102
78
  // minify
103
- const str = print(tree, {
79
+ print(src, {
104
80
  indent: false,
105
81
  newline: false
106
82
  })
107
83
  // (func (export "double")(param f64)(result f64)(f64.mul (local.get 0)(f64.const 2)))
108
84
  ```
109
85
 
110
- <!--
111
- ## Limitations
112
-
113
- It may miss some edge cases and nice error messages.
114
- For better REPL/dev experience use [wabt](https://github.com/AssemblyScript/wabt.js).
115
-
86
+ ### Parse
116
87
 
117
- Ambiguous syntax is prohibited in favor of explicit lispy notation. Each instruction must have prefix signature with parenthesized immediates and arguments.
88
+ Parse input wasm text into syntax tree.
118
89
 
119
- ```wast
120
- (func (result i32)
121
- i32.const 1 ;; ✘ stacked arguments
122
- drop
123
- i32.const 0
124
- i32.load offset=0 align=4 ;; ✘ ungrouped immediates
125
- )
90
+ ```js
91
+ import { parse } from 'watr'
126
92
 
127
- (func (result i32)
128
- (drop (i32.const 1)) ;; ✔ nested arguments
129
- (i32.load offset=0 align=4 (i32.const 0)) ;; grouped immediates
130
- )
93
+ parse(`(func (export "double") (param f64) (result f64) (f64.mul (local.get 0) (f64.const 2)))`)
94
+ // [
95
+ // 'func', ['export', '"double"'], ['param', 'f64'], ['result', 'f64'],
96
+ // ['f64.mul', ['local.get', 0], ['f64.const', 2]]
97
+ // ]
131
98
  ```
132
99
 
133
- ```wast
134
- (local.get 0) ;; ✘ stacked argument
135
- if (result i32) ;; ✘ inline instruction
136
- (i32.const 1)
137
- end
138
-
139
- (if (result i32) (local.get 0) ;; ✔ explicit signature
140
- (i32.const 1)
141
- )
142
- ```
100
+ ## Limitations
143
101
 
144
- ```wast
145
- (f32.const 0x1.fffffep+127) ;; ✘ floating HEX - not supported
146
- ```
147
- -->
102
+ No floating HEX support, eg. `(f32.const 0x1.fffffep+127)`.
148
103
 
104
+ ## Projects using watr
149
105
 
106
+ * [auro](https://github.com/audio-lab/auro) – audio processing language
150
107
 
151
108
  ## Useful links
152
109
 
@@ -162,20 +119,19 @@ end
162
119
  * [wabt source search](https://github.com/WebAssembly/wabt/search?p=5&q=then)
163
120
  * [wat control flow](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Control_flow)
164
121
  * [ontouchstart wasm book](https://ontouchstart.pages.dev/chapter_wasm_binary)
165
- * [wat-compiler](https://github.com/stagas/wat-compiler/)
166
122
  * [hackernoon](https://web.archive.org/web/20210215171830/https://hackernoon.com/webassembly-binary-format-explained-part-2-hj1t33yp?source=rss)
167
123
  * [webassemblyjs](https://github.com/xtuc/webassemblyjs)
168
124
  * [chasm](https://github.com/ColinEberhardt/chasm/blob/master/src/encoding.ts)
169
125
  * [WebBS](https://github.com/j-s-n/WebBS)
170
126
  * [leb128a](https://github.com/minhducsun2002/leb128/blob/master/src/index.ts)
171
127
  * [leb128b](https://github.com/shmishtopher/wasm-LEB128/tree/master/esm)
172
-
173
128
  -->
174
129
 
175
130
  ## Alternatives
176
131
 
177
132
  * [wabt](https://www.npmjs.com/package/wabt) − port of WABT for the web, de-facto standard.
178
133
  * [wat-compiler](https://www.npmjs.com/package/wat-compiler) − compact alternative for WABT, older brother of _watr_.
134
+ * [wassemble](https://github.com/wingo/wassemble)
179
135
  * [web49](https://github.com/FastVM/Web49)
180
136
 
181
137
  <p align=center><a href="https://github.com/krsnzd/license/">🕉</a></p>
package/src/compile.js CHANGED
@@ -2,8 +2,7 @@ import { uleb, leb, bigleb, f64, f32 } from './util.js'
2
2
  import { OP, SECTION, ALIGN, TYPE, KIND } from './const.js'
3
3
  import parse from './parse.js'
4
4
 
5
- // some inlinable instructions
6
- const INLINE = { loop: 1, block: 1, if: 1, end: -1, return: -1 }
5
+ const OP_END = 0xb, OP_I32_CONST = 0x41, OP_I64_CONST = 0x42, OP_F32_CONST = 0x43, OP_F64_CONST = 0x44
7
6
 
8
7
  // convert wat tree to wasm binary
9
8
  export default (nodes) => {
@@ -43,13 +42,11 @@ export default (nodes) => {
43
42
  for (let node of nodes) {
44
43
  node[0] === name ? postcall.push(build[name](node, sections)) : remaining.push(node)
45
44
  }
46
-
47
45
  nodes = remaining
48
46
  }
49
47
 
50
48
  // code must be compiled after all definitions
51
- for (let cb of postcall) cb && cb.call && cb()
52
-
49
+ for (let cb of postcall) cb?.()
53
50
 
54
51
  // 3. build binary
55
52
  for (let name in sections) {
@@ -69,37 +66,16 @@ const build = {
69
66
  // (type $name? (func (param $x i32) (param i64 i32) (result i32 i64)))
70
67
  // signature part is identical to function
71
68
  // FIXME: handle non-function types
72
- type([, typeName, decl], ctx) {
73
- if (typeName[0] !== '$') decl = typeName, typeName = null
74
- let params = [], result = [], [kind, ...sig] = decl, idx, bytes
75
-
76
- if (kind === 'func') {
77
- // collect params
78
- while (sig[0]?.[0] === 'param') {
79
- let [, ...types] = sig.shift()
80
- if (types[0]?.[0] === '$') params[types.shift()] = params.length
81
- params.push(...types.map(t => TYPE[t]))
82
- }
83
-
84
- // collect result type
85
- if (sig[0]?.[0] === 'result') result = sig.shift().slice(1).map(t => TYPE[t])
86
-
87
- // reuse existing type or register new one
88
- bytes = [TYPE.func, ...uleb(params.length), ...params, ...uleb(result.length), ...result]
89
-
90
- idx = ctx.type.findIndex((prevType) => prevType.every((byte, i) => byte === bytes[i]))
91
- if (idx < 0) idx = ctx.type.push(bytes) - 1
92
- }
93
-
69
+ type([, typeName, [kind, ...sig]], ctx) {
70
+ if (kind !== 'func') err(`Unknown type kind '${kind}'`)
71
+ const [idx] = consumeType(sig, ctx)
94
72
  if (typeName) ctx.type[typeName] = idx
95
-
96
- return [idx, params, result]
97
73
  },
98
74
 
99
75
  // (func $name? ...params result ...body)
100
76
  func([, ...body], ctx) {
101
77
  let locals = [], // list of local variables
102
- callstack = []
78
+ blocks = [] // control instructions / blocks stack
103
79
 
104
80
  // fn name
105
81
  if (body[0]?.[0] === '$') ctx.func[body.shift()] = ctx.func.length
@@ -107,10 +83,10 @@ const build = {
107
83
  // export binding
108
84
  if (body[0]?.[0] === 'export') build.export([...body.shift(), ['func', ctx.func.length]], ctx)
109
85
 
110
- // register type
111
- let [typeIdx, params, result] = build.type([, ['func', ...body]], ctx)
112
- // FIXME: try merging with build.type: it should be able to consume body
113
- while (body[0]?.[0] === 'param' || body[0]?.[0] === 'result') body.shift()
86
+ // register/consume type info
87
+ let [typeIdx, params, result] = consumeType(body, ctx)
88
+
89
+ // register new function
114
90
  ctx.func.push([typeIdx])
115
91
 
116
92
  // collect locals
@@ -125,164 +101,148 @@ const build = {
125
101
  // squash local types
126
102
  let locTypes = locals.reduce((a, type) => (type == a[a.length - 1] ? a[a.length - 2]++ : a.push(1, type), a), [])
127
103
 
128
- // map code instruction into bytes: [args, opCode, immediates]
129
- const instr = (group) => {
130
- let [op, ...nodes] = group
131
- let opCode = OP[op], argc = 0, before = [], after = [], id
132
-
133
- // NOTE: we could reorganize ops by groups and detect signature as `op in STORE`
134
- // but numeric comparison is faster than generic hash lookup
135
- // FIXME: we often use OP.end or alike: what if we had list of global constants?
136
-
137
- // binary/unary
138
- if (opCode >= 69) {
139
- argc = opCode >= 167 ||
140
- (opCode <= 159 && opCode >= 153) ||
141
- (opCode <= 145 && opCode >= 139) ||
142
- (opCode <= 123 && opCode >= 121) ||
143
- (opCode <= 105 && opCode >= 103) ||
144
- opCode == 80 || opCode == 69 ? 1 : 2
145
- }
146
- // instruction
147
- else {
148
- // (i32.store align=n offset=m at value)
149
- if (opCode >= 40 && opCode <= 62) {
150
- // FIXME: figure out point in Math.log2 aligns
151
- let o = { align: ALIGN[op], offset: 0 }, p
152
- while (nodes[0]?.includes('=')) p = nodes.shift().split('='), o[p[0]] = Number(p[1])
153
- after = [Math.log2(o.align), ...uleb(o.offset)]
154
- argc = opCode >= 54 ? 2 : 1
155
- }
104
+ // convert sequence of instructions from input nodes to out bytes
105
+ const consume = (nodes, out = []) => {
106
+ if (!nodes?.length) return out
156
107
 
157
- // (i32.const 123)
158
- else if (opCode >= 65 && opCode <= 68) {
159
- after = (opCode == 65 ? leb : opCode == 66 ? bigleb : opCode == 67 ? f32 : f64)(nodes.shift())
160
- }
108
+ let op = nodes.shift(), opCode, args = nodes, immed, id, group
161
109
 
162
- // (local.get $id), (local.tee $id x)
163
- else if (opCode >= 32 && opCode <= 34) {
164
- after = uleb(nodes[0]?.[0] === '$' ? params[id = nodes.shift()] || locals[id] : nodes.shift())
165
- if (opCode > 32) argc = 1
166
- }
110
+ // groups are flattened, eg. (cmd z w) -> z w cmd
111
+ if (group = Array.isArray(op)) {
112
+ args = [...op] // op is immutable
113
+ opCode = OP.indexOf(op = args.shift())
114
+ }
115
+ else opCode = OP.indexOf(op)
167
116
 
168
- // (global.get id), (global.set id)
169
- else if (opCode == 35 || opCode == 36) {
170
- after = uleb(nodes[0]?.[0] === '$' ? ctx.global[nodes.shift()] : nodes.shift())
171
- if (opCode > 35) argc = 1
172
- }
117
+ // NOTE: numeric comparison is faster than generic hash lookup
173
118
 
174
- // (call id ...nodes)
175
- else if (opCode == 16) {
176
- let fnName = nodes.shift()
177
- after = uleb(id = fnName[0] === '$' ? ctx.func[fnName] ?? err('Unknown function `' + fnName + '`') : fnName);
178
- // FIXME: how to get signature of imported function
179
- [, argc] = ctx.type[ctx.func[id][0]]
180
- }
119
+ // binary/unary - just consume immed
120
+ if (opCode >= 69) { }
181
121
 
182
- // (call_indirect (type $typeName) (idx) ...nodes)
183
- else if (opCode == 17) {
184
- let typeId = nodes.shift()[1];
185
- [, argc] = ctx.type[typeId = typeId[0] === '$' ? ctx.type[typeId] : typeId]
186
- argc++
187
- after = uleb(typeId), after.push(0) // extra afterediate indicates table idx (reserved)
188
- }
122
+ // (i32.store align=n offset=m at value)
123
+ else if (opCode >= 40 && opCode <= 62) {
124
+ // FIXME: figure out point in Math.log2 aligns
125
+ let o = { align: ALIGN[op], offset: 0 }, param
126
+ while (args[0]?.includes('=')) param = args.shift().split('='), o[param[0]] = Number(param[1])
127
+ immed = [Math.log2(o.align), ...uleb(o.offset)]
128
+ }
189
129
 
190
- // FIXME (memory.grow $idx?)
191
- else if (opCode == 63 || opCode == 64) {
192
- after = [0]
193
- argc = 1
194
- }
130
+ // (i32.const 123)
131
+ else if (opCode >= 65 && opCode <= 68) {
132
+ immed = (opCode == 65 ? leb : opCode == 66 ? bigleb : opCode == 67 ? f32 : f64)(args.shift())
133
+ }
195
134
 
196
- // (if (result i32)? (local.get 0) (then a b) (else a b)?)
197
- else if (opCode == 4) {
198
- callstack.push(opCode)
199
- let [, type] = nodes[0][0] === 'result' ? nodes.shift() : [, 'void']
200
- after = [TYPE[type]]
201
-
202
- argc = 0, before.push(...instr(nodes.shift()))
203
- let body
204
- if (nodes[0]?.[0] === 'then') [, ...body] = nodes.shift(); else body = nodes
205
- after.push(...consume(body))
206
-
207
- callstack.pop(), callstack.push(OP.else)
208
- if (nodes[0]?.[0] === 'else') {
209
- [, ...body] = nodes.shift()
210
- if (body.length) after.push(OP.else, ...consume(body))
211
- }
212
- callstack.pop()
213
- after.push(OP.end)
214
- }
135
+ // (local.get $id), (local.tee $id x)
136
+ else if (opCode >= 32 && opCode <= 34) {
137
+ immed = uleb(args[0]?.[0] === '$' ? params[id = args.shift()] || locals[id] : args.shift())
138
+ }
215
139
 
216
- // (drop arg?), (return arg?)
217
- else if (opCode == 0x1a || opCode == 0x0f) { argc = nodes.length ? 1 : 0 }
140
+ // (global.get id), (global.set id)
141
+ else if (opCode == 35 || opCode == 36) {
142
+ immed = uleb(args[0]?.[0] === '$' ? ctx.global[args.shift()] : args.shift())
143
+ }
218
144
 
219
- // (select a b cond)
220
- else if (opCode == 0x1b) { argc = 3 }
145
+ // (call id ...nodes)
146
+ else if (opCode == 16) {
147
+ let fnName = args.shift()
148
+ immed = uleb(id = fnName[0] === '$' ? ctx.func[fnName] ?? err('Unknown function `' + fnName + '`') : fnName);
149
+ // FIXME: how to get signature of imported function
150
+ }
221
151
 
222
- // (block ...), (loop ...)
223
- else if (opCode == 2 || opCode == 3) {
224
- callstack.push(opCode)
225
- if (nodes[0]?.[0] === '$') (callstack[nodes.shift()] = callstack.length)
226
- let [, type] = nodes[0]?.[0] === 'result' ? nodes.shift() : [, 'void']
227
- after = [TYPE[type], ...consume(nodes)]
152
+ // (call_indirect (type $typeName) (idx) ...nodes)
153
+ else if (opCode == 17) {
154
+ let typeId = args.shift()[1];
155
+ typeId = typeId[0] === '$' ? ctx.type[typeId] : typeId
156
+ immed = uleb(typeId), immed.push(0) // extra immediate indicates table idx (reserved)
157
+ }
228
158
 
229
- if (!group.inline) callstack.pop(), after.push(OP.end) // inline loop/block expects end to be separately provided
230
- }
159
+ // FIXME (memory.grow $idx?)
160
+ else if (opCode == 63 || opCode == 64) {
161
+ immed = [0]
162
+ }
231
163
 
232
- // (end)
233
- else if (opCode == 0x0b) callstack.pop()
164
+ // (if ...), (block ...), (loop ...)
165
+ else if (opCode > 1 && opCode < 5) {
166
+ blocks.push(opCode)
234
167
 
235
- // (br $label result?)
236
- // (br_if $label cond result?)
237
- else if (opCode == 0x0c || opCode == 0x0d) {
238
- // br index indicates how many callstack items to pop
239
- after = uleb(nodes[0]?.[0] === '$' ? callstack.length - callstack[nodes.shift()] : nodes.shift())
240
- argc = (opCode == 0x0d ? 1 + (nodes.length > 1) : !!nodes.length)
241
- }
168
+ // (block $x) (loop $y)
169
+ if (opCode < 4 && args[0]?.[0] === '$') (blocks[args.shift()] = blocks.length)
242
170
 
243
- // (br_table 1 2 3 4 0 selector result?)
244
- else if (opCode == 0x0e) {
245
- after = []
246
- while (!Array.isArray(nodes[0])) id = nodes.shift(), after.push(...uleb(id[0][0] === '$' ? callstack.length - callstack[id] : id))
247
- after.unshift(...uleb(after.length - 1))
248
- argc = 1 + (nodes.length > 1)
171
+ // get type
172
+ // (result i32)
173
+ if (args[0]?.[0] === 'result') {
174
+ if (args[0].length < 3) {
175
+ let [, type] = args.shift()
176
+ immed = [TYPE[type]]
177
+ }
178
+ // (result i32 i32)
179
+ else {
180
+ let [typeId] = consumeType(args, ctx)
181
+ immed = [typeId]
182
+ }
183
+ }
184
+ else immed = [TYPE.void]
185
+
186
+ if (group) {
187
+ nodes.unshift('end')
188
+
189
+ // (block xxx) -> block xxx end
190
+ if (opCode < 4) while (args.length) nodes.unshift(args.pop())
191
+
192
+ // (if cond a) -> cond if a end
193
+ else if (args.length < 3) nodes.unshift(args.pop())
194
+ // (if cond (then a) (else b)) -> `cond if a else b end`
195
+ else {
196
+ nodes.unshift(args.pop())
197
+ // (if cond a b) -> (if cond a else b)
198
+ if (nodes[0][0] !== 'else') nodes.unshift('else')
199
+ // (if a b (else)) -> (if a b)
200
+ else if (nodes[0].length < 2) nodes.shift()
201
+ nodes.unshift(args.pop())
202
+ }
249
203
  }
204
+ }
250
205
 
251
- else if (opCode == null) err(`Unknown instruction \`${op}\``)
206
+ // (else)
207
+ else if (opCode === 5) {
208
+ // (else xxx) -> else xxx
209
+ if (group) while (args.length) nodes.unshift(args.pop())
252
210
  }
253
211
 
254
- // consume arguments
255
- if (nodes.length < argc) err(`Stack arguments are not supported at \`${op}\``)
256
- while (argc--) before.push(...instr(nodes.shift()))
257
- if (nodes.length) err(`Too many arguments for \`${op}\`.`)
212
+ // (end)
213
+ else if (opCode == 0x0b) blocks.pop()
258
214
 
259
- return [...before, opCode, ...after]
260
- }
215
+ // (br $label result?)
216
+ // (br_if $label cond result?)
217
+ else if (opCode == 0x0c || opCode == 0x0d) {
218
+ // br index indicates how many block items to pop
219
+ immed = uleb(args[0]?.[0] === '$' ? blocks.length - blocks[args.shift()] : args.shift())
220
+ }
261
221
 
262
- // consume sequence of nodes
263
- const consume = nodes => {
264
- let result = []
265
- while (nodes.length) {
266
- let node = nodes.shift(), c
267
-
268
- if (typeof node === 'string') {
269
- // permit some inline instructions: loop $label ... end, br $label, arg return
270
- if (c = INLINE[node]) {
271
- node = [node], node.inline = true
272
- if (c > 0) nodes[0]?.[0] === '$' && node.push(nodes.shift())
273
- }
274
- else err(`Inline instruction \`${node}\` is not supported`)
275
- }
222
+ // (br_table 1 2 3 4 0 selector result?)
223
+ else if (opCode == 0x0e) {
224
+ immed = []
225
+ while (!Array.isArray(args[0])) id = args.shift(), immed.push(...uleb(id[0][0] === '$' ? blocks.length - blocks[id] : id))
226
+ immed.unshift(...uleb(immed.length - 1))
227
+ }
276
228
 
277
- node && result.push(...instr(node))
229
+ // if group (cmd im1 im2 arg1 arg2) - insert any remaining args first: arg1 arg2
230
+ // because inline case has them in stack already
231
+ if (group) {
232
+ while (args.length) consume(args, out)
278
233
  }
279
- return result
234
+
235
+ // ignore (then) and other unknown instructions
236
+ if (opCode >= 0) out.push(opCode)
237
+ if (immed) out.push(...immed)
280
238
  }
281
239
 
282
- // evaluates after all definitions
240
+ // evaluates after all definitions (need globals, elements, data etc.)
241
+ // FIXME: get rid of this postcall
283
242
  return () => {
284
- let code = consume(body)
285
- ctx.code.push([...uleb(code.length + 2 + locTypes.length), ...uleb(locTypes.length >> 1), ...locTypes, ...code, OP.end])
243
+ const bytes = []
244
+ while (body.length) consume(body, bytes)
245
+ ctx.code.push([...uleb(bytes.length + 2 + locTypes.length), ...uleb(locTypes.length >> 1), ...locTypes, ...bytes, OP_END])
286
246
  }
287
247
  },
288
248
 
@@ -337,7 +297,7 @@ const build = {
337
297
  if (kind === 'func') {
338
298
  // we track imported funcs in func section to share namespace, and skip them on final build
339
299
  if (name) ctx.func[name] = ctx.func.length
340
- let [typeIdx] = build.type([, ['func', ...parts]], ctx)
300
+ let [typeIdx] = consumeType(parts, ctx)
341
301
  ctx.func.push(details = uleb(typeIdx))
342
302
  ctx.func.importc = (ctx.func.importc || 0) + 1
343
303
  }
@@ -372,8 +332,8 @@ const build = {
372
332
 
373
333
  // (i32.const 0) - instantiation time initializer
374
334
  const iinit = ([op, literal], ctx) => op[0] === 'f' ?
375
- [OP[op], ...(op[1] === '3' ? f32 : f64)(literal), OP.end] :
376
- [OP[op], ...(op[1] === '3' ? leb : bigleb)(literal[0] === '$' ? ctx.global[literal] : literal), OP.end]
335
+ [op[1] === '3' ? OP_F32_CONST : OP_F64_CONST, ...(op[1] === '3' ? f32 : f64)(literal), OP_END] :
336
+ [op[1] === '3' ? OP_I32_CONST : OP_I64_CONST, ...(op[1] === '3' ? leb : bigleb)(literal[0] === '$' ? ctx.global[literal] : literal), OP_END]
377
337
 
378
338
  const escape = { n: 10, r: 13, t: 9, v: 1 }
379
339
 
@@ -391,8 +351,33 @@ const str = str => {
391
351
  return res
392
352
  }
393
353
 
394
-
395
354
  // build range/limits sequence (non-consuming)
396
355
  const range = ([min, max, shared]) => isNaN(parseInt(max)) ? [0, ...uleb(min)] : [shared === 'shared' ? 3 : 1, ...uleb(min), ...uleb(max)]
397
356
 
357
+ // get type info from (params) (result) nodes sequence (consumes nodes)
358
+ // returns registered (reused) type idx, params bytes, result bytes
359
+ // eg. (type $return_i32 (func (result i32)))
360
+ const consumeType = (nodes, ctx) => {
361
+ let params = [], result = [], idx, bytes
362
+
363
+ // collect params
364
+ while (nodes[0]?.[0] === 'param') {
365
+ let [, ...types] = nodes.shift()
366
+ if (types[0]?.[0] === '$') params[types.shift()] = params.length
367
+ params.push(...types.map(t => TYPE[t]))
368
+ }
369
+
370
+ // collect result type
371
+ if (nodes[0]?.[0] === 'result') result = nodes.shift().slice(1).map(t => TYPE[t])
372
+
373
+ // reuse existing type or register new one
374
+ bytes = [TYPE.func, ...uleb(params.length), ...params, ...uleb(result.length), ...result]
375
+ idx = ctx.type.findIndex((t) => t.every((byte, i) => byte === bytes[i]))
376
+
377
+ // register new type, if not found
378
+ if (idx < 0) idx = ctx.type.push(bytes) - 1
379
+
380
+ return [idx, params, result]
381
+ }
382
+
398
383
  const err = text => { throw Error(text) }
package/src/const.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // ref: https://github.com/stagas/wat-compiler/blob/main/lib/const.js
2
2
  // NOTE: squashing into a string doesn't save up gzipped size
3
3
  export const OP = [
4
- 'unreachable', 'nop', 'block', 'loop', 'if', 'else', ,,,,,
5
- 'end', 'br', 'br_if', 'br_table', 'return', 'call', 'call_indirect', ,,,,,,,,
6
- 'drop', 'select', ,,,,
7
- 'local.get', 'local.set', 'local.tee', 'global.get', 'global.set', ,,,
4
+ 'unreachable', 'nop', 'block', 'loop', 'if', 'else', , , , , ,
5
+ 'end', 'br', 'br_if', 'br_table', 'return', 'call', 'call_indirect', , , , , , , , ,
6
+ 'drop', 'select', , , , ,
7
+ 'local.get', 'local.set', 'local.tee', 'global.get', 'global.set', , , ,
8
8
  'i32.load', 'i64.load', 'f32.load', 'f64.load',
9
9
  'i32.load8_s', 'i32.load8_u', 'i32.load16_s', 'i32.load16_u',
10
10
  'i64.load8_s', 'i64.load8_u', 'i64.load16_s', 'i64.load16_u', 'i64.load32_s', 'i64.load32_u',
@@ -14,8 +14,8 @@ export const OP = [
14
14
  'i32.const', 'i64.const', 'f32.const', 'f64.const',
15
15
  'i32.eqz', 'i32.eq', 'i32.ne', 'i32.lt_s', 'i32.lt_u', 'i32.gt_s', 'i32.gt_u', 'i32.le_s', 'i32.le_u', 'i32.ge_s', 'i32.ge_u',
16
16
  'i64.eqz', 'i64.eq', 'i64.ne', 'i64.lt_s', 'i64.lt_u', 'i64.gt_s', 'i64.gt_u', 'i64.le_s', 'i64.le_u', 'i64.ge_s', 'i64.ge_u',
17
- 'f32.eq', 'f32.ne', 'f32.lt', 'f32.gt', 'f32.le', 'f32.ge',
18
- 'f64.eq', 'f64.ne', 'f64.lt', 'f64.gt', 'f64.le', 'f64.ge',
17
+ 'f32.eq', 'f32.ne', 'f32.lt', 'f32.gt', 'f32.le', 'f32.ge',
18
+ 'f64.eq', 'f64.ne', 'f64.lt', 'f64.gt', 'f64.le', 'f64.ge',
19
19
  'i32.clz', 'i32.ctz', 'i32.popcnt', 'i32.add', 'i32.sub', 'i32.mul', 'i32.div_s', 'i32.div_u', 'i32.rem_s', 'i32.rem_u', 'i32.and', 'i32.or', 'i32.xor', 'i32.shl', 'i32.shr_s', 'i32.shr_u', 'i32.rotl', 'i32.rotr',
20
20
  'i64.clz', 'i64.ctz', 'i64.popcnt', 'i64.add', 'i64.sub', 'i64.mul', 'i64.div_s', 'i64.div_u', 'i64.rem_s', 'i64.rem_u', 'i64.and', 'i64.or', 'i64.xor', 'i64.shl', 'i64.shr_s', 'i64.shr_u', 'i64.rotl', 'i64.rotr',
21
21
  'f32.abs', 'f32.neg', 'f32.ceil', 'f32.floor', 'f32.trunc', 'f32.nearest', 'f32.sqrt', 'f32.add', 'f32.sub', 'f32.mul', 'f32.div', 'f32.min', 'f32.max', 'f32.copysign',
@@ -27,15 +27,16 @@ export const OP = [
27
27
  'f64.convert_i32_s', 'f64.convert_i32_u', 'f64.convert_i64_s', 'f64.convert_i64_u', 'f64.promote_f32',
28
28
  'i32.reinterpret_f32', 'i64.reinterpret_f64', 'f32.reinterpret_i32', 'f64.reinterpret_i64',
29
29
  ],
30
- SECTION = { type:1, import:2, func:3, table:4, memory:5, global:6, export:7, start:8, elem:9, code:10, data:11 },
31
- TYPE = { i32:0x7f, i64:0x7e, f32:0x7d, f64:0x7c, void:0x40, func:0x60, funcref:0x70 },
32
- KIND = { func: 0, table: 1, memory: 2, global: 3 },
33
- ALIGN = {
34
- 'i32.load': 4, 'i64.load': 8, 'f32.load': 4, 'f64.load': 8,
35
- 'i32.load8_s': 1, 'i32.load8_u': 1, 'i32.load16_s': 2, 'i32.load16_u': 2,
36
- 'i64.load8_s': 1, 'i64.load8_u': 1, 'i64.load16_s': 2, 'i64.load16_u': 2, 'i64.load32_s': 4, 'i64.load32_u': 4, 'i32.store': 4,
37
- 'i64.store': 8, 'f32.store': 4, 'f64.store': 8,
38
- 'i32.store8': 1, 'i32.store16': 2, 'i64.store8': 1, 'i64.store16': 2, 'i64.store32': 4,
39
- }
40
-
41
- OP.map((op,i)=>OP[op]=i) // init op names
30
+ SECTION = { type: 1, import: 2, func: 3, table: 4, memory: 5, global: 6, export: 7, start: 8, elem: 9, code: 10, data: 11 },
31
+ TYPE = { i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c, void: 0x40, func: 0x60, funcref: 0x70 },
32
+ KIND = { func: 0, table: 1, memory: 2, global: 3 },
33
+ ALIGN = {
34
+ 'i32.load': 4, 'i64.load': 8, 'f32.load': 4, 'f64.load': 8,
35
+ 'i32.load8_s': 1, 'i32.load8_u': 1, 'i32.load16_s': 2, 'i32.load16_u': 2,
36
+ 'i64.load8_s': 1, 'i64.load8_u': 1, 'i64.load16_s': 2, 'i64.load16_u': 2, 'i64.load32_s': 4, 'i64.load32_u': 4, 'i32.store': 4,
37
+ 'i64.store': 8, 'f32.store': 4, 'f64.store': 8,
38
+ 'i32.store8': 1, 'i32.store16': 2, 'i64.store8': 1, 'i64.store16': 2, 'i64.store32': 4,
39
+ },
40
+ BLOCK = {
41
+ loop: 1, block: 1, if: 1, end: -1, return: -1
42
+ }