Reggie, the experimental Gleam -> WASM compiler (mirror) reggie.desertthunder.dev
regulus docs book src chapter_7 modules_stack_machine.md
3.4 kB · 109 lines
Markdown
at main

WebAssembly modules and the stack machine #

A WebAssembly program is packaged as a module. The core specification treats a module as the unit of deployment, loading, and compilation. A module can contain types, imports, globals, memories, tables, functions, data segments, element segments, exports, and an optional start function.[1]

For a compiler backend, the module is the object being built. Regulus turns one lowered IR module into one WebAssembly module:

(module
  (func $id (export "id") (param $0 i64) (result i64)
    local.get $0
  )
)

This module has one function and one export. Larger modules add memory, runtime helpers, static data, imports, and more functions.

Functions and locals #

WebAssembly functions declare their parameter and result types. Parameters are locals too: they live at the front of the function's local index space, followed by explicitly declared local variables.[1:1]

(func $main (export "main") (param $0 i64) (result i64)
  (local $1 i64)
  local.get $0
  local.set $1
  local.get $1
)

In this example, $0 is the parameter and $1 is a mutable local. The backend uses this model directly because core IR already assigns stable local IDs.

Operand stack #

WebAssembly execution is based on an operand stack. Instructions execute in order. Simple instructions pop their operands from the stack and push their results back.[2]

(func $answer (result i64)
  i64.const 42
)

i64.const 42 pushes one i64. The function has one i64 result, so the value left on the stack becomes the return value.

Calls follow the same rule. Arguments are pushed first, and call consumes them:

(func $id (param $0 i64) (result i64)
  local.get $0
)

(func $main (result i64)
  i64.const 1
  call $id
)

The specification notes that an implementation does not need to literally keep an operand stack. It can compile stack positions into registers or machine temporaries. The important contract for a compiler emitting Wasm is that validation can prove the stack height and types at every instruction.[2:1]

Structured control flow #

WebAssembly has structured control flow: block, loop, if, else, br, and br_if target well-nested constructs rather than arbitrary byte offsets. This is different from many native assembly formats, but it is a good fit for a compiler IR that already represents blocks and branch expressions.

(func $choose (param $x i64) (result i64)
  local.get $x
  i64.const 0
  i64.eq
  if (result i64)
    i64.const 1
  else
    i64.const 2
  end
)

Both arms leave one i64 on the stack, matching the if result type and the function result type.

Validation as a backend guardrail #

Before a module can execute, the host validates it. Validation checks that the module is meaningful and safe, including type checking instruction sequences and ensuring the operand stack is used consistently.[2:2]

That makes invalid stack code visible quickly. If a Regulus backend change emits a function that declares (result i64) but leaves no value on the stack, WAT assembly or WebAssembly validation fails before Wasmtime can call the export.


  1. WebAssembly Core Specification, "Modules": https://webassembly.github.io/spec/core/syntax/modules.html ↩︎ ↩︎

  2. WebAssembly Core Specification, "Overview": https://webassembly.github.io/spec/core/intro/overview.html ↩︎ ↩︎ ↩︎