How do block params work in WebAssembly (with multivalue support)?

Viewed 238

Blocks can consume values from the stack and push results back to it, which is cool, but I cannot find any detail on how this works.

The spec for loop says (at 5) that the values are popped from the stack (before any code within the block gets executed), and the other block instructions (block and if) are described similarly. They all pop the params before executing the block:

loop blocktype instr* end

  1. Assert: due to validation, expand(blocktype) is defined.
  2. Let [1]→[2] be the function type expand(blocktype).
  3. Let be the label whose arity is and whose continuation is the start of the loop.
  4. Assert: due to validation, there are at least values on the top of the stack.
  5. Pop the values val from the stack.
  6. Enter the block val instrāˆ— with label .

Where are block params popped to? Blocks do not have their own locals, so I have no idea how to actually use the params (presumably they are not just thrown away).

The spec does not say anything about loop blocks popping params on each iteration, but I thought that was how they work.

I also struggled to establish whether loop blocks push results to the stack on each iteration, or only once, when the loop exits. Presumably, it's once, but with my ignorance on the other points, I'm not confident about that either.

I found this code in the proposal that may make things clearer (to somebody):

(func $fac (param i64) (result i64)
    (i64.const 1) (get_local 0)
    (loop $l (param i64 i64) (result i64)
        (pick 1) (pick 1) (i64.mul)
        (pick 1) (i64.const 1) (i64.sub)
        (pick 0) (i64.const 0) (i64.gt_u)
        (br_if $l)
        (pick 1) (return)
    )
)
2 Answers

I am new to WebAssembly, but here is how I was able to use it (replace loop by block if needed):

Following .wat adds 1 to input param

(module
    (func (export "myFunc") (param i32) (result i32)
        get_local 0
        (loop (param i32) (result i32)
            i32.const 1
            i32.add
        )
    )
)

index.html

<div id="out"></div>
<script>
const out = document.getElementById("out");
WebAssembly.instantiateStreaming(fetch('module.wasm'))
    .then(obj => {
        out.innerHTML = obj.instance.exports.myFunc(3);
    });
</script>

Alternatively, without loop param you could do the following

(module
    (func (export "myFunc") (param i32) (result i32)
        (loop (result i32)
            get_local 0
            i32.const 1
            i32.add
        )
    )
)

There are really not many examples I could find, so I am using unit tests as material

I too am struggling to understand the baroque way that blocks are defined in WebAssembly. This is more or less what the loop spec says:

  • pop the m values val_m from the stack
  • enter the block val_m instr* with label L

This last step means: push label L; push val_m; and start executing the instruction stream.

What they mean here is that the program must behave as if this procedure had been followed. The effect is to insert the label L (a local return address, if you like) into the stack, underneath the block parameters.

Of course no real-world implementation would do this; it is just a way of specifying the required behaviour, which people are free to implement as they see fit.

Related