Locals and Globals

function parameters and local variables

The quickstart example is rather useless, because it always returns 1. Let's do an actual computation by passing arguments to a function:

# compute (x - y)^2:
$ wasmtime run --invoke sqrsub sqrsub.wat 8 5
9

Every WASM function has a list of locals. Their list starts with params passed to the function, with (local <name>? <type>) definitions after that.

Locals are referred to by their statically known index (e.g. 0, 1) or index alias (e.g. $x, $y). Locals can be loaded on the stack with local.get <idx> and set to a value from stack with local.set <idx> (this value is popped from the stack). The type of the value on the stack must match the type of the local, otherwise the module will be rejected during validation; i.e. (local $x i64) (local.set $x (f32.const 42)) does not pass module validation.

(module
  ;; compute (x-y)^2
  (func (export "sqrsub") (param $x f64) (param $y f64) (result f64)
    ;; declare a local variable $diff of type f64
    (local $diff f64)   

    ;; assign a local:
    (local.set $diff
       (f64.sub (local.get $x) (local.get $y)))

    ;; return a value computed using a local:
    (return (f64.mul (local.get $diff) (local.get $diff)))
  )
)

The same module in a more low-level form reads as

(module
  (func                 ;; (func 0)
    (param f64)         ;; local 0, $x above
    (param f64)         ;; local 1, $y above
    (result f64)
    (local f64)         ;; local 2, $diff above
                    ;; the stack: []
    local.get 0     ;; [ $x ]
    local.get 1     ;; [ $x $y ]
    f64.sub         ;; [ ($x-$y) ]
    local.set 2     ;; []

    local.get 2     ;; [ $diff ]
    local.get 2     ;; [ $diff $diff ]
    f64.mul         ;; [ ($diff*$diff) ], one f64 return value
  )

  (export "sqrsub" (func 0))
)

Note that the WASM VM does not have a dup opcode, an intermediate value must be saved into a local if it is used twice.

The locals of sqrsub are:

  • two function parameters with <localidx> 0 and 1 (aliased to $x and $y in the first example)
  • one local variable with <localidx> 2 (aliased to $diff).

Functions themselves are indexed within a module, e.g. (func 0) refers to the first function in the module.

This example can be optimized using local.tee and reusing one of parameters as a variable:

  (func                 ;; (func 0)
    (param f64)         ;; local 0, $x above
    (param f64)         ;; local 1, $y above
    (result f64)
                    ;; the stack: []
    local.get 0     ;; [ $0 ]
    local.get 1     ;; [ $0 $1 ]
    f64.sub         ;; [ ($0-$1) ]
    local.tee 0     ;; set `local 0` to the value, but also keep it on the stack
    local.get 0     ;; [ $0 $0 ]
    f64.mul         ;; [ ($0*$0) ], one f64 return value 
  )

globals

Modules can have their own top-level constants, globals. Globals have their own index namespace (i.e. globalidx=0, localidx=0, funcidx=0 all refer to different things).

(module
  (global           ;; a declaration of a global
    $the-answer         ;; an alias for globalidx=0
    i32                 ;; its (constant) type
    (i32.const 42)      ;; its initializer expression
  )
  
  (func (export "the_answer") (result i32)
    (global.get $the-answer))    ;; push its value on stack
)

Values of globals can be pushed on stack using global.get <idx>.

$the-answer is a constant: it's initialized once and cannot change its value. Only a restricted set of instructions for constant expressions is allowed in an initializer context.

mutable globals

Globals can be mutable globals when their type is specified with (mut <type>). Their value can be popped from stack and set with global.set <idx>.

For example:

(module
  (global $call-times (mut i32)
    (i32.const 0))

  ;; increment the counter on every call
  (func (export ("call_me"))
    (global.set $call-times
      (i32.add
        (i32.const 1)
        (global.get $call-times))))

  ;; get the counter
  (func (export "stats") (return i32)
    (global.get $call-times))
)

TODO: more about initialization

stack validation

All WASM instructions have statically known effects on the stack. This means that:

  • instructions always pop a known number of values
  • instructions always push a known number of values
  • it's possible to know the depth of the stack at each instruction
  • it's possible to know the maximum depth of the stack during a function call
  • stacks for then/else branches of if must be balanced.
  • loops cannot grow stack dynamically; linear memory must be used for that instead.

TODO: param/result notation

stack tricks

Some neat stack-based tricks apply.

swapping two values

local.get $a   ;; [ $a ]
local.get $b   ;; [ $a $b ]
local.set $a   ;; [ $a ]
local.set $b   ;; [ ]