Why is wasm2js from AssemblyScript input hanging?
Closed this issue · 15 comments
Question
I've compiled Rust source code to WASM, then compiled that WASM to JavaScript with wasm2js, then used a WASI implementation from here https://gitlab.com/-/snippets/4782260 to execute the JavaScript output by wasm2js.
// ...
import WASI from "./wasi.js";
let wasi = new WASI();
var retasmFunc = asmFunc({
"wasi_snapshot_preview1": wasi.exports,
});
export var memory = retasmFunc.memory;
export var _start = retasmFunc._start;
export var __main_void = retasmFunc.__main_void;
wasi.memory = memory;
_start();
echo '3 4' | node permutations-rust.js
4 of 5 (0-indexed, factorial 6) => [2, 0, 1]
I've done something similar with Bytecode Alliance's Javy, and Facebook's Static Hermes.
Here either arguments or stdin is read and passed to the relevant function
if (process.argv.length > 1) {
input = process.argv.at(-2);
lex = process.argv.at(-1);
} else {
let stdin = process.stdin;
let buffer = new ArrayBuffer(64);
let n: number = stdin.read(buffer);
if (n > 0) {
let data = String.UTF8.decode(buffer);
input = data.slice(0, data.indexOf(" "));
lex = data.slice(data.indexOf(" "), data.length);
}
}
input = input.trim();
lex = lex.trim();
if (<i32> parseInt(input) < 2 || <i32> parseInt(lex) < 0) {
process.stdout.write(`Expected n > 2, m >= 0, got ${input}, ${lex}`); // eval(input)
process.exit(1);
}
array_nth_permutation(<i32> parseInt(input), <i32> parseInt(lex));
// ...
function asmFunc(imports) {
var buffer = new ArrayBuffer(65536);
var HEAP8 = new Int8Array(buffer);
var HEAP16 = new Int16Array(buffer);
var HEAP32 = new Int32Array(buffer);
var HEAPU8 = new Uint8Array(buffer);
var HEAPU16 = new Uint16Array(buffer);
var HEAPU32 = new Uint32Array(buffer);
var HEAPF32 = new Float32Array(buffer);
var HEAPF64 = new Float64Array(buffer);
var Math_imul = Math.imul;
var Math_fround = Math.fround;
var Math_abs = Math.abs;
var Math_clz32 = Math.clz32;
var Math_min = Math.min;
var Math_max = Math.max;
var Math_floor = Math.floor;
var Math_ceil = Math.ceil;
var Math_trunc = Math.trunc;
var Math_sqrt = Math.sqrt;
var wasi_snapshot_preview1 = imports.wasi_snapshot_preview1;
var fimport$0 = wasi_snapshot_preview1.args_sizes_get;
var fimport$1 = wasi_snapshot_preview1.fd_write;
var fimport$2 = wasi_snapshot_preview1.proc_exit;
var fimport$3 = wasi_snapshot_preview1.args_get;
var fimport$4 = wasi_snapshot_preview1.fd_read;
console.log("We get here...");
// ...
function $131($0_1, $1_1) {
console.log($0_1, $1_1);
// ...
echo '7 8' | node --no-warnings module.js
We get here...
7 8
The resulting JavaScript from the AssemblyScript source to WASM then to JavaScript hangs. Why? And how to fix this?
I'm confused...what's the source code?
module.ts
export function array_nth_permutation(len: i32, n: i32): void { //Array<f64>
let lex = n;
let b: number[] = []; // copy of the set a.slice()
for (let x: i32 = 0; x < len; x++) {
b[x] = x;
}
// let len = a; // length of the set
const res: number[] = []; // return value, undefined
let i: i32 = 1;
let f: i32 = 1;
// compute f = factorial(len)
for (; i <= len; i++) {
f *= i;
}
let fac = f;
// if the permutation number is within range
if (n >= 0 && n < f) {
// start with the empty set, loop for len elements
// let result_len = 0;
for (; len > 0; len--) {
// determine the next element:
// there are f/len subsets for each possible element,
f /= len;
// a simple division gives the leading element index
i = (n - n % f) / f; // Math.floor(n / f);
// alternately: i = (n - n % f) / f;
// res[(result_len)++] = b[i];
// for (let j = i; j < len; j++) {
// b[j] = b[j + 1]; // shift elements left
// }
res.push(b.splice(i, 1)[0]);
// reduce n for the remaining subset:
// compute the remainder of the above division
n %= f;
// extract the i-th element from b and push it at the end of res
}
let result: string = "[";
for (let x: i32 = 0; x < res.length; x++) {
let m: string = res[x].toString();
let i: i32 = 0;
do {
result += m[i];
i++;
} while (m[i] !== ".");
if (x < res.length -1) {
result += ",";
}
}
result += "]";
process.stdout.write(
`${lex} of ${fac - 1} (0-indexed, factorial ${fac}) => ${result}\n`,
);
process.exit(0);
} else {
if (n === 0) {
process.stdout.write(`${n} = 0`);
}
process.stdout.write(`${n} >= 0 && ${n} < ${f}: ${n >= 0 && n < f}`);
process.exit(1);
}
}
let input: string = "0";
let lex: string = "0";
if (process.argv.length > 1) {
input = process.argv.at(-2);
lex = process.argv.at(-1);
} else {
let stdin = process.stdin;
let buffer = new ArrayBuffer(64);
let n: number = stdin.read(buffer);
if (n > 0) {
let data = String.UTF8.decode(buffer);
input = data.slice(0, data.indexOf(" "));
lex = data.slice(data.indexOf(" "), data.length);
}
}
input = input.trim();
lex = lex.trim();
if (<i32> parseInt(input) < 2 || <i32> parseInt(lex) < 0) {
process.stdout.write(`Expected n > 2, m >= 0, got ${input}, ${lex}`); // eval(input)
process.exit(1);
}
array_nth_permutation(<i32> parseInt(input), <i32> parseInt(lex));
Compiled with
node_modules/.bin/asc --enable simd --exportStart --config ./node_modules/@assemblyscript/wasi-shim/asconfig.json module.ts -o module.wasm
Then using Binaryen's wasm2js to get the JavaScript output
node_modules/.bin/wasm2js module.wasm --enable-bulk-memory --enable-nontrapping-float-to-int -o module.js
I do the same or similar with Bytecode Alliance's Javy, Rust, and Facebook's Static Hermes https://gist.github.com/guest271314/b10eac16be88350ffcd19387e22ad4d5. Pardon, this is the WASI implementation I'm using https://github.com/guest271314/deno-wasi/blob/runtime-agnostic-nodejs-api/wasi.js.
I think I figured out the issue in the source AssemblyScript code
I included this part in the algorithm specifically for AssemblyScript because AssemblyScript is representing integers as decimals
let result: string = "[";
for (let x: i32 = 0; x < res.length; x++) {
let m: string = res[x].toString();
let i: i32 = 0;
do {
result += m[i];
i++;
} while (m[i] !== ".");
if (x < res.length -1) {
result += ",";
}
}
result += "]";
That is, when I replace that part with
let result: string = `[${res}]`;
/*
"[";
for (let x: i32 = 0; x < res.length; x++) {
let m: string = res[x].toString();
let i: i32 = 0;
do {
result += m[i];
i++;
} while (m[i] !== ".");
if (x < res.length -1) {
result += ",";
}
}
result += "]";
*/
and run with wasmtime this is the result, where I'm expecting integers reflecting indexes, not decimals
echo '4 5' | wasmtime module.wasm
5 of 23 (0-indexed, factorial 24) => [0.0,3.0,2.0,1.0]
However, if I use that input WASM to wasm2js I get zeros - with the decimal
import WASI from "./wasi.js";
import fs from "node:fs";
let wasi = new WASI({
args:[, '4', '5']
});
var memasmFunc = new ArrayBuffer(0);
var retasmFunc = asmFunc({
"wasi_snapshot_preview1": {
memory: { buffer: memasmFunc },
...wasi.exports,
}
});
export var array_nth_permutation = retasmFunc.array_nth_permutation;
export var memory = retasmFunc.memory;
export var _start = retasmFunc._start;
wasi.memory = memory;
_start();
node --no-warnings module.js
5 of 23 (0-indexed, factorial 24) => [0.0,.0,.0,.0]
How do we get integers instead of decimals in AssemblyScript?
const res: i32[] = [];, not const res: number[] = [];.
Also, you can do i = n / f; instead of i = (n - n % f) / f;
That's it. Also requires res.push(<i32>b.splice(i, 1)[0]);
Ah, you can have b be an i32[] too.
Anyhow, in AS, number is an alias to f64.
Alright, I am able to compile the source to JavaScript using tsc in the tsd directory with a couple // @ts-ignores for AssemblyScript's read() and String.UTF8.decode(), and creating node in types directory and copying @types/node into that folder. This is how the AssemblyScript code looks now.
Works with wasmtime and JavaScript runtimes with WASI support.
import "assemblyscript/std/portable.js";
import process from "node:process";
// array_nth_permutation
export function array_nth_permutation(len: i32, n: i32): void { //Array<f64>
let lex = n; // length of the set
let b: i32[] = []; // copy of the set a.slice()
for (let x: i32 = 0; x < len; x++) {
b.push(x);
}
const res: i32[] = []; // return value, undefined
let i: i32 = 1;
let f: i32 = 1;
// compute f = factorial(len)
for (; i <= len; i++) {
f *= i;
}
let fac = f;
// if the permutation number is within range
if (n >= 0 && n < f) {
// start with the empty set, loop for len elements
// let result_len = 0;
for (; len > 0; len--) {
// determine the next element:
// there are f/len subsets for each possible element,
f /= len;
// a simple division gives the leading element index
i = (n - n % f) / f; // Math.floor(n / f);
// alternately: i = (n - n % f) / f;
// res[(result_len)++] = b[i];
// for (let j = i; j < len; j++) {
// b[j] = b[j + 1]; // shift elements left
// }
res.push(<i32>b.splice(i, 1)[0]);
// reduce n for the remaining subset:
// compute the remainder of the above division
n %= f;
// extract the i-th element from b and push it at the end of res
}
let result: string = `[${res}]`;
/*
"[";
for (let x: i32 = 0; x < res.length; x++) {
let m: string = res[x].toString();
let i: i32 = 0;
do {
result += m[i];
i++;
} while (m[i] !== ".");
if (x < res.length -1) {
result += ",";
}
}
result += "]";
*/
process.stdout.write(
`${lex} of ${fac - 1} (0-indexed, factorial ${fac}) => ${result}\n`,
);
process.exit(0);
} else {
if (n === 0) {
process.stdout.write(`${n} = 0`);
}
process.stdout.write(`${n} >= 0 && ${n} < ${f}: ${n >= 0 && n < f}`);
process.exit(1);
}
}
let input: string = "0";
let lex: string = "0";
if (process.argv.length > 1) {
input = process.argv.at(-2);
lex = process.argv.at(-1);
} else {
let stdin = process.stdin;
let buffer = new ArrayBuffer(64);
// @ts-ignore
let n: number = stdin.read(buffer);
if (n > 0) {
// @ts-ignore
let data = String.UTF8.decode(buffer);
input = data.slice(0, data.indexOf(" "));
lex = data.slice(data.indexOf(" "), data.length);
}
}
input = input.trim();
lex = lex.trim();
if (<i32> parseInt(input) < 2 || <i32> parseInt(lex) < 0) {
process.stdout.write(`Expected n > 2, m >= 0, got ${input}, ${lex}`); // eval(input)
process.exit(1);
}
array_nth_permutation(<i32> parseInt(input), <i32> parseInt(lex));
wasmtime module.wasm 12 2
2 of 479001599 (0-indexed, factorial 479001600) => [0,1,2,3,4,5,6,7,8,10,9,11]
and compile to JavaScript using Binaryen's wasm2js, that has an issue
// ...
return {
"array_nth_permutation": $126,
"memory": Object.create(Object.prototype, {
"grow": {
"value": __wasm_memory_grow
},
"buffer": {
"get": function () {
return buffer;
}
}
}),
"_start": $96
};
}
import WASI from "./wasi.js";
import fs from "node:fs";
let wasi = new WASI();
var memasmFunc = new ArrayBuffer(0);
var retasmFunc = asmFunc({
"wasi_snapshot_preview1": {
memory: { buffer: memasmFunc },
...wasi.exports,
}
});
export var array_nth_permutation = retasmFunc.array_nth_permutation;
export var memory = retasmFunc.memory;
export var _start = retasmFunc._start;
wasi.memory = memory;
_start();
echo '12 2' | node --no-warnings module.js
2 of 49001599 (0-indexed, factorial 49001600) => [0,1,2,3,4,5,6,7,8,10,911]
See that 911? Should be 9,11.
I'm not sure the output is different using JavaScript compiled using wasm2js.
I do think there's a bug in AssemblyScript somewhere.
Consider the same algorithm using AssemblyScript
echo '13 2' | wasmtime module.wasm
Error: failed to run main module `module.wasm`
Caused by:
0: failed to invoke command default
1: error while executing at wasm backtrace:
0: 0x3683 - <unknown>!<wasm function 131>
1: 0x3b77 - <unknown>!<wasm function 132>
2: 0x200d - <unknown>!<wasm function 101>
2: wasm trap: integer divide by zero
compare Bytecode Alliance Javy output for factorial 13
echo '13 2' | wasmtime --preload javy_quickjs_provider_v3=../plugin.wasm ../nm_javy_permutations.wasm
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]
@CountBleck Follow-up question. Let's say I'm not using WASI. How do I write the values to memory in WASM so I can then read those values from memory in JavaScript and/or using wasmtime or wasmer?
The memory is exported by default under the name "memory". Use Wasmtime/Wasmer's APIs for accessing that export and manipulate it through there.
This is what I tried. Compiles though throws
export function array_nth_permutation(len: i64, n: i64): void {
let lex = n;
let b: i64[] = [];
for (let x: i64 = 0; x < len; x++) {
b.push(x);
}
const res: i64[] = [];
let i: i64 = 1;
let f: i64 = 1;
for (; i <= len; i++) {
f *= i;
}
let fac = f;
// if the permutation number is within range
if (n >= 0 && n < f) {
for (; len > 0; len--) {
f /= len;
i = (n - n % f) / f; // Math.floor(n / f);
res.push(<i64> b.splice(<i32> i, 1)[0]);
n %= f;
}
}
for (let i: i32 = 0; i < res.length; i++) {
store<i64>(i, res[i]);
}
}
node_modules/.bin/asc fac.ts -o fac.wasm
import { readFile } from "node:fs/promises";
import process from "node:process";
try {
const embeddedModule = await compileModule("./fac.wasm");
const result = await runWasm(embeddedModule);
} catch (e) {
process.stdout.write(e.message, "utf8");
} finally {
process.exit();
}
async function compileModule(wasmPath) {
const bytes = await readFile(new URL(wasmPath, import.meta.url));
return WebAssembly.compile(bytes);
}
async function runWasm(embeddedModule) {
try {
const memory = new WebAssembly.Memory({ initial: 1 });
const instance = await WebAssembly.instantiate(
embeddedModule,
{memory: memory,
env: {
abort: (...args) => console.log(...args),
},
},
);
// Pass numbers to WASM
instance.exports.array_nth_permutation(4, 5);
// Read values from memory
console.log(
// new TextDecoder().decode(new Uint8Array(instance.exports.memory.buffer)),
);
return;
} catch (e) {
if (e instanceof WebAssembly.RuntimeError) {
if (e) {
throw new Error(e);
}
}
throw e;
}
}
bun fac.js
Invalid argument type in ToBigInt operation
@CountBleck You wrote about using store() over here #2869 (comment).
I'm curious how to make use of that AssemblyScript capability for the option to do something like exports.array_nth_permutation(4, 5), write the integers into memory in AssemblyScript, and then read the WebAssembly.Memory object data into a Uint8Array in JavaScript.