WebAssembly / binaryen

Optimizer and compiler/toolchain library for WebAssembly
Apache License 2.0
7.51k stars 745 forks source link

asm2wasm translates modulus operator to import call instead of i32.rem_s #667

Open ceautery opened 8 years ago

ceautery commented 8 years ago

The asm.js line:

if (val % i == 0) return 0;

...where "val" and "i" are both ints, gets translated as an external import in the header and a call to that import:

(type $FUNCSIG$iii (func (param i32 i32) (result i32)))
(import $i32s-rem "asm2wasm" "i32s-rem" (param i32 i32) (result i32))

and then

(if
  (i32.eq
    (call_import $i32s-rem
      (get_local $0)
      (get_local $4)
    )
    (i32.const 0)
  )
  (return
    (i32.const 0)
  )
)

This compiles fine with wasm-as, but fails in the browser with an instantiation error of

Import #0 module="asm2wasm" function="i32s-rem" error: FFI is not an object

If I edit the asm2wasm output to remove the imports and swap the call_import line with:

(i32.rem_s

...wasm-as compiles it fine, and the module works as expected in the browser. Can asm2wasm use the built-in S expression i32.rem_s for integer modulus operations, or is there an overriding reason to stick with the external call? If so, how do I reference the import from JavaScript?

Thanks!

kripken commented 8 years ago

The problem is that wasm % can trap (on corner cases) while asm.js % cannot. But if you build with asm2wasm --imprecise it will ignore the corner cases, and do what you want here.

I am surprised about that browser error you saw, though. Do you have a testcase + full steps to reproduce?

ceautery commented 8 years ago

@kripken - I'm running 64 bit Chrome Canary 54.0.2827.0 with chrome://flags/#enable-webassembly enabled, and a local web server. To reproduce what I'm seeing...

Create test.asm.js with the following content:

function TestModule() {
  "use asm";

  function test(n) {
    n = n | 0;
    return n % 2
  }

   return { test: test }
}

Convert to S-expr syntax, and assemble with:

binaryen/bin/asm2wasm test.asm.js -o test.wast
binaryen/bin/wasm-as test.wast -o test.wasm

Place test.wasm in web server's root directory. Create test.html with this content:

<!doctype html>
<html>
    <head>
        <script>
            var module;

            fetch("test.wasm")
                .then(resp => resp.arrayBuffer())
                .then(buffer => module = Wasm.instantiateModule(new Uint8Array(buffer)));
        </script>
    </head>
    <body>
    </body>
</html>

Navigate to localhost(:port)/test.html, then open the JavaScript console. The complete error I'm seeing is:

Uncaught (in promise) WasmModule::Instantiate(): Import #0 module="asm2wasm" function="i32s-rem" error: FFI is not an object

Running asm2wasm with -i, as you suggested, corrected the import error. Thanks for the reply and the suggestion!

kripken commented 8 years ago

That looks like invalid asm.js. You must coerce on a % because otherwise it can't tell if it's a signed or unsigned operation.

I didn't realize you were using handwritten asm.js here. asm2wasm is only tested on emscripten output, so it's possible you'll find bugs, although in this case, I'm not sure yet.

ceautery commented 8 years ago

Ah. I'll conjecture emscripten would have also created the $i32s-rem import... somewhere, so this likely isn't a bug, as you said.

asm2wasm handles handwritten asm.js code liberally. I've had success using much fewer type coercions than the spec would suggest you need. In general, once a variable's type has been referenced in a function, you seem pretty free to operate on it willy-nilly with variables of the same type. Here's an example, from my (amateur - I've been playing with asm.js for just a few days) attempts at writing various prime number calculators, where I only needed to include the -i flag on asm2wasm, and wasm-as didn't complain about at all, which ran fine in a browser:

function PrimeModule() {
    "use asm";

    var HEAP8 = new global.Int8Array(buffer);
    var HEAP32 = new global.Int32Array(buffer);
    var sqrt = global.Math.sqrt;

    function sieve(max) {
        max = max | 0;

        var sieveOffset = 0, upperLimit = 0, i = 0, outputOffset = 0;

        upperLimit = ~~sqrt(+max);

        for (sieveOffset = 2; sieveOffset <= upperLimit; sieveOffset = sieveOffset + 1) {
            if (HEAP8[sieveOffset >> 0] == 0) {
                for (i = sieveOffset * sieveOffset; i <= max; i = i + sieveOffset) {
                    HEAP8[i >> 0] = 1;
                }
            }
        }
        outputOffset = max + ((4 - max % 4) % 4);

        for (i = 2; i <= max; i = i + 1) {
            if (HEAP8[i >> 0] == 0) {
                HEAP32[outputOffset >> 2] = i;
                outputOffset = outputOffset + 4;
            }
        }

        return (outputOffset - max - ((4 - max % 4) % 4)) >> 2;
    }

   return { getPrimes: sieve };

}

...and...

function PrimeModule() {
  "use asm";

  var HEAP32 = new global.Int32Array(buffer);

  function getPrimes(max) {
    max = max | 0;
    var byteOffset = 0, candidate = 0, limit = 0, square = 0;

    candidate = 3;
    limit = 2;
    square = 4;
    HEAP32[byteOffset >> 2] = 2; // First prime

    while (candidate < max) {
      if ((isPrime(candidate, byteOffset, limit) | 0) == 1) {
        byteOffset = byteOffset + 4;
        HEAP32[byteOffset >> 2] = candidate;
      }
      candidate = candidate + 2;
      if (candidate >= square) {
        square = square + (limit * 2) + 1;
        limit = limit + 1;
      }
    }
    return (byteOffset >> 2) + 1;
  }

  function isPrime(candidate, stopByteOffset, limit) {
    candidate = candidate | 0;
    stopByteOffset = stopByteOffset | 0;
    limit = limit | 0;

    var primeDivisor = 0, byteOffset = 0;

    byteOffset = 4;
    while (byteOffset <= stopByteOffset) {
      primeDivisor = HEAP32[byteOffset >> 2];
      if (primeDivisor > limit) break;
      if (candidate % primeDivisor == 0) return 0;
      byteOffset = byteOffset + 4;
    }

    return 1;
  }

   return { getPrimes: getPrimes };
}

Note the general lack of any coercions after variables are declared.

Anyway, thanks for your time. I didn't realize on the outset that these tools were specifically meant to be used in conjunction with emscripten... and they've turned out to be quite usable directly.

kripken commented 8 years ago

It would be great if it did work on all asm.js, just I've only had time for emscripten output ;) Fixes for non-emscripten output would be welcome of course.