

# WebAssembly Runtime (`std::wasm`)

`std::wasm` is the standard library surface for **executing WebAssembly (WASM)
modules** from Silk.

This module is about *running* wasm. It is not the Silk compiler’s wasm output
backend (see [backend wasm](/silk/docs/compiler/backend-wasm/)).

## Exported API

The public Silk surface is centered around:

- `Engine` — a runtime context and configuration (currently minimal).
- `Module` — a validated module (owned bytes + parsed metadata).
- `Instance` — an instantiated module (module + runtime state).
- `Func` — an exported function handle for calling.
- `Imports` — host-provided imports used during instantiation.
- linear memory access via `Instance.memory_bytes()` (returns `None` when the module has no memory; otherwise `Some(ByteSlice)`, possibly length 0).
- `Val` / `ValType` — a small tagged value representation for wasm values
 (currently used for results).

## Host Imports
`Module.instantiate_with_imports(imports: Imports)` uses `Imports` to resolve
wasm imports (functions, globals, memory, table).

### Imported Functions

Imported functions are dispatched through a single host callback:

- `Imports.func_call: HostCall`

`HostCall` uses a scalar-only calling convention so it can be stored as a
first-class value and passed around in Silk currently:

```silk
type fn HostCall = fn (import_index: i64,
                    args_ptr: u64, args_len: i64,
                    mem_ptr: u64,  mem_len: i64,
                    out_bits_ptr: u64) -> int;
```

Semantics:

- `import_index` is 0-based in the order declared by the wasm module.
- `(args_ptr, args_len)` describes a `u64` slice of raw argument bits (i32 uses
 the low 32 bits; f32 also uses the low 32 bits).
- `(mem_ptr, mem_len)` describes the instance linear memory as a raw byte view
 (or `(0, 0)` when the module has no memory).
- When the imported function returns `i32`, `i64`, `f32`, or `f64`, the host
 writes the raw result bits to `out_bits_ptr` (at offset 0, as a `u64`).
 - `i32`/`f32` use the low 32 bits.
 - `i64`/`f64` use the full 64 bits.
- The return value is 0 on success; non-zero values are treated as
 `WASMError.code` (with `offset = -1`).

### Named Imported Functions

For embedders that want **named** host functions (instead of a single
index-based dispatcher), `std::wasm` also provides a helper that links imported
functions by `(module_name, import_name)` using [`std::map`](/silk/wiki/std/map/):

- `export struct ImportFuncName { module_name: string, name: string }`
- `export fn hash_import_func_name (k: ImportFuncName) -> u64;`
- `export fn eq_import_func_name (a: ImportFuncName, b: ImportFuncName) -> bool;`
- `Module.instantiate_with_named_func_imports(imports: Imports, func_imports: &std::map::HashMap(ImportFuncName, HostCall))`

Semantics:

- Every imported function in the module must have a corresponding entry in
 `func_imports`, otherwise instantiation fails with `LinkError`.
- Extra entries in `func_imports` are ignored.
- The linked callbacks are stored in a per-instance dispatch table so the map
 itself does not need to outlive instantiation.

Example:

```silk
import std::map;
import std::wasm;

type ImportMap = std::map::HashMap(std::wasm::ImportFuncName, std::wasm::HostCall);

fn main () -> int {
  match (ImportMap.init(8, std::wasm::hash_import_func_name, std::wasm::eq_import_func_name)) {
    Ok(map) => {
      let mut m: ImportMap = map;

      // Link the wasm import `(import "env" "add1" ...)` to a host callback.
      let _ = m.put(std::wasm::ImportFuncName{ module_name: "env", name: "add1" }, host_call);

      let inst_r = module.instantiate_with_named_func_imports(imports, &m);
      m.drop();
      // ...
      return 0;
    },
    Err(_) => {
      return 1;
    },
  }
}
```

### Imported Globals

Imported globals are provided as raw bits:

- `Imports.global_bits: Slice(u64)`

Semantics:

- The slice length must equal the number of imported globals in the module.
- Globals are ordered by the wasm module’s import order (0-based).
- For imported `i32` globals, only the low 32 bits are used.
- For imported `i64` globals, the full 64 bits are used.
- For imported `f32` globals, only the low 32 bits are used (raw IEEE bits).
- For imported `f64` globals, the full 64 bits are used (raw IEEE bits).

### Imported Memory/Table

`Imports` can configure the initial sizes of imported memory/table:

- `Imports.memory_pages: i64` (for imported memory)
- `Imports.table_size: i64` (for imported table)

Semantics:

- `-1` means “use the module minimum”.
- Any other value must satisfy the module’s declared limits.

## Error Model

`std::wasm` uses a single typed error:

- `WASMError { code, offset, requested, trap }`

Where:

- `code` is a stable kind code.
- `offset` is a byte offset into the wasm input when known (otherwise `-1`).
- `requested` is used for allocation failures (otherwise `0`).
- `trap` is a stable trap-kind code when `code == Trap` (otherwise `0`).

`WASMError.kind()` and `WASMError.message()` provide a semantic view over the
stable integer codes.

## Supported Features
The implementation is a **pure Silk interpreter** intended to be:

- self-contained (no external dependencies),
- correct for the WebAssembly 1.0 baseline semantics it supports,
- explicit about unsupported extensions (returns `Unsupported`).

### Targets

- Only **wasm32** modules are supported.
- The module must be a valid wasm binary module (magic + version 1).

### Sections

Supported:

- custom sections are ignored,
- core sections: `type`, `import`, `function`, `table`, `memory`, `global`,
 `export`, `start`, `element`, `code`, `data`.

Notes / current constraints (baseline):

- At most one table and one memory are supported (baseline constraint).
- `start` is executed automatically during `Module.instantiate` after
 instantiation initialization.
 - the start function must have signature `[] -> []` (no parameters, no results).
- `data_count` and all non-baseline extensions are rejected as `Unsupported`.

### Values and Function Calls

- Supported `ValType`: `I32`, `I64`, `F32`, `F64`.
- Supported function signatures: any number of `i32`/`i64`/`f32`/`f64`
 parameters and 0–1 `i32`/`i64`/`f32`/`f64` results.
- Calls pass arguments as raw bits ([`std::arrays::Slice(u64)`](/silk/docs/std/arrays/)):
 - for `i32`/`f32` parameters, only the low 32 bits are used,
 - for `i64`/`f64` parameters, the full 64 bits are used.
- Calls return 0–1 results as `Val?`.

### Instructions

The interpreter supports a practical wasm32 subset sufficient for “real” wasm
code (including `f32`/`f64`):

- control/parametric/variable: `unreachable`, `nop`, `block`, [`loop`](/silk/wiki/language/flow-loop/), `if`,
 `else`, `end`, `br`, `br_if`, `br_table`, `return`, `call`, `call_indirect`,
 `drop`, `select` (with block results for `i32`/`i64`/`f32`/`f64`)
- memory: all baseline loads/stores (including sign/zero-ext forms), `memory.size`,
 `memory.grow`
- numerics: `i32`/`i64`/`f32`/`f64` operators and conversions (including
 float↔int conversions and bit reinterpret ops).
 - Float→int truncation traps on `NaN` and out-of-range inputs
 (`WASMTrapKind::InvalidConversionToInteger`).

Unsupported opcodes/extensions are rejected as `Unsupported` with an `offset`
pointing at the opcode.

## Ownership Rules

- A `Module` owns the wasm bytes and parsed metadata.
- `Module.drop()` delegates destruction of its owned byte buffer to
 `BufferU8.drop()` and then releases the remaining metadata exactly once.
- An `Instance` owns its own runtime state and takes ownership of the module’s
 owned allocations during instantiation.
 - `Module.instantiate(mut self: &Module)` consumes the module by moving its
 owned allocations into the returned `Instance`.
 - after a successful call, the original `Module` is left in an empty, inert
 state.
 - if instantiation fails (including start traps), no instance is produced and
 the original `Module` remains intact.
- `Func` is a lightweight view into an `Instance`, and memory is accessed by
 calling `Instance.memory_bytes()` when present.

## Example

```silk
import std::wasm;
import std::arrays;
import std::buffer;

using Engine = std::wasm::Engine;
using EngineResult = std::wasm::EngineResult;
using Module = std::wasm::Module;
using ModuleResult = std::wasm::ModuleResult;
using Instance = std::wasm::Instance;
using InstanceResult = std::wasm::InstanceResult;
using Func = std::wasm::Func;
using Val = std::wasm::Val;
using CallResult = std::wasm::CallResult;
using ByteSlice = std::arrays::ByteSlice;
using BufferU8 = std::buffer::BufferU8;
using U64Slice = std::arrays::Slice(u64);

fn main () -> int {

  // Minimal wasm module:
  // (module
  //   (func (export "answer") (result i32)
  //     i32.const 7))
  // Note: `u8[N]` arrays use the scalar-slot memory model (not packed bytes).
  // Build a packed `ByteSlice` via `BufferU8` when supplying raw wasm bytes.
  let wasm_bytes: u8[39] = [
    0, 97, 115, 109, 1, 0, 0, 0,
    1, 5, 1, 96, 0, 1, 127,
    3, 2, 1, 0,
    7, 10, 1, 6, 97, 110, 115, 119, 101, 114, 0, 0,
    10, 6, 1, 4, 0, 65, 7, 11
  ];

  match (Engine.init_default()) {
    Ok(engine) => {
      match (BufferU8.init(39)) {
        Ok(buffer) => {
          let mut buf: BufferU8 = buffer;
          var i: i64 = 0;
          while i < 39 {
            let push_err = buf.push(wasm_bytes[i]);
            if push_err != None {
              buf.drop();
              return 2;
            }
            i = i + 1;
          }
          let bytes: ByteSlice = buf.as_bytes();

          match (engine.compile(bytes)) {
            Ok(module) => {
              let mut m: Module = module;
              buf.drop();
              match (m.instantiate()) {
                Ok(instance) => {
                  let mut inst: Instance = instance;
                  let f_opt: Func? = inst.export_func("answer");
                  if f_opt == None { return 4; }
                  let f: Func = f_opt ?? Func{ index: 0 };

                  let args: U64Slice = { ptr: 0, len: 0 };
                  match (inst.call(f, args)) {
                    Ok(out_opt) => {
                      if out_opt == None { return 6; }
                      let out: Val = out_opt ?? Val.i32(0);

                      let got_opt: i32? = out.as_i32();
                      if got_opt == None { return 7; }
                      if (got_opt ?? 0) != 7 { return 8; }

                      return 0;
                    },
                    Err(_) => {
                      return 5;
                    },
                  }
                },
                Err(_) => {
                  return 3;
                },
              }
            },
            Err(_) => {
              buf.drop();
              return 2;
            },
          }
        },
        Err(_) => {
          return 2;
        },
      }
    },
    Err(_) => {
      return 1;
    },
  }
}
```

## Considerations
- A stable, ergonomic host import resolver API (beyond the baseline `Imports` list).
- WASI bindings and host library shims.
- Post-baseline proposals: bulk memory, reference types, SIMD (`v128`), threads, multi-value.
- A native runtime/JIT implementation for performance.

## Design goals

- Provide a portable API for:
 - loading/validating wasm bytes,
 - instantiating modules (including imports),
 - calling exported functions,
 - interacting with exported linear memory.
- Keep the module swappable, like the rest of `std::`: embedders may provide an
 alternate implementation (for example a wasm engine binding) while preserving
 the same public API.
- Record a clear path to a full native runtime and JIT in later phases (no
 dependency on external language toolchains).
