Improve ergonomics of recursion
The call opcode requires passing a Func object, but at the time you would make the call to a recursive function, no such object exists. It would be nice if there was some way to define some kind of typed function reference without filling in its body. (It would be especially nice for my particular weird use case if Func objects containing recursive calls could somehow not contain circular references and thus remain JSON-serializable, but I recognize this may be impossible without breaking the current data model, and I think I can work around it.)
It does seem to at least be possible to do this with Object.assign, thanks to the design choice of making internal wasmati objects simple data:
async function compileFunc<T extends Dependency.Export>(f: T) {
return (await Module({ exports: { f } }).instantiate()).instance.exports.f;
}
test('mutually recursive functions', async () => {
let double = func({ in: [i32, i32], out: [i32]}, ([val, count]) => { i32.const(0); });
let decrement = func({ in: [i32, i32], out: [i32]}, ([val, count]) => { i32.const(0); });
Object.assign(double, func({ in: [i32, i32], out: [i32]}, ([val, count]) => {
i32.eqz(count);
if_({ out: [i32] },
() => { local.get(val) },
() => { call(decrement, [i32.add(val, val), i32.sub(count, 1)]); }
);
}));
Object.assign(decrement, func({ in: [i32, i32], out: [i32]}, ([val, count]) => {
i32.eqz(count);
if_({ out: [i32] },
() => { local.get(val) },
() => { call(double, [i32.sub(val, 1), i32.sub(count, 1)]); }
);
}));
expect((await compileFunc(double))(5, 3)).toBe( ((5 * 2) - 1) * 2 );
});
Great point, I didn't think of recursion at all!
I think this can be supported by adding a more verbose two-step way of declaring funcs, first only with the type and then associate an implementation with that. Basically a cleaner-looking version of your working code example.
(Also your code snippet reminds me that I wanted to add a command to compile a single function, which wraps it in a module :))