porffor 0.0.0-8c0bdaa

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,436 @@
1
+ import { Opcodes, Valtype } from "./wasmSpec.js";
2
+ import { number } from "./embedding.js";
3
+
4
+ // deno compat
5
+ if (typeof process === 'undefined' && typeof Deno !== 'undefined') {
6
+ const textEncoder = new TextEncoder();
7
+ globalThis.process = { argv: ['', '', ...Deno.args], stdout: { write: str => Deno.writeAllSync(Deno.stdout, textEncoder.encode(str)) } };
8
+ }
9
+
10
+ const performWasmOp = (op, a, b) => {
11
+ switch (op) {
12
+ case Opcodes.add: return a + b;
13
+ case Opcodes.sub: return a - b;
14
+ case Opcodes.mul: return a * b;
15
+ }
16
+ };
17
+
18
+ export default (funcs, globals) => {
19
+ const optLevel = parseInt(process.argv.find(x => x.startsWith('-O'))?.[2] ?? 1);
20
+ if (optLevel === 0) return;
21
+
22
+ const tailCall = process.argv.includes('-tail-call');
23
+ if (tailCall) log('opt', 'tail call proposal is not widely implemented! (you used -tail-call)');
24
+
25
+ if (optLevel >= 2 && !process.argv.includes('-opt-no-inline')) {
26
+ // inline pass (very WIP)
27
+ // get candidates for inlining
28
+ // todo: pick smart in future (if func is used <N times? or?)
29
+ const callsSelf = f => f.wasm.some(x => x[0] === Opcodes.call && x[1] === f.index);
30
+ const suitableReturns = wasm => wasm.reduce((acc, x) => acc + (x[0] === Opcodes.return), 0) <= 1;
31
+ 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();
32
+ if (optLog) {
33
+ log('opt', `found inline candidates: ${candidates.map(x => x.name).join(', ')} (${candidates.length}/${funcs.length - 1})`);
34
+
35
+ let reasons = {};
36
+ for (const f of funcs) {
37
+ if (f.name === 'main') continue;
38
+ reasons[f.name] = [];
39
+
40
+ if (f.name === 'main') reasons[f.name].push('main');
41
+ if (Object.keys(f.locals).length !== f.params.length) reasons[f.name].push('cannot inline funcs with locals yet');
42
+ if (f.returns.length !== 0 && !suitableReturns(f.wasm)) reasons[f.name].push('cannot inline funcs with multiple returns yet');
43
+ if (callsSelf(f)) reasons[f.name].push('cannot inline func calling itself');
44
+ if (f.throws) reasons[f.name].push('will not inline funcs throwing yet');
45
+ }
46
+
47
+ 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`)
48
+ }
49
+
50
+ for (const c of candidates) {
51
+ const cWasm = c.wasm;
52
+
53
+ for (const t of funcs) {
54
+ const tWasm = t.wasm;
55
+ if (t.name === c.name) continue; // skip self
56
+
57
+ for (let i = 0; i < tWasm.length; i++) {
58
+ const inst = tWasm[i];
59
+ if (inst[0] === Opcodes.call && inst[1] === c.index) {
60
+ if (optLog) log('opt', `inlining call for ${c.name} (in ${t.name})`);
61
+ tWasm.splice(i, 1); // remove this call
62
+
63
+ // add params as locals and set in reverse order
64
+ const paramIdx = {};
65
+ let localIdx = Math.max(-1, ...Object.values(t.locals).map(x => x.idx)) + 1;
66
+ for (let j = c.params.length - 1; j >= 0; j--) {
67
+ const name = `__porf_inline_${c.name}_param_${j}`;
68
+
69
+ if (t.locals[name] === undefined) {
70
+ t.locals[name] = { idx: localIdx++, type: c.params[j] };
71
+ }
72
+
73
+ const idx = t.locals[name].idx;
74
+ paramIdx[j] = idx;
75
+
76
+ tWasm.splice(i, 0, [ Opcodes.local_set, idx ]);
77
+ i++;
78
+ }
79
+
80
+ let iWasm = cWasm.slice().map(x => x.slice()); // deep clone arr (depth 2)
81
+ // remove final return
82
+ if (iWasm.length !== 0 && iWasm[iWasm.length - 1][0] === Opcodes.return) iWasm = iWasm.slice(0, -1);
83
+
84
+ // adjust local operands to go to correct param index
85
+ for (const inst of iWasm) {
86
+ if ((inst[0] === Opcodes.local_get || inst[0] === Opcodes.local_set) && inst[1] < c.params.length) {
87
+ if (optLog) log('opt', `replacing local operand in inlined wasm (${inst[1]} -> ${paramIdx[inst[1]]})`);
88
+ inst[1] = paramIdx[inst[1]];
89
+ }
90
+ }
91
+
92
+ tWasm.splice(i, 0, ...iWasm);
93
+ i += iWasm.length;
94
+ }
95
+ }
96
+
97
+ if (t.index > c.index) t.index--; // adjust index if after removed func
98
+ if (c.memory) t.memory = true;
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) {
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 this 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 (tailCall && lastInst[0] === Opcodes.call && inst[0] === Opcodes.return) {
263
+ // replace call, return with tail calls (return_call)
264
+ // call X
265
+ // return
266
+ // -->
267
+ // return_call X
268
+
269
+ lastInst[0] = Opcodes.return_call; // change last inst return -> return_call
270
+
271
+ wasm.splice(i, 1); // remove this inst (return)
272
+ i--;
273
+ if (optLog) log('opt', `tail called return, call`);
274
+ continue;
275
+ }
276
+
277
+ if (false && i === wasm.length - 1 && inst[0] === Opcodes.return) {
278
+ // replace final return, end -> end (wasm has implicit return)
279
+ // return
280
+ // end
281
+ // -->
282
+ // end
283
+
284
+ wasm.splice(i, 1); // remove this inst (return)
285
+ i--;
286
+ // if (optLog) log('opt', `removed redundant return at end`);
287
+ continue;
288
+ }
289
+
290
+ if (i < 2) continue;
291
+ const lastLastInst = wasm[i - 2];
292
+
293
+ if (depth.length === 2) {
294
+ // hack to remove unneeded before get in for loops with (...; i++)
295
+ if (lastLastInst[0] === Opcodes.end && lastInst[1] === inst[1] && lastInst[0] === Opcodes.local_get && inst[0] === Opcodes.local_get) {
296
+ // local.get 1
297
+ // local.get 1
298
+ // -->
299
+ // local.get 1
300
+
301
+ // remove drop at the end as well
302
+ if (wasm[i + 4][0] === Opcodes.drop) {
303
+ wasm.splice(i + 4, 1);
304
+ }
305
+
306
+ wasm.splice(i, 1); // remove this inst (second get)
307
+ i--;
308
+ continue;
309
+ }
310
+ }
311
+
312
+ if (lastLastInst[1] === inst[1] && inst[0] === Opcodes.local_get && lastInst[0] === Opcodes.local_tee && lastLastInst[0] === Opcodes.local_set) {
313
+ // local.set x
314
+ // local.tee y
315
+ // local.get x
316
+ // -->
317
+ // <nothing>
318
+
319
+ wasm.splice(i - 2, 3); // remove this, last, 2nd last insts
320
+ if (optLog) log('opt', `removed redundant inline param local handling`);
321
+ i -= 3;
322
+ continue;
323
+ }
324
+ }
325
+
326
+ if (optLevel < 2) continue;
327
+
328
+ if (optLog) log('opt', `get counts: ${Object.keys(f.locals).map(x => `${x} (${f.locals[x].idx}): ${getCount[f.locals[x].idx]}`).join(', ')}`);
329
+
330
+ // remove unneeded var: remove pass
331
+ // 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
332
+ let unneededCandidates = Object.keys(getCount).filter(x => getCount[x] === 0 || (getCount[x] === 1 && setCount[x] === 0)).map(x => parseInt(x));
333
+ if (optLog) log('opt', `found unneeded locals candidates: ${unneededCandidates.join(', ')} (${unneededCandidates.length}/${Object.keys(getCount).length})`);
334
+
335
+ // note: disabled for now due to instability
336
+ if (unneededCandidates.length > 0 && false) for (let i = 0; i < wasm.length; i++) {
337
+ if (i < 1) continue;
338
+
339
+ const inst = wasm[i];
340
+ const lastInst = wasm[i - 1];
341
+
342
+ if (lastInst[1] === inst[1] && lastInst[0] === Opcodes.local_set && inst[0] === Opcodes.local_get && unneededCandidates.includes(inst[1])) {
343
+ // local.set N
344
+ // local.get N
345
+ // -->
346
+ // <nothing>
347
+
348
+ wasm.splice(i - 1, 2); // remove insts
349
+ i -= 2;
350
+ delete f.locals[Object.keys(f.locals)[inst[1]]]; // remove from locals
351
+ if (optLog) log('opt', `removed redundant local (get set ${inst[1]})`);
352
+ }
353
+
354
+ if (inst[0] === Opcodes.local_tee && unneededCandidates.includes(inst[1])) {
355
+ // local.tee N
356
+ // -->
357
+ // <nothing>
358
+
359
+ wasm.splice(i, 1); // remove inst
360
+ i--;
361
+
362
+ const localName = Object.keys(f.locals)[inst[1]];
363
+ const removedIdx = f.locals[localName].idx;
364
+ delete f.locals[localName]; // remove from locals
365
+
366
+ // fix locals index for locals after
367
+ for (const x in f.locals) {
368
+ const local = f.locals[x];
369
+ if (local.idx > removedIdx) local.idx--;
370
+ }
371
+
372
+ for (const inst of wasm) {
373
+ if ((inst[0] === Opcodes.local_get || inst[0] === Opcodes.local_set || inst[0] === Opcodes.local_tee) && inst[1] > removedIdx) inst[1]--;
374
+ }
375
+
376
+ unneededCandidates.splice(unneededCandidates.indexOf(inst[1]), 1);
377
+ unneededCandidates = unneededCandidates.map(x => x > removedIdx ? (x - 1) : x);
378
+
379
+ if (optLog) log('opt', `removed redundant local ${localName} (tee ${inst[1]})`);
380
+ }
381
+ }
382
+
383
+ const useCount = {};
384
+ for (const x in f.locals) useCount[f.locals[x].idx] = 0;
385
+
386
+ // final pass
387
+ depth = [];
388
+ for (let i = 0; i < wasm.length; i++) {
389
+ let inst = wasm[i];
390
+ if (inst[0] === Opcodes.local_get || inst[0] === Opcodes.local_set || inst[0] === Opcodes.local_tee) useCount[inst[1]]++;
391
+
392
+ if (inst[0] === Opcodes.if || inst[0] === Opcodes.loop || inst[0] === Opcodes.block) depth.push(inst[0]);
393
+ if (inst[0] === Opcodes.end) depth.pop();
394
+
395
+ if (i < 2) continue;
396
+ const lastInst = wasm[i - 1];
397
+ const lastLastInst = wasm[i - 2];
398
+
399
+ // todo: add more math ops
400
+ if (optLevel >= 3 && (inst[0] === Opcodes.add || inst[0] === Opcodes.sub || inst[0] === Opcodes.mul) && lastLastInst[0] === Opcodes.const && lastInst[0] === Opcodes.const) {
401
+ // inline const math ops
402
+ // i32.const a
403
+ // i32.const b
404
+ // i32.add
405
+ // -->
406
+ // i32.const a + b
407
+
408
+ // does not work with leb encoded
409
+ if (lastInst.length > 2 || lastLastInst.length > 2) continue;
410
+
411
+ let a = lastLastInst[1];
412
+ let b = lastInst[1];
413
+
414
+ const val = performWasmOp(inst[0], a, b);
415
+ if (optLog) log('opt', `inlined math op (${a} ${inst[0].toString(16)} ${b} -> ${val})`);
416
+
417
+ wasm.splice(i - 2, 3, ...number(val)); // remove consts, math op and add new const
418
+ i -= 2;
419
+ }
420
+ }
421
+
422
+ const localIdxs = Object.values(f.locals).map(x => x.idx);
423
+ // remove unused locals (cleanup)
424
+ for (const x in useCount) {
425
+ if (useCount[x] === 0) {
426
+ const name = Object.keys(f.locals)[localIdxs.indexOf(parseInt(x))];
427
+ if (optLog) log('opt', `removed internal local ${x} (${name})`);
428
+ delete f.locals[name];
429
+ }
430
+ }
431
+
432
+ if (optLog) log('opt', `final use counts: ${Object.keys(f.locals).map(x => `${x} (${f.locals[x].idx}): ${useCount[f.locals[x].idx]}`).join(', ')}`);
433
+ }
434
+
435
+ // return funcs;
436
+ };
@@ -0,0 +1,8 @@
1
+ const { parse } = (await import(globalThis.document ? 'https://esm.sh/acorn' : 'acorn'));
2
+
3
+ export default (input, flags) => {
4
+ return parse(input, {
5
+ ecmaVersion: 'latest',
6
+ sourceType: flags.includes('module') ? 'module' : 'script'
7
+ });
8
+ };