Data Types and Stack Machine
a quickstart example
Every computation in WASM happens on the stack. For examples of stack-based languages see Forth. JVM or CLR are another examples of abstract stack machines.
As an example, subtracting two i32
s (32-bit signed integers) looks like this:
;; the run-time stack: [...]
i32.const 3
;; [... 3], constant value 3 of type i32 is pushed on the stack
i32.const 2
;; [... 3 2]
i32.sub
;; [... 1] - the result 1 is left on the stack
This can also be writen in a more structured, folded form:
(i32.sub
(i32.const 3)
(i32.const 2))
Both formats translate into the same binary encoding:
41 03 ;; i32.const 3
41 02 ;; i32.const 2
6b ;; i32.sub
Stacks are local to a function call.
(func $sub (result i32)
(i32.sub (i32.const 3) (i32.const 2))
;; always returns 1
)
Here:
$sub
gives this function an optional human-readable name, which is an alias to the number (index, idx) of this function in its module.(func (result i32) ...)
is also valid.- if a function takes any parameters, each of them is listed after its name as
(param $opt-arg-name i32)
wherei32
(or any other type) is the argument type.$opt-arg-name
is again an optional human-readable name and an alias to the parameter index inside the function. - if a function returns a value, it should be specified as
(result i32)
(i32
or any other type). - the VM instructions follow
running a module
In order to be runnable from outside, functions must be wrapped in a module and export-ed from it:
(module
(func (export "sub") (result i32)
(i32.sub (i32.const 3) (i32.const 2))
))
You can run this example with a WASM runtime like wasmtime, wasmer, wasm3 and many others. NodeJS/Firefox/Chrome also contain a built-in WASM runtime accessible via a Javascript API:
$ wasmtime run --invoke sub sub.wat
1
.wat
files can be translated into binary modules using e.g. wasm-as
from the binaryen utilities suite:
$ wasm-as sub.wat -o sub.wasm
$ xxd sub.wasm
00000000: 0061 736d 0100 0000 0105 0160 0001 7f03 .asm.......`....
00000010: 0201 0007 0701 0373 7562 0000 0a09 0107 .......sub......
00000020: 0041 0341 026b 0b .A.A.k.
or disassembled back into wat using wasm-dis
:
$ wasm-dis sub.wasm
(module
(type $0 (func (result i32)))
(export "sub" (func $0))
(func $0 (result i32)
(i32.sub
(i32.const 3)
(i32.const 2)
)
)
)
We will look into the details of the binary encoding later.
The produced sub.wasm
can also be transfered and executed somewhere, e.g. in NodeJS:
#!/usr/bin/env node
const fs = require('fs');
async function main(wasmPath, wasmFunc, ...args) {
const wasmBuffer = fs.readFileSync(wasmPath);
let wasmModule = await WebAssembly.instantiate(wasmBuffer);
let func = wasmModule.instance.exports[wasmFunc];
let result = await func(...args);
console.log(result);
}
const process = require('process');
main(...process.argv.slice(2)).catch(e => {
console.error(e);
process.exit(1);
})
$ ./wasmrun.js sub.wasm sub
1
module validation
There's an invisible step that happens before running a module: module validation. WASM specification has a rich set of invariants that are statically verified for a module, e.g. all of the following fails validation:
- setting a variable of one type to a value of another type without explicit casting.
- referring to a variable that does not exist
- trying to call a value as a function (unless it's a
funcref
) - having more than one value on the stack of a function when returning one value from it
- stack underflow is statically impossible
- ...
data types and operations
In the example above, we have already seen the data type i32
and instructions that work on it, like i32.const
, i32.sub
. What else is there?
TODO: numeric types
TODO: conversions between numerics types
TODO: funcref
TODO: externref
dropping values
If something produces a value that you don't need (e.g. a function called for its side effects, which also produces a value on the stack which can be ignored), it can be discarded from the stack with drop
:
i32.const 42 ;; [ 42 ]
i32.const -1 ;; [ 42 -1 ]
drop ;; [ 42 ]