Skip to main content

WIT Frontend Design

This document defines the design for a frontend that parses WIT (WebAssembly Interface Types) files and produces Morphir IR v4.

Overview

The WIT frontend enables Morphir to consume existing Component Model interfaces, generating type-safe Morphir bindings.

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│ .wit files │────►│ WIT Frontend │────►│ Morphir IR │
│ (interfaces) │ │ │ │ (Distribution) │
└─────────────────┘ └─────────────────┘ └─────────────────┘

├── Parse WIT
├── Resolve imports
├── Map types
└── Generate modules

Use Cases

  1. Import Component Interfaces: Generate Morphir types from WIT interfaces to call external components
  2. Validate Exports: Check that Morphir exports match a WIT contract
  3. Polyglot Interop: Share type definitions between Morphir and other Component Model languages

Architecture

┌──────────────────────────────────────────────────────────────────┐
│ WIT Frontend │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: Parsing │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Lexer │ │ Parser │ │ AST │ │
│ │ │──►│ │──►│ Builder │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Phase 2: Resolution │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Import │ │ Type │ │ World │ │
│ │ Resolution │ │ Resolution │ │ Expansion │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Phase 3: IR Generation │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Type │ │ Function │ │ Module │ │
│ │ Mapping │ │ Mapping │ │ Generation │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘

WIT to Morphir IR Type Mapping

Primitive Types

WIT TypeMorphir IR TypeConstraints
boolmorphir/sdk:basics#bool
s8morphir/sdk:int#int8Signed { bits: 8 }
s16morphir/sdk:int#int16Signed { bits: 16 }
s32morphir/sdk:int#int32Signed { bits: 32 }
s64morphir/sdk:int#int64Signed { bits: 64 }
u8morphir/sdk:uint#uint8Unsigned { bits: 8 }
u16morphir/sdk:uint#uint16Unsigned { bits: 16 }
u32morphir/sdk:uint#uint32Unsigned { bits: 32 }
u64morphir/sdk:uint#uint64Unsigned { bits: 64 }
f32morphir/sdk:float#float32FloatingPoint { bits: 32 }
f64morphir/sdk:basics#floatFloatingPoint { bits: 64 }
charmorphir/sdk:char#charUnicode Scalar Value
stringmorphir/sdk:string#stringStringConstraint { encoding: UTF8 }

Compound Types

WIT TypeMorphir IR Type
list<T>Type.Reference("morphir/sdk:list#list", [T'])
option<T>Type.Reference("morphir/sdk:maybe#maybe", [T'])
result<T, E>Type.Reference("morphir/sdk:result#result", [E', T'])
tuple<T1, T2, ...>Type.Tuple([T1', T2', ...])
record { ... }Type.Record([...])
variant { ... }Custom type definition
enum { ... }Custom type (unit constructors)
flags { ... }Custom type or record of bools

Example: Record Mapping

WIT:

record person {
name: string,
age: u32,
active: bool,
}

Morphir IR:

{
"Record": {
"attributes": {},
"fields": {
"name": {
"Reference": {
"attributes": {
"constraints": { "string": { "encoding": "UTF8" } }
},
"fqname": "morphir/sdk:string#string"
}
},
"age": {
"Reference": {
"attributes": {
"constraints": { "numeric": { "Unsigned": { "bits": 32 } } }
},
"fqname": "morphir/sdk:uint#uint32"
}
},
"active": {
"Reference": { "fqname": "morphir/sdk:basics#bool" }
}
}
}
}

Example: Variant Mapping

WIT:

variant shape {
circle(f64),
rectangle(f64, f64),
point,
}

Morphir IR (Custom Type Definition):

{
"CustomTypeDefinition": {
"params": [],
"access": {
"access": "Public",
"value": [
{
"name": "circle",
"args": [["radius", { "Reference": { "fqname": "morphir/sdk:basics#float" } }]]
},
{
"name": "rectangle",
"args": [
["width", { "Reference": { "fqname": "morphir/sdk:basics#float" } }],
["height", { "Reference": { "fqname": "morphir/sdk:basics#float" } }]
]
},
{
"name": "point",
"args": []
}
]
}
}
}

Interface and World Mapping

WIT Interface → Morphir Module

Each WIT interface becomes a Morphir module with:

  • Type definitions for all defined types
  • Value specifications for all functions

WIT:

interface calculator {
add: func(a: s32, b: s32) -> s32;
subtract: func(a: s32, b: s32) -> s32;
}

Morphir Module Specification:

{
"ModuleSpecification": {
"types": {},
"values": {
"add": {
"ValueSpecification": {
"inputs": {
"a": { "Reference": { "fqname": "morphir/sdk:int#int32" } },
"b": { "Reference": { "fqname": "morphir/sdk:int#int32" } }
},
"output": { "Reference": { "fqname": "morphir/sdk:int#int32" } }
}
},
"subtract": {
"ValueSpecification": {
"inputs": {
"a": { "Reference": { "fqname": "morphir/sdk:int#int32" } },
"b": { "Reference": { "fqname": "morphir/sdk:int#int32" } }
},
"output": { "Reference": { "fqname": "morphir/sdk:int#int32" } }
}
}
}
}
}

WIT World → Morphir Package

A WIT world defines imports and exports. This maps to:

  • Imports: Module specifications (contracts to be fulfilled by external components)
  • Exports: Module definitions (implementations provided by this component)

WIT:

world my-component {
import logging;
export calculator;
}

Morphir Package Structure:

my-component/
├── imports/
│ └── Logging.morphir # Module specification only
└── exports/
└── Calculator.morphir # Module definition (implementation)

Implementation

WIT AST Types

// WIT AST representation
enum WitType:
case Bool
case S8, S16, S32, S64
case U8, U16, U32, U64
case F32, F64
case Char
case String
case List(elem: WitType)
case Option(inner: WitType)
case Result(ok: Option[WitType], err: Option[WitType])
case Tuple(elems: List[WitType])
case Record(fields: List[WitField])
case Variant(cases: List[WitCase])
case Enum(cases: List[String])
case Flags(flags: List[String])
case Own(resource: String)
case Borrow(resource: String)
case Named(name: String)

case class WitField(name: String, typ: WitType)
case class WitCase(name: String, payload: Option[WitType])

case class WitFunction(
name: String,
params: List[(String, WitType)],
results: WitResults,
)

enum WitResults:
case Named(results: List[(String, WitType)])
case Anon(typ: WitType)
case Empty

case class WitInterface(
name: String,
types: Map[String, WitTypeDef],
functions: List[WitFunction],
resources: List[WitResource],
)

case class WitTypeDef(
name: String,
typ: WitType,
)

case class WitResource(
name: String,
methods: List[WitFunction],
)

case class WitWorld(
name: String,
imports: List[WitWorldItem],
exports: List[WitWorldItem],
)

enum WitWorldItem:
case Interface(name: String)
case Function(func: WitFunction)
case Type(typedef: WitTypeDef)

Type Mapping Implementation

class WitToMorphirMapper(packageName: PackageName):

def mapType(wit: WitType): Type =
wit match
case WitType.Bool =>
Type.Reference(
TypeAttributes.empty,
FQName.sdk("Basics", "Bool"),
List.empty,
)

case WitType.S8 =>
Type.Reference(
TypeAttributes(constraints = Some(TypeConstraints(
numeric = Some(NumericConstraint.Signed(IntWidth.I8))
))),
FQName.sdk("Int", "Int8"),
List.empty,
)

case WitType.S16 =>
Type.Reference(
TypeAttributes(constraints = Some(TypeConstraints(
numeric = Some(NumericConstraint.Signed(IntWidth.I16))
))),
FQName.sdk("Int", "Int16"),
List.empty,
)

case WitType.S32 =>
Type.Reference(
TypeAttributes(constraints = Some(TypeConstraints(
numeric = Some(NumericConstraint.Signed(IntWidth.I32))
))),
FQName.sdk("Int", "Int32"),
List.empty,
)

case WitType.S64 =>
Type.Reference(
TypeAttributes(constraints = Some(TypeConstraints(
numeric = Some(NumericConstraint.Signed(IntWidth.I64))
))),
FQName.sdk("Int", "Int64"),
List.empty,
)

case WitType.U8 =>
Type.Reference(
TypeAttributes(constraints = Some(TypeConstraints(
numeric = Some(NumericConstraint.Unsigned(IntWidth.I8))
))),
FQName.sdk("UInt", "UInt8"),
List.empty,
)

// ... similar for U16, U32, U64

case WitType.F32 =>
Type.Reference(
TypeAttributes(constraints = Some(TypeConstraints(
numeric = Some(NumericConstraint.FloatingPoint(FloatWidth.F32))
))),
FQName.sdk("Float", "Float32"),
List.empty,
)

case WitType.F64 =>
Type.Reference(
TypeAttributes(constraints = Some(TypeConstraints(
numeric = Some(NumericConstraint.FloatingPoint(FloatWidth.F64))
))),
FQName.sdk("Basics", "Float"),
List.empty,
)

case WitType.String =>
Type.Reference(
TypeAttributes(constraints = Some(TypeConstraints(
string = Some(StringConstraint(encoding = Some(StringEncoding.UTF8)))
))),
FQName.sdk("String", "String"),
List.empty,
)

case WitType.Char =>
Type.Reference(
TypeAttributes.empty,
FQName.sdk("Char", "Char"),
List.empty,
)

case WitType.List(elem) =>
Type.Reference(
TypeAttributes.empty,
FQName.sdk("List", "List"),
List(mapType(elem)),
)

case WitType.Option(inner) =>
Type.Reference(
TypeAttributes.empty,
FQName.sdk("Maybe", "Maybe"),
List(mapType(inner)),
)

case WitType.Result(ok, err) =>
Type.Reference(
TypeAttributes.empty,
FQName.sdk("Result", "Result"),
List(
err.map(mapType).getOrElse(Type.Unit(TypeAttributes.empty)),
ok.map(mapType).getOrElse(Type.Unit(TypeAttributes.empty)),
),
)

case WitType.Tuple(elems) =>
Type.Tuple(
TypeAttributes.empty,
elems.map(mapType),
)

case WitType.Record(fields) =>
Type.Record(
TypeAttributes.empty,
fields.map(f => Field(Name(f.name), mapType(f.typ))),
)

case WitType.Named(name) =>
Type.Reference(
TypeAttributes.empty,
FQName(packageName, ModuleName.empty, Name(name)),
List.empty,
)

case WitType.Variant(cases) =>
// Variants become references to generated custom types
throw new UnsupportedOperationException(
"Variants must be processed as type definitions, not inline"
)

def mapFunction(func: WitFunction): ValueSpecification =
val inputs = func.params.map { (name, typ) =>
(Name(name), mapType(typ))
}

val output = func.results match
case WitResults.Empty =>
Type.Unit(TypeAttributes.empty)
case WitResults.Anon(typ) =>
mapType(typ)
case WitResults.Named(results) if results.size == 1 =>
mapType(results.head._2)
case WitResults.Named(results) =>
Type.Record(
TypeAttributes.empty,
results.map((n, t) => Field(Name(n), mapType(t))),
)

ValueSpecification(inputs, output)

def mapInterface(iface: WitInterface): ModuleSpecification =
val types = iface.types.map { (name, typedef) =>
(Name(name), mapTypeDefinition(typedef))
}

val values = iface.functions.map { func =>
(Name(func.name), mapFunction(func))
}.toMap

ModuleSpecification(types, values)

Resource Handling

WIT resources map to Morphir's opaque types with associated functions:

WIT:

resource file-handle {
constructor(path: string);
read: func(size: u32) -> list<u8>;
write: func(data: list<u8>) -> result<u32, string>;
close: func();
}

Morphir IR:

// Opaque type for the resource
OpaqueTypeSpecification(params = List.empty)

// Constructor as function
ValueSpecification(
inputs = List(("path", StringType)),
output = FileHandleType,
)

// Methods as functions taking resource as first arg
ValueSpecification(
inputs = List(("self", FileHandleType), ("size", UInt32Type)),
output = ListType(UInt8Type),
)

Resources are marked in extensions to indicate ownership semantics:

{
"Reference": {
"attributes": {
"extensions": {
"morphir/wasm:resource#ownership": {
"Literal": { "literal": { "StringLiteral": { "value": "own" } } }
}
}
},
"fqname": "my-package:files#file-handle"
}
}

Configuration

[frontend.wit]
# Package naming
package_prefix = "wit-imports"

# Type mapping preferences
use_sdk_types = true # Use Morphir.SDK types vs custom
generate_constraints = true # Add numeric/string constraints

# Module organization
flatten_interfaces = false # Put all interfaces in one module
resource_style = "methods" # "methods" | "functions"

# Validation
strict_mode = true # Error on unsupported features

Open Questions

  1. Resource Lifecycle: How should Morphir handle resource ownership (own vs borrow)?

  2. Async Functions: WIT supports async functions. How should these map to Morphir?

  3. Flags Type: Should WIT flags become a record of bools or a custom set type?

  4. Package Versioning: How to handle WIT package versions in Morphir FQNames?