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 CHANGED
@@ -1,21 +1,18 @@
1
1
  {
2
2
  "name": "watr",
3
- "version": "1.3.2",
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
- "watr.js",
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
- // minify (default)
91
+ // pretty-print (default)
92
92
  const str = print(tree, {
93
- indent: false,
94
- newline: false,
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
+ }