Research: Lowering Datafun to WebAssembly Components

Research date: 2026-01-08

1. WebAssembly Component Model Status (2026)

1.1 Current State

The WebAssembly Component Model is now mature and widely adopted:

The component model solves cross-language interoperability by defining:

1.2 Key Features

WIT Interface Definition Language:

package datalove:datafun@0.1.0;

interface types {
    record point { x: s32, y: s32 }
    variant result-val { ok(string), err(string) }
    type my-list = list<u32>;
}

world script-world {
    import wasi:io/streams;
    export run: func() -> result<string, string>;
}

Canonical ABI Representation:

Datafun Type WIT Type Linear Memory ABI
bool bool i32 (0 or 1)
u8-u64 u8-u64 i32/i64
i8-i64 s8-s64 i32/i64
f32 f32 f32
int (bigint) list<u8> ptr + len (limbs)
string string ptr + len (UTF-8)
List<T> list<T> ptr + len
Option<T> option<T> discriminant + payload
Result<T> result<T, string> discriminant + ok/err
tuple tuple flattened fields
struct record flattened fields
enum variant discriminant + payload

Memory Allocation:

1.3 Tooling Ecosystem

Production-ready:

Runtimes:

1.4 WASIp3 Async Model

WASI 0.3 introduces:

This is relevant for datafun if we want to support async I/O in the future.

2. Datafun Compiler Architecture

See compiler-guide.md: the module pipeline, the IR (SSA values plus mutable slots, block parameters rather than phis), and the execution backends. A Wasm backend would sit beside the Cranelift AOT and C backends, consume the same IR, and call the runtime through the same dtlv_rti_* C ABI that datalove-rt exports.

3. Lowering Strategy for WebAssembly Components

3.1 Architecture Options

Option A: Generate Core Wasm + Wrap to Component

IR -> Wasm core module -> wasm-tools component new -> Component

Option B: Direct Component Generation

IR -> Component binary (with canonical ABI)

Recommended: Option A - Follows established ecosystem patterns.

3.2 New Crate Structure

crates/
  datalove-datafun-aot-wasm/      # New crate
    src/
      lib.rs                       # WasmCompiler entry point
      codegen/                     # IR -> Wasm translation
        mod.rs
        expr.rs                    # Expression codegen
        stmt.rs                    # Statement codegen
        func.rs                    # Function codegen
        types.rs                   # IrType -> Wasm type mapping
      layout.rs                    # Linear memory layout
      runtime.rs                   # Runtime import declarations
      wit/                         # WIT interface generation
        mod.rs
        types.rs                   # IrType -> WIT type
        interface.rs               # Module interface generation

3.3 Type Mapping

IrType Wasm Core WIT ABI
Unit (none) (empty tuple) -
Bool i32 bool 0/1
U8-U32 i32 u8-u32 zero-extended
U64 i64 u64 direct
I8-I32 i32 s8-s32 sign-extended
I64 i64 s64 direct
F32 f32 f32 direct
F64 f64 f64 direct
Int i32,i32 list<u8> ptr+len (limbs)
String i32,i32 string ptr+len (UTF-8)
List<T> i32,i32 list<T> ptr+len
Set<T> i32,i32 list<T> ptr+len (sorted)
Map<K,V> i32,i32 list<tuple<K,V>> ptr+len
Option<T> i32,T* option<T> discriminant+payload
Result<T> i32,T*,i32,i32 result<T,string> disc+ok+err_ptr+len
Tuple(fields) flattened tuple<...> flattened
Struct(fields) flattened record flattened
Enum(variants) i32,max_payload variant disc+max_payload

Flattening Rules:

3.4 Runtime Strategy

Option 1: Embed Runtime in Component

Option 2: Import Runtime from Host

Option 3: Minimal Wasm Runtime

Recommended: Option 1 initially, with Option 2 as optimization path.

3.5 Memory Layout

Linear memory organization:

0x0000 - 0x0FFF: Reserved (null page)
0x1000 - heap_base: Static data (TyDescs, string literals)
heap_base - ...: Dynamic heap (managed by allocator)

Stack frame layout (same as Cranelift backend):

3.6 Implementation Phases

Phase 1: Core Wasm Generation

Phase 2: Runtime Integration

Phase 3: Full Type Support

Phase 4: Component Wrapping

Phase 5: Testing Infrastructure

3.7 WIT Generation Example

Given a datafun module:

fun add(a: u32, b: u32): u32
  ret a + b
end fun

fun greet(name: string): string
  ret "Hello, " + name
end fun

Generate WIT:

package datalove:example@0.1.0;

interface example {
    add: func(a: u32, b: u32) -> u32;
    greet: func(name: string) -> string;
}

world example-world {
    export example;
}

3.8 Codegen Sketch

// In datalove-datafun-aot-wasm/src/codegen/func.rs

impl FunctionCompiler {
    fn compile_instruction(&mut self, inst: &Instruction) {
        match inst {
            Instruction::Const { dest, value } => {
                let wasm_val = self.const_to_wasm(value);
                self.set_local(*dest, wasm_val);
            }
            Instruction::BinOp { dest, op, lhs, rhs } => {
                let l = self.get_operand(lhs);
                let r = self.get_operand(rhs);
                let result = match op {
                    BinOp::Add => self.builder.i32_add(l, r),
                    BinOp::Sub => self.builder.i32_sub(l, r),
                    // ...
                };
                self.set_local(*dest, result);
            }
            Instruction::Call { dest, func, args } => {
                let func_idx = self.resolve_func(func);
                let wasm_args: Vec<_> = args.iter()
                    .map(|a| self.get_operand(a))
                    .collect();
                let result = self.builder.call(func_idx, &wasm_args);
                self.set_local(*dest, result);
            }
            // ...
        }
    }
}

3.9 Challenges and Considerations

Linear Memory Management:

Bigint Support:

String Encoding:

Error Handling:

Testing Strategy:

3.10 The Boundary: Ownership, Collections, Bigints

Parameter modes map onto WIT ownership:

Mode WIT
in (move) T, ownership transfers
out return value
ref borrow<T>
mut no equivalent yet; WIT has no mutable borrow

Collections cross one of two ways. Copied, a [T] becomes list<T>, a set a sorted list<T> and a map a list<tuple<K, V>>: simple, but every crossing copies. Or kept in their runtime representation and exposed as resource handles, which shares memory and preserves identity under mutation:

resource list<T> {
    push: func(elem: T);
    get: func(idx: u32) -> option<T>;
    len: func() -> u32;
}

int (arbitrary precision) has no WIT counterpart. The options are an opaque resource int, a record int { limbs: list<u32>, negative: bool }, or a base-10 string. The table in 3.3 assumes limbs.

Open questions:

4. References