Currently, functions and instructions consume multiple operands but can produce at most one result
value* -> value?value* -> value?[] -> value?In a stack machine, these asymmetries are artificial restrictions
Generalised semantics is well-understood
Semi-complete implementation of multiple results in V8
Multiple return values for functions:
Multiple results for instructions:
Inputs to blocks:
pick operator to cross block boundaryi32.select3 = dup if ... else ... endA simple swap function.
(func $swap (param i32 i32) (result i32 i32) (local.get 1) (local.get 0) )
An addition function returning an additional carry bit.
(func $add64_u_with_carry (param $i i64) (param $j i64) (param $c i32) (result i64 i32) (local $k i64) (local.set $k (i64.add (i64.add (local.get $i) (local.get $j)) (i64.extend_i32_u (local.get $c))) ) (return (local.get $k) (i64.lt_u (local.get $k) (local.get $i))) )
iNN.divrem : [iNN iNN] -> [iNN iNN]iNN.add_carry : [iNN iNN i32] -> [iNN i32]iNN.sub_carry : [iNN iNN i32] -> [iNN i32]Conditionally manipulating a stack operand without using a local.
(func $add64_u_saturated (param i64 i64) (result i64) (call $add64_u_with_carry (local.get 0) (local.get 1) (i32.const 0)) (if (param i64) (result i64) (then (drop) (i64.const 0xffff_ffff_ffff_ffff)) ) )
An iterative factorial function whose loop doesn't use locals, but uses arguments like phis.
(func $pick0 (param i64) (result i64 i64) (local.get 0) (local.get 0) ) (func $pick1 (param i64 i64) (result i64 i64 i64) (local.get 0) (local.get 1) (local.get 0) ) (func $fac (param i64) (result i64) (i64.const 1) (local.get 0) (loop $l (param i64 i64) (result i64) (call $pick1) (call $pick1) (i64.mul) (call $pick1) (i64.const 1) (i64.sub) (call $pick0) (i64.const 0) (i64.gt_u) (br_if $l) (call $pick1) (return) ) )
Macro definition of an instruction expanding into an if.
i64.select3 =
dup if (param i64 i64 i64 i32) (result i64) … select ... else … end
Macro expansion of if itself.
if (param t*) (result u*) A else B end =
block (param t* i32) (result u*)
block (param t* i32) (result t*) (br_if 0) B (br 1) end A
end
The structure of the language is mostly unaffected. The only changes are to the type syntax:
block, loop, if instructions) are generalised from resulttype to functypeArity restrictions are removed:
Validation for block instructions is generalised:
block, loop, and if is the functype [t1*] -> [t2*] given as the block typeblock and if is [t2*]loop is [t1*]Nothing much needs to be done for multiple results:
The only non-mechanical change involves entering blocks with operands:
The binary requires a change to allow function types as block types. That requires extending the current ad-hoc encoding to allow references to function types.
blocktype is extended to the following format:blocktype ::= 0x40 => [] -> []
| t:valtype => [] -> [t]
| ft:typeidx => ft
The text format is mostly unaffected, except that the syntax for block types is generalised:
resulttype is replaced with blocktype, whose syntax is
blocktype ::= vec(param) vec(result)
block, loop, and if instructions contain a blocktype instead of resulttype.
The existing abbreviations for functions apply to block types.
The typing of administrative instructions need to be generalised, see the paper.
Instead of inline function types, could use references to the type section
Could also allow both
Could even unify the encoding of block types with function types everywhere
pick instruction (generalised dup)let instruction (if you squint, a generalised swap)