# Phase 0 findings — toolchain & runtime spike

- **Status: EXIT CRITERION PASSED** (2026-06-12, on k-uce)
- **Runtime selected: Wasmtime** (v45.0.1, C API). WAMR rejected — evidence below.
- Exceptions decision (§11.1, error codes / `-fno-exceptions`) **confirmed viable**:
  both the stubs and two real generated units compile with `-fno-exceptions`,
  no try/catch blocker anywhere.

The exit criterion ran end-to-end: a core stub (libc/libc++ statically linked,
owns memory/allocator) and a unit stub (PIC, `dylink.0`) were linked **at
runtime** by `loader.cpp` and produced:

```
hello from unit; unit-data-segment-ok; counter=7; mapsum=3; core-string+unit[cb:42][got-func-ok][fn:42]
core_counter (in linear memory): before=7 after=8
PHASE0 EXIT CRITERION: PASS
```

which validates, in one render call: unit data-segment relocation
(`__memory_base`), GOT.mem read **and write** of a core global, `std::string`
/`std::map` in unit code on the core's heap, a heap C++ object created in core
and mutated/read by the unit, function pointers crossing unit→core→unit
through the shared table, GOT.func resolution, and a `std::function` lambda
allocated in the unit and invoked by core. That is the §3.4 contract
("DValue inside the workspace: no serialization, ever") demonstrated at the
ABI level.

## Toolchain pins

| What | Version | Where on k-uce |
|---|---|---|
| wasi-sdk | 33 (clang 22.1.0-wasi-sdk) | `/opt/wasi-sdk` |
| target triple | `wasm32-wasip1` (`wasm32-wasi` is deprecated) | — |
| Wasmtime C API | v45.0.1 (prebuilt x86_64-linux release) | `/opt/wasmtime` |
| WAMR (rejected) | WAMR-2.4.4, built from source | `/opt/wamr` |
| cmake / ninja | 3.31.6 / 1.12.1 (apt) | — |

## Runtime selection: why not WAMR

WAMR was tried first per §9 ("preferred ... use Wasmtime only if blocked").
We are blocked, on the load-bearing requirement itself:

1. **WAMR's wasm-c-api ignores imported memories and tables.** At unit
   instantiation it logs `"doesn't support import memories and tables for
   now, ignore them"` (`wasm_c_api.c`) and gives the instance its own
   memory/table — which silently destroys the shared-workspace model.
2. **Host-side `wasm_table_grow` / `wasm_memory_grow` are explicitly
   unsupported** ("Only allow growing a table via the opcode table.grow").
3. Its build banner lists *Import/Export of Mutable Globals* as unsupported —
   the dylink ABI imports `__stack_pointer` and every `GOT.*` entry as a
   mutable global.

Its multi-module feature is name-based auto-resolution, not host-orchestrated
dylink (no host-computed `__memory_base`/`__table_base`, no GOT). Making WAMR
fit means implementing import binding through the c-api layer and runtime
internals — a runtime-development project, not a patch.

**Wasmtime v45.0.1 passed everything on the first run** through the standard
`wasm.h` C API: host-created funcref table shared by both instances, exported
memory imported by the unit, host-created (mutable) globals, cross-instance
export→import wiring. The remaining §9 criteria also favor it: AOT artifacts
(`.cwasm` precompilation) for the unit cache, epoch interruption for CPU
limits, and built-in copy-on-write memory-image instantiation for the §6 core
snapshot (machinery we'd have had to build ourselves on WAMR).

Trade-off accepted (was already in §10): Rust codebase, heavier to
vendor/patch. Pin the release artifact (lib + headers, checksummed) the way
sqlite is vendored; building from source stays possible but is not the
default path. The C API .so is ~27 MB.

## Module build recipe (what `build_modules.sh` settled on)

Core (non-PIC reactor, owns libc/libc++/allocator):
```
clang++ --target=wasm32-wasip1 -mexec-model=reactor -O1 -fno-exceptions \
    core.cpp -o core.wasm \
    -Wl,--export-all -Wl,--import-table \
    -Wl,--export=__stack_pointer -Wl,--export=__heap_base \
    -Wl,--undefined=_ZTVN10__cxxabiv117__class_type_infoE
```

Unit (PIC side module):
```
clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default \
    -fvisibility-inlines-hidden -O1 -fno-exceptions -c unit.cpp
wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \
    --Bsymbolic unit.o -o unit.wasm --export=<entry>
```

Hard-won flag findings:

1. **`--unresolved-symbols=import-dynamic`** is required for the side-module
   link; undefined symbols then become `env.*` function imports and `GOT.*`
   globals exactly per the Emscripten dylink ABI.
2. **`-fvisibility-inlines-hidden` is mandatory.** Without it one libc++
   vague-linkage lambda (`std::map` tree-emplace internals, missing libc++'s
   usual hide-from-ABI attribute) is emitted as *both* an export and an
   import of the unit — a self-import the loader cannot satisfy at
   instantiation time without lazy-binding trampolines. `--Bsymbolic` alone
   did **not** bind it.
3. **Core symbol closure**: `--export-all` only exports what got *linked*.
   The unit needed `__cxxabiv1::__class_type_info`'s vtable (RTTI machinery
   behind `std::function`), which the core never references — forced in with
   `--undefined=`. The production core needs a closure strategy:
   `--whole-archive` for libc/libc++/libc++abi, or a curated keep-list. The
   loader also implements the complementary fallback (resolve `GOT.mem` of
   weak data from the unit's *own* exports post-instantiation, patching the
   provisional GOT global).
4. **`--import-table` on the core** + a **host-created table** is the right
   shape (see loader notes); `--export-table`/`--growable-table` was the
   first attempt and died on WAMR's host-grow limitation, but host-created
   stays the better design under Wasmtime too: the loader picks table size
   (core's declared minimum + headroom) before any instantiation.
5. `wasm32-wasi` triple is deprecated in wasi-sdk 33 → use `wasm32-wasip1`.

## Loader notes (`loader.cpp`, ~450 lines, standard wasm-c-api)

Sequence proven: instantiate core (45 WASI imports satisfied with named trap
stubs — none was ever called) → parse `dylink.0` (`mem_info`: memsize/align,
tablesize) → `__memory_base` = call core's exported `malloc` → `__table_base`
= bump pointer starting at core's table-import minimum → build the unit's
import vector (memory/table/`__stack_pointer` shared from core; `env.*`
functions from core exports; `GOT.mem.*` as host mutable i32 globals holding
addresses read from core's exported data-symbol globals) → instantiate →
patch deferred GOT entries → `__wasm_apply_data_relocs` → `__wasm_call_ctors`
→ call the entry export.

- **Erratum (found in Phase 3): self-resolved `GOT.mem` values must add
  `__memory_base`.** A PIC module's exported data symbols are i32 globals
  holding offsets *relative to its `__memory_base`*, not absolute addresses —
  the linker adds the base when patching deferred GOT entries (there is no
  `__wasm_apply_global_relocs` export to do it). Copying the export verbatim
  reads/writes core memory at low addresses and renders silently wrong values;
  the Phase 3 fixture's `self-got`/`callback` markers exist to catch exactly
  this. `GOT.mem` entries resolved from the *core's* exports are absolute
  already (the core is non-PIC) and need no adjustment.
- **GOT.func is resolved guest-side**: the core exports a helper returning
  `(intptr_t)&func` — taking the address forces a link-time elem entry and a
  wasm function pointer *is* its table index. No host-side funcref injection
  is needed at all (it was WAMR-unsupported; under Wasmtime it would work but
  the guest-side registry is simpler and runtime-agnostic). The production
  core should carry a name→funcptr registry (dlsym-shaped) for its API
  surface.
- **wasi-libc gotcha**: `_initialize` has a double-init guard ending in
  `__builtin_trap()`. WAMR runs `_initialize` automatically at instantiation
  (so calling it again traps "unreachable"); Wasmtime does not (so you must
  call it). Cost one debugging round; recorded here so it never costs another.
- Export-name `wasm_name_t` may include the trailing NUL in `size` (WAMR
  did); trim when indexing exports by name.

## Real generated units (delegated grind — full log in `realunit-report.md`)

`site/demo/collections.uce.cpp` and `hello.uce.cpp` (taken verbatim from the
live unit cache at `/tmp/uce/work/...`) both compile and link as PIC side
modules with `dylink.0`, **no allocator definitions**, with only shim-level
intervention. `collections.wasm`: 42 KB, 52 imports — including exactly the
predicted `GOT.mem.context` for the global `Request*`. Friction points that
become Phase 2 work items:

1. **`types.h` defines global `operator new/delete` in every unit** — must be
   gated (`#ifdef`) out of side-module builds; allocator ownership belongs to
   the core (§3.2: "the one fatal misconfiguration").
2. **`sys.h` includes `<signal.h>`** → wasi needs `-D_WASI_EMULATED_SIGNAL`
   (+ `-lwasi-emulated-signal` in the core) or an `#ifdef __wasm__` carve-out;
   signals/fork/exec/sockets in `sys.h` have no wasi equivalent and move
   behind hostcalls anyway (§5.1).
3. **Generated units include `uce_lib.h` by absolute path** — the
   preprocessor should emit a logical include so the wasm build can supply
   its own include order.
4. **Header-inline connector wrappers (MySQL etc.) get pulled into every
   unit** regardless of use; the §3.3 membrane split (thin wasm-side shim,
   host-side implementation) resolves this and shrinks unit import lists.
5. The real-unit compile used the pre-`-fvisibility-inlines-hidden` flag set
   (parallel work); the final unit flag set above should be used from
   Phase 2 on.

## Implications for the next phases

- **Phase 1 (DValue C ABI, native)**: unaffected by any of this; proceed as
  written.
- **Phase 2 (core module + membrane)**: add the closure strategy
  (whole-archive), the GOT.func name→funcptr registry, the `types.h`
  allocator gate, the signal emulation define, and the preprocessor include
  change. Compile `uce_lib` with the core recipe above.
- **Phase 3 (loader)**: `loader.cpp` here is the skeleton — dylink parsing,
  base allocation, GOT resolution, and init sequencing are all proven; what
  remains is the registry/dispatch layer, ABI stamping, and multi-unit
  placement.
- **Phase 4**: use Wasmtime's epoch interruption for CPU limits and its
  memory-image/CoW instantiation for the core snapshot rather than building
  either by hand.

## Artifacts (on k-uce, not in git)

- `/tmp/uce/wasm-phase0/{core.wasm,unit.wasm,loader}` — exit-criterion run
- `/tmp/uce/wasm-phase0/realunit/` — real-unit compiles + shim tree + inspector dumps
- `/opt/wasi-sdk`, `/opt/wasmtime`, `/opt/wamr` — toolchains/runtimes
