Skip to main content

logicaffeine_compile/vm/
instruction.rs

1//! Bytecode instruction set and the compiled-program container.
2//!
3//! Registers are per-frame `u16` indices (the compiler assigns locals to
4//! registers at compile time; frames are capped at `MAX_REGISTERS_PER_FRAME`).
5//! Jump targets are absolute instruction indices. This is the growing core of
6//! work/VM_PLAN.md's 87-opcode set.
7
8use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11
12use crate::intern::Symbol;
13
14/// Serialize an interned [`Symbol`] as its `u32` index (HOTSWAP §P12). Sound for the
15/// tier cache because the cache key pins the exact source — re-parsing it reproduces
16/// the same interning order, so the index round-trips to the same symbol.
17pub(crate) mod symbol_serde {
18    use crate::intern::Symbol;
19    use serde::{Deserialize, Deserializer, Serializer};
20    pub fn serialize<S: Serializer>(s: &Symbol, ser: S) -> Result<S::Ok, S::Error> {
21        ser.serialize_u32(s.index() as u32)
22    }
23    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Symbol, D::Error> {
24        Ok(Symbol::from_index(u32::deserialize(de)? as usize))
25    }
26}
27
28/// Serialize an `f64` by its raw bits so the cache round-trips bit-exactly (a text
29/// format would otherwise mangle NaN / ±∞ and risk precision drift).
30pub(crate) mod f64_bits {
31    use serde::{Deserialize, Deserializer, Serializer};
32    pub fn serialize<S: Serializer>(v: &f64, ser: S) -> Result<S::Ok, S::Error> {
33        ser.serialize_u64(v.to_bits())
34    }
35    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<f64, D::Error> {
36        Ok(f64::from_bits(u64::deserialize(de)?))
37    }
38}
39
40pub type Reg = u16;
41pub type ConstIdx = u32;
42pub type FuncIdx = u16;
43
44/// A constant-pool entry.
45#[derive(Clone, Debug, Serialize, Deserialize)]
46pub enum Constant {
47    Int(i64),
48    Float(#[serde(with = "f64_bits")] f64),
49    Bool(bool),
50    Text(String),
51    Char(char),
52    Nothing,
53    Duration(i64),
54    Date(i32),
55    Moment(i64),
56    Span { months: i32, days: i32 },
57    Time(i64),
58}
59
60/// A bytecode instruction. Every field is a small `Copy` scalar (registers,
61/// constant-pool indices, interned symbols), so the dispatch loop reads each
62/// op by value instead of `clone()`-ing through the `Clone` machinery.
63/// The declared element type of a `Pipe of T`, carried on [`Op::ChanNew`] so a consumer that
64/// models a channel as a typed FIFO queue (the direct-WASM AOT) can type a `Receive`/`select`
65/// arm's bound variable even when the pipe is never sent to (an empty `Pipe of Text` in a
66/// timeout-only `select`). A `Copy` tag — the scalar element types cover every pipe the corpus
67/// declares; a struct/enum element resolves to [`ChanElem::Unknown`] (falls back to the untyped
68/// queue, exactly as before).
69#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
70pub enum ChanElem {
71    Unknown,
72    Int,
73    Float,
74    Bool,
75    Text,
76}
77
78impl ChanElem {
79    /// Resolve a pipe's declared element type name (`Pipe of <name>`) to its tag.
80    pub fn from_type_name(name: &str) -> ChanElem {
81        match name {
82            "Int" | "Nat" => ChanElem::Int,
83            "Float" => ChanElem::Float,
84            "Bool" => ChanElem::Bool,
85            "Text" => ChanElem::Text,
86            _ => ChanElem::Unknown,
87        }
88    }
89}
90
91#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
92pub enum Op {
93    /// `R[dst] = constants[idx]`
94    LoadConst { dst: Reg, idx: ConstIdx },
95    /// `R[dst] = R[src]` (shallow clone)
96    Move { dst: Reg, src: Reg },
97    /// Copy-on-write barrier: if `R[reg]`'s collection is shared (`Rc` strong > 1),
98    /// deep-clone it in place so `R[reg]` becomes the sole owner. Emitted before an
99    /// argument is passed to a function whose corresponding parameter is an inferred
100    /// MUTABLE BORROW (mutated in place and returned): the callee's element writes
101    /// then land on a buffer no OTHER live handle can observe, so an aliasing caller
102    /// (`Let y be arr; Set arr to f(arr)`) still sees value semantics. A no-op when
103    /// already uniquely owned (the common `Set x to f(x)` consume-reassign), so the
104    /// hot path pays only a strong-count check. Mirrors the AOT's call-site `.cow()`.
105    EnsureOwned { reg: Reg },
106
107    Add { dst: Reg, lhs: Reg, rhs: Reg },
108    /// `R[dst] = R[dst] + R[src]` — the `Set x to x + …` shape. Semantically
109    /// identical to `Add { dst, lhs: dst, rhs: src }`; the dedicated form lets
110    /// the VM append in place when `R[dst]` is a sole-owner Text (turning the
111    /// O(n²) build-a-string-by-concatenation loop into amortized O(n)).
112    AddAssign { dst: Reg, src: Reg },
113    Sub { dst: Reg, lhs: Reg, rhs: Reg },
114    Mul { dst: Reg, lhs: Reg, rhs: Reg },
115    Div { dst: Reg, lhs: Reg, rhs: Reg },
116    /// EXACT division (`7 / 2 → 7/2`, a Rational), the type-directed sibling of
117    /// [`Op::Div`] — emitted for `BinaryOpKind::ExactDivide`.
118    ExactDiv { dst: Reg, lhs: Reg, rhs: Reg },
119    /// FLOOR division (`-7 // 2 → -4`, toward negative infinity) — emitted for
120    /// `BinaryOpKind::FloorDivide`, distinct from the truncating [`Op::Div`].
121    FloorDiv { dst: Reg, lhs: Reg, rhs: Reg },
122    Mod { dst: Reg, lhs: Reg, rhs: Reg },
123    /// `dst = lhs / 2^k` (signed, round toward zero) — emitted only when the
124    /// divisor is a literal power of two AND the Oracle proved `lhs` is `Int`.
125    /// A single op (the JIT lowers it to the side-exit-free `divpow2` shift
126    /// stencil) so it fires for loop-invariant divisors the in-region JIT
127    /// detector misses, without the scratch-register pressure of an expansion.
128    DivPow2 { dst: Reg, lhs: Reg, k: u8 },
129    /// `dst = lhs / c` (`mul_back == 0`) or `dst = lhs % c` (`mul_back == c`),
130    /// where `c` is a compile-time-constant divisor that is NOT a power of two
131    /// (W5/`DivPow2` handles powers of two), computed by the Granlund–Montgomery
132    /// / libdivide UNSIGNED magic-reciprocal sequence (a `mul`-high + shift,
133    /// ~3 cycles) instead of `idiv` (~25 cycles). `magic`/`more` are the
134    /// precomputed constants (the exact [`logicaffeine_data::LogosDivU64`]
135    /// encoding — low 6 bits of `more` are the shift, `0x40` is the 65-bit
136    /// add-marker path). Emitted ONLY when the Oracle proves `lhs` is `Int` and
137    /// NON-NEGATIVE: the unsigned magic equals the signed truncating `/`/`%`
138    /// only for a non-negative dividend (for `x < 0` the signs disagree, exactly
139    /// as for the `% 2^k → &` rewrite). The remainder is derived as
140    /// `lhs - q*c` (wrapping), bit-exact with the kernel's `wrapping_rem` for
141    /// non-negative `lhs`.
142    MagicDivU { dst: Reg, lhs: Reg, magic: u64, more: u8, mul_back: i64 },
143
144    Lt { dst: Reg, lhs: Reg, rhs: Reg },
145    Gt { dst: Reg, lhs: Reg, rhs: Reg },
146    LtEq { dst: Reg, lhs: Reg, rhs: Reg },
147    GtEq { dst: Reg, lhs: Reg, rhs: Reg },
148    Eq { dst: Reg, lhs: Reg, rhs: Reg },
149    /// Tolerant float comparison (`a is approximately b`) — the shared
150    /// isclose semantics (`logicaffeine_data::ops::logos_approx_eq`);
151    /// `==`/`Eq` stays IEEE bit-exact.
152    ApproxEq { dst: Reg, lhs: Reg, rhs: Reg },
153    NotEq { dst: Reg, lhs: Reg, rhs: Reg },
154
155    Not { dst: Reg, src: Reg },
156
157    Concat { dst: Reg, lhs: Reg, rhs: Reg },
158    /// `a followed by b` — merge two sequences into one.
159    SeqConcat { dst: Reg, lhs: Reg, rhs: Reg },
160    /// `a ** b` — exponentiation. Integer power is exact (promotes to BigInt on
161    /// overflow); a Float operand uses `powf`; a negative Int exponent errors.
162    Pow { dst: Reg, lhs: Reg, rhs: Reg },
163    BitXor { dst: Reg, lhs: Reg, rhs: Reg },
164    /// `a & b` — bitwise AND on Int, logical on Bool, intersection on Sets.
165    BitAnd { dst: Reg, lhs: Reg, rhs: Reg },
166    /// `a | b` (see `BitAnd`) — union on Sets.
167    BitOr { dst: Reg, lhs: Reg, rhs: Reg },
168    Shl { dst: Reg, lhs: Reg, rhs: Reg },
169    Shr { dst: Reg, lhs: Reg, rhs: Reg },
170
171    /// Unconditional jump to absolute instruction index.
172    Jump { target: usize },
173    /// Jump if `R[cond]` is falsey.
174    JumpIfFalse { cond: Reg, target: usize },
175    /// Jump if `R[cond]` is truthy.
176    JumpIfTrue { cond: Reg, target: usize },
177
178    /// Call a user function. The caller has placed `arg_count` arguments in
179    /// consecutive registers starting at `args_start` (relative to the caller's
180    /// frame base). The result is written to `R[dst]`. Uses register windowing.
181    Call { dst: Reg, func: FuncIdx, args_start: Reg, arg_count: u16 },
182    /// Call a kernel builtin with already-evaluated arguments (arity was
183    /// validated at compile time, mirroring the tree-walker's
184    /// arity-before-evaluation rule).
185    CallBuiltin {
186        dst: Reg,
187        builtin: crate::semantics::builtins::BuiltinId,
188        args_start: Reg,
189        arg_count: u16,
190    },
191    /// Call the closure value in `R[callee]`. `name_for_err` is the function
192    /// name when this came from a by-name call (`Unknown function: f` when the
193    /// value is not callable) or `u32::MAX` for a call-by-expression
194    /// (`Cannot call value of type T`).
195    CallValue { dst: Reg, callee: Reg, args_start: Reg, arg_count: u16, name_for_err: ConstIdx },
196    /// Build a closure over `program.functions[func]`: its `captures` list is
197    /// snapshotted — local captures deep-cloned from the register window at
198    /// `locals_start`, global captures from the globals table (skipped when
199    /// still undefined; the body then falls through to the live global).
200    MakeClosure { dst: Reg, func: FuncIdx, locals_start: Reg },
201
202    /// `Check subject can/is predicate (of object)` — kernel policy check.
203    /// `object == Reg::MAX` means no object.
204    CheckPolicy {
205        subject: Reg,
206        #[serde(with = "symbol_serde")]
207        predicate: Symbol,
208        is_capability: bool,
209        object: Reg,
210        source_text: ConstIdx,
211    },
212
213    /// `Push value to obj's field` — kernel push into a struct's List field.
214    /// `field` is a Text constant (the resolved field name).
215    ListPushField { obj: Reg, field: ConstIdx, src: Reg },
216
217    // ---- Globals (Main top-level bindings visible inside functions) ----
218    /// `R[dst] = globals[idx]`, error "Undefined variable: {name}" when unset.
219    GlobalGet { dst: Reg, idx: u16 },
220    /// `globals[idx] = R[src]` (defines or overwrites).
221    GlobalSet { idx: u16, src: Reg },
222    /// Return `R[src]` from the current function.
223    Return { src: Reg },
224    /// Return `nothing` from the current function.
225    ReturnNothing,
226
227    // ---- Collections ----
228    /// `R[dst] = [R[start], …, R[start+count-1]]` (a new list).
229    NewList { dst: Reg, start: Reg, count: u16 },
230    NewEmptyList { dst: Reg },
231    /// `R[dst] = a new half-width (`Vec<i32>`) Int sequence` — emitted (behind
232    /// `LOGOS_NARROW_VM`) for a `new Seq of Int` declaration the narrowing
233    /// proof certified fits `i32`. Observably identical to `NewEmptyList`; only
234    /// the storage width differs (see [`crate::interpreter::ListRepr::IntsI32`]).
235    NewEmptyListI32 { dst: Reg },
236    NewEmptySet { dst: Reg },
237    NewEmptyMap { dst: Reg },
238    /// `R[dst] = [R[start]..=R[end]]` (inclusive integer range as a list).
239    NewRange { dst: Reg, start: Reg, end: Reg },
240    /// Append `R[value]` to the list in `R[list]` (mutates in place).
241    ListPush { list: Reg, value: Reg },
242    /// Add `R[value]` to the set in `R[set]` (no-op if already present).
243    SetAdd { set: Reg, value: Reg },
244    /// Remove `R[value]` from the set/map in `R[collection]`.
245    RemoveFrom { collection: Reg, value: Reg },
246    /// `R[collection][R[index]] = R[value]` (1-based list set, or map insert).
247    SetIndex { collection: Reg, index: Reg, value: Reg },
248    /// Like `SetIndex` but the Oracle PROVED the index in `[1, length]`
249    /// (range analysis, M9) — bounds-check elimination for the STORE. The
250    /// interpreter still checks (free defense-in-depth); the JIT lowers it to
251    /// an UNCHECKED array store (no bounds branch). Only listy collections
252    /// with a stable length earn this, via `index_provably_in_bounds`.
253    SetIndexUnchecked { collection: Reg, index: Reg, value: Reg },
254    /// `R[dst] = R[collection][R[index]]` (1-based for ordered collections).
255    Index { dst: Reg, collection: Reg, index: Reg },
256    /// Like `Index` but the Oracle (range analysis, M9) PROVED the index
257    /// in `[1, length]` at this point — bounds-check elimination, the V8/LLVM
258    /// way. The bytecode interpreter still checks (a sound proof makes the
259    /// check never fire; keeping it is free defense-in-depth), but the JIT
260    /// lowers it to an UNCHECKED array load (no bounds branch, no deopt).
261    /// Only listy collections with a stable length earn this; the compiler
262    /// emits it solely behind `index_provably_in_bounds`.
263    IndexUnchecked { dst: Reg, collection: Reg, index: Reg },
264    /// REGION-ENTRY bounds-check hoist (V8 TurboFan loop bound-check
265    /// elimination). For a loop `while iv </<= bound` reading/writing
266    /// `R[array]` at affine indices, this asserts ONCE — at native region
267    /// entry — that the array is long enough for the whole loop:
268    /// `length(R[array]) >= R[bound] + add_max` and `R[iv] + add_min >= 1`.
269    /// If it holds, the region runs every covered access UNCHECKED; if not,
270    /// the VM declines the region and replays on bytecode (where the accesses
271    /// are checked). A pure no-op in the interpreter and the function tier —
272    /// speculation is region-only, made safe solely by this entry guard.
273    RegionBoundsGuard { array: Reg, bound: Reg, iv: Reg, add_max: i32, add_min: i32 },
274    /// `R[dst] = length of R[collection]`.
275    Length { dst: Reg, collection: Reg },
276    /// `R[dst] = R[collection] contains R[value]`.
277    Contains { dst: Reg, collection: Reg, value: Reg },
278
279    // ---- Strings / slices / tuples / sets / temporal ----
280    /// `R[dst] = Text(debug_prefix? + format(R[src], spec?))` — one
281    /// interpolated-string segment. `u32::MAX` = no spec / no prefix.
282    FormatValue { dst: Reg, src: Reg, spec: ConstIdx, debug_prefix: ConstIdx },
283    /// `R[dst] = R[collection][R[start]..=R[end]]` (1-indexed inclusive).
284    SliceOp { dst: Reg, collection: Reg, start: Reg, end: Reg },
285    /// `R[dst] = deep clone of R[src]`.
286    DeepClone { dst: Reg, src: Reg },
287    /// `R[dst] = (R[start], …, R[start+count-1])` (immutable tuple).
288    NewTuple { dst: Reg, start: Reg, count: u16 },
289    UnionOp { dst: Reg, lhs: Reg, rhs: Reg },
290    IntersectOp { dst: Reg, lhs: Reg, rhs: Reg },
291    /// `R[dst] = today` (Date; honors the test fixed-clock).
292    LoadToday { dst: Reg },
293    /// `R[dst] = now` (Moment; honors the test fixed-clock).
294    LoadNow { dst: Reg },
295
296    // ---- Structs / enums / Inspect / CRDT ----
297    /// `R[dst] = Struct { type_name: constants[type_name], fields: {} }`.
298    NewStruct { dst: Reg, type_name: ConstIdx },
299    /// Insert `R[value]` under field `constants[field]` into the struct in
300    /// `R[obj]` (construction and SetField — structs have VALUE semantics, so
301    /// in-place mutation of the register equals the tree-walker's
302    /// clone-mutate-reassign).
303    StructInsert { obj: Reg, field: ConstIdx, value: Reg },
304    /// `R[dst] = R[obj].field` — "Field '{f}' not found" / "Cannot access
305    /// field on {type}".
306    GetField { dst: Reg, obj: Reg, field: ConstIdx },
307    /// `R[dst] = Inductive { type, constructor, args: R[args_start..+count] }`.
308    NewInductive { dst: Reg, type_name: ConstIdx, ctor: ConstIdx, args_start: Reg, count: u16 },
309    /// `R[dst] = Bool(struct type-name or inductive constructor == constants[variant])`;
310    /// false for any other value.
311    TestArm { dst: Reg, target: Reg, variant: ConstIdx },
312    /// Inspect-arm binding: a Struct target binds `fields[constants[field]]`,
313    /// an Inductive target binds `args[index]`. A missing field/index leaves
314    /// `R[dst]` unwritten (the tree-walker skips the bind — unreachable for
315    /// parsed programs, whose fields always exist after default-fill).
316    BindArm { dst: Reg, target: Reg, field: ConstIdx, index: u16 },
317    /// GCounter/PNCounter bump of `R[obj].field` by `R[amount]` (negated for
318    /// Decrease — also selects the tree-walker's increment/decrement wording).
319    CrdtBump { obj: Reg, field: ConstIdx, amount: Reg, negate: bool },
320    /// GCounter merge: fold every field of `R[source]` into `R[target]`.
321    CrdtMerge { target: Reg, source: Reg },
322    /// `R[dst]` = a fresh, empty rich CRDT — `kind` 0 = SharedSet (OR-Set),
323    /// 1 = SharedSequence (RGA), 2 = Divergent (MV-register). Used to default-fill a
324    /// `Shared` struct's CRDT fields, mirroring the tree-walker's `new`-struct init.
325    NewCrdt { dst: Reg, kind: u8 },
326    /// RGA append: push `R[value]` onto the replicated sequence in `R[seq]` (mutates the
327    /// shared CRDT in place, so a field access propagates).
328    CrdtAppend { seq: Reg, value: Reg },
329    /// Resolve `R[obj].field` to `R[value]`: a real MV-register resolves in place, a plain
330    /// field is overwritten — the same fallback the tree-walker's `Resolve` takes.
331    CrdtResolve { obj: Reg, field: ConstIdx, value: Reg },
332
333    // ---- Repeat (snapshot iteration) ----
334    /// Snapshot `R[iterable]` (List/Set items, Text chars, Map (k,v) tuples)
335    /// and push it onto the iterator stack.
336    IterPrepare { iterable: Reg },
337    /// Load the next snapshot element into `R[dst]` and advance; jump to
338    /// `exit` when exhausted (the iterator stays pushed — `IterPop` at the
339    /// exit point drops it).
340    IterNext { dst: Reg, exit: usize },
341    /// Drop the top iterator.
342    IterPop,
343    /// `R[dst] = R[list].pop()` — Nothing when empty (not an error).
344    ListPop { list: Reg, dst: Reg },
345    /// Sleep for `R[nanos]` (Duration nanos, or Int milliseconds).
346    Sleep { duration: Reg },
347    /// Tuple-pattern binding: `R[start..start+count] = tuple elements` with
348    /// zip semantics (stops at the shorter side, like the tree-walker).
349    /// Errors when `R[src]` is not a Tuple.
350    DestructureTuple { src: Reg, start: Reg, count: u16 },
351
352    /// Emit `R[src].to_display_string()` to the output stream.
353    Show { src: Reg },
354    /// `R[dst] = the program argument vector` as a `Seq of Text` (the
355    /// interpreter's `args()` system native, mirroring the compiled binary's
356    /// `env::args()`: index 0 is the program name). Outside the JIT integer
357    /// subset, so the adapters bail on it and it always runs in the VM.
358    Args { dst: Reg },
359    // ─── Go-like concurrency (Phase 54 / T10) ───────────────────────────────
360    // Args travel through register ranges (`Op` is `Copy`). Channel/task handles
361    // are ordinary `Value`s (`RuntimeValue::Chan` / `::TaskHandle`). Every op that
362    // can block suspends the resumable VM (`run_until_block`) and is serviced by
363    // the deterministic scheduler, exactly as the tree-walker's `yield_request`.
364
365    /// `R[dst] = a new channel`; `cap < 0` ⇒ the scheduler's default capacity. `elem` is the
366    /// declared `Pipe of T` element type (a hint for a typed-queue consumer; the scheduler ignores it).
367    ChanNew { dst: Reg, cap: i32, elem: ChanElem },
368    /// Send `R[val]` into channel `R[chan]` (blocks if the channel is full).
369    ChanSend { chan: Reg, val: Reg },
370    /// `R[dst] = receive from channel R[chan]` (blocks if the channel is empty).
371    ChanRecv { dst: Reg, chan: Reg },
372    /// `R[dst] = bool` — non-blocking send of `R[val]` into `R[chan]`.
373    ChanTrySend { dst: Reg, chan: Reg, val: Reg },
374    /// `R[dst] = received value, or Nothing` — non-blocking receive from `R[chan]`.
375    ChanTryRecv { dst: Reg, chan: Reg },
376    /// Close channel `R[chan]`.
377    ChanClose { chan: Reg },
378    /// Spawn `functions[func]` with args in `R[args_start..+arg_count]` (fire-and-forget).
379    Spawn { func: FuncIdx, args_start: Reg, arg_count: u16 },
380    /// `R[dst] = task handle` of a spawned `functions[func]` (same arg convention).
381    SpawnHandle { dst: Reg, func: FuncIdx, args_start: Reg, arg_count: u16 },
382    /// `R[dst] = result of awaiting task R[handle]` (Nothing if it was aborted).
383    TaskAwait { dst: Reg, handle: Reg },
384    /// Abort task `R[handle]`.
385    TaskAbort { handle: Reg },
386    /// Register a `Receive var from chan` arm for the next `SelectWait`.
387    SelectArmRecv { chan: Reg, var: Reg },
388    /// Register an `After ticks` timeout arm for the next `SelectWait`.
389    SelectArmTimeout { ticks: Reg },
390    /// Block on the registered select arms; `R[dst_arm] = the winning arm index`
391    /// (a recv arm's received value is already in its `var` register).
392    SelectWait { dst_arm: Reg },
393
394    // ─── peer networking over the relay (async-tier). Each suspends the resumable VM via a
395    // `VmBlock::Net*` request the async VM driver services with the shared `NetInbox` — the same
396    // inbox the tree-walker uses, so a `Send`/`Await` runs byte-identically on both tiers. ───
397    /// Dial the relay at `R[url]` (async); resume when connected.
398    NetConnect { url: Reg },
399    /// Subscribe this node's inbox to `R[topic]` (async); resume when subscribed.
400    NetListen { topic: Reg },
401    /// Encode `R[msg]` and publish it to peer `R[to]`.
402    NetSend { to: Reg, msg: Reg },
403    /// Batch-stream the list `R[values]` to peer `R[to]`.
404    NetStream { to: Reg, values: Reg },
405    /// `R[dst] = await a message (or a batch stream, if `stream`) from peer `R\[from\]`` (blocks).
406    NetAwait { dst: Reg, from: Reg, stream: bool },
407    /// `R[dst] = a PeerAgent handle for address `R\[addr\]`` (its canonical relay topic). Pure.
408    NetMakePeer { dst: Reg, addr: Reg },
409    /// CRDT sync point on topic `R[topic]`: publish `R[dst]`'s counter, merge what has arrived,
410    /// and write the merged value back to `R[dst]`.
411    NetSync { dst: Reg, topic: Reg },
412
413    /// Fail with the Text constant at `msg` — used for constructs whose
414    /// tree-walker semantics are "error WHEN EXECUTED" (an unbound `Set`, an
415    /// unsupported statement). Never fails at compile time: dead branches must
416    /// stay free.
417    FailWith { msg: ConstIdx },
418    /// Stop execution.
419    Halt,
420}
421
422/// A parameter's (or struct field's) declared type, RESOLVED at compile time into a self-contained
423/// form a STATICALLY-typed consumer can use without the AST or the interner. The dynamically-typed
424/// tree-walker/VM and the scalar-only native JIT never needed static types, so they were not put in
425/// the bytecode before; they are now a first-class part of the compiled program (a struct is
426/// referenced by name, its layout living in [`CompiledProgram::struct_types`]).
427#[derive(Clone, Debug, PartialEq)]
428pub enum BoundaryType {
429    Int,
430    Float,
431    Bool,
432    Text,
433    Date,
434    Moment,
435    /// A `Word32`/`Word64` wrapping-integer (ℤ/2ⁿ) — a native `i32`/`i64` scalar, so a `Seq of Word32`
436    /// parameter (a crypto state array) resolves its element kind cross-region.
437    Word32,
438    Word64,
439    /// `Seq of <elem>`.
440    Seq(Box<BoundaryType>),
441    /// A user struct, by name — its field layout is in [`CompiledProgram::struct_types`].
442    Struct(String),
443    /// A user enum (sum type), by name. Carried as one i32 handle whose word 0 is the constructor
444    /// tag; `Inspect`/`TestArm` read the tag directly, so no separate layout is needed.
445    Enum(String),
446    /// `Map of <key> to <value>` — one i32 handle. The key kind is recovered at each access from the
447    /// key expression's own kind (no map metadata needed); the VALUE kind is carried here so a
448    /// parameter map's `item k of m` resolves its result kind cross-region.
449    Map(Box<BoundaryType>, Box<BoundaryType>),
450    /// A HETEROGENEOUS tuple (`Pair`/`Triple` of mixed element types) — one i32 handle to a buffer of
451    /// 8-byte slots, each holding its element at its own kind. The per-position element types are
452    /// carried so a `item N of t` (constant `N`) resolves its result kind cross-region. (A homogeneous
453    /// tuple resolves to [`BoundaryType::Seq`] instead — it shares the list buffer layout.)
454    Tuple(Vec<BoundaryType>),
455    /// A first-class BUILTIN value type by name (`Uuid`, `Duration`, `Time`, `Span`, `Money`,
456    /// `Quantity`, `Rational`, `Complex`, `Decimal`, `Modular`) — a scalar/handle type whose wasm
457    /// register kind is fixed by its name, not by a user-defined layout. Carried by name (like
458    /// [`BoundaryType::Struct`]/[`BoundaryType::Enum`]) so a cross-region parameter/return of one of
459    /// these types (`uuidV5(namespace: Uuid) -> Uuid`) seeds the correct handle kind; a name with no
460    /// wasm kind resolves to `None` and is left soundly unmodeled.
461    Builtin(String),
462}
463
464/// A struct type's resolved field layout (fields in declaration / slot order), carried in the
465/// bytecode so any consumer can address a struct's fields without re-deriving them from the AST.
466#[derive(Clone, Debug)]
467pub struct StructTypeDef {
468    pub name: String,
469    pub fields: Vec<(String, BoundaryType)>,
470}
471
472/// One variant of an enum: its constructor name and the resolved types of its payload fields, in
473/// position order (empty for a nullary variant). A `BindArm` extracts payload position `index`.
474#[derive(Clone, Debug)]
475pub struct EnumVariantDef {
476    pub name: String,
477    pub field_types: Vec<BoundaryType>,
478}
479
480/// An enum type's resolved per-variant payload layout, carried in the bytecode so a consumer can
481/// type a `When V (binds)` payload extraction on a value whose construction isn't visible (an enum
482/// PARAMETER) — the sum-type analog of [`StructTypeDef`].
483#[derive(Clone, Debug)]
484pub struct EnumTypeDef {
485    pub name: String,
486    pub variants: Vec<EnumVariantDef>,
487}
488
489/// A compiled user function (or closure body). All bodies share the program's
490/// single `code` vector; `entry_pc` is where this one begins. A closure body's
491/// frame layout is `[params… , capture values… , capture-present flags…]`.
492#[derive(Clone, Debug)]
493pub struct CompiledFunction {
494    pub name: Symbol,
495    pub entry_pc: usize,
496    pub param_count: u16,
497    pub register_count: usize,
498    /// Capture list for a closure body (empty for plain functions), in frame
499    /// order. Each entry: the captured name and, when that name is a promoted
500    /// global, its global index (the live-fallback source).
501    pub captures: Vec<(Symbol, Option<u16>)>,
502    /// Which of THIS frame's registers carry a user-visible name (params,
503    /// captures, Let targets, loop variables) — the region JIT's
504    /// observability map for loops tiering up inside this function.
505    pub named_regs: Vec<bool>,
506    /// DECLARED parameter kinds: scalars (`x: Float`) ride i64 slots,
507    /// `Seq of <scalar>` pins at the boundary; `None` for types native
508    /// code cannot represent (Map, Text, nested Seq, …). Closures (no
509    /// declarations) default to all-Int — exactly the old entry-guard
510    /// contract.
511    pub param_kinds: Vec<Option<super::native_tier::ParamKind>>,
512    /// Declared return kind; `None` falls back to the adapter's Int/Bool
513    /// return inference.
514    pub ret_kind: Option<super::native_tier::SlotKind>,
515    /// Each parameter's FULL declared type, resolved at compile time (`None` for a closure body —
516    /// no declarations — or a type the resolver does not model). Unlike `param_kinds` (a scalar/list
517    /// performance hint for the native tier), this carries struct/Text/etc. so a statically-typed
518    /// consumer can type a `f(p: Point)` / `f(s: Text)` parameter from the bytecode alone.
519    pub param_types: Vec<Option<BoundaryType>>,
520    /// The function's FULL declared RETURN type, resolved at compile time (`None` if undeclared or
521    /// unmodeled). Carries struct/map/enum/etc. so a caller can type the inline use of a returned
522    /// composite (`item k of f()`, `Inspect f()`) the same way a parameter of that type is typed.
523    pub return_type: Option<BoundaryType>,
524    /// Frame registers (parameter positions) of `mutable` parameters. A `mutable`
525    /// parameter passes BY REFERENCE under value semantics, so mutating it in
526    /// place must propagate to the caller — copy-on-write is suppressed for these
527    /// registers (mirrors the tree-walker's `is_mutable_param` skip). Empty for
528    /// closures and functions with no `mutable` parameters.
529    pub mutable_param_regs: Vec<Reg>,
530}
531
532/// A compiled program: the constant pool, the linear bytecode (Main first, then
533/// every function body), the size of Main's register frame, and the function
534/// table (indexed by `FuncIdx`, with a name → index map for call resolution).
535#[derive(Clone, Debug, Default)]
536pub struct CompiledProgram {
537    pub constants: Vec<Constant>,
538    pub code: Vec<Op>,
539    pub register_count: usize,
540    pub functions: Vec<CompiledFunction>,
541    pub fn_index: HashMap<Symbol, FuncIdx>,
542    /// Names of the promoted globals (Main top-level bindings referenced from
543    /// function/closure bodies), for "Undefined variable" errors.
544    pub globals: Vec<String>,
545    /// Which Main-frame registers carry a user-visible NAME (Let targets,
546    /// loop variables). Everything else is a statement-local scratch — dead
547    /// at every statement boundary by the allocator's recycling discipline —
548    /// so the region JIT neither writes it back nor preserves its pre-state.
549    pub named_regs: Vec<bool>,
550    /// `loop_locals[head]` = the registers bound INSIDE the loop whose region
551    /// head (back-edge target) is the absolute pc `head` — names lexically dead
552    /// the moment that loop exits. The region JIT subtracts these from the
553    /// write-back set, so copy-prop/CSE/fusion can treat them as true scratch.
554    /// Keyed by absolute pc (one code array); valued by the head's OWNING frame's
555    /// register indices (Main or the enclosing function).
556    pub loop_locals: HashMap<usize, Vec<bool>>,
557    /// DEBUG-ONLY: Main-frame register index → source variable name, populated only
558    /// by the debugger's compile path ([`crate::vm::Compiler::compile_for_debug`]).
559    /// Empty on every production build, so the runtime pays nothing; it just lets the
560    /// Studio debug drawer show `x` instead of `R0`.
561    pub reg_names: Vec<(u16, String)>,
562    /// The program's struct type definitions (name → field layout), resolved at compile time. The
563    /// bytecode's static type registry — the dynamically-typed tree-walker/VM never needed it, but a
564    /// statically-typed consumer (the AOT backend) addresses a struct's fields through it.
565    pub struct_types: Vec<StructTypeDef>,
566    /// The program's enum type definitions (name → per-variant payload layout), resolved at compile
567    /// time — the sum-type companion to `struct_types`. Lets the AOT backend type a `When V (binds)`
568    /// payload extraction on an enum whose construction isn't in scope (an enum PARAMETER).
569    pub enum_types: Vec<EnumTypeDef>,
570    /// Each promoted GLOBAL's resolved composite type (by global index; `None` for scalar /
571    /// self-describing / unmodeled). Lets the AOT type a closure that CAPTURES a composite global with
572    /// the value's shape — the capture analog of a parameter's declared type.
573    pub global_types: Vec<Option<BoundaryType>>,
574}
575
576#[cfg(test)]
577mod concurrency_op_tests {
578    use super::*;
579
580    /// Every concurrency op must survive the tier cache's `serde_json` roundtrip
581    /// (`CompiledProgram`/`FnBytecode` are cached as JSON), or a cached program
582    /// containing one would fail to reload. `Op` is `Copy` but not `PartialEq`,
583    /// so we compare its `Debug` form.
584    #[test]
585    fn concurrency_ops_serde_roundtrip() {
586        let ops = [
587            Op::ChanNew { dst: 1, cap: -1, elem: ChanElem::Int },
588            Op::ChanNew { dst: 2, cap: 8, elem: ChanElem::Text },
589            Op::ChanSend { chan: 3, val: 4 },
590            Op::ChanRecv { dst: 5, chan: 6 },
591            Op::ChanTrySend { dst: 7, chan: 8, val: 9 },
592            Op::ChanTryRecv { dst: 10, chan: 11 },
593            Op::ChanClose { chan: 12 },
594            Op::Spawn { func: 1, args_start: 13, arg_count: 2 },
595            Op::SpawnHandle { dst: 14, func: 2, args_start: 15, arg_count: 0 },
596            Op::TaskAwait { dst: 16, handle: 17 },
597            Op::TaskAbort { handle: 18 },
598            Op::SelectArmRecv { chan: 19, var: 20 },
599            Op::SelectArmTimeout { ticks: 21 },
600            Op::SelectWait { dst_arm: 22 },
601        ];
602        for op in ops {
603            let json = serde_json::to_string(&op).expect("serialize");
604            let back: Op = serde_json::from_str(&json).expect("deserialize");
605            assert_eq!(format!("{op:?}"), format!("{back:?}"), "roundtrip {op:?} via {json}");
606        }
607    }
608}