Skip to main content

WAT Code Generation Design

This document defines the design for generating WAT (WebAssembly Text Format) from the Wasm backend's internal representation.

Overview

WAT provides a human-readable text format for WebAssembly, useful for:

  • Debugging: Inspect generated code
  • Diffing: Version control friendly
  • Learning: Understand compilation output
  • Testing: Validate specific instructions
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│ Wasm Backend │────►│ WAT Emitter │────►│ .wat file │
│ (WasmModule) │ │ │ │ (text) │
└─────────────────┘ └─────────────────┘ └─────────────────┘

├── Format instructions
├── Add comments
├── Name functions
└── Pretty print

WAT Format Overview

WAT uses S-expressions:

(module
;; Type declarations
(type $add_type (func (param i32 i32) (result i32)))

;; Function definitions
(func $add (type $add_type) (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)

;; Exports
(export "add" (func $add))
)

Architecture

WAT Emitter Components

┌──────────────────────────────────────────────────────────────────┐
│ WAT Emitter │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Module │ │ Function │ │ Instruction │ │
│ │ Emitter │ │ Emitter │ │ Emitter │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Type │ │ Comment │ │ Formatter │ │
│ │ Emitter │ │ Generator │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘

Implementation

Core WAT Writer

class WatEmitter(config: WatEmitConfig):
private val sb = StringBuilder()
private var indent = 0

def emit(module: WasmModule): String =
emitModule(module)
sb.toString

private def emitModule(module: WasmModule): Unit =
line("(module")
indent += 1

// Emit module name if present
if config.emitNames then
line(s";; Generated from Morphir IR")
line(s";; ${java.time.Instant.now}")
line("")

// Types section
if module.types.nonEmpty then
line(";; Type declarations")
module.types.zipWithIndex.foreach { (typ, i) =>
emitType(typ, i)
}
line("")

// Imports section
if module.imports.nonEmpty then
line(";; Imports")
module.imports.foreach(emitImport)
line("")

// Function declarations (for forward references)
if module.functions.nonEmpty then
line(";; Functions")
module.functions.foreach(emitFunction)
line("")

// Memory section
if module.memories.nonEmpty then
line(";; Memory")
module.memories.zipWithIndex.foreach { (mem, i) =>
emitMemory(mem, i)
}
line("")

// Globals section
if module.globals.nonEmpty then
line(";; Globals")
module.globals.foreach(emitGlobal)
line("")

// Exports section
if module.exports.nonEmpty then
line(";; Exports")
module.exports.foreach(emitExport)
line("")

// Data section
if module.data.nonEmpty then
line(";; Data")
module.data.foreach(emitData)

indent -= 1
line(")")

private def emitType(typ: WasmFuncType, index: Int): Unit =
val name = if config.emitNames then s"$$type_$index" else ""
val params = typ.params.map(p => s"(param ${valType(p)})").mkString(" ")
val results = typ.results.map(r => s"(result ${valType(r)})").mkString(" ")
line(s"(type $name (func $params $results))")

private def emitFunction(func: WasmFunction): Unit =
val name = if config.emitNames then s"$$${func.name}" else ""

// Function signature
val params = func.params.zipWithIndex.map { (typ, i) =>
val paramName = if config.emitNames then s"$$p$i" else ""
s"(param $paramName ${valType(typ)})"
}.mkString(" ")

val results = func.results.map(r => s"(result ${valType(r)})").mkString(" ")

val locals = func.locals.zipWithIndex.map { (typ, i) =>
s"(local $$l$i ${valType(typ)})"
}.mkString(" ")

line(s"(func $name $params $results")
indent += 1

if locals.nonEmpty then
line(locals)

// Emit function body with comments
if config.emitSourceComments && func.sourceLocation.isDefined then
line(s";; Source: ${func.sourceLocation.get}")

emitInstructions(func.body)

indent -= 1
line(")")

private def emitInstructions(instrs: List[WasmInstr]): Unit =
instrs.foreach(emitInstruction)

private def emitInstruction(instr: WasmInstr): Unit =
instr match
// Constants
case WasmInstr.I32Const(v) => line(s"i32.const $v")
case WasmInstr.I64Const(v) => line(s"i64.const $v")
case WasmInstr.F32Const(v) => line(s"f32.const $v")
case WasmInstr.F64Const(v) => line(s"f64.const $v")

// Local variables
case WasmInstr.LocalGet(i) =>
val name = if config.emitNames then s"$$l$i" else i.toString
line(s"local.get $name")
case WasmInstr.LocalSet(i) =>
val name = if config.emitNames then s"$$l$i" else i.toString
line(s"local.set $name")
case WasmInstr.LocalTee(i) =>
val name = if config.emitNames then s"$$l$i" else i.toString
line(s"local.tee $name")

// Globals
case WasmInstr.GlobalGet(name) => line(s"global.get $$$name")
case WasmInstr.GlobalSet(name) => line(s"global.set $$$name")

// Memory operations
case WasmInstr.I32Load(offset, align) =>
line(s"i32.load offset=$offset align=$align")
case WasmInstr.I32Store(offset, align) =>
line(s"i32.store offset=$offset align=$align")
case WasmInstr.I64Load(offset, align) =>
line(s"i64.load offset=$offset align=$align")
case WasmInstr.I64Store(offset, align) =>
line(s"i64.store offset=$offset align=$align")

// Arithmetic
case WasmInstr.I32Add => line("i32.add")
case WasmInstr.I32Sub => line("i32.sub")
case WasmInstr.I32Mul => line("i32.mul")
case WasmInstr.I32DivS => line("i32.div_s")
case WasmInstr.I32DivU => line("i32.div_u")
case WasmInstr.I64Add => line("i64.add")
case WasmInstr.I64Sub => line("i64.sub")
case WasmInstr.I64Mul => line("i64.mul")
case WasmInstr.F64Add => line("f64.add")
case WasmInstr.F64Sub => line("f64.sub")
case WasmInstr.F64Mul => line("f64.mul")
case WasmInstr.F64Div => line("f64.div")

// Comparisons
case WasmInstr.I32Eq => line("i32.eq")
case WasmInstr.I32Ne => line("i32.ne")
case WasmInstr.I32LtS => line("i32.lt_s")
case WasmInstr.I32GtS => line("i32.gt_s")
case WasmInstr.I32LeS => line("i32.le_s")
case WasmInstr.I32GeS => line("i32.ge_s")

// Control flow
case WasmInstr.Block(label, body) =>
line(s"(block $$$label")
indent += 1
emitInstructions(body)
indent -= 1
line(")")

case WasmInstr.Loop(label, body) =>
line(s"(loop $$$label")
indent += 1
emitInstructions(body)
indent -= 1
line(")")

case WasmInstr.If(thenBranch, elseBranch) =>
line("(if")
indent += 1
line("(then")
indent += 1
emitInstructions(thenBranch)
indent -= 1
line(")")
if elseBranch.nonEmpty then
line("(else")
indent += 1
emitInstructions(elseBranch)
indent -= 1
line(")")
indent -= 1
line(")")

case WasmInstr.Br(label) => line(s"br $$$label")
case WasmInstr.BrIf(label) => line(s"br_if $$$label")
case WasmInstr.Return => line("return")

// Calls
case WasmInstr.Call(name) => line(s"call $$$name")
case WasmInstr.CallIndirect(typeIdx) =>
line(s"call_indirect (type $typeIdx)")

// Misc
case WasmInstr.Drop => line("drop")
case WasmInstr.Unreachable => line("unreachable")

private def emitExport(exp: WasmExport): Unit =
val kind = exp.kind match
case ExportKind.Func => "func"
case ExportKind.Table => "table"
case ExportKind.Memory => "memory"
case ExportKind.Global => "global"
line(s"""(export "${exp.name}" ($kind $$${exp.internalName}))""")

private def emitImport(imp: WasmImport): Unit =
imp match
case WasmImport.Func(module, name, typeIdx) =>
line(s"""(import "$module" "$name" (func $$${name} (type $typeIdx)))""")
case WasmImport.Memory(module, name, min, max) =>
val maxStr = max.map(m => s" $m").getOrElse("")
line(s"""(import "$module" "$name" (memory $min$maxStr))""")

private def emitMemory(mem: WasmMemory, index: Int): Unit =
val name = if config.emitNames then s"$$memory_$index" else ""
val maxStr = mem.max.map(m => s" $m").getOrElse("")
line(s"(memory $name ${mem.min}$maxStr)")

private def emitGlobal(glob: WasmGlobal): Unit =
val mutability = if glob.mutable then "mut" else ""
val typ = valType(glob.typ)
val init = glob.init match
case WasmInstr.I32Const(v) => s"i32.const $v"
case WasmInstr.I64Const(v) => s"i64.const $v"
case WasmInstr.F64Const(v) => s"f64.const $v"
case _ => throw new IllegalArgumentException("Invalid global init")
line(s"(global $$${glob.name} ($mutability $typ) ($init))")

private def emitData(data: WasmData): Unit =
val bytes = data.bytes.map(b => f"\\$b%02x").mkString
line(s"""(data (i32.const ${data.offset}) "$bytes")""")

private def valType(t: WasmValType): String =
t match
case WasmValType.I32 => "i32"
case WasmValType.I64 => "i64"
case WasmValType.F32 => "f32"
case WasmValType.F64 => "f64"

private def line(s: String): Unit =
sb.append(" " * indent)
sb.append(s)
sb.append("\n")

Example Output

Input: Simple Addition Function

add : Int32 -> Int32 -> Int32
add a b = a + b

Generated WAT

(module
;; Generated from Morphir IR
;; 2026-01-17T07:30:00Z

;; Type declarations
(type $type_0 (func (param i32) (param i32) (result i32)))

;; Functions
(func $add (type $type_0) (param $p0 i32) (param $p1 i32) (result i32)
;; Source: MyModule.elm:5:1
local.get $p0
local.get $p1
i32.add
)

;; Exports
(export "add" (func $add))
)

Input: String Function

greet : String -> String
greet name = "Hello, " ++ name ++ "!"

Generated WAT

(module
;; Type declarations
(type $type_0 (func (param i32 i32) (result i32 i32)))

;; Memory
(memory $memory_0 1)

;; Globals
(global $heap_ptr (mut i32) (i32.const 1024))

;; Functions
(func $greet (type $type_0) (param $name_ptr i32) (param $name_len i32) (result i32 i32)
;; Source: MyModule.elm:8:1
(local $result_ptr i32)
(local $result_len i32)
(local $prefix_ptr i32)
(local $suffix_ptr i32)

;; Allocate result buffer
;; ... string concatenation logic ...

local.get $result_ptr
local.get $result_len
)

(func $malloc (param $size i32) (result i32)
global.get $heap_ptr
local.tee 0
local.get $size
i32.add
global.set $heap_ptr
)

;; Data
(data (i32.const 0) "Hello, ")
(data (i32.const 7) "!")

;; Exports
(export "greet" (func $greet))
)

Configuration

case class WatEmitConfig(
// Naming
emitNames: Boolean = true, // Use symbolic names ($func_name)
emitSourceComments: Boolean = true, // Add source location comments

// Formatting
indentSize: Int = 2,
maxLineWidth: Int = 100,

// Sections
emitTypeSection: Boolean = true,
groupBySection: Boolean = true, // Group imports, exports, etc.

// Debug
emitDebugComments: Boolean = false, // Verbose debug info
)
# morphir.toml
[backend.wat]
emit_names = true
emit_source_comments = true
indent_size = 2

Integration with Binary Emission

WAT can be converted to binary Wasm using wat2wasm:

def emitBinaryViaWat(module: WasmModule, config: WatEmitConfig): Array[Byte] =
val wat = WatEmitter(config).emit(module)

// Option 1: Shell out to wat2wasm
val process = ProcessBuilder("wat2wasm", "-", "-o", "-")
.redirectInput(ProcessBuilder.Redirect.PIPE)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.start()

process.getOutputStream.write(wat.getBytes)
process.getOutputStream.close()

val binary = process.getInputStream.readAllBytes()
process.waitFor()
binary

// Option 2: Use wasmparser/wasm-tools library
// wasmparser.parse(wat)

Open Questions

  1. Folded vs. Flat Format: Should we emit folded S-expressions or flat instruction sequences?

  2. Custom Sections: Should we emit Morphir-specific debug info in custom sections?

  3. Source Maps: Generate DWARF-style debug info for source-level debugging?

  4. Optimization Annotations: Emit hints for wasm-opt or other optimizers?