Chapter 5

Functions

Functions are where all WebAssembly code lives. There is no code outside them: no top-level statements, no initialisers that run arbitrary code (apart from one optional start function), no code in data. This chapter covers defining and calling functions, multiple results, and the call stack, which is the first place WebAssembly differs sharply from a native target.

#Signatures and calls5.1

A function has a type: a list of parameter types and a list of result types. call takes a function index, pops that function's parameters off the stack, runs it, and pushes its results:

(module
  (func $square (param i32) (result i32)
    (i32.mul (local.get 0) (local.get 0)))
  (func (export "cube") (param $x i32) (result i32)
    (i32.mul (call $square (local.get $x)) (local.get $x))))
called as cube(3) and cube(-4)
27
-64

Arguments are pushed in order, first argument first, exactly like the operands of an instruction. A call is, as far as the stack is concerned, just an instruction whose type is the callee's signature.

The return instruction leaves a function early, taking the function's results from the top of the stack:

(module
  (func (export "abs") (param $x i32) (result i32)
    (if (i32.ge_s (local.get $x) (i32.const 0))
      (then (return (local.get $x))))
    (i32.sub (i32.const 0) (local.get $x))))
called as abs(-5) and abs(5)
5
5

(if is the subject of the next chapter.)

#Recursion5.2

Functions can call themselves, and each other, freely:

(module
  (func $fac (export "fac") (param $n i64) (result i64)
    (if (result i64) (i64.le_u (local.get $n) (i64.const 1))
      (then (i64.const 1))
      (else
        (i64.mul
          (local.get $n)
          (call $fac (i64.sub (local.get $n) (i64.const 1))))))))
called as fac(10n) and fac(20n)
3628800
2432902008176640000

The function takes an i64, so the host passed the arguments as BigInts, 10n and 20n. Passing a plain JavaScript number where an i64 is expected is an error, raised before any WebAssembly code runs:

(module
  (func $fac (export "fac") (param $n i64) (result i64)
    (if (result i64) (i64.le_u (local.get $n) (i64.const 1))
      (then (i64.const 1))
      (else
        (i64.mul
          (local.get $n)
          (call $fac (i64.sub (local.get $n) (i64.const 1))))))))
called as fac(10)
TypeError: Cannot convert 10 to a BigInt

#Multiple results5.3

A function can return more than one value. This was added in WebAssembly 2.0, and every engine supports it:

(module
  (func (export "divmod") (param $a i32) (param $b i32) (result i32 i32)
    (i32.div_s (local.get $a) (local.get $b))
    (i32.rem_s (local.get $a) (local.get $b))))
called as divmod(17, 5) and divmod(-17, 5)
3 2
-3 -2

The results are left on the stack in order, the first result deepest. A caller in WebAssembly gets them all pushed onto its stack. A caller in JavaScript gets an array, which the book's host prints separated by spaces.

(module
  (import "env" "print_i32" (func $print (param i32)))
  (func $divmod (param $a i32) (param $b i32) (result i32 i32)
    (i32.div_s (local.get $a) (local.get $b))
    (i32.rem_s (local.get $a) (local.get $b)))
  (func (export "main")
    (local $q i32)
    (local $r i32)
    (call $divmod (i32.const 100) (i32.const 7))
    local.set $r          ;; the last result is on top
    local.set $q
    (call $print (local.get $q))
    (call $print (local.get $r))))
14
2

#Types and the type section5.4

Every function's signature lives in the module's type section, and the function refers to it by index. The text format has let you write signatures inline so far, but you can also declare a type by name and refer to it:

(module
  (type $binop (func (param i32 i32) (result i32)))
  (func (export "add") (type $binop)
    (i32.add (local.get 0) (local.get 1)))
  (func (export "sub") (type $binop)
    (i32.sub (local.get 0) (local.get 1))))
called as add(1, 2) and sub(1, 2)
3
-1

When you write a signature inline, the assembler looks for an existing type with that signature and uses it, or adds one if there is none. Named types matter more once function references and indirect calls are involved (Chapter 11), because there you have to name the type of the function you expect to call.

#The function index space5.5

Functions are numbered in one index space, and imported functions come first. A module with two imports and three definitions has functions 0 and 1 imported, and 2, 3 and 4 defined. The same is true of every other kind of thing a module can import: memories, tables, globals and tags each have an index space, with imports first.

(module
  (import "env" "print_i32" (func $print_i32 (param i32)))
  (import "env" "print_f64" (func $print_f64 (param f64)))
  (func $a)
  (func $b)
  (func $c (export "main")
    (call $print_i32 (i32.const 1))))
wasm-objdump -x -j Import indices.wasm | tail -n +6
wasm-objdump -x -j Function indices.wasm | tail -n +6
Import[2]:
 - func[0] sig=0 <print_i32> <- env.print_i32
 - func[1] sig=1 <print_f64> <- env.print_f64
Function[3]:
 - func[2] sig=2 <a>
 - func[3] sig=2 <b>
 - func[4] sig=2 <c>

The names in angle brackets come from the module's name section, an optional part of the binary where the assembler recorded the $names from the text, for tools and debuggers to show. Chapter 14 covers it.

This matters to a compiler in one practical way: if you decide to add an import late in compilation, every function index you have already emitted shifts. Most compilers collect their imports first, or number functions symbolically and resolve the numbers when they write the binary.

#The invisible call stack5.6

Here is the most important thing in this chapter. In WebAssembly, you cannot see the call stack.

There is no stack pointer register. There is no instruction to read a return address, or walk up the frames, or take the address of a local variable. Locals are not in memory at all, as far as WebAssembly code can tell; they are just numbered slots that local.get and local.set reach. The engine keeps return addresses and spilled registers on a stack of its own, in memory the module cannot read or write.

This is part of WebAssembly's safety. A buffer overflow in linear memory cannot overwrite a return address, because return addresses are not in linear memory. A module cannot jump to an address it computed, because it has no way to name an address in code.

It also means a lot of things a native compiler takes for granted need another approach:

#Stack overflow5.7

The engine's stack is finite, and a call that would overflow it is reported as a JavaScript RangeError, the same error as runaway recursion in JavaScript:

(module
  (func $forever (export "main")
    (call $forever)))
RangeError: Maximum call stack size exceeded

How deep you can recurse depends on the engine, its settings, and the size of each frame, which depends on how many locals the engine had to spill. In V8 with default settings it is typically some tens of thousands of frames. A compiler for a language with deep recursion, or a language that promises unbounded recursion, cannot rely on it. Chapter 21 shows how tail calls help with one important case.

#What you have seen5.8

Functions have types made of parameter and result lists, which live in a type section shared by the whole module. call pops arguments and pushes results, and a function can return several values. Functions are numbered in an index space with imports first. The call stack belongs to the engine: you cannot read it, take addresses in it, or rely on its depth.

That ends Part I. Part II is about control flow inside functions, and about modules as a whole.