watr 1.0.0 → 1.2.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/watr.js CHANGED
@@ -1,112 +1,204 @@
1
+ // encoding ref: https://github.com/j-s-n/WebBS/blob/master/compiler/byteCode.js
2
+ const uleb = (number, buffer=[]) => {
3
+ if (typeof number === 'string') number = parseInt(number.replaceAll('_',''));
4
+
5
+ let byte = number & 0b01111111;
6
+ number = number >>> 7;
7
+
8
+ if (number === 0) {
9
+ buffer.push(byte);
10
+ return buffer;
11
+ } else {
12
+ buffer.push(byte | 0b10000000);
13
+ return uleb(number, buffer);
14
+ }
15
+ };
16
+
17
+ function leb (n, buffer=[]) {
18
+ if (typeof n === 'string') n = parseInt(n.replaceAll('_',''));
19
+
20
+ while (true) {
21
+ const byte = Number(n & 0x7F);
22
+ n >>= 7;
23
+ if ((n === 0 && (byte & 0x40) === 0) || (n === -1 && (byte & 0x40) !== 0)) {
24
+ buffer.push(byte);
25
+ break
26
+ }
27
+ buffer.push((byte | 0x80));
28
+ }
29
+ return buffer
30
+ }
31
+
32
+ function bigleb(n, buffer=[]) {
33
+ if (typeof n === 'string') {
34
+ n = n.replaceAll('_','');
35
+ n = n[0]==='-'?-BigInt(n.slice(1)):BigInt(n);
36
+ byteView.setBigInt64(0, n);
37
+ n = byteView.getBigInt64(0);
38
+ }
39
+
40
+ while (true) {
41
+ const byte = Number(n & 0x7Fn);
42
+ n >>= 7n;
43
+ if ((n === 0n && (byte & 0x40) === 0) || (n === -1n && (byte & 0x40) !== 0)) {
44
+ buffer.push(byte);
45
+ break
46
+ }
47
+ buffer.push((byte | 0x80));
48
+ }
49
+ return buffer
50
+ }
51
+
52
+ // generalized float cases parser
53
+ const flt = input => input==='nan'||input==='+nan'?NaN:input==='-nan'?-NaN:
54
+ input==='inf'||input==='+inf'?Infinity:input==='-inf'?-Infinity:parseFloat(input.replaceAll('_',''));
55
+
56
+ const byteView = new DataView(new BigInt64Array(1).buffer);
57
+
58
+ const F32_SIGN = 0x80000000, F32_NAN = 0x7f800000;
59
+ function f32 (input, value, idx) {
60
+ if (~(idx=input.indexOf('nan:'))) {
61
+ value = parseInt(input.slice(idx+4));
62
+ value |= F32_NAN;
63
+ if (input[0] === '-') value |= F32_SIGN;
64
+ byteView.setInt32(0, value);
65
+ }
66
+ else {
67
+ value=typeof input === 'string' ? flt(input) : input;
68
+ byteView.setFloat32(0, value);
69
+ }
70
+
71
+ return [
72
+ byteView.getUint8(3),
73
+ byteView.getUint8(2),
74
+ byteView.getUint8(1),
75
+ byteView.getUint8(0)
76
+ ];
77
+ }
78
+
79
+ const F64_SIGN = 0x8000000000000000n, F64_NAN = 0x7ff0000000000000n;
80
+ function f64 (input, value, idx) {
81
+ if (~(idx=input.indexOf('nan:'))) {
82
+ value = BigInt(input.slice(idx+4));
83
+ value |= F64_NAN;
84
+ if (input[0] === '-') value |= F64_SIGN;
85
+ byteView.setBigInt64(0, value);
86
+ }
87
+ else {
88
+ value=typeof input === 'string' ? flt(input) : input;
89
+ byteView.setFloat64(0, value);
90
+ }
91
+
92
+ return [
93
+ byteView.getUint8(7),
94
+ byteView.getUint8(6),
95
+ byteView.getUint8(5),
96
+ byteView.getUint8(4),
97
+ byteView.getUint8(3),
98
+ byteView.getUint8(2),
99
+ byteView.getUint8(1),
100
+ byteView.getUint8(0)
101
+ ];
102
+ }
103
+
1
104
  // ref: https://github.com/stagas/wat-compiler/blob/main/lib/const.js
105
+ // NOTE: squashing into a string doesn't save up gzipped size
2
106
  const OP = [
3
107
  'unreachable', 'nop', 'block', 'loop', 'if', 'else', ,,,,,
4
-
5
108
  'end', 'br', 'br_if', 'br_table', 'return', 'call', 'call_indirect', ,,,,,,,,
6
-
7
109
  'drop', 'select', ,,,,
8
-
9
110
  'local.get', 'local.set', 'local.tee', 'global.get', 'global.set', ,,,
10
-
11
111
  'i32.load', 'i64.load', 'f32.load', 'f64.load',
12
112
  'i32.load8_s', 'i32.load8_u', 'i32.load16_s', 'i32.load16_u',
13
113
  'i64.load8_s', 'i64.load8_u', 'i64.load16_s', 'i64.load16_u', 'i64.load32_s', 'i64.load32_u',
14
-
15
114
  'i32.store', 'i64.store', 'f32.store', 'f64.store',
16
115
  'i32.store8', 'i32.store16', 'i64.store8', 'i64.store16', 'i64.store32',
17
-
18
116
  'memory.size', 'memory.grow',
19
-
20
117
  'i32.const', 'i64.const', 'f32.const', 'f64.const',
21
118
  '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',
22
119
  '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',
23
120
  'f32.eq', 'f32.ne', 'f32.lt', 'f32.gt', 'f32.le', 'f32.ge',
24
121
  'f64.eq', 'f64.ne', 'f64.lt', 'f64.gt', 'f64.le', 'f64.ge',
25
-
26
122
  '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',
27
123
  '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',
28
-
29
124
  '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',
30
125
  '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',
31
-
32
126
  'i32.wrap_i64',
33
-
34
127
  'i32.trunc_f32_s', 'i32.trunc_f32_u', 'i32.trunc_f64_s', 'i32.trunc_f64_u', 'i64.extend_i32_s', 'i64.extend_i32_u',
35
128
  'i64.trunc_f32_s', 'i64.trunc_f32_u', 'i64.trunc_f64_s', 'i64.trunc_f64_u',
36
-
37
129
  'f32.convert_i32_s', 'f32.convert_i32_u', 'f32.convert_i64_s', 'f32.convert_i64_u', 'f32.demote_f64',
38
130
  'f64.convert_i32_s', 'f64.convert_i32_u', 'f64.convert_i64_s', 'f64.convert_i64_u', 'f64.promote_f32',
39
-
40
131
  'i32.reinterpret_f32', 'i64.reinterpret_f64', 'f32.reinterpret_i32', 'f64.reinterpret_i64',
41
132
  ],
42
- SECTION = {type:1, import:2, func:3, table:4, memory:5, global:6, export:7, start:8, elem:9, code:10, data:11},
43
- TYPE = {i32:0x7f, i64:0x7e, f32:0x7d, f64:0x7c, void:0x40, func:0x60, funcref:0x70},
44
- KIND = {func: 0, table: 1, memory: 2, global: 3},
133
+ SECTION = { type:1, import:2, func:3, table:4, memory:5, global:6, export:7, start:8, elem:9, code:10, data:11 },
134
+ TYPE = { i32:0x7f, i64:0x7e, f32:0x7d, f64:0x7c, void:0x40, func:0x60, funcref:0x70 },
135
+ KIND = { func: 0, table: 1, memory: 2, global: 3 },
45
136
  ALIGN = {
46
- 'i32.load': 4,
47
- 'i64.load': 8,
48
- 'f32.load': 4,
49
- 'f64.load': 8,
50
-
51
- 'i32.load8_s': 1,
52
- 'i32.load8_u': 1,
53
- 'i32.load16_s': 2,
54
- 'i32.load16_u': 2,
55
-
56
- 'i64.load8_s': 1,
57
- 'i64.load8_u': 1,
58
- 'i64.load16_s': 2,
59
- 'i64.load16_u': 2,
60
- 'i64.load32_s': 4,
61
- 'i64.load32_u': 4,
62
-
63
- 'i32.store': 4,
64
- 'i64.store': 8,
65
- 'f32.store': 4,
66
- 'f64.store': 8,
67
-
68
- 'i32.store8': 1,
69
- 'i32.store16': 2,
70
- 'i64.store8': 1,
71
- 'i64.store16': 2,
72
- 'i64.store32': 4,
137
+ 'i32.load': 4, 'i64.load': 8, 'f32.load': 4, 'f64.load': 8,
138
+ 'i32.load8_s': 1, 'i32.load8_u': 1, 'i32.load16_s': 2, 'i32.load16_u': 2,
139
+ '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,
140
+ 'i64.store': 8, 'f32.store': 4, 'f64.store': 8,
141
+ 'i32.store8': 1, 'i32.store16': 2, 'i64.store8': 1, 'i64.store16': 2, 'i64.store32': 4,
73
142
  };
74
143
 
75
- OP.map((op,i)=>OP[op]=i); // init op names
144
+ OP.map((op,i)=>OP[op]=i); // init op names
145
+
146
+ // some inlinable instructions
147
+ const INLINE = {loop: 1, block: 1, if: 1, end: -1, return: -1};
76
148
 
77
149
  // convert wat tree to wasm binary
78
150
  var compile = (nodes) => {
79
- // NOTE: alias is stored directly to section array by key, eg. section.func.$name = idx
151
+ // IR. Alias is stored directly to section array by key, eg. section.func.$name = idx
80
152
  let sections = {
81
153
  type: [], import: [], func: [], table: [], memory: [], global: [], export: [], start: [], elem: [], code: [], data: []
82
- },
83
- binary = [
154
+ }, binary = [
84
155
  0x00, 0x61, 0x73, 0x6d, // magic
85
156
  0x01, 0x00, 0x00, 0x00, // version
86
157
  ];
87
158
 
159
+ // 1. transform tree
88
160
  // (func) → [(func)]
89
161
  if (typeof nodes[0] === 'string' && nodes[0] !== 'module') nodes = [nodes];
90
162
 
91
- // build nodes in order of sections, to properly initialize indexes/aliases
92
- // must come separate from binary builder: func can define types etc.
93
- for (let name in sections) {
163
+ // (global $a (import "a" "b") (mut i32)) (import "a" "b" (global $a (mut i32)))
164
+ // (memory (import "a" "b") min max shared) (import "a" "b" (memory min max shared))
165
+ nodes = nodes.map(node => {
166
+ if (node[2]?.[0]==='import') {
167
+ let [kind, name, imp, ...args] = node;
168
+ return [...imp, [kind, name, ...args]]
169
+ }
170
+ else if (node[1]?.[0]==='import') {
171
+ let [kind, imp, ...args] = node;
172
+ return [...imp, [kind, ...args]]
173
+ }
174
+ return node
175
+ });
176
+
177
+ // 2. build IR. import must be initialized first, global before func, elem after func
178
+ let order = ['type', 'import', 'table', 'memory', 'global', 'func', 'export', 'start', 'elem', 'data'], postcall = [];
179
+
180
+ for (let name of order) {
94
181
  let remaining = [];
95
- for (let node of nodes) node[0] === name ? build[name](node, sections) : remaining.push(node);
182
+ for (let node of nodes) {
183
+ node[0] === name ? postcall.push(build[name](node, sections)) : remaining.push(node);
184
+ }
185
+
96
186
  nodes = remaining;
97
187
  }
98
188
 
99
- // console.log(sections)
100
- // build binary sections
189
+ // code must be compiled after all definitions
190
+ for (let cb of postcall) cb && cb.call && cb();
191
+
192
+
193
+ // 3. build binary
101
194
  for (let name in sections) {
102
195
  let items=sections[name];
103
- if (items.importc) items = items.slice(items.importc); // discard imported functions
196
+ if (items.importc) items = items.slice(items.importc); // discard imported functions/globals
104
197
  if (!items.length) continue
105
- let sizePtr = binary.length+1;
106
- binary.push(SECTION[name], 0);
107
- if (binary[sizePtr-1]!==8) binary.push(items.length); // skip start section count
108
- for (let item of items) binary.push(...item);
109
- binary[sizePtr] = binary.length - sizePtr - 1;
198
+ let sectionCode = SECTION[name], bytes = [];
199
+ if (sectionCode!==8) bytes.push(items.length); // skip start section count
200
+ for (let item of items) bytes.push(...item);
201
+ binary.push(sectionCode, ...uleb(bytes.length), ...bytes);
110
202
  }
111
203
 
112
204
  return new Uint8Array(binary)
@@ -118,21 +210,21 @@ const build = {
118
210
  // FIXME: handle non-function types
119
211
  type([, typeName, decl], ctx) {
120
212
  if (typeName[0]!=='$') decl=typeName, typeName=null;
121
- let params = [], result = [], kind = decl.shift(), idx, bytes;
213
+ let params = [], result = [], [kind,...sig] = decl, idx, bytes;
122
214
 
123
215
  if (kind==='func') {
124
216
  // collect params
125
- while (decl[0]?.[0] === 'param') {
126
- let [, ...types] = decl.shift();
217
+ while (sig[0]?.[0] === 'param') {
218
+ let [, ...types] = sig.shift();
127
219
  if (types[0]?.[0] === '$') params[types.shift()] = params.length;
128
220
  params.push(...types.map(t => TYPE[t]));
129
221
  }
130
222
 
131
223
  // collect result type
132
- if (decl[0]?.[0] === 'result') result = decl.shift().slice(1).map(t => TYPE[t]);
224
+ if (sig[0]?.[0] === 'result') result = sig.shift().slice(1).map(t => TYPE[t]);
133
225
 
134
226
  // reuse existing type or register new one
135
- bytes = [TYPE.func, params.length, ...params, result.length, ...result];
227
+ bytes = [TYPE.func, ...uleb(params.length), ...params, ...uleb(result.length), ...result];
136
228
 
137
229
  idx = ctx.type.findIndex((prevType) => prevType.every((byte, i) => byte === bytes[i]));
138
230
  if (idx < 0) idx = ctx.type.push(bytes)-1;
@@ -145,14 +237,14 @@ const build = {
145
237
 
146
238
  // (func $name? ...params result ...body)
147
239
  func([,...body], ctx) {
148
- let idx=ctx.func.length, // fn index comes after impoted fns
149
- locals=[]; // list of local variables
240
+ let locals=[], // list of local variables
241
+ callstack=[];
150
242
 
151
243
  // fn name
152
- if (body[0]?.[0] === '$') ctx.func[body.shift()] = idx;
244
+ if (body[0]?.[0] === '$') ctx.func[body.shift()] = ctx.func.length;
153
245
 
154
246
  // export binding
155
- if (body[0]?.[0] === 'export') build.export([...body.shift(), ['func', idx]], ctx);
247
+ if (body[0]?.[0] === 'export') build.export([...body.shift(), ['func', ctx.func.length]], ctx);
156
248
 
157
249
  // register type
158
250
  let [typeIdx, params, result] = build.type([,['func',...body]], ctx);
@@ -162,136 +254,193 @@ const build = {
162
254
 
163
255
  // collect locals
164
256
  while (body[0]?.[0] === 'local') {
165
- let [, ...localTypes] = body.shift(), name;
166
- if (localTypes[0][0]==='$')
167
- params[name=localTypes.shift()] ? err('Ambiguous name '+name) : name,
257
+ let [, ...types] = body.shift(), name;
258
+ if (types[0][0]==='$')
259
+ params[name=types.shift()] ? err('Ambiguous name '+name) :
168
260
  locals[name] = params.length + locals.length;
169
- localTypes.forEach(t => locals.push(TYPE[t]));
261
+ locals.push(...types.map(t => TYPE[t]));
170
262
  }
171
263
 
264
+ // squash local types
265
+ let locTypes = locals.reduce((a, type) => (type==a[a.length-1] ? a[a.length-2]++ : a.push(1,type), a), []);
266
+
172
267
  // map code instruction into bytes: [args, opCode, immediates]
173
- const instr = ([op, ...stack]) => {
174
- if (op.length===1) err(`Inline instructions are not supported \`${op+stack.join('')}\``);
175
-
176
- let opCode = OP[op], argc=0, args=[], imm=[];
177
- // FIXME: Maybe make use of iinit also?
178
-
179
- // (i32.store align=n offset=m at value)
180
- if (opCode>=40&&opCode<=62) {
181
- // FIXME: figure out point in Math.log2 aligns
182
- let o = {align: ALIGN[op], offset: 0}, p;
183
- while (stack[0]?.[0] in o) p = stack.shift(), o[p[0]] = +p[1];
184
- imm = [Math.log2(o.align), Math.log2(o.offset)];
185
- argc = opCode >= 54 ? 2 : 1;
268
+ const instr = (group) => {
269
+ let [op, ...nodes] = group;
270
+ let opCode = OP[op], argc=0, before=[], after=[], id;
271
+
272
+ // NOTE: we could reorganize ops by groups and detect signature as `op in STORE`
273
+ // but numeric comparison is faster than generic hash lookup
274
+ // FIXME: we often use OP.end or alike: what if we had list of global constants?
275
+
276
+ // binary/unary
277
+ if (opCode>=69) {
278
+ argc = opCode>=167 ||
279
+ (opCode<=159 && opCode>=153) ||
280
+ (opCode<=145 && opCode>=139) ||
281
+ (opCode<=123 && opCode>=121) ||
282
+ (opCode<=105 && opCode>=103) ||
283
+ opCode==80 || opCode==69 ? 1 : 2;
186
284
  }
285
+ // instruction
286
+ else {
287
+ // (i32.store align=n offset=m at value)
288
+ if (opCode>=40&&opCode<=62) {
289
+ // FIXME: figure out point in Math.log2 aligns
290
+ let o = {align: ALIGN[op], offset: 0}, p;
291
+ while (nodes[0]?.[0] in o) p = nodes.shift(), o[p[0]] = +p[1];
292
+ after = [Math.log2(o.align), ...uleb(o.offset)];
293
+ argc = opCode >= 54 ? 2 : 1;
294
+ }
187
295
 
188
- // (i32.const 123)
189
- else if (opCode>=65&&opCode<=68) imm = i32(stack.shift());
296
+ // (i32.const 123)
297
+ else if (opCode>=65&&opCode<=68) {
298
+ after = (opCode==65?leb:opCode==66?bigleb:opCode==67?f32:f64)(nodes.shift());
299
+ }
190
300
 
191
- // (local.get $id), (local.tee $id x)
192
- else if (opCode>=32&&opCode<=34) {
193
- let id = stack.shift();
194
- imm = i32(id[0]==='$' ? params[id] || locals[id] : id);
195
- if (opCode>32) argc = 1;
196
- }
301
+ // (local.get $id), (local.tee $id x)
302
+ else if (opCode>=32&&opCode<=34) {
303
+ after = uleb(nodes[0]?.[0]==='$' ? params[id=nodes.shift()] || locals[id] : nodes.shift());
304
+ if (opCode>32) argc = 1;
305
+ }
197
306
 
198
- // (global.get id), (global.set id)
199
- else if (opCode==35||opCode==36) {
200
- let id = stack.shift();
201
- imm = i32(id[0]==='$' ? ctx.global[id] : id);
202
- if (opCode>35) argc = 1;
203
- }
307
+ // (global.get id), (global.set id)
308
+ else if (opCode==35||opCode==36) {
309
+ after = uleb(nodes[0]?.[0]==='$' ? ctx.global[nodes.shift()] : nodes.shift());
310
+ if (opCode>35) argc = 1;
311
+ }
204
312
 
205
- // (call id ...stack)
206
- else if (opCode==16) {
207
- let id = stack.shift();
208
- imm = i32(id = id[0]==='$' ? ctx.func[id] : id);
209
- // FIXME: how to get signature of imported function
210
- [,argc] = ctx.type[ctx.func[id][0]];
211
- }
313
+ // (call id ...nodes)
314
+ else if (opCode==16) {
315
+ let fnName = nodes.shift();
316
+ after = uleb(id = fnName[0]==='$' ? ctx.func[fnName] ?? err('Unknown function `' + fnName + '`') : fnName);
317
+ // FIXME: how to get signature of imported function
318
+ [,argc] = ctx.type[ctx.func[id][0]];
319
+ }
212
320
 
213
- // (call_indirect (type $typeName) (idx) ...stack)
214
- else if (opCode==17) {
215
- let typeId = stack.shift()[1];
216
- [,argc] = ctx.type[typeId = typeId[0]==='$'?ctx.type[typeId]:typeId];
217
- argc++;
218
- imm = [+typeId, 0]; // extra immediate indicates table idx (reserved)
219
- }
321
+ // (call_indirect (type $typeName) (idx) ...nodes)
322
+ else if (opCode==17) {
323
+ let typeId = nodes.shift()[1];
324
+ [,argc] = ctx.type[typeId = typeId[0]==='$'?ctx.type[typeId]:typeId];
325
+ argc++;
326
+ after = uleb(typeId), after.push(0); // extra afterediate indicates table idx (reserved)
327
+ }
220
328
 
221
- // (memory.grow $idx?)
222
- else if (opCode==63||opCode==64) {
223
- imm = [0];
224
- argc = 1;
225
- }
329
+ // FIXME (memory.grow $idx?)
330
+ else if (opCode==63||opCode==64) {
331
+ after = [0];
332
+ argc = 1;
333
+ }
226
334
 
227
- // (i32.add a b) - binary/unaries
228
- else if (opCode>=106) {
229
- argc = opCode>=167 || (opCode<=159 && opCode>=153) || (opCode<=145 && opCode>=139) || (opCode<=123 && opCode>=121) || (opCode<=105 && opCode>=103) ? 1 : 2;
230
- }
335
+ // (if (result i32)? (local.get 0) (then a b) (else a b)?)
336
+ else if (opCode==4) {
337
+ callstack.push(opCode);
338
+ let [,type] = nodes[0][0]==='result' ? nodes.shift() : [,'void'];
339
+ after=[TYPE[type]];
340
+
341
+ argc = 0, before.push(...instr(nodes.shift()));
342
+ let body;
343
+ if (nodes[0]?.[0]==='then') [,...body] = nodes.shift(); else body = nodes;
344
+ after.push(...consume(body));
345
+
346
+ callstack.pop(), callstack.push(OP.else);
347
+ if (nodes[0]?.[0]==='else') {
348
+ [,...body] = nodes.shift();
349
+ if (body.length) after.push(OP.else,...consume(body));
350
+ }
351
+ callstack.pop();
352
+ after.push(OP.end);
353
+ }
354
+
355
+ // (drop arg?), (return arg?)
356
+ else if (opCode==0x1a || opCode==0x0f) { argc = nodes.length?1:0; }
231
357
 
232
- // (if (result i32)? (local.get 0)
233
- // (then a b)
234
- // (else a b)?
235
- // )
236
- else if (opCode==4) {
237
- let [,type] = stack[0][0]==='result' ? stack.shift() : [];
238
- argc = 0, args.push(...instr(stack.shift()));
239
- imm=[TYPE[type]];
240
-
241
- let body;
242
- if (stack[0][0]==='then') [,...body] = stack.shift(); else body = stack;
243
- imm.push(...body.flatMap(instr));
244
-
245
- if (stack[0][0]==='else') {
246
- [,...body] = stack.shift();
247
- imm.push(OP.else, ...body.flatMap(instr));
358
+ // (select a b cond)
359
+ else if (opCode==0x1b) { argc = 3; }
360
+
361
+ // (block ...), (loop ...)
362
+ else if (opCode==2||opCode==3) {
363
+ callstack.push(opCode);
364
+ if (nodes[0]?.[0]==='$') (callstack[nodes.shift()] = callstack.length);
365
+ let [,type] = nodes[0]?.[0]==='result' ? nodes.shift() : [,'void'];
366
+ after=[TYPE[type], ...consume(nodes)];
367
+
368
+ if (!group.inline) callstack.pop(), after.push(OP.end); // inline loop/block expects end to be separately provided
248
369
  }
249
370
 
250
- imm.push(OP.end);
251
- }
371
+ // (end)
372
+ else if (opCode==0x0b) callstack.pop();
252
373
 
253
- // (drop)
254
- else if (opCode==0x1a) { argc = 1; }
374
+ // (br $label result?)
375
+ // (br_if $label cond result?)
376
+ else if (opCode==0x0c||opCode==0x0d) {
377
+ // br index indicates how many callstack items to pop
378
+ after = uleb(nodes[0]?.[0]==='$' ? callstack.length-callstack[nodes.shift()] : nodes.shift());
379
+ argc = (opCode==0x0d ? 1 + (nodes.length > 1) : !!nodes.length);
380
+ }
255
381
 
256
- else err(`Unknown instruction \`${op}\``);
382
+ // (br_table 1 2 3 4 0 selector result?)
383
+ else if (opCode==0x0e) {
384
+ after = [];
385
+ while (!Array.isArray(nodes[0])) id=nodes.shift(), after.push(...uleb(id[0][0]==='$'?callstack.length-callstack[id]:id));
386
+ after.unshift(...uleb(after.length-1));
387
+ argc = 1 + (nodes.length>1);
388
+ }
257
389
 
258
- if (stack.length < argc) err(`Stack arguments are not supported at \`${op}\``);
259
- while (argc--) args.push(...instr(stack.shift()));
260
- if (stack.length) err(`Too many arguments for \`${op}\`.`);
390
+ else if (opCode==null) err(`Unknown instruction \`${op}\``);
391
+ }
261
392
 
262
- return [...args, opCode, ...imm]
393
+ // consume arguments
394
+ if (nodes.length < argc) err(`Stack arguments are not supported at \`${op}\``);
395
+ while (argc--) before.push(...instr(nodes.shift()));
396
+ if (nodes.length) err(`Too many arguments for \`${op}\`.`);
397
+
398
+ return [...before, opCode, ...after]
263
399
  };
264
400
 
265
- let code = body.flatMap(instr);
401
+ // consume sequence of nodes
402
+ const consume = nodes => {
403
+ let result = [];
404
+ while (nodes.length) {
405
+ let node = nodes.shift(), c;
406
+
407
+ if (typeof node === 'string') {
408
+ // permit some inline instructions: loop $label ... end, br $label, arg return
409
+ if (c=INLINE[node]) {
410
+ node = [node], node.inline = true;
411
+ if (c>0) nodes[0]?.[0]==='$' && node.push(nodes.shift());
412
+ }
413
+ else err(`Inline instruction \`${node}\` is not supported`);
414
+ }
415
+
416
+ node && result.push(...instr(node));
417
+ }
418
+ return result
419
+ };
266
420
 
267
- // FIXME: smush local type defs
268
- ctx.code.push([code.length+2+locals.length*2, locals.length, ...locals.flatMap(type => [1, type]), ...code, OP.end]);
421
+ // evaluates after all definitions
422
+ return () => {
423
+ let code = consume(body);
424
+ ctx.code.push([...uleb(code.length+2+locTypes.length), ...uleb(locTypes.length>>1), ...locTypes, ...code, OP.end]);
425
+ }
269
426
  },
270
427
 
271
428
  // (memory min max shared)
272
429
  // (memory $name min max shared)
273
- // (memory (import "js" "mem") min max shared)
430
+ // (memory (export "mem") 5)
274
431
  memory([, ...parts], ctx) {
275
432
  if (parts[0][0]==='$') ctx.memory[parts.shift()] = ctx.memory.length;
276
- if (parts[0][0] === 'import') {
277
- let [imp, ...limits] = parts;
278
- // (import "js" "mem" (memory 1))
279
- return build.import([...imp, ['memory', ...limits]], ctx)
280
- }
281
-
433
+ if (parts[0][0] === 'export') build.export([...parts.shift(), ['memory', ctx.memory.length]], ctx);
282
434
  ctx.memory.push(range(parts));
283
435
  },
284
436
 
285
437
  // (global i32 (i32.const 42))
286
438
  // (global $id i32 (i32.const 42))
287
439
  // (global $id (mut i32) (i32.const 42))
288
- // FIXME (global $g1 (import "js" "g1") (mut i32)) ;; import from js
289
440
  global([, ...args], ctx) {
290
441
  let name = args[0][0]==='$' && args.shift();
291
442
  if (name) ctx.global[name] = ctx.global.length;
292
-
293
- let [type, init] = args, mut = type[0] === 'mut';
294
-
443
+ let [type, init] = args, mut = type[0] === 'mut' ? 1 : 0;
295
444
  ctx.global.push([TYPE[mut ? type[1] : type], mut, ...iinit(init)]);
296
445
  },
297
446
 
@@ -300,7 +449,6 @@ const build = {
300
449
  table([, ...args], ctx) {
301
450
  let name = args[0][0]==='$' && args.shift();
302
451
  if (name) ctx.table[name] = ctx.table.length;
303
-
304
452
  let lims = range(args);
305
453
  ctx.table.push([TYPE[args.pop()], ...lims]);
306
454
  },
@@ -308,42 +456,51 @@ const build = {
308
456
  // (elem (i32.const 0) $f1 $f2), (elem (global.get 0) $f1 $f2)
309
457
  elem([, offset, ...elems], ctx) {
310
458
  const tableIdx = 0; // FIXME: table index can be defined
311
- ctx.elem.push([tableIdx, ...iinit(offset, ctx), elems.length, ...elems.map(el => el[0]==='$' ? ctx.func[el] : +el)]);
459
+ ctx.elem.push([tableIdx, ...iinit(offset, ctx), ...uleb(elems.length), ...elems.flatMap(el => uleb(el[0]==='$' ? ctx.func[el] : el))]);
312
460
  },
313
461
 
314
462
  // (export "name" (kind $name|idx))
315
463
  export([, name, [kind, idx]], ctx) {
316
464
  if (idx[0]==='$') idx = ctx[kind][idx];
317
- ctx.export.push([...str(name), KIND[kind], +idx]);
465
+ ctx.export.push([...str(name), KIND[kind], ...uleb(idx)]);
318
466
  },
319
467
 
320
468
  // (import "math" "add" (func $add (param i32 i32 externref) (result i32)))
321
469
  // (import "js" "mem" (memory 1))
322
470
  // (import "js" "mem" (memory $name 1))
323
- import([, mod, name, ref], ctx) {
324
- // FIXME: forward here from particular nodes instead: definition for import is same, we should DRY import code
325
- // build[ref[0]]([ref[0], ['import', mod, name], ...ref.slice(1)])
471
+ // (import "js" "v" (global $name (mut f64)))
472
+ import([, mod, field, ref], ctx) {
473
+ let details, [kind, ...parts] = ref,
474
+ name = parts[0]?.[0]==='$' && parts.shift();
326
475
 
327
- let details, [kind, ...parts] = ref;
328
476
  if (kind==='func') {
329
477
  // we track imported funcs in func section to share namespace, and skip them on final build
330
- if (parts[0]?.[0]==='$') ctx.func[parts.shift()] = ctx.func.length;
478
+ if (name) ctx.func[name] = ctx.func.length;
331
479
  let [typeIdx] = build.type([, ['func', ...parts]], ctx);
332
- ctx.func.push(details = [typeIdx]);
480
+ ctx.func.push(details = uleb(typeIdx));
333
481
  ctx.func.importc = (ctx.func.importc||0)+1;
334
482
  }
335
483
  else if (kind==='memory') {
336
- if (parts[0][0]==='$') ctx.memory[parts.shift()] = ctx.memory.length;
484
+ if (name) ctx.memory[name] = ctx.memory.length;
337
485
  details = range(parts);
338
486
  }
487
+ else if (kind==='global') {
488
+ // imported globals share namespace with internal globals - we skip them in final build
489
+ if (name) ctx.global[name] = ctx.global.length;
490
+ let [type] = parts, mut = type[0] === 'mut' ? 1 : 0;
491
+ details = [TYPE[mut ? type[1] : type], mut];
492
+ ctx.global.push(details);
493
+ ctx.global.importc = (ctx.global.importc||0)+1;
494
+ }
495
+ else throw Error('Unimplemented ' + kind)
339
496
 
340
- ctx.import.push([...str(mod), ...str(name), KIND[kind], ...details]);
497
+ ctx.import.push([...str(mod), ...str(field), KIND[kind], ...details]);
341
498
  },
342
499
 
343
- // (data (i32.const 0) "\2a")
344
- data([, offset, init], ctx) {
500
+ // (data (i32.const 0) "\aa" "\bb"?)
501
+ data([, offset, ...inits], ctx) {
345
502
  // FIXME: first is mem index
346
- ctx.data.push([0, ...iinit(offset,ctx), ...str(init)]);
503
+ ctx.data.push([0, ...iinit(offset,ctx), ...str(inits.map(i=>i[0]==='"'?i.slice(1,-1):i).join(''))]);
347
504
  },
348
505
 
349
506
  // (start $main)
@@ -353,62 +510,52 @@ const build = {
353
510
  };
354
511
 
355
512
  // (i32.const 0) - instantiation time initializer
356
- const iinit = ([op, literal], ctx) =>
357
- [OP[op], ...i32(literal[0] === '$' ? ctx.global[literal] : literal), OP.end];
513
+ const iinit = ([op, literal], ctx) => op[0]==='f' ?
514
+ [OP[op], ...(op[1]==='3'?f32:f64)(literal), OP.end] :
515
+ [OP[op], ...(op[1]==='3'?leb:bigleb)(literal[0] === '$' ? ctx.global[literal] : literal), OP.end];
516
+
517
+ const escape = {n:10, r:13, t:9, v:1};
358
518
 
359
519
  // build string binary
360
520
  const str = str => {
361
521
  str = str[0]==='"' ? str.slice(1,-1) : str;
362
- let res = [0], i = 0, c, BSLASH=92;
522
+ let res = [], i = 0, c, BSLASH=92;
363
523
  // spec https://webassembly.github.io/spec/core/text/values.html#strings
364
- for (; i < str.length;) c=str.charCodeAt(i++), res.push(c===BSLASH ? parseInt(str.slice(i,i+=2), 16) : c);
365
- res[0]=res.length-1;
524
+ for (;i < str.length;) {
525
+ c=str.charCodeAt(i++);
526
+ res.push(c===BSLASH ? escape[str[i++]] || parseInt(str.slice(i-1,++i), 16) : c);
527
+ }
528
+
529
+ res.unshift(...uleb(res.length));
366
530
  return res
367
531
  };
368
532
 
369
533
  // build range/limits sequence (non-consuming)
370
- const range = ([min, max, shared]) => isNaN(parseInt(max)) ? [0, +min] : [shared==='shared'?3:1, +min, +max];
534
+ const range = ([min, max, shared]) => isNaN(parseInt(max)) ? [0, ...uleb(min)] : [shared==='shared'?3:1, ...uleb(min), ...uleb(max)];
371
535
 
372
- // direct wiki example https://en.wikipedia.org/wiki/LEB128#Signed_LEB128
373
- const i32 = (value) => {
374
- value |= 0;
375
- const result = [];
376
- while (true) {
377
- const byte_ = value & 0x7f;
378
- value >>= 7;
379
- if (
380
- (value === 0 && (byte_ & 0x40) === 0) ||
381
- (value === -1 && (byte_ & 0x40) !== 0)
382
- ) {
383
- result.push(byte_);
384
- return result;
385
- }
386
- result.push(byte_ | 0x80);
387
- }
388
- };
389
-
390
- const err = text => { throw Error(text) };
391
-
392
- const OPAREN=40, CPAREN=41, SPACE=32, SEMIC=59;
536
+ const err = text => { throw Error(text) };
537
+
538
+ const OPAREN=40, CPAREN=41, SPACE=32, DQUOTE=34, SEMIC=59;
393
539
 
394
540
  var parse = (str) => {
395
541
  let i = 0, level = [], buf='';
396
542
 
397
543
  const commit = () => buf && (
398
- level.push(~buf.indexOf('=') ? buf.split('=') : buf),
544
+ level.push(buf[0]!=='"' && ~buf.indexOf('=') ? buf.split('=') : buf),
399
545
  buf = ''
400
546
  );
401
547
 
402
548
  const parseLevel = () => {
403
549
  for (let c, root; i < str.length; ) {
404
550
  c = str.charCodeAt(i);
405
- if (c === OPAREN) {
551
+ if (c === DQUOTE) commit(), buf = str.slice(i++, i=str.indexOf('"', i)+1), commit();
552
+ else if (c === OPAREN) {
406
553
  if (str.charCodeAt(i+1) === SEMIC) i=str.indexOf(';)', i)+2; // (; ... ;)
407
- else i++, (root=level).push(level=[]), parseLevel(), level=root;
554
+ else commit(), i++, (root=level).push(level=[]), parseLevel(), level=root;
408
555
  }
409
556
  else if (c === SEMIC) i=str.indexOf('\n', i)+1; // ; ...
410
557
  else if (c <= SPACE) commit(), i++;
411
- else if (c === CPAREN) return commit(), i++//, Object.freeze(level) // FIXME: tree should be immutable
558
+ else if (c === CPAREN) return commit(), i++
412
559
  else buf+=str[i++];
413
560
  }
414
561
 
@@ -418,11 +565,11 @@ var parse = (str) => {
418
565
  parseLevel();
419
566
 
420
567
  return level.length>1 ? level : level[0]
421
- };
422
-
568
+ };
569
+
423
570
  var watr = src => (
424
571
  src = typeof src === 'string' ? parse(src) : src,
425
572
  compile(src)
426
- );
427
-
428
- export { compile, watr as default, parse };
573
+ );
574
+
575
+ export { compile, watr as default, parse };