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}