AssemblyScript/assemblyscript

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.