Skip to main content

Morphir and the WebAssembly Component Model

This document is a comprehensive guide to integrating Morphir with the WebAssembly Component Model ecosystem. It explains the Canonical ABI, type mappings, and how Morphir IR can serve as a rich intermediate representation for Wasm component development.

Executive Summary

The WebAssembly Component Model defines how Wasm modules interoperate through well-defined interfaces. Morphir IR can:

  1. Represent all WIT constructs — Full coverage of Component Model types
  2. Add compile-time validation — Catch boundary errors before runtime
  3. Enable richer tooling — Automated ABI generation, breaking-change detection
  4. Preserve semantic information — Constraints, documentation, and metadata

Relationship: WIT ⊂ Morphir IR for Component Model interfaces.


Part 1: Core Concepts

1.1 The Wasm Type Boundary Problem

WebAssembly core modules only support four value types at function boundaries:

┌─────────────────────────────────────────────────────────────┐
│ Core Wasm Value Types (MVP + Extensions) │
│ │
│ i32 32-bit integer (signedness determined by ops) │
│ i64 64-bit integer │
│ f32 32-bit IEEE 754 float │
│ f64 64-bit IEEE 754 float │
│ │
│ That's it. No strings, no structs, no lists, no variants. │
└─────────────────────────────────────────────────────────────┘

The Component Model and Canonical ABI solve this by defining:

  • How high-level types map to core Wasm types
  • Memory layout conventions for compound types
  • Lifting (core → high-level) and lowering (high-level → core) procedures

1.2 Lifting and Lowering

┌─────────────────────────────────────────────────────────────┐
│ Canonical ABI Operations │
│ │
│ LIFTING: Core Wasm → High-Level │
│ (i32, i32) → String │
│ Read ptr and len, decode UTF-8 from linear memory │
│ │
│ LOWERING: High-Level → Core Wasm │
│ String → (i32, i32) │
│ Encode UTF-8, allocate in linear memory, return ptr+len │
│ │
│ Every boundary crossing requires lift/lower operations │
└─────────────────────────────────────────────────────────────┘

1.3 Morphir's Role

Morphir IR captures semantic information that enables:

CapabilityWIT AloneMorphir IR
Type definitions
Boundary validationRuntimeCompile-time
Numeric constraintsNoSigned/Unsigned/Bounded
String constraintsNoEncoding/Length/Pattern
Breaking change detectionManualAutomated
ABI generationExternal toolingIntegrated

Part 2: Type Mappings

2.1 Primitive Types

Morphir TypeWIT TypeCore WasmNotes
Int (arbitrary)Requires constraintDefault: error at boundary
Int + Signed(32)s32i32Signed 32-bit
Int + Unsigned(32)u32i32Unsigned 32-bit
Int + Signed(64)s64i64Signed 64-bit
Floatf64f64IEEE 754 double
Float + FloatingPoint(32)f32f32IEEE 754 single
Boolbooli320 = false, 1 = true
Charchari32Unicode scalar value

2.2 Strings

┌─────────────────────────────────────────────────────────────┐
│ String Representation │
│ │
│ Morphir: String (abstract) │
│ WIT: string │
│ Memory: UTF-8 encoded bytes │
│ ABI: (ptr: i32, len: i32) │
│ │
│ ┌────────────────────────────────────┐ │
│ │ h │ e │ l │ l │ o │ │ Memory at ptr │
│ └────────────────────────────────────┘ │
│ ↑ │
│ ptr = 0x1000, len = 5 │
└─────────────────────────────────────────────────────────────┘

Morphir can add constraints via StringConstraint:

pub type StringConstraint {
Unconstrained
Encoding(encoding: StringEncoding)
LengthBounded(min: Option(Int), max: Option(Int))
Pattern(regex: String)
}

pub type StringEncoding {
Utf8
Utf16
Latin1
}

2.3 Lists and Arrays

┌─────────────────────────────────────────────────────────────┐
│ List Memory Layout │
│ │
│ list<T> → (ptr: i32, len: i32) │
│ │
│ Memory at ptr: │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │ elem[0] │ elem[1] │ elem[2] │ ... │ │
│ └──────────┴──────────┴──────────┴──────────┘ │
│ │
│ Element stride = align_to(sizeof(T), alignment(T)) │
└─────────────────────────────────────────────────────────────┘

2.4 Records (Structs)

┌─────────────────────────────────────────────────────────────┐
│ Record Memory Layout │
│ │
│ record point { x: f32, y: f32 } │
│ │
│ ┌─────────────┬─────────────┐ │
│ │ x: f32 │ y: f32 │ │
│ │ (4 bytes) │ (4 bytes) │ │
│ └─────────────┴─────────────┘ │
│ Total: 8 bytes, alignment: 4 │
│ │
│ At boundary: pass as pointer (i32) to this layout │
└─────────────────────────────────────────────────────────────┘

2.5 Variants (Enums and Tagged Unions)

┌─────────────────────────────────────────────────────────────┐
│ Variant Memory Layout │
│ │
│ variant shape { circle(f32), rect(f32, f32) } │
│ │
│ ┌──────┬────────────────────────────────────┐ │
│ │ tag │ payload (max size of variants) │ │
│ │ i32 │ [f32, f32] = 8 bytes │ │
│ └──────┴────────────────────────────────────┘ │
│ │
│ tag=0 (circle): payload = [radius, padding] │
│ tag=1 (rect): payload = [width, height] │
└─────────────────────────────────────────────────────────────┘

2.6 Option and Result

Both are special cases of variants with optimized representations:

┌─────────────────────────────────────────────────────────────┐
│ Option Flattening │
│ │
│ option<T> where T is non-nullable: │
│ Some(v) → (1, v) │
│ None → (0, _) │
│ │
│ option<option<T>> cannot be flattened (nested) │
│ │
│ Result Flattening │
│ │
│ result<T, E>: │
│ Ok(v) → (0, v_or_padding) │
│ Err(e) → (1, e_or_padding) │
│ │
│ Payload size = max(sizeof(T), sizeof(E)) │
└─────────────────────────────────────────────────────────────┘

2.7 Functions

Morphir functions are curried; Canonical ABI expects uncurried:

┌─────────────────────────────────────────────────────────────┐
│ Function Lowering │
│ │
│ Morphir: add : Int → Int → Int │
│ ↓ uncurry │
│ Flattened: add : (Int, Int) → Int │
│ ↓ lower types │
│ Core Wasm: (func $add (param i32 i32) (result i32)) │
│ │
│ Parameter flattening limit: ~16 scalars │
│ Beyond limit: pass via pointer │
└─────────────────────────────────────────────────────────────┘

2.8 Resources (Handles)

Resources represent opaque, stateful objects:

┌─────────────────────────────────────────────────────────────┐
│ Resource Representation │
│ │
│ resource file { read, write, close } │
│ │
│ At boundary: i32 handle index into resource table │
│ │
│ own<file> Transfer ownership (receiver must drop) │
│ borrow<file> Temporary access (caller retains ownership) │
│ │
│ Morphir: opaque type + extension attributes │
└─────────────────────────────────────────────────────────────┘

Part 3: Morphir IR Extensions

3.1 Type Constraints

Morphir IR v4 introduces TypeConstraints to capture boundary-relevant information:

pub type NumericConstraint {
/// Arbitrary precision (Morphir default)
Arbitrary
/// Signed fixed-width integer
Signed(bits: IntWidth)
/// Unsigned fixed-width integer
Unsigned(bits: IntWidth)
/// IEEE 754 floating point
FloatingPoint(bits: FloatWidth)
/// Bounded range (any underlying representation)
Bounded(min: Option(BigInt), max: Option(BigInt))
/// Fixed-point decimal
Decimal(precision: Int, scale: Int)
}

pub type IntWidth {
Bits8
Bits16
Bits32
Bits64
}

3.2 Boundary Extensions

Functions crossing component boundaries carry ABI metadata:

{
"ValueDefinition": {
"name": "calculatePremium",
"attributes": {
"extensions": {
"morphir/wasm:boundary#export": { "BoolLiteral": true },
"morphir/wasm:boundary#abi-signature": {
"params": [
{ "name": "policy_ptr", "type": "i32" },
{ "name": "risk_ptr", "type": "i32" }
],
"results": [{ "type": "i32" }]
}
}
}
}
}

3.3 Resource Extensions

Resources leverage Morphir's opaque type system:

{
"TypeDefinition": {
"name": "File",
"definition": { "OpaqueType": {} },
"attributes": {
"extensions": {
"morphir/wasm:resource#definition": {
"constructor": "file_open",
"destructor": "file_close",
"methods": {
"read": { "receiver": "borrow" },
"write": { "receiver": "borrow" }
}
}
}
}
}
}

Part 4: Implementation Architecture

4.1 Compilation Pipeline

┌─────────────────────────────────────────────────────────────┐
│ Morphir → Wasm Pipeline │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ Morphir │───>│ Analysis │───>│ Lowering │───>│ Codegen│ │
│ │ IR │ │ Phase │ │ Phase │ │ Phase │ │
│ └──────────┘ └──────────┘ └──────────┘ └────────┘ │
│ │ │ │ │ │
│ v v v v │
│ Source types Boundary info Wasm types .wasm │
│ + constraints + ABI sigs + layouts binary │
└─────────────────────────────────────────────────────────────┘

4.2 Analysis Phase

pub type BoundaryInfo {
BoundaryInfo(
direction: BoundaryDirection,
morphir_type: Type,
abi_signature: AbiSignature,
memory_layouts: Dict(FQName, MemoryLayout),
)
}

pub fn analyze_boundary(
func: ValueDefinition,
config: WasmConfig,
) -> Result(BoundaryInfo, BoundaryError) {
// 1. Check for generic types (not allowed at boundaries)
use _ <- result.try(reject_type_variables(func.input_types))

// 2. Resolve all type constraints
use constraints <- result.try(resolve_constraints(func))

// 3. Compute ABI signature
use abi <- result.try(compute_abi_signature(func, constraints))

// 4. Compute memory layouts
let layouts = compute_layouts(func.input_types ++ [func.output_type])

Ok(BoundaryInfo(
direction: get_direction(func),
morphir_type: func.signature,
abi_signature: abi,
memory_layouts: layouts,
))
}

4.3 Lowering Phase

The lowering phase transforms Morphir IR into Wasm-compatible representations:

pub type WasmIR {
WasmIR(
functions: List(WasmFunction),
tables: List(WasmTable),
memories: List(WasmMemory),
globals: List(WasmGlobal),
exports: List(WasmExport),
imports: List(WasmImport),
)
}

pub fn lower_module(
module: ModuleDefinition,
boundaries: Dict(FQName, BoundaryInfo),
) -> WasmIR {
let functions =
module.values
|> dict.to_list
|> list.map(fn(pair) {
let #(name, def) = pair
case dict.get(boundaries, name) {
Ok(boundary) -> lower_boundary_function(def, boundary)
Error(_) -> lower_internal_function(def)
}
})

WasmIR(
functions: functions,
tables: generate_tables(module),
memories: [WasmMemory(min_pages: 1, max_pages: None)],
globals: generate_globals(module),
exports: generate_exports(boundaries),
imports: generate_imports(boundaries),
)
}

Part 5: Error Handling

5.1 Result Types

Morphir's Result e a maps directly to WIT's result<a, e>:

┌─────────────────────────────────────────────────────────────┐
│ Result Lowering │
│ │
│ Morphir: Result ValidationError Int │
│ WIT: result<s32, validation-error> │
│ │
│ Memory layout: │
│ ┌──────┬───────────────────────────────────┐ │
│ │ tag │ payload │ │
│ │ i32 │ max(sizeof(ok), sizeof(err)) │ │
│ └──────┴───────────────────────────────────┘ │
│ │
│ tag = 0: Ok, payload contains success value │
│ tag = 1: Err, payload contains error value │
└─────────────────────────────────────────────────────────────┘

5.2 Traps vs Results

ApproachUse CaseRecoverability
Result typeDomain errors, validationCaller handles
Wasm trapProgrammer errors, invariant violationsUnrecoverable

Morphir prefers explicit Result types for all domain errors.


Part 6: Versioning and Evolution

6.1 Compatible Changes

Change TypeCompatibilityVersion Bump
Add new function✅ CompatibleMinor
Add optional record field✅ CompatibleMinor
Add new variant case⚠️ May break exhaustive matchesMinor
Remove function❌ BreakingMajor
Change function signature❌ BreakingMajor
Rename anything❌ BreakingMajor

6.2 Morphir Advantage: Automated Detection

Morphir can compare IR versions structurally:

pub type CompatibilityResult {
Compatible(adapters: List(Adapter))
Breaking(changes: List(BreakingChange))
}

pub fn check_compatibility(
old_ir: Distribution,
new_ir: Distribution,
) -> CompatibilityResult {
let type_changes = diff_types(old_ir.types, new_ir.types)
let func_changes = diff_functions(old_ir.values, new_ir.values)

let breaking =
list.filter(type_changes, is_breaking) ++
list.filter(func_changes, is_breaking)

case breaking {
[] -> Compatible(generate_adapters(type_changes, func_changes))
_ -> Breaking(breaking)
}
}

Part 7: Performance Considerations

7.1 Boundary Crossing Costs

TypeApproximate CostStrategy
Scalar (i32, f64)~0Direct register
StringO(n)Copy UTF-8 bytes
ListO(n × elem)Allocate + copy
RecordO(fields)Allocate + copy

7.2 Optimization Strategies

  1. Batch operations — Single boundary call with multiple items
  2. Coarse-grained interfaces — Return whole records, not field-by-field
  3. Preallocate memory — Analyze functions to predict allocation needs
  4. Inline pure functions — Small functions that don't cross boundaries

7.3 Performance Hints

Morphir IR can carry optimization hints:

{
"attributes": {
"extensions": {
"morphir/wasm:perf#inline": "always",
"morphir/wasm:perf#allocation-hint": 1024,
"morphir/wasm:perf#pure": true
}
}
}

Part 8: Summary — Can Morphir IR Replace WIT?

Coverage Assessment

ModuleWIT FeatureMorphir IRStatus
Scalarss8-s64, u8-u64, f32, f64Via NumericConstraint✅ Full
StringsstringString + StringConstraint✅ Full
Listslist<T>List a✅ Full
Recordsrecord Record type✅ Full
Variantsvariant Custom types✅ Full
Enumsenum Enum variants✅ Full
Optionsoption<T>Maybe a✅ Full
Resultsresult<T, E>Result e a✅ Full
Functionsfunc(...) -> ...Function type✅ Full
Resourcesresource Opaque + extensions✅ Full
Flagsflags Custom type✅ Full

Morphir Advantages

  1. Compile-time boundary validation — Catch errors before runtime
  2. Richer constraints — Numeric bounds, string patterns, custom validators
  3. Automated ABI generation — No manual signature maintenance
  4. Breaking change detection — Structural IR comparison
  5. Semantic preservation — Documentation, constraints, and metadata in IR

Relationship

┌─────────────────────────────────────────────────────────────┐
│ │
│ WIT ⊂ Morphir IR (for Component Model interfaces) │
│ │
│ WIT describes what crosses the boundary. │
│ Morphir IR describes that AND internal semantics. │
│ │
└─────────────────────────────────────────────────────────────┘

Appendix A: Configuration

morphir.toml Settings

[wasm]
# Target output format
output = "component" # "component" | "core" | "wat"

[wasm.boundary]
# How to handle arbitrary-precision Int at boundaries
arbitrary_int = "error" # "error" | "warn" | "i64" | "i32"
arbitrary_float = "f64" # "error" | "warn" | "f64"

[wasm.memory]
# Initial memory configuration
initial_pages = 1
maximum_pages = 256

[wasm.optimization]
# Optimization level
level = "size" # "none" | "speed" | "size"
inline_threshold = 50

Appendix B: Glossary

TermDefinition
Canonical ABIThe standard calling convention for Component Model interfaces
LiftingConverting core Wasm values to high-level types
LoweringConverting high-level types to core Wasm values
ComponentA Wasm module with typed imports/exports per Component Model
ResourceAn opaque handle type with lifecycle management
WITWebAssembly Interface Types — IDL for Component Model

Appendix C: References