WebAssembly
WebAssembly support in kit spans the whole tree but is organized around one
shared in-memory model, the WasmModule. A single binary/text/validation layer
under src/wasm/ owns all format mechanics; three independent consumers sit on
top of it — a frontend that lowers Wasm into native code, a backend that emits
Wasm from kit's codegen API, and an object backend that reads/writes .wasm
containers. This document describes that layering, the data flow through it, and
why the seams sit where they do. See FRONTENDS.md,
ARCH.md, and OBJ.md for the surfaces this hangs off.
The two directions
kit treats Wasm as both an input language and an output target, and the same module model serves both:
Wasm as input. A
.wasmor.watfile is a frontend source language (KIT_LANG_WASM).lang/wasmdecodes/validates it and lowers it through the publicKitCgAPI onto a native target (aa64/x64/rv64), where it compiles and JITs/links like any other frontend. The module's runtime state — memory, globals, tables, imports — is reified as an explicit instance struct.Wasm as target. C or toy is compiled to a Wasm module via
KIT_ARCH_WASM/KIT_OBJ_WASM. The backend insrc/arch/wasmis a codegen target that records into IR and replays into a private Wasm emitter, producing a tool-conventions-shaped.wasmfile.
Neither direction builds a private reader or writer: both go through the one
src/wasm layer. That non-duplication is the central design constraint.
src/wasm/ (format mechanics + WasmModule model)
decode.c encode.c wat.c validate.c insn.c module.c
|
boundary header: src/wasm/wasm.h
/ | \
lang/wasm/cg.c src/arch/wasm/* src/obj/wasm/{emit,read}.c
(Wasm -> native CG) (CG -> Wasm) (.wasm container <-> ObjBuilder)
The shared model and the boundary header
src/wasm/wasm.h is the contract that decouples format mechanics from the three
consumers. It declares the in-memory WasmModule (types, funcs, memories,
tables, globals, element/data segments, exports, custom sections, start
function, plus a WasmFeatureSet bitfield) and the small leaf types it is built
from (WasmFuncType, WasmFunc, WasmInsn, WasmValType, the
WasmInsnKind enum of decoded opcodes). It also declares the entry points the
consumers call: wasm_decode_binary, wasm_parse_wat, wasm_validate,
wasm_encode, wasm_emit_cg, plus the per-section builder helpers
(wasm_add_func, wasm_intern_func_type, wasm_func_add_insn, ...) and the
instruction-classification helpers (wasm_insn_is_load, wasm_atomic_rmw_op,
wasm_conversion_kind, ...).
Three design choices keep this boundary clean:
One model represents every shape of module. The same
WasmModuleholds a fully-decoded executable module, a WAT-parsed module, and a backend-synthesized module. Imports are modelled inline on each kind (WasmFunc.is_import,WasmMemory.is_import, ...) rather than in a separate import vector, so a synthesized module and a decoded one look identical to a consumer. Decoded instruction bodies live in the per-functioninsnsvector; decode/validate populate it, the backend builds it, and encode flushes it.The header uses public
Kit*aliases on purpose. It includes only the publickit/cg.h,kit/compile.h,kit/core.h,kit/frontend.hheaders, not libkit internals. That lets every Wasm caller in the tree — includinglang/wasm, which is a frontend and must stay on public APIs — share the model without pulling in compiler internals. All allocation hangs off the module'sKitHeap*(wasm_realloc/wasm_strdup); there is no global decode state, per the project's no-global-state rule.Diagnostics route through the compiler.
wasm_errorforwards tokit_frontend_vfatal, so a malformed module or an unsupported feature is a clean front-end diagnostic with a source location rather than a crash. Every unsupported feature is diagnosed explicitly rather than silently dropped.
Format mechanics (src/wasm/)
This layer knows the wire format and the type system; it knows nothing about who is calling it.
Binary decode (decode.c). A bounds-checked BinReader cursor walks the
sectioned binary: magic/version, then size-prefixed sections in order. LEB128 is
decoded with explicit overflow guards — bin_uleb/bin_uleb64 reject
over-long encodings, and the signed reader accumulates in uint64_t and casts
at the end to avoid shifting into the sign bit. wasm_is_binary sniffs the four
magic bytes \0asm; this is what kit_detect_fmt (src/api/object_detect.c)
keys on to classify link inputs as KIT_BIN_WASM. A second entry point,
wasm_decode_one_insn, decodes a single instruction into a caller-owned scratch
module — the disassembler uses it so the opcode mapping has exactly one source
of truth.
Text parse (wat.c). A from-scratch S-expression tokenizer + parser for the
accepted WAT subset: modules, funcs, params/results/locals, folded and flat
expressions, $name and numeric index references, type definitions, memories,
data/element segments, globals, exports, imports, comments, and the full literal
grammar (signed/hex/underscored integers with boundary diagnostics, f32/f64
floats, string and byte escapes). It lowers into the same WasmModule as the
binary decoder and then runs the same validation, so text is a developer/test
convenience, not a parallel semantic path. wasm_parse_wat_body parses a bare
instruction sequence into a caller-supplied function — the backend's inline-asm
path reuses it.
Validation (validate.c). A typed operand-stack + control-stack validator.
wasm_validate checks module-level invariants (index spaces, start-function
signature, section consistency) then validates each function body via
wasm_validate_func, which tracks the value stack and a control-frame stack
with block result types and unreachable-after-branch handling. Validation runs
before any lowering: a malformed module never reaches CG emission or encoding.
wasm_validate_func is exposed separately so callers that synthesize scratch
functions (the Wasm-target inline-asm path) can validate them in isolation.
Encode (encode.c). The inverse of decode: writes magic/version and the
section sequence with LEB128 immediates, through the public KitWriter. This
is the single writer used by both kit_wasm_wat_to_wasm and the
object-backend emit_wasm.
Model + helpers (module.c, insn.c). module.c owns construction and
teardown of WasmModule and its heap-grown sub-vectors. insn.c is the
shared classification table: predicates over WasmInsnKind (is-load,
is-store, the atomic families), the mnemonic table, feature gating
(wasm_feature_enabled, wasm_require_feature), and the maps from opcode kind
to CG-level operations that the frontend lowering consumes.
Frontend: lang/wasm (Wasm → native CG)
lang/wasm is a normal kit frontend (kit_wasm_frontend_vtable, registered
for .wat/.wasm; the driver also accepts -x wasm/-x wat and the dbg
:language switch). It compiles a Wasm module's semantics onto a native
target. This is distinct from read_wasm, which treats a .wasm file as a
link-time object; the frontend rejects relocatable objects (a linking custom
section) as input, telling the user to supply them as objects instead.
lang/wasm/wasm.c is thin: decode-or-parse (binary vs WAT by magic), validate,
then hand the module to wasm_emit_cg in cg.c. The interesting work is the
whole-module-to-native lowering in lang/wasm/cg.c.
Instance model. A Wasm module instance has state — linear memories, mutable
globals, tables, imported functions, and the ability to trap. kit reifies that
state as a generated per-module KitWasmInstance record (built field-by-field
in wasm_cg_build_runtime): a KitWasmMemory per memory, a function-pointer
slot per import, a func-ref entry per defined function, a slot per global, table
storage, and passive data/elem segment slots. The runtime ABI types
(KitWasmMemory, KitWasmTable, the passive-segment slots) live in
lang/wasm/runtime_abi.h, not in the public API.
Every lowered function — including direct calls, call_indirect arms, the start
dispatcher, and the generated __kit_wasm_init — receives a hidden
KitWasmInstance*. There is no standalone-export fast path: module state is
always explicit, and an unused instance pointer is left for later optimization
to remove. Imported functions are indirect calls through the instance's import
slots. Memory accesses read the active memory base/size from the instance and
bounds-check before the native load/store. Traps call non-returning runtime
helpers (__kit_wasm_trap_*, one per WasmTrapKind: unreachable, division,
invalid-conversion, bounds, table, signature).
Coverage. The MVP numeric/control/memory core, plus mutable globals, imported
function declarations, active tables/elements for call_indirect, start
functions, growable single-memory state, bulk-memory ops
(memory.copy/fill/init, data.drop, the table equivalents) with
bounds-check prologues, and non-trapping float-to-int conversion. Bulk ops and
non-trapping conversions are gated behind their WasmFeatureSet bits.
Host imports. When a frontend-lowered module declares imports, an embedder
binds them by name through the public API in include/kit/wasm.h
(kit_wasm_set_host_imports with a static table and/or a dynamic resolver).
The lowered image carries three readonly metadata symbols —
__kit_wasm_imports, __kit_wasm_nimports, __kit_wasm_types (wire
format in runtime_abi.h). The runtime binder in lang/wasm/host_imports.c
walks that metadata, looks up each (module, field) pair, validates the bound
function's signature against the module's recorded WasmFuncType, and writes the
pointer into the instance's import slot. Unbound imports trap on first call. The
binder deliberately mirrors the raw WasmValType byte encoding rather than
including the internal src/wasm header, keeping the public runtime free of
compiler internals.
Backend: src/arch/wasm (CG → Wasm)
The Wasm backend is a codegen target with an unusual shape: it is a CGBackend
without the native parts of ArchImpl. arch_impl_wasm (in arch.c)
registers cgtarget_new and a disassembler (wasm_disasm_new, which renders the
code section as WAT for objdump) but leaves asm_new, link, the label-fixup
hook, and all register-file hooks NULL — there is no machine code, no native
assembler, and no native image layout for wasm32. It also installs
clang-compatible predefined macros (__wasm__, __wasm32__, __ILP32__, ...).
Record-then-replay. Rather than lower CG operations to Wasm directly, the
backend uses the shared CG IR recorder (CODEGEN.md, IR.md):
wasm_cgtarget_new (in target.c) wraps a private WTarget emitter in a
cg_ir_recorder (src/cg/ir_recorder). The frontend's CG calls are first
recorded into a CgIrModule; at finalize, wasm_emit_ir_module (ir_emit.c)
replays that IR into the WTarget. The replay drives a second, private IR — the
WIR list defined in internal.h — one record per emitted operation, kept
separate from the final WasmFunc body. This two-stage buffering is what makes
deferred, whole-function structuring possible: the WIR can be reordered and
rewritten before it is linearized into the structured Wasm a function body
requires.
The recorder config also carries the backend's diagnose-before-emit policy: it
opts out of local-static-data emission, supplies the &&label-in-static-data
diagnostic, and reports why a tail call is unrealizable (e.g. a variadic callee
whose vararg buffer lives in a torn-down frame).
WIR and the structurer. Each WTarget SSA Reg becomes a Wasm local,
materialized by local.get/local.set. Control flow is the hard problem: Wasm
has no arbitrary jumps, only structured block/loop/if with relative branch
depths. CG scopes (SCOPE_LOOP/SCOPE_BLOCK/SCOPE_IF) map directly, but
frontends also emit free labels and goto. structure.c's wasm_structurize
runs over the recorded WIR before linearization and rewrites it so every
reachable free label becomes the break target of a synthetic forward
SCOPE_BLOCK or the continue target of a backward SCOPE_LOOP; jumps then
resolve through the ordinary scope-bound branch machinery. A
unroll_switch_islands pass reorders the frontend's switch shape
(jump-to-dispatch / case bodies / dispatch / selector / SWITCH) so the selector
and switch precede the case bodies, turning case labels into uniform forward
references. Irreducible control flow and unliftable cross-scope labels are
diagnosed, not miscompiled.
ABI (abi.c). wasm32_vtable implements the tool-conventions BasicCABI for
wasm32 (ILP32): void ignored; scalars ≤ 8 bytes direct as i32/i64/f32/f64;
pointers as a 4-byte int part; empty structs ignored; a singleton-scalar struct
passed as that scalar; all other aggregates and arrays passed indirectly
(sret/byval). __int128 and binary128 long double have no Wasm representation
and panic at classification with a specific message rather than routing silently
through memory. Aggregates use a downward-growing linear-memory frame through a
__stack_pointer global; byval params arrive as i32 pointers and are copied into
a callee-isolated frame buffer; varargs are caller-packed into a uniform-slot
linear-memory buffer with a hidden trailing i32 pointer.
Emit (emit.c). The largest piece, and the WIR→WasmFunc linearizer. It
allocates Wasm locals, interns function types (wasm_intern_func_type), lays out
the linear-memory data image compactly (SF_ALLOC sections assigned aligned bases
from a low null guard upward, with symbol-address fixups patched once every
section size is known), builds the func-ref table for address-taken functions,
and emits the structured body. It also lowers atomics through the wasm-threads
opcodes (promoting the single memory to shared on first use), compiler
intrinsics (clz/ctz/popcount/bswap, checked-overflow arithmetic, memcpy/memset
via memory.copy/fill), and the standard-runtime conventions: linear memory
exported as "memory", functions exported by user name with no synthesized
_start, and undefined function symbols promoted to (import "env" "<sym>" ...)
honoring __attribute__((import_module/import_name)) overrides (the C frontend
records those into a side table under OBJ_EXT_WASM_IMPORTS, read back here).
The accumulating WasmModule is attached to the ObjBuilder under
OBJ_EXT_WASM for the object backend to flush.
Object backend: src/obj/wasm (minimal)
The object backend is intentionally small and one-directional today. emit.c's
emit_wasm reads the WasmModule the codegen backend attached under
OBJ_EXT_WASM and flushes it through the shared wasm_encode (or, when no module
is attached, writes a bare magic+version header). It does not synthesize a
relocatable object: emit_wasm produces a single-TU final module with no
linking/reloc.* custom sections and no separate object-metadata structure.
read.c's read_wasm is the reverse glue used by the linker/objdump path
(LINK.md): it mirrors each binary section into a format-neutral
ObjBuilder section carrying the original payload bytes (so objdump -h/-s
show the real container, the code section marked SF_EXEC so -d disassembles
it as WAT), and adds one function symbol per defined function (named from the
name section, an export, or a synthesized placeholder, with the symbol value the
byte offset of the body's locals vector so disassembly lines up). It decodes the
full module only to recover names; container metadata beyond raw bytes is not
otherwise interpreted. These hooks plug into ObjFormatImpl in
src/obj/registry.c under KIT_OBJ_WASM / KIT_BIN_WASM. The reserved
Wasm relocation kinds in obj/obj.h (R_WASM_FUNCIDX, R_WASM_TABLEIDX,
R_WASM_MEMOFS, R_WASM_TYPEIDX) and the custom-section semantic tag
SSEM_WASM_CUSTOM are the format-neutral vocabulary the generic object/link
registry holds for Wasm, so relocatable-object support can grow on this layer
without disturbing the format-neutral core.
Cross-tree boundary, restated
The whole arrangement holds together because each tier depends only downward,
through src/wasm/wasm.h:
src/wasmknows the format and the type system, nothing about callers.lang/wasmconsumes the model to read Wasm into native code; it stays on public APIs and owns the instance/runtime contract.src/arch/wasmproduces the model from CG; it owns the WIR, the structurer, the wasm32 ABI, and the linear-memory/import conventions.src/obj/wasmadapts the model to/from the format-neutralObjBuilderand the generic object/link registry.
Module metadata that does not belong in any one tier rides on the shared
ObjBuilder via typed extension payloads (OBJ_EXT_WASM for the module itself,
OBJ_EXT_WASM_IMPORTS for import-attribute overrides), keeping the
format-neutral object core free of Wasm specifics.