watr 1.3.2 → 1.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/package.json +3 -8
- package/readme.md +10 -12
- package/src/compile.js +398 -0
- package/src/const.js +41 -0
- package/src/parse.js +32 -0
- package/src/print.js +61 -0
- package/src/util.js +102 -0
- package/watr.js +5 -619
- package/watr.min.js +0 -1
package/package.json
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "watr",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Ligth & fast WAT compiler",
|
|
5
5
|
"main": "watr.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node test"
|
|
9
|
-
"build": "rollup src/watr.js --file watr.js --format esm --name \"Watr\"",
|
|
10
|
-
"min": "terser watr.js -o watr.min.js --module -c passes=3 -m"
|
|
8
|
+
"test": "node test"
|
|
11
9
|
},
|
|
12
10
|
"repository": {
|
|
13
11
|
"type": "git",
|
|
14
12
|
"url": "git+https://github.com/audio-lab/watr.git"
|
|
15
13
|
},
|
|
16
14
|
"files": [
|
|
17
|
-
"
|
|
18
|
-
"watr.min.js"
|
|
15
|
+
"src"
|
|
19
16
|
],
|
|
20
17
|
"keywords": [
|
|
21
18
|
"wat",
|
|
@@ -29,8 +26,6 @@
|
|
|
29
26
|
},
|
|
30
27
|
"homepage": "https://github.com/audio-lab/watr#readme",
|
|
31
28
|
"devDependencies": {
|
|
32
|
-
"rollup": "^2.70.1",
|
|
33
|
-
"terser": "^5.12.1",
|
|
34
29
|
"tst": "^7.1.1",
|
|
35
30
|
"wabt": "^1.0.28",
|
|
36
31
|
"wat-compiler": "^1.0.0"
|
package/readme.md
CHANGED
|
@@ -88,25 +88,23 @@ const tree = [
|
|
|
88
88
|
['f64.mul', ['local.get', 0], ['f64.const', 2]]
|
|
89
89
|
]
|
|
90
90
|
|
|
91
|
-
//
|
|
91
|
+
// pretty-print (default)
|
|
92
92
|
const str = print(tree, {
|
|
93
|
-
indent:
|
|
94
|
-
newline:
|
|
95
|
-
pad: false
|
|
96
|
-
})
|
|
97
|
-
// (func (export "double")(param f64)(result f64)(f64.mul (local.get 0)(f64.const 2)))
|
|
98
|
-
|
|
99
|
-
// pretty-print
|
|
100
|
-
const str = print(tree, {
|
|
101
|
-
indent: ' ', // indentation step
|
|
102
|
-
newline: '\n', // new line
|
|
103
|
-
pad: '', // pad each newline with chars
|
|
93
|
+
indent: ' ', // indentation chars
|
|
94
|
+
newline: '\n', // new line chars
|
|
104
95
|
})
|
|
105
96
|
// (func (export "double")
|
|
106
97
|
// (param f64) (result f64)
|
|
107
98
|
// (f64.mul
|
|
108
99
|
// (local.get 0)
|
|
109
100
|
// (f64.const 2)))
|
|
101
|
+
|
|
102
|
+
// minify
|
|
103
|
+
const str = print(tree, {
|
|
104
|
+
indent: false,
|
|
105
|
+
newline: false
|
|
106
|
+
})
|
|
107
|
+
// (func (export "double")(param f64)(result f64)(f64.mul (local.get 0)(f64.const 2)))
|
|
110
108
|
```
|
|
111
109
|
|
|
112
110
|
<!--
|
package/src/compile.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { uleb, leb, bigleb, f64, f32 } from './util.js'
|
|
2
|
+
import { OP, SECTION, ALIGN, TYPE, KIND } from './const.js'
|
|
3
|
+
import parse from './parse.js'
|
|
4
|
+
|
|
5
|
+
// some inlinable instructions
|
|
6
|
+
const INLINE = { loop: 1, block: 1, if: 1, end: -1, return: -1 }
|
|
7
|
+
|
|
8
|
+
// convert wat tree to wasm binary
|
|
9
|
+
export default (nodes) => {
|
|
10
|
+
if (typeof nodes === 'string') nodes = parse(nodes);
|
|
11
|
+
|
|
12
|
+
// IR. Alias is stored directly to section array by key, eg. section.func.$name = idx
|
|
13
|
+
let sections = {
|
|
14
|
+
type: [], import: [], func: [], table: [], memory: [], global: [], export: [], start: [], elem: [], code: [], data: []
|
|
15
|
+
}, binary = [
|
|
16
|
+
0x00, 0x61, 0x73, 0x6d, // magic
|
|
17
|
+
0x01, 0x00, 0x00, 0x00, // version
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
// 1. transform tree
|
|
21
|
+
// (func) → [(func)]
|
|
22
|
+
if (typeof nodes[0] === 'string' && nodes[0] !== 'module') nodes = [nodes]
|
|
23
|
+
|
|
24
|
+
// (global $a (import "a" "b") (mut i32)) → (import "a" "b" (global $a (mut i32)))
|
|
25
|
+
// (memory (import "a" "b") min max shared) → (import "a" "b" (memory min max shared))
|
|
26
|
+
nodes = nodes.map(node => {
|
|
27
|
+
if (node[2]?.[0] === 'import') {
|
|
28
|
+
let [kind, name, imp, ...args] = node
|
|
29
|
+
return [...imp, [kind, name, ...args]]
|
|
30
|
+
}
|
|
31
|
+
else if (node[1]?.[0] === 'import') {
|
|
32
|
+
let [kind, imp, ...args] = node
|
|
33
|
+
return [...imp, [kind, ...args]]
|
|
34
|
+
}
|
|
35
|
+
return node
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// 2. build IR. import must be initialized first, global before func, elem after func
|
|
39
|
+
let order = ['type', 'import', 'table', 'memory', 'global', 'func', 'export', 'start', 'elem', 'data'], postcall = []
|
|
40
|
+
|
|
41
|
+
for (let name of order) {
|
|
42
|
+
let remaining = []
|
|
43
|
+
for (let node of nodes) {
|
|
44
|
+
node[0] === name ? postcall.push(build[name](node, sections)) : remaining.push(node)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
nodes = remaining
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// code must be compiled after all definitions
|
|
51
|
+
for (let cb of postcall) cb && cb.call && cb()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// 3. build binary
|
|
55
|
+
for (let name in sections) {
|
|
56
|
+
let items = sections[name]
|
|
57
|
+
if (items.importc) items = items.slice(items.importc) // discard imported functions/globals
|
|
58
|
+
if (!items.length) continue
|
|
59
|
+
let sectionCode = SECTION[name], bytes = []
|
|
60
|
+
if (sectionCode !== 8) bytes.push(items.length) // skip start section count
|
|
61
|
+
for (let item of items) bytes.push(...item)
|
|
62
|
+
binary.push(sectionCode, ...uleb(bytes.length), ...bytes)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new Uint8Array(binary)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const build = {
|
|
69
|
+
// (type $name? (func (param $x i32) (param i64 i32) (result i32 i64)))
|
|
70
|
+
// signature part is identical to function
|
|
71
|
+
// 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
|
+
|
|
94
|
+
if (typeName) ctx.type[typeName] = idx
|
|
95
|
+
|
|
96
|
+
return [idx, params, result]
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// (func $name? ...params result ...body)
|
|
100
|
+
func([, ...body], ctx) {
|
|
101
|
+
let locals = [], // list of local variables
|
|
102
|
+
callstack = []
|
|
103
|
+
|
|
104
|
+
// fn name
|
|
105
|
+
if (body[0]?.[0] === '$') ctx.func[body.shift()] = ctx.func.length
|
|
106
|
+
|
|
107
|
+
// export binding
|
|
108
|
+
if (body[0]?.[0] === 'export') build.export([...body.shift(), ['func', ctx.func.length]], ctx)
|
|
109
|
+
|
|
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()
|
|
114
|
+
ctx.func.push([typeIdx])
|
|
115
|
+
|
|
116
|
+
// collect locals
|
|
117
|
+
while (body[0]?.[0] === 'local') {
|
|
118
|
+
let [, ...types] = body.shift(), name
|
|
119
|
+
if (types[0][0] === '$')
|
|
120
|
+
params[name = types.shift()] ? err('Ambiguous name ' + name) :
|
|
121
|
+
locals[name] = params.length + locals.length
|
|
122
|
+
locals.push(...types.map(t => TYPE[t]))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// squash local types
|
|
126
|
+
let locTypes = locals.reduce((a, type) => (type == a[a.length - 1] ? a[a.length - 2]++ : a.push(1, type), a), [])
|
|
127
|
+
|
|
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
|
+
}
|
|
156
|
+
|
|
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
|
+
}
|
|
161
|
+
|
|
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
|
+
}
|
|
167
|
+
|
|
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
|
+
}
|
|
173
|
+
|
|
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
|
+
}
|
|
181
|
+
|
|
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
|
+
}
|
|
189
|
+
|
|
190
|
+
// FIXME (memory.grow $idx?)
|
|
191
|
+
else if (opCode == 63 || opCode == 64) {
|
|
192
|
+
after = [0]
|
|
193
|
+
argc = 1
|
|
194
|
+
}
|
|
195
|
+
|
|
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
|
+
}
|
|
215
|
+
|
|
216
|
+
// (drop arg?), (return arg?)
|
|
217
|
+
else if (opCode == 0x1a || opCode == 0x0f) { argc = nodes.length ? 1 : 0 }
|
|
218
|
+
|
|
219
|
+
// (select a b cond)
|
|
220
|
+
else if (opCode == 0x1b) { argc = 3 }
|
|
221
|
+
|
|
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)]
|
|
228
|
+
|
|
229
|
+
if (!group.inline) callstack.pop(), after.push(OP.end) // inline loop/block expects end to be separately provided
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// (end)
|
|
233
|
+
else if (opCode == 0x0b) callstack.pop()
|
|
234
|
+
|
|
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
|
+
}
|
|
242
|
+
|
|
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)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
else if (opCode == null) err(`Unknown instruction \`${op}\``)
|
|
252
|
+
}
|
|
253
|
+
|
|
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}\`.`)
|
|
258
|
+
|
|
259
|
+
return [...before, opCode, ...after]
|
|
260
|
+
}
|
|
261
|
+
|
|
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
|
+
}
|
|
276
|
+
|
|
277
|
+
node && result.push(...instr(node))
|
|
278
|
+
}
|
|
279
|
+
return result
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// evaluates after all definitions
|
|
283
|
+
return () => {
|
|
284
|
+
let code = consume(body)
|
|
285
|
+
ctx.code.push([...uleb(code.length + 2 + locTypes.length), ...uleb(locTypes.length >> 1), ...locTypes, ...code, OP.end])
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
// (memory min max shared)
|
|
290
|
+
// (memory $name min max shared)
|
|
291
|
+
// (memory (export "mem") 5)
|
|
292
|
+
memory([, ...parts], ctx) {
|
|
293
|
+
if (parts[0][0] === '$') ctx.memory[parts.shift()] = ctx.memory.length
|
|
294
|
+
if (parts[0][0] === 'export') build.export([...parts.shift(), ['memory', ctx.memory.length]], ctx)
|
|
295
|
+
ctx.memory.push(range(parts))
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
// (global i32 (i32.const 42))
|
|
299
|
+
// (global $id i32 (i32.const 42))
|
|
300
|
+
// (global $id (mut i32) (i32.const 42))
|
|
301
|
+
global([, ...args], ctx) {
|
|
302
|
+
let name = args[0][0] === '$' && args.shift()
|
|
303
|
+
if (name) ctx.global[name] = ctx.global.length
|
|
304
|
+
let [type, init] = args, mut = type[0] === 'mut' ? 1 : 0
|
|
305
|
+
ctx.global.push([TYPE[mut ? type[1] : type], mut, ...iinit(init)])
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// (table 1 2? funcref)
|
|
309
|
+
// (table $name 1 2? funcref)
|
|
310
|
+
table([, ...args], ctx) {
|
|
311
|
+
let name = args[0][0] === '$' && args.shift()
|
|
312
|
+
if (name) ctx.table[name] = ctx.table.length
|
|
313
|
+
let lims = range(args)
|
|
314
|
+
ctx.table.push([TYPE[args.pop()], ...lims])
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// (elem (i32.const 0) $f1 $f2), (elem (global.get 0) $f1 $f2)
|
|
318
|
+
elem([, offset, ...elems], ctx) {
|
|
319
|
+
const tableIdx = 0 // FIXME: table index can be defined
|
|
320
|
+
ctx.elem.push([tableIdx, ...iinit(offset, ctx), ...uleb(elems.length), ...elems.flatMap(el => uleb(el[0] === '$' ? ctx.func[el] : el))])
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// (export "name" (kind $name|idx))
|
|
324
|
+
export([, name, [kind, idx]], ctx) {
|
|
325
|
+
if (idx[0] === '$') idx = ctx[kind][idx]
|
|
326
|
+
ctx.export.push([...str(name), KIND[kind], ...uleb(idx)])
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
// (import "math" "add" (func $add (param i32 i32 externref) (result i32)))
|
|
330
|
+
// (import "js" "mem" (memory 1))
|
|
331
|
+
// (import "js" "mem" (memory $name 1))
|
|
332
|
+
// (import "js" "v" (global $name (mut f64)))
|
|
333
|
+
import([, mod, field, ref], ctx) {
|
|
334
|
+
let details, [kind, ...parts] = ref,
|
|
335
|
+
name = parts[0]?.[0] === '$' && parts.shift();
|
|
336
|
+
|
|
337
|
+
if (kind === 'func') {
|
|
338
|
+
// we track imported funcs in func section to share namespace, and skip them on final build
|
|
339
|
+
if (name) ctx.func[name] = ctx.func.length
|
|
340
|
+
let [typeIdx] = build.type([, ['func', ...parts]], ctx)
|
|
341
|
+
ctx.func.push(details = uleb(typeIdx))
|
|
342
|
+
ctx.func.importc = (ctx.func.importc || 0) + 1
|
|
343
|
+
}
|
|
344
|
+
else if (kind === 'memory') {
|
|
345
|
+
if (name) ctx.memory[name] = ctx.memory.length
|
|
346
|
+
details = range(parts)
|
|
347
|
+
}
|
|
348
|
+
else if (kind === 'global') {
|
|
349
|
+
// imported globals share namespace with internal globals - we skip them in final build
|
|
350
|
+
if (name) ctx.global[name] = ctx.global.length
|
|
351
|
+
let [type] = parts, mut = type[0] === 'mut' ? 1 : 0
|
|
352
|
+
details = [TYPE[mut ? type[1] : type], mut]
|
|
353
|
+
ctx.global.push(details)
|
|
354
|
+
ctx.global.importc = (ctx.global.importc || 0) + 1
|
|
355
|
+
}
|
|
356
|
+
else throw Error('Unimplemented ' + kind)
|
|
357
|
+
|
|
358
|
+
ctx.import.push([...str(mod), ...str(field), KIND[kind], ...details])
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
// (data (i32.const 0) "\aa" "\bb"?)
|
|
362
|
+
data([, offset, ...inits], ctx) {
|
|
363
|
+
// FIXME: first is mem index
|
|
364
|
+
ctx.data.push([0, ...iinit(offset, ctx), ...str(inits.map(i => i[0] === '"' ? i.slice(1, -1) : i).join(''))])
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
// (start $main)
|
|
368
|
+
start([, name], ctx) {
|
|
369
|
+
if (!ctx.start.length) ctx.start.push([name[0] === '$' ? ctx.func[name] : name])
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// (i32.const 0) - instantiation time initializer
|
|
374
|
+
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]
|
|
377
|
+
|
|
378
|
+
const escape = { n: 10, r: 13, t: 9, v: 1 }
|
|
379
|
+
|
|
380
|
+
// build string binary
|
|
381
|
+
const str = str => {
|
|
382
|
+
str = str[0] === '"' ? str.slice(1, -1) : str
|
|
383
|
+
let res = [], i = 0, c, BSLASH = 92
|
|
384
|
+
// spec https://webassembly.github.io/spec/core/text/values.html#strings
|
|
385
|
+
for (; i < str.length;) {
|
|
386
|
+
c = str.charCodeAt(i++)
|
|
387
|
+
res.push(c === BSLASH ? escape[str[i++]] || parseInt(str.slice(i - 1, ++i), 16) : c)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
res.unshift(...uleb(res.length))
|
|
391
|
+
return res
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
// build range/limits sequence (non-consuming)
|
|
396
|
+
const range = ([min, max, shared]) => isNaN(parseInt(max)) ? [0, ...uleb(min)] : [shared === 'shared' ? 3 : 1, ...uleb(min), ...uleb(max)]
|
|
397
|
+
|
|
398
|
+
const err = text => { throw Error(text) }
|
package/src/const.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ref: https://github.com/stagas/wat-compiler/blob/main/lib/const.js
|
|
2
|
+
// NOTE: squashing into a string doesn't save up gzipped size
|
|
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', ,,,
|
|
8
|
+
'i32.load', 'i64.load', 'f32.load', 'f64.load',
|
|
9
|
+
'i32.load8_s', 'i32.load8_u', 'i32.load16_s', 'i32.load16_u',
|
|
10
|
+
'i64.load8_s', 'i64.load8_u', 'i64.load16_s', 'i64.load16_u', 'i64.load32_s', 'i64.load32_u',
|
|
11
|
+
'i32.store', 'i64.store', 'f32.store', 'f64.store',
|
|
12
|
+
'i32.store8', 'i32.store16', 'i64.store8', 'i64.store16', 'i64.store32',
|
|
13
|
+
'memory.size', 'memory.grow',
|
|
14
|
+
'i32.const', 'i64.const', 'f32.const', 'f64.const',
|
|
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
|
+
'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',
|
|
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
|
+
'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
|
+
'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',
|
|
22
|
+
'f64.abs', 'f64.neg', 'f64.ceil', 'f64.floor', 'f64.trunc', 'f64.nearest', 'f64.sqrt', 'f64.add', 'f64.sub', 'f64.mul', 'f64.div', 'f64.min', 'f64.max', 'f64.copysign',
|
|
23
|
+
'i32.wrap_i64',
|
|
24
|
+
'i32.trunc_f32_s', 'i32.trunc_f32_u', 'i32.trunc_f64_s', 'i32.trunc_f64_u', 'i64.extend_i32_s', 'i64.extend_i32_u',
|
|
25
|
+
'i64.trunc_f32_s', 'i64.trunc_f32_u', 'i64.trunc_f64_s', 'i64.trunc_f64_u',
|
|
26
|
+
'f32.convert_i32_s', 'f32.convert_i32_u', 'f32.convert_i64_s', 'f32.convert_i64_u', 'f32.demote_f64',
|
|
27
|
+
'f64.convert_i32_s', 'f64.convert_i32_u', 'f64.convert_i64_s', 'f64.convert_i64_u', 'f64.promote_f32',
|
|
28
|
+
'i32.reinterpret_f32', 'i64.reinterpret_f64', 'f32.reinterpret_i32', 'f64.reinterpret_i64',
|
|
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
|
package/src/parse.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const OPAREN = 40, CPAREN = 41, OBRACK = 91, CBRACK = 93, SPACE = 32, DQUOTE = 34, PERIOD = 46,
|
|
2
|
+
_0 = 48, _9 = 57, SEMIC = 59, NEWLINE = 32, PLUS = 43, MINUS = 45, COLON = 58
|
|
3
|
+
|
|
4
|
+
export default (str) => {
|
|
5
|
+
let i = 0, level = [], buf = ''
|
|
6
|
+
|
|
7
|
+
const commit = () => buf && (
|
|
8
|
+
level.push(buf),
|
|
9
|
+
buf = ''
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const parseLevel = () => {
|
|
13
|
+
for (let c, root; i < str.length;) {
|
|
14
|
+
c = str.charCodeAt(i)
|
|
15
|
+
if (c === DQUOTE) commit(), buf = str.slice(i++, i = str.indexOf('"', i) + 1), commit()
|
|
16
|
+
else if (c === OPAREN) {
|
|
17
|
+
if (str.charCodeAt(i + 1) === SEMIC) i = str.indexOf(';)', i) + 2 // (; ... ;)
|
|
18
|
+
else commit(), i++, (root = level).push(level = []), parseLevel(), level = root
|
|
19
|
+
}
|
|
20
|
+
else if (c === SEMIC) i = str.indexOf('\n', i) + 1 // ; ...
|
|
21
|
+
else if (c <= SPACE) commit(), i++
|
|
22
|
+
else if (c === CPAREN) return commit(), i++
|
|
23
|
+
else buf += str[i++]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
commit()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
parseLevel()
|
|
30
|
+
|
|
31
|
+
return level.length > 1 ? level : level[0]
|
|
32
|
+
}
|
package/src/print.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import parse from './parse.js';
|
|
2
|
+
|
|
3
|
+
let indent = '', newline = '\n'
|
|
4
|
+
|
|
5
|
+
export default function print(tree, options = {}) {
|
|
6
|
+
if (typeof tree === 'string') tree = parse(tree);
|
|
7
|
+
|
|
8
|
+
({ indent=' ', newline='\n' } = options);
|
|
9
|
+
indent ||= '', newline ||= '';
|
|
10
|
+
|
|
11
|
+
let out = typeof tree[0] === 'string' ? printNode(tree) : tree.map(node => printNode(node)).join(newline)
|
|
12
|
+
console.log(out)
|
|
13
|
+
return out
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const INLINE = [
|
|
17
|
+
'param',
|
|
18
|
+
'drop',
|
|
19
|
+
'f32.const',
|
|
20
|
+
'f64.const',
|
|
21
|
+
'i32.const',
|
|
22
|
+
'i64.const',
|
|
23
|
+
'local.get',
|
|
24
|
+
'global.get',
|
|
25
|
+
'memory.size',
|
|
26
|
+
'result',
|
|
27
|
+
'export',
|
|
28
|
+
'unreachable',
|
|
29
|
+
'nop'
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
function printNode(node, level = 0) {
|
|
33
|
+
if (!Array.isArray(node)) return node + ''
|
|
34
|
+
|
|
35
|
+
let content = node[0]
|
|
36
|
+
|
|
37
|
+
for (let i = 1; i < node.length; i++) {
|
|
38
|
+
// new node doesn't need space separator, eg. [x,[y]] -> `x(y)`
|
|
39
|
+
if (Array.isArray(node[i])) {
|
|
40
|
+
// inline nodes like (param x)(param y)
|
|
41
|
+
// (func (export "xxx")..., but not (func (export "a")(param "b")...
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
INLINE.includes(node[i][0]) &&
|
|
45
|
+
(!Array.isArray(node[i - 1]) || INLINE.includes(node[i - 1][0]))
|
|
46
|
+
) {
|
|
47
|
+
if (!Array.isArray(node[i - 1])) content += ` `
|
|
48
|
+
} else {
|
|
49
|
+
content += newline
|
|
50
|
+
if (node[i]) content += indent.repeat(level + 1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
content += printNode(node[i], level + 1)
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
content += ` `
|
|
57
|
+
content += node[i]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return `(${content})`
|
|
61
|
+
}
|