porffor 0.0.0-05f898f

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.
@@ -0,0 +1,466 @@
1
+ import { Opcodes, Valtype } from "./wasmSpec.js";
2
+ import { number } from "./embedding.js";
3
+ import { read_signedLEB128, read_ieee754_binary64 } from "./encoding.js";
4
+
5
+ // deno compat
6
+ if (typeof process === 'undefined' && typeof Deno !== 'undefined') {
7
+ const textEncoder = new TextEncoder();
8
+ globalThis.process = { argv: ['', '', ...Deno.args], stdout: { write: str => Deno.writeAllSync(Deno.stdout, textEncoder.encode(str)) } };
9
+ }
10
+
11
+ const performWasmOp = (op, a, b) => {
12
+ switch (op) {
13
+ case Opcodes.add: return a + b;
14
+ case Opcodes.sub: return a - b;
15
+ case Opcodes.mul: return a * b;
16
+ }
17
+ };
18
+
19
+ export default (funcs, globals) => {
20
+ const optLevel = parseInt(process.argv.find(x => x.startsWith('-O'))?.[2] ?? 1);
21
+ if (optLevel === 0) return;
22
+
23
+ const tailCall = process.argv.includes('-tail-call');
24
+ if (tailCall) log('opt', 'warning: tail call proposal is not widely implemented! (you used -tail-call)');
25
+
26
+ if (optLevel >= 2 && !process.argv.includes('-opt-no-inline')) {
27
+ // inline pass (very WIP)
28
+ // get candidates for inlining
29
+ // todo: pick smart in future (if func is used <N times? or?)
30
+ const callsSelf = f => f.wasm.some(x => x[0] === Opcodes.call && x[1] === f.index);
31
+ const suitableReturns = wasm => wasm.reduce((acc, x) => acc + (x[0] === Opcodes.return), 0) <= 1;
32
+ const candidates = funcs.filter(x => x.name !== 'main' && Object.keys(x.locals).length === x.params.length && (x.returns.length === 0 || suitableReturns(x.wasm)) && !callsSelf(x) && !x.throws).reverse();
33
+ if (optLog) {
34
+ log('opt', `found inline candidates: ${candidates.map(x => x.name).join(', ')} (${candidates.length}/${funcs.length - 1})`);
35
+
36
+ let reasons = {};
37
+ for (const f of funcs) {
38
+ if (f.name === 'main') continue;
39
+ reasons[f.name] = [];
40
+
41
+ if (f.name === 'main') reasons[f.name].push('main');
42
+ if (Object.keys(f.locals).length !== f.params.length) reasons[f.name].push('cannot inline funcs with locals yet');
43
+ if (f.returns.length !== 0 && !suitableReturns(f.wasm)) reasons[f.name].push('cannot inline funcs with multiple returns yet');
44
+ if (callsSelf(f)) reasons[f.name].push('cannot inline func calling itself');
45
+ if (f.throws) reasons[f.name].push('will not inline funcs throwing yet');
46
+ }
47
+
48
+ if (Object.values(reasons).some(x => x.length > 0)) console.log(` reasons not:\n${Object.keys(reasons).filter(x => reasons[x].length > 0).map(x => ` ${x}: ${reasons[x].join(', ')}`).join('\n')}\n`)
49
+ }
50
+
51
+ for (const c of candidates) {
52
+ const cWasm = c.wasm;
53
+
54
+ for (const t of funcs) {
55
+ const tWasm = t.wasm;
56
+ if (t.name === c.name) continue; // skip self
57
+
58
+ for (let i = 0; i < tWasm.length; i++) {
59
+ const inst = tWasm[i];
60
+ if (inst[0] === Opcodes.call && inst[1] === c.index) {
61
+ if (optLog) log('opt', `inlining call for ${c.name} (in ${t.name})`);
62
+ tWasm.splice(i, 1); // remove this call
63
+
64
+ // add params as locals and set in reverse order
65
+ const paramIdx = {};
66
+ let localIdx = Math.max(-1, ...Object.values(t.locals).map(x => x.idx)) + 1;
67
+ for (let j = c.params.length - 1; j >= 0; j--) {
68
+ const name = `__porf_inline_${c.name}_param_${j}`;
69
+
70
+ if (t.locals[name] === undefined) {
71
+ t.locals[name] = { idx: localIdx++, type: c.params[j] };
72
+ }
73
+
74
+ const idx = t.locals[name].idx;
75
+ paramIdx[j] = idx;
76
+
77
+ tWasm.splice(i, 0, [ Opcodes.local_set, idx ]);
78
+ i++;
79
+ }
80
+
81
+ let iWasm = cWasm.slice().map(x => x.slice()); // deep clone arr (depth 2)
82
+ // remove final return
83
+ if (iWasm.length !== 0 && iWasm[iWasm.length - 1][0] === Opcodes.return) iWasm = iWasm.slice(0, -1);
84
+
85
+ // adjust local operands to go to correct param index
86
+ for (const inst of iWasm) {
87
+ if ((inst[0] === Opcodes.local_get || inst[0] === Opcodes.local_set) && inst[1] < c.params.length) {
88
+ if (optLog) log('opt', `replacing local operand in inlined wasm (${inst[1]} -> ${paramIdx[inst[1]]})`);
89
+ inst[1] = paramIdx[inst[1]];
90
+ }
91
+ }
92
+
93
+ tWasm.splice(i, 0, ...iWasm);
94
+ i += iWasm.length;
95
+ }
96
+ }
97
+
98
+ if (t.index > c.index) t.index--; // adjust index if after removed func
99
+ }
100
+
101
+ funcs.splice(funcs.indexOf(c), 1); // remove func from funcs
102
+ }
103
+ }
104
+
105
+ if (process.argv.includes('-opt-inline-only')) return;
106
+
107
+ // wasm transform pass
108
+ for (const f of funcs) {
109
+ const wasm = f.wasm;
110
+
111
+ let depth = [];
112
+
113
+ let getCount = {}, setCount = {};
114
+ for (const x in f.locals) {
115
+ getCount[f.locals[x].idx] = 0;
116
+ setCount[f.locals[x].idx] = 0;
117
+ }
118
+
119
+ // main pass
120
+ for (let i = 0; i < wasm.length; i++) {
121
+ let inst = wasm[i];
122
+
123
+ if (inst[0] === Opcodes.if || inst[0] === Opcodes.loop || inst[0] === Opcodes.block) depth.push(inst[0]);
124
+ if (inst[0] === Opcodes.end) depth.pop();
125
+
126
+ if (inst[0] === Opcodes.local_get) getCount[inst[1]]++;
127
+ if (inst[0] === Opcodes.local_set || inst[0] === Opcodes.local_tee) setCount[inst[1]]++;
128
+
129
+ if (inst[0] === Opcodes.block) {
130
+ // remove unneeded blocks (no brs inside)
131
+ // block
132
+ // ...
133
+ // end
134
+ // -->
135
+ // ...
136
+
137
+ let hasBranch = false, j = i, depth = 0;
138
+ for (; j < wasm.length; j++) {
139
+ const op = wasm[j][0];
140
+ if (op === Opcodes.if || op === Opcodes.block || op === Opcodes.loop || op === Opcodes.try) depth++;
141
+ if (op === Opcodes.end) {
142
+ depth--;
143
+ if (depth <= 0) break;
144
+ }
145
+ if (op === Opcodes.br || op === Opcodes.br_if) {
146
+ hasBranch = true;
147
+ break;
148
+ }
149
+ }
150
+
151
+ if (!hasBranch) {
152
+ wasm.splice(i, 1); // remove this inst (block)
153
+ i--;
154
+ inst = wasm[i];
155
+
156
+ wasm.splice(j - 1, 1); // remove end of this block
157
+
158
+ if (optLog) log('opt', `removed unneeded block in for loop`);
159
+ }
160
+ }
161
+
162
+ if (i < 1) continue;
163
+ let lastInst = wasm[i - 1];
164
+
165
+ if (lastInst[1] === inst[1] && lastInst[0] === Opcodes.local_set && inst[0] === Opcodes.local_get) {
166
+ // replace set, get -> tee (sets and returns)
167
+ // local.set 0
168
+ // local.get 0
169
+ // -->
170
+ // local.tee 0
171
+
172
+ lastInst[0] = Opcodes.local_tee; // replace last inst opcode (set -> tee)
173
+ wasm.splice(i, 1); // remove this inst (get)
174
+
175
+ getCount[inst[1]]--;
176
+ i--;
177
+ // if (optLog) log('opt', `consolidated set, get -> tee`);
178
+ continue;
179
+ }
180
+
181
+ if ((lastInst[0] === Opcodes.local_get || lastInst[0] === Opcodes.global_get) && inst[0] === Opcodes.drop) {
182
+ // replace get, drop -> nothing
183
+ // local.get 0
184
+ // drop
185
+ // -->
186
+ //
187
+
188
+ getCount[lastInst[1]]--;
189
+
190
+ wasm.splice(i - 1, 2); // remove this inst and last
191
+ i -= 2;
192
+ continue;
193
+ }
194
+
195
+ if (lastInst[0] === Opcodes.local_tee && inst[0] === Opcodes.drop) {
196
+ // replace tee, drop -> set
197
+ // local.tee 0
198
+ // drop
199
+ // -->
200
+ // local.set 0
201
+
202
+ getCount[lastInst[1]]--;
203
+
204
+ lastInst[0] = Opcodes.local_set; // change last op
205
+
206
+ wasm.splice(i, 1); // remove this inst
207
+ i--;
208
+ continue;
209
+ }
210
+
211
+ if ((lastInst[0] === Opcodes.i32_const || lastInst[0] === Opcodes.i64_const || lastInst[0] === Opcodes.f64_const) && inst[0] === Opcodes.drop) {
212
+ // replace const, drop -> <nothing>
213
+ // i32.const 0
214
+ // drop
215
+ // -->
216
+ // <nothing>
217
+
218
+ wasm.splice(i - 1, 2); // remove these inst
219
+ i -= 2;
220
+ continue;
221
+ }
222
+
223
+ if (inst[0] === Opcodes.eq && lastInst[0] === Opcodes.const && lastInst[1] === 0 && valtype !== 'f64') {
224
+ // replace const 0, eq -> eqz
225
+ // i32.const 0
226
+ // i32.eq
227
+ // -->
228
+ // i32.eqz
229
+
230
+ inst[0] = Opcodes.eqz[0][0]; // eq -> eqz
231
+ wasm.splice(i - 1, 1); // remove const 0
232
+ i--;
233
+ continue;
234
+ }
235
+
236
+ if (inst[0] === Opcodes.i32_wrap_i64 && (lastInst[0] === Opcodes.i64_extend_i32_s || lastInst[0] === Opcodes.i64_extend_i32_u)) {
237
+ // remove unneeded i32 -> i64 -> i32
238
+ // i64.extend_i32_s
239
+ // i32.wrap_i64
240
+ // -->
241
+ // <nothing>
242
+
243
+ wasm.splice(i - 1, 2); // remove this inst and last
244
+ i -= 2;
245
+ // if (optLog) log('opt', `removed redundant i32 -> i64 -> i32 conversion ops`);
246
+ continue;
247
+ }
248
+
249
+ if (inst[0] === Opcodes.i32_trunc_sat_f64_s[0] && (lastInst[0] === Opcodes.f64_convert_i32_u || lastInst[0] === Opcodes.f64_convert_i32_s)) {
250
+ // remove unneeded i32 -> f64 -> i32
251
+ // f64.convert_i32_s || f64.convert_i32_u
252
+ // i32.trunc_sat_f64_s || i32.trunc_sat_f64_u
253
+ // -->
254
+ // <nothing>
255
+
256
+ wasm.splice(i - 1, 2); // remove this inst and last
257
+ i -= 2;
258
+ // if (optLog) log('opt', `removed redundant i32 -> f64 -> i32 conversion ops`);
259
+ continue;
260
+ }
261
+
262
+ if (lastInst[0] === Opcodes.const && (inst === Opcodes.i32_to || inst === Opcodes.i32_to_u)) {
263
+ // change const and immediate i32 convert to i32 const
264
+ // f64.const 0
265
+ // i32.trunc_sat_f64_s || i32.trunc_sat_f64_u
266
+ // -->
267
+ // i32.const 0
268
+
269
+ wasm[i - 1] = number((valtype === 'f64' ? read_ieee754_binary64 : read_signedLEB128)(lastInst.slice(1)), Valtype.i32)[0]; // f64.const -> i32.const
270
+
271
+ wasm.splice(i, 1); // remove this inst
272
+ i--;
273
+ if (optLog) log('opt', `converted const -> i32 convert into i32 const`);
274
+ continue;
275
+ }
276
+
277
+ if (lastInst[0] === Opcodes.i32_const && (inst === Opcodes.i32_from || inst === Opcodes.i32_from_u)) {
278
+ // change i32 const and immediate convert to const (opposite way of previous)
279
+ // i32.const 0
280
+ // f64.convert_i32_s || f64.convert_i32_u
281
+ // -->
282
+ // f64.const 0
283
+
284
+ wasm[i - 1] = number(read_signedLEB128(lastInst.slice(1)))[0]; // i32.const -> f64.const
285
+
286
+ wasm.splice(i, 1); // remove this inst
287
+ i--;
288
+ if (optLog) log('opt', `converted i32 const -> convert into const`);
289
+ continue;
290
+ }
291
+
292
+ if (tailCall && lastInst[0] === Opcodes.call && inst[0] === Opcodes.return) {
293
+ // replace call, return with tail calls (return_call)
294
+ // call X
295
+ // return
296
+ // -->
297
+ // return_call X
298
+
299
+ lastInst[0] = Opcodes.return_call; // change last inst return -> return_call
300
+
301
+ wasm.splice(i, 1); // remove this inst (return)
302
+ i--;
303
+ if (optLog) log('opt', `tail called return, call`);
304
+ continue;
305
+ }
306
+
307
+ if (false && i === wasm.length - 1 && inst[0] === Opcodes.return) {
308
+ // replace final return, end -> end (wasm has implicit return)
309
+ // return
310
+ // end
311
+ // -->
312
+ // end
313
+
314
+ wasm.splice(i, 1); // remove this inst (return)
315
+ i--;
316
+ // if (optLog) log('opt', `removed redundant return at end`);
317
+ continue;
318
+ }
319
+
320
+ if (i < 2) continue;
321
+ const lastLastInst = wasm[i - 2];
322
+
323
+ if (depth.length === 2) {
324
+ // hack to remove unneeded before get in for loops with (...; i++)
325
+ if (lastLastInst[0] === Opcodes.end && lastInst[1] === inst[1] && lastInst[0] === Opcodes.local_get && inst[0] === Opcodes.local_get) {
326
+ // local.get 1
327
+ // local.get 1
328
+ // -->
329
+ // local.get 1
330
+
331
+ // remove drop at the end as well
332
+ if (wasm[i + 4][0] === Opcodes.drop) {
333
+ wasm.splice(i + 4, 1);
334
+ }
335
+
336
+ wasm.splice(i, 1); // remove this inst (second get)
337
+ i--;
338
+ continue;
339
+ }
340
+ }
341
+
342
+ if (lastLastInst[1] === inst[1] && inst[0] === Opcodes.local_get && lastInst[0] === Opcodes.local_tee && lastLastInst[0] === Opcodes.local_set) {
343
+ // local.set x
344
+ // local.tee y
345
+ // local.get x
346
+ // -->
347
+ // <nothing>
348
+
349
+ wasm.splice(i - 2, 3); // remove this, last, 2nd last insts
350
+ if (optLog) log('opt', `removed redundant inline param local handling`);
351
+ i -= 3;
352
+ continue;
353
+ }
354
+ }
355
+
356
+ if (optLevel < 2) continue;
357
+
358
+ if (optLog) log('opt', `get counts: ${Object.keys(f.locals).map(x => `${x} (${f.locals[x].idx}): ${getCount[f.locals[x].idx]}`).join(', ')}`);
359
+
360
+ // remove unneeded var: remove pass
361
+ // locals only got once. we don't need to worry about sets/else as these are only candidates and we will check for matching set + get insts in wasm
362
+ let unneededCandidates = Object.keys(getCount).filter(x => getCount[x] === 0 || (getCount[x] === 1 && setCount[x] === 0)).map(x => parseInt(x));
363
+ if (optLog) log('opt', `found unneeded locals candidates: ${unneededCandidates.join(', ')} (${unneededCandidates.length}/${Object.keys(getCount).length})`);
364
+
365
+ // note: disabled for now due to instability
366
+ if (unneededCandidates.length > 0 && false) for (let i = 0; i < wasm.length; i++) {
367
+ if (i < 1) continue;
368
+
369
+ const inst = wasm[i];
370
+ const lastInst = wasm[i - 1];
371
+
372
+ if (lastInst[1] === inst[1] && lastInst[0] === Opcodes.local_set && inst[0] === Opcodes.local_get && unneededCandidates.includes(inst[1])) {
373
+ // local.set N
374
+ // local.get N
375
+ // -->
376
+ // <nothing>
377
+
378
+ wasm.splice(i - 1, 2); // remove insts
379
+ i -= 2;
380
+ delete f.locals[Object.keys(f.locals)[inst[1]]]; // remove from locals
381
+ if (optLog) log('opt', `removed redundant local (get set ${inst[1]})`);
382
+ }
383
+
384
+ if (inst[0] === Opcodes.local_tee && unneededCandidates.includes(inst[1])) {
385
+ // local.tee N
386
+ // -->
387
+ // <nothing>
388
+
389
+ wasm.splice(i, 1); // remove inst
390
+ i--;
391
+
392
+ const localName = Object.keys(f.locals)[inst[1]];
393
+ const removedIdx = f.locals[localName].idx;
394
+ delete f.locals[localName]; // remove from locals
395
+
396
+ // fix locals index for locals after
397
+ for (const x in f.locals) {
398
+ const local = f.locals[x];
399
+ if (local.idx > removedIdx) local.idx--;
400
+ }
401
+
402
+ for (const inst of wasm) {
403
+ if ((inst[0] === Opcodes.local_get || inst[0] === Opcodes.local_set || inst[0] === Opcodes.local_tee) && inst[1] > removedIdx) inst[1]--;
404
+ }
405
+
406
+ unneededCandidates.splice(unneededCandidates.indexOf(inst[1]), 1);
407
+ unneededCandidates = unneededCandidates.map(x => x > removedIdx ? (x - 1) : x);
408
+
409
+ if (optLog) log('opt', `removed redundant local ${localName} (tee ${inst[1]})`);
410
+ }
411
+ }
412
+
413
+ const useCount = {};
414
+ for (const x in f.locals) useCount[f.locals[x].idx] = 0;
415
+
416
+ // final pass
417
+ depth = [];
418
+ for (let i = 0; i < wasm.length; i++) {
419
+ let inst = wasm[i];
420
+ if (inst[0] === Opcodes.local_get || inst[0] === Opcodes.local_set || inst[0] === Opcodes.local_tee) useCount[inst[1]]++;
421
+
422
+ if (inst[0] === Opcodes.if || inst[0] === Opcodes.loop || inst[0] === Opcodes.block) depth.push(inst[0]);
423
+ if (inst[0] === Opcodes.end) depth.pop();
424
+
425
+ if (i < 2) continue;
426
+ const lastInst = wasm[i - 1];
427
+ const lastLastInst = wasm[i - 2];
428
+
429
+ // todo: add more math ops
430
+ if (optLevel >= 3 && (inst[0] === Opcodes.add || inst[0] === Opcodes.sub || inst[0] === Opcodes.mul) && lastLastInst[0] === Opcodes.const && lastInst[0] === Opcodes.const) {
431
+ // inline const math ops
432
+ // i32.const a
433
+ // i32.const b
434
+ // i32.add
435
+ // -->
436
+ // i32.const a + b
437
+
438
+ // does not work with leb encoded
439
+ if (lastInst.length > 2 || lastLastInst.length > 2) continue;
440
+
441
+ let a = lastLastInst[1];
442
+ let b = lastInst[1];
443
+
444
+ const val = performWasmOp(inst[0], a, b);
445
+ if (optLog) log('opt', `inlined math op (${a} ${inst[0].toString(16)} ${b} -> ${val})`);
446
+
447
+ wasm.splice(i - 2, 3, ...number(val)); // remove consts, math op and add new const
448
+ i -= 2;
449
+ }
450
+ }
451
+
452
+ const localIdxs = Object.values(f.locals).map(x => x.idx);
453
+ // remove unused locals (cleanup)
454
+ for (const x in useCount) {
455
+ if (useCount[x] === 0) {
456
+ const name = Object.keys(f.locals)[localIdxs.indexOf(parseInt(x))];
457
+ if (optLog) log('opt', `removed internal local ${x} (${name})`);
458
+ delete f.locals[name];
459
+ }
460
+ }
461
+
462
+ if (optLog) log('opt', `final use counts: ${Object.keys(f.locals).map(x => `${x} (${f.locals[x].idx}): ${useCount[f.locals[x].idx]}`).join(', ')}`);
463
+ }
464
+
465
+ // return funcs;
466
+ };
@@ -0,0 +1,9 @@
1
+ // import { parse } from 'acorn';
2
+ const { parse } = (await import(globalThis.document ? 'https://esm.sh/acorn' : 'acorn'));
3
+
4
+ export default (input, flags) => {
5
+ return parse(input, {
6
+ ecmaVersion: 'latest',
7
+ sourceType: flags.includes('module') ? 'module' : 'script'
8
+ });
9
+ };