logicaffeine_compile/vm/wasm/mod.rs
1//! WebAssembly code generation from VM bytecode — one shared codegen, two consumers.
2//!
3//! WASM has only *structured* control flow, so the register-machine bytecode (`&[Op]`, with
4//! absolute jumps) is lowered through a basic-block dispatch loop (a `loop` of `block`s with a
5//! `br_table` on a "next block" local). That lowering (`cfg`) and the hand-rolled binary
6//! encoder (`encode`) are the hard, validated core; both of the following build on it:
7//!
8//! - `region_jit` — the WS6 browser **JIT tier**: emits one WebAssembly module per hot
9//! *function* and runs it on `wasmi` (native) or the host's real `WebAssembly` (wasm32).
10//! Gated behind the `wasm-jit` feature.
11//! - the direct **AOT backend** (`module`) — `assemble_program` (and its source-level wrapper
12//! [`crate::compile::compile_to_wasm`]): emits one self-contained `.wasm` for a *whole program*
13//! with NO rustc/cargo/wasm-bindgen/linker. Feature-independent (needs only
14//! `encode`/`cfg`/`kind`, not `wasmi`/`js_sys`); exposed today as a library API.
15//!
16//! # The AOT backend
17//!
18//! ## Value model (`kind`)
19//!
20//! The bytecode's arithmetic and `Show` are runtime-polymorphic (one `Op::Add` adds Ints or
21//! Floats), so a standalone module must know each register's *static* kind. A dataflow fixpoint
22//! infers a `kind::Kind` per register, each mapping to exactly one wasm value type:
23//!
24//! - **Scalars, by value:** `Int`/`Bool`/`Moment` → `i64`, `Float` → `f64`, `Date` → `i32`.
25//! - **Heap values, an `i32` handle into linear memory:** `Text`, `Struct`, `Map`, `Set`, `Enum`,
26//! `Closure`, `Tuple` (heterogeneous), and the sequences `SeqInt`/`SeqFloat`/`SeqText`/
27//! `SeqStruct`/`SeqEnum`/`SeqSeqInt` (plus `SeqAny`, a not-yet-refined empty sequence).
28//!
29//! `Int` and `Bool` share `i64`, so a register reused across them coalesces. A register reused
30//! across kinds of *different* value types in disjoint live ranges (e.g. an `Enum` loop variable
31//! whose slot a later `Int` reuses) is `Unsupported` — a sound refusal (never a miscompile),
32//! pending register-live-range splitting.
33//!
34//! ## Heap value model (self-contained, no linker)
35//!
36//! When a program uses a heap op the module gets a linear memory + a bump-allocator pointer global
37//! `__heap_ptr` (scalar-only modules stay memory-free). Every heap value is a **stable `i32`
38//! handle** so a realloc-on-grow never invalidates the register holding it:
39//!
40//! - A **sequence** or **Set** is a 16-byte header `[len:i32][cap:i32][data_ptr:i32]` over a
41//! separate buffer of 8-byte slots — a scalar element by value, a handle element (Text / Struct /
42//! Enum / inner sequence) in the slot's low word. Indexing is 1-based and bounds-checked (trap on
43//! OOB); `Push`/`Add` reallocate the buffer and rewrite the header in place. A `Seq of Struct`
44//! flows the element's field layout to an extracted element so `(item N of xs)'s field` resolves.
45//! - A **Map** is the same header over 16-byte `[key][value]` entries — Int **or** Text keys,
46//! Int/Float/Bool values — looked up by linear scan, order-independent so byte-identical to the
47//! VM's hashmap for get/insert/contains/length.
48//! - A **Struct** is a header over 8-byte field slots (field names are compile-time only; a
49//! `struct_layout` analysis maps each field to its slot + kind). A **Tuple** is the same, by
50//! position. An **Enum** is `[tag:i32][arg slots…]` (the tag is the constructor's constant index,
51//! so an `i32.eq` on tags is the VM's constructor-name compare; payload args follow). A
52//! **Closure** is `[func_idx:i32][captured values…]`, invoked by `call_indirect` through the
53//! module's function table; captures (locals and snapshotted globals) pass as trailing parameters.
54//!
55//! `Show` of a scalar, a Text, a sequence of scalars/Text, or a `Set` (all insertion-ordered, so
56//! deterministic) is byte-identical to the VM; whole-`Map`/whole-`Struct` display (hashmap/field
57//! order is non-reproducible against the AOT's insertion order) is the deferred remainder.
58//!
59//! ## Host interface
60//!
61//! Raw `env.*` imports (no wasm-bindgen — runs under `wasmi`/`wasmtime`/a browser shim): the
62//! `print_*` sinks (the host formats to match the tree-walker's `to_display_string`, reading
63//! sequences / Sets / Text out of the exported memory), `pow_ff`/`pow_fi` (exact `powf`/`powi`),
64//! `today`/`now` (honoring the test fixed clock), and `fmt_*_into` (interpolating scalar operands
65//! into a module buffer). A runtime error — out-of-bounds index, undefined variable, an overflow
66//! the oracle could not rule out — lowers to a wasm `unreachable` trap (the standalone module has
67//! no VM to surface the message), a contract the lock proves as tw-error ⟺ wasm-trap.
68//!
69//! ## The lock (`tests/wasm_aot_lock.rs`)
70//!
71//! WASM == VM == Tree-walker, mirroring the Futamura locks. An EXHAUSTIVE `op_support(&Op)` match
72//! (compiler-enforced: a new VM op fails to build until classified Supported/Deferred) plus a
73//! behavioural biconditional (a program compiles IFF every op is Supported, then runs
74//! byte-identically) plus a full-language coverage proof (every Supported instruction is exercised
75//! end-to-end over the curated corpus). Goal: the Deferred set shrinks to ∅.
76
77pub mod encode;
78pub mod func;
79
80/// Shared control-flow lowering (basic blocks + the `block`/`loop`/`br_table` dispatch loop)
81/// used by both `func` (the JIT region emitter) and the AOT whole-program emitter.
82mod cfg;
83
84/// Static per-register kind inference (Int/Bool/Float/…) for the AOT backend — the bytecode's
85/// arithmetic/`Show` ops are runtime-polymorphic, so a standalone module needs the static
86/// types the JIT tier gets for free from its all-Int runtime gate.
87mod kind;
88
89/// The direct AOT backend: a whole program → one self-contained `.wasm` module.
90mod module;
91
92/// Register live-range splitting: the structural pre-pass that gives a VM register reused across
93/// disjoint live ranges of different wasm value types one local per range (so kind inference, which
94/// soundly refuses a single conflicting local, succeeds). Identity on programs already accepted.
95mod regsplit;
96
97/// The P2 linker: emit LLVM-compatible RELOCATABLE wasm objects (linking + `reloc.CODE` sections)
98/// that call the prebuilt Rust runtime by undefined `logos_rt_*` symbol, and link them with the Rust
99/// toolchain's `rust-lld` into one module with one shared linear memory + one allocator. This is how
100/// features the emitted wasm can't self-contain (BigInt, the real collection/transport runtime) work
101/// Logos→native-wasm without a second, divergent implementation. Gated behind `wasm-jit` while the
102/// only consumer is its own `wasmi` link smoke test; a later slice wires it into `compile_to_wasm` and
103/// drops the gate + the `dead_code` allowance (emit/link are feature-independent, only running needs
104/// `wasmi`).
105#[cfg(feature = "wasm-jit")]
106#[allow(dead_code)]
107mod link;
108
109/// The relocatable-object transform (S4): rewrite a finished self-contained module's `call` targets to
110/// relocatable 5-byte LEBs + append the `linking`/`reloc.CODE` sections, so [`link`] can link the real
111/// Rust runtime into a real Logos program. A total-or-refuse decoder keeps it sound (converts what it
112/// understands, declines otherwise — never miscompiles). Gated like `link` until wired into
113/// `compile_to_wasm`.
114#[cfg(feature = "wasm-jit")]
115#[allow(dead_code)]
116mod reloc;
117
118/// The WS6 browser WASM-JIT tier (region/function emit + host instantiation). Only the
119/// *running* of emitted modules touches `wasmi`/`js_sys`, so this — and only this — is gated
120/// behind the `wasm-jit` feature; `encode`/`func` (pure byte emission) are always built.
121#[cfg(feature = "wasm-jit")]
122pub mod region_jit;
123
124pub use module::assemble_program;
125
126/// The linker-tier entry points ([`crate::compile::compile_to_wasm_linked`]): the BigInt-aware emitter,
127/// the relocatable transform, and the `rust-lld` link against the real `logicaffeine_base` runtime.
128pub(crate) use module::assemble_program_linked;
129#[cfg(feature = "wasm-jit")]
130pub(crate) use reloc::module_to_relocatable;
131#[cfg(feature = "wasm-jit")]
132pub(crate) use link::link_relocatable_bigint;
133
134/// Why the AOT WebAssembly backend declined to lower a program. The backend is *total* on its
135/// supported fragment and rejects everything else explicitly (never miscompiles): the corpus
136/// ratchet asserts each program is either lowered-and-correct or on the allowed-unsupported
137/// list, so a program silently leaving the fragment fails a test rather than emitting wrong code.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum WasmLowerError {
140 /// An op, type, or shape outside the current phase's supported fragment (the `&str` names
141 /// what is not yet handled, e.g. `"collection op"`, `"non-scalar parameter"`).
142 Unsupported(&'static str),
143}
144
145impl std::fmt::Display for WasmLowerError {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 WasmLowerError::Unsupported(what) => {
149 write!(f, "wasm AOT backend: unsupported {what}")
150 }
151 }
152 }
153}
154
155impl std::error::Error for WasmLowerError {}