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.
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/compiler/builtins/base64.js +92 -0
- package/compiler/builtins.js +770 -0
- package/compiler/codeGen.js +2027 -0
- package/compiler/decompile.js +102 -0
- package/compiler/embedding.js +19 -0
- package/compiler/encoding.js +217 -0
- package/compiler/expression.js +70 -0
- package/compiler/index.js +67 -0
- package/compiler/opt.js +436 -0
- package/compiler/parse.js +8 -0
- package/compiler/prototype.js +272 -0
- package/compiler/sections.js +154 -0
- package/compiler/wasmSpec.js +200 -0
- package/compiler/wrap.js +119 -0
- package/package.json +23 -0
- package/porf.cmd +2 -0
- package/publish.js +11 -0
- package/runner/compare.js +35 -0
- package/runner/index.js +34 -0
- package/runner/info.js +54 -0
- package/runner/profile.js +47 -0
- package/runner/repl.js +104 -0
- package/runner/sizes.js +38 -0
- package/runner/transform.js +36 -0
- package/sw.js +26 -0
- package/util/enum.js +20 -0
package/compiler/opt.js
ADDED
@@ -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
|
+
};
|