logicaffeine-compile 0.10.1

LOGOS compilation pipeline - codegen and interpreter
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
//! Bytecode instruction set and the compiled-program container.
//!
//! Registers are per-frame `u16` indices (the compiler assigns locals to
//! registers at compile time; frames are capped at `MAX_REGISTERS_PER_FRAME`).
//! Jump targets are absolute instruction indices. This is the growing core of
//! work/VM_PLAN.md's 87-opcode set.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::intern::Symbol;

/// Serialize an interned [`Symbol`] as its `u32` index (HOTSWAP §P12). Sound for the
/// tier cache because the cache key pins the exact source — re-parsing it reproduces
/// the same interning order, so the index round-trips to the same symbol.
pub(crate) mod symbol_serde {
    use crate::intern::Symbol;
    use serde::{Deserialize, Deserializer, Serializer};
    pub fn serialize<S: Serializer>(s: &Symbol, ser: S) -> Result<S::Ok, S::Error> {
        ser.serialize_u32(s.index() as u32)
    }
    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Symbol, D::Error> {
        Ok(Symbol::from_index(u32::deserialize(de)? as usize))
    }
}

/// Serialize an `f64` by its raw bits so the cache round-trips bit-exactly (a text
/// format would otherwise mangle NaN / ±∞ and risk precision drift).
pub(crate) mod f64_bits {
    use serde::{Deserialize, Deserializer, Serializer};
    pub fn serialize<S: Serializer>(v: &f64, ser: S) -> Result<S::Ok, S::Error> {
        ser.serialize_u64(v.to_bits())
    }
    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<f64, D::Error> {
        Ok(f64::from_bits(u64::deserialize(de)?))
    }
}

pub type Reg = u16;
pub type ConstIdx = u32;
pub type FuncIdx = u16;

/// A constant-pool entry.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Constant {
    Int(i64),
    Float(#[serde(with = "f64_bits")] f64),
    Bool(bool),
    Text(String),
    Char(char),
    Nothing,
    Duration(i64),
    Date(i32),
    Moment(i64),
    Span { months: i32, days: i32 },
    Time(i64),
}

/// A bytecode instruction. Every field is a small `Copy` scalar (registers,
/// constant-pool indices, interned symbols), so the dispatch loop reads each
/// op by value instead of `clone()`-ing through the `Clone` machinery.
/// The declared element type of a `Pipe of T`, carried on [`Op::ChanNew`] so a consumer that
/// models a channel as a typed FIFO queue (the direct-WASM AOT) can type a `Receive`/`select`
/// arm's bound variable even when the pipe is never sent to (an empty `Pipe of Text` in a
/// timeout-only `select`). A `Copy` tag — the scalar element types cover every pipe the corpus
/// declares; a struct/enum element resolves to [`ChanElem::Unknown`] (falls back to the untyped
/// queue, exactly as before).
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum ChanElem {
    Unknown,
    Int,
    Float,
    Bool,
    Text,
}

impl ChanElem {
    /// Resolve a pipe's declared element type name (`Pipe of <name>`) to its tag.
    pub fn from_type_name(name: &str) -> ChanElem {
        match name {
            "Int" | "Nat" => ChanElem::Int,
            "Float" => ChanElem::Float,
            "Bool" => ChanElem::Bool,
            "Text" => ChanElem::Text,
            _ => ChanElem::Unknown,
        }
    }
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Op {
    /// `R[dst] = constants[idx]`
    LoadConst { dst: Reg, idx: ConstIdx },
    /// `R[dst] = R[src]` (shallow clone)
    Move { dst: Reg, src: Reg },
    /// Copy-on-write barrier: if `R[reg]`'s collection is shared (`Rc` strong > 1),
    /// deep-clone it in place so `R[reg]` becomes the sole owner. Emitted before an
    /// argument is passed to a function whose corresponding parameter is an inferred
    /// MUTABLE BORROW (mutated in place and returned): the callee's element writes
    /// then land on a buffer no OTHER live handle can observe, so an aliasing caller
    /// (`Let y be arr; Set arr to f(arr)`) still sees value semantics. A no-op when
    /// already uniquely owned (the common `Set x to f(x)` consume-reassign), so the
    /// hot path pays only a strong-count check. Mirrors the AOT's call-site `.cow()`.
    EnsureOwned { reg: Reg },

    Add { dst: Reg, lhs: Reg, rhs: Reg },
    /// `R[dst] = R[dst] + R[src]` — the `Set x to x + …` shape. Semantically
    /// identical to `Add { dst, lhs: dst, rhs: src }`; the dedicated form lets
    /// the VM append in place when `R[dst]` is a sole-owner Text (turning the
    /// O(n²) build-a-string-by-concatenation loop into amortized O(n)).
    AddAssign { dst: Reg, src: Reg },
    Sub { dst: Reg, lhs: Reg, rhs: Reg },
    Mul { dst: Reg, lhs: Reg, rhs: Reg },
    Div { dst: Reg, lhs: Reg, rhs: Reg },
    /// EXACT division (`7 / 2 → 7/2`, a Rational), the type-directed sibling of
    /// [`Op::Div`] — emitted for `BinaryOpKind::ExactDivide`.
    ExactDiv { dst: Reg, lhs: Reg, rhs: Reg },
    /// FLOOR division (`-7 // 2 → -4`, toward negative infinity) — emitted for
    /// `BinaryOpKind::FloorDivide`, distinct from the truncating [`Op::Div`].
    FloorDiv { dst: Reg, lhs: Reg, rhs: Reg },
    Mod { dst: Reg, lhs: Reg, rhs: Reg },
    /// `dst = lhs / 2^k` (signed, round toward zero) — emitted only when the
    /// divisor is a literal power of two AND the Oracle proved `lhs` is `Int`.
    /// A single op (the JIT lowers it to the side-exit-free `divpow2` shift
    /// stencil) so it fires for loop-invariant divisors the in-region JIT
    /// detector misses, without the scratch-register pressure of an expansion.
    DivPow2 { dst: Reg, lhs: Reg, k: u8 },
    /// `dst = lhs / c` (`mul_back == 0`) or `dst = lhs % c` (`mul_back == c`),
    /// where `c` is a compile-time-constant divisor that is NOT a power of two
    /// (W5/`DivPow2` handles powers of two), computed by the Granlund–Montgomery
    /// / libdivide UNSIGNED magic-reciprocal sequence (a `mul`-high + shift,
    /// ~3 cycles) instead of `idiv` (~25 cycles). `magic`/`more` are the
    /// precomputed constants (the exact [`logicaffeine_data::LogosDivU64`]
    /// encoding — low 6 bits of `more` are the shift, `0x40` is the 65-bit
    /// add-marker path). Emitted ONLY when the Oracle proves `lhs` is `Int` and
    /// NON-NEGATIVE: the unsigned magic equals the signed truncating `/`/`%`
    /// only for a non-negative dividend (for `x < 0` the signs disagree, exactly
    /// as for the `% 2^k → &` rewrite). The remainder is derived as
    /// `lhs - q*c` (wrapping), bit-exact with the kernel's `wrapping_rem` for
    /// non-negative `lhs`.
    MagicDivU { dst: Reg, lhs: Reg, magic: u64, more: u8, mul_back: i64 },

    Lt { dst: Reg, lhs: Reg, rhs: Reg },
    Gt { dst: Reg, lhs: Reg, rhs: Reg },
    LtEq { dst: Reg, lhs: Reg, rhs: Reg },
    GtEq { dst: Reg, lhs: Reg, rhs: Reg },
    Eq { dst: Reg, lhs: Reg, rhs: Reg },
    /// Tolerant float comparison (`a is approximately b`) — the shared
    /// isclose semantics (`logicaffeine_data::ops::logos_approx_eq`);
    /// `==`/`Eq` stays IEEE bit-exact.
    ApproxEq { dst: Reg, lhs: Reg, rhs: Reg },
    NotEq { dst: Reg, lhs: Reg, rhs: Reg },

    Not { dst: Reg, src: Reg },

    Concat { dst: Reg, lhs: Reg, rhs: Reg },
    /// `a followed by b` — merge two sequences into one.
    SeqConcat { dst: Reg, lhs: Reg, rhs: Reg },
    /// `a ** b` — exponentiation. Integer power is exact (promotes to BigInt on
    /// overflow); a Float operand uses `powf`; a negative Int exponent errors.
    Pow { dst: Reg, lhs: Reg, rhs: Reg },
    BitXor { dst: Reg, lhs: Reg, rhs: Reg },
    /// `a & b` — bitwise AND on Int, logical on Bool, intersection on Sets.
    BitAnd { dst: Reg, lhs: Reg, rhs: Reg },
    /// `a | b` (see `BitAnd`) — union on Sets.
    BitOr { dst: Reg, lhs: Reg, rhs: Reg },
    Shl { dst: Reg, lhs: Reg, rhs: Reg },
    Shr { dst: Reg, lhs: Reg, rhs: Reg },

    /// Unconditional jump to absolute instruction index.
    Jump { target: usize },
    /// Jump if `R[cond]` is falsey.
    JumpIfFalse { cond: Reg, target: usize },
    /// Jump if `R[cond]` is truthy.
    JumpIfTrue { cond: Reg, target: usize },

    /// Call a user function. The caller has placed `arg_count` arguments in
    /// consecutive registers starting at `args_start` (relative to the caller's
    /// frame base). The result is written to `R[dst]`. Uses register windowing.
    Call { dst: Reg, func: FuncIdx, args_start: Reg, arg_count: u16 },
    /// Call a kernel builtin with already-evaluated arguments (arity was
    /// validated at compile time, mirroring the tree-walker's
    /// arity-before-evaluation rule).
    CallBuiltin {
        dst: Reg,
        builtin: crate::semantics::builtins::BuiltinId,
        args_start: Reg,
        arg_count: u16,
    },
    /// Call the closure value in `R[callee]`. `name_for_err` is the function
    /// name when this came from a by-name call (`Unknown function: f` when the
    /// value is not callable) or `u32::MAX` for a call-by-expression
    /// (`Cannot call value of type T`).
    CallValue { dst: Reg, callee: Reg, args_start: Reg, arg_count: u16, name_for_err: ConstIdx },
    /// Build a closure over `program.functions[func]`: its `captures` list is
    /// snapshotted — local captures deep-cloned from the register window at
    /// `locals_start`, global captures from the globals table (skipped when
    /// still undefined; the body then falls through to the live global).
    MakeClosure { dst: Reg, func: FuncIdx, locals_start: Reg },

    /// `Check subject can/is predicate (of object)` — kernel policy check.
    /// `object == Reg::MAX` means no object.
    CheckPolicy {
        subject: Reg,
        #[serde(with = "symbol_serde")]
        predicate: Symbol,
        is_capability: bool,
        object: Reg,
        source_text: ConstIdx,
    },

    /// `Push value to obj's field` — kernel push into a struct's List field.
    /// `field` is a Text constant (the resolved field name).
    ListPushField { obj: Reg, field: ConstIdx, src: Reg },

    // ---- Globals (Main top-level bindings visible inside functions) ----
    /// `R[dst] = globals[idx]`, error "Undefined variable: {name}" when unset.
    GlobalGet { dst: Reg, idx: u16 },
    /// `globals[idx] = R[src]` (defines or overwrites).
    GlobalSet { idx: u16, src: Reg },
    /// Return `R[src]` from the current function.
    Return { src: Reg },
    /// Return `nothing` from the current function.
    ReturnNothing,

    // ---- Collections ----
    /// `R[dst] = [R[start], …, R[start+count-1]]` (a new list).
    NewList { dst: Reg, start: Reg, count: u16 },
    NewEmptyList { dst: Reg },
    /// `R[dst] = a new half-width (`Vec<i32>`) Int sequence` — emitted (behind
    /// `LOGOS_NARROW_VM`) for a `new Seq of Int` declaration the narrowing
    /// proof certified fits `i32`. Observably identical to `NewEmptyList`; only
    /// the storage width differs (see [`crate::interpreter::ListRepr::IntsI32`]).
    NewEmptyListI32 { dst: Reg },
    NewEmptySet { dst: Reg },
    NewEmptyMap { dst: Reg },
    /// `R[dst] = [R[start]..=R[end]]` (inclusive integer range as a list).
    NewRange { dst: Reg, start: Reg, end: Reg },
    /// Append `R[value]` to the list in `R[list]` (mutates in place).
    ListPush { list: Reg, value: Reg },
    /// Add `R[value]` to the set in `R[set]` (no-op if already present).
    SetAdd { set: Reg, value: Reg },
    /// Remove `R[value]` from the set/map in `R[collection]`.
    RemoveFrom { collection: Reg, value: Reg },
    /// `R[collection][R[index]] = R[value]` (1-based list set, or map insert).
    SetIndex { collection: Reg, index: Reg, value: Reg },
    /// Like `SetIndex` but the Oracle PROVED the index in `[1, length]`
    /// (range analysis, M9) — bounds-check elimination for the STORE. The
    /// interpreter still checks (free defense-in-depth); the JIT lowers it to
    /// an UNCHECKED array store (no bounds branch). Only listy collections
    /// with a stable length earn this, via `index_provably_in_bounds`.
    SetIndexUnchecked { collection: Reg, index: Reg, value: Reg },
    /// `R[dst] = R[collection][R[index]]` (1-based for ordered collections).
    Index { dst: Reg, collection: Reg, index: Reg },
    /// Like `Index` but the Oracle (range analysis, M9) PROVED the index
    /// in `[1, length]` at this point — bounds-check elimination, the V8/LLVM
    /// way. The bytecode interpreter still checks (a sound proof makes the
    /// check never fire; keeping it is free defense-in-depth), but the JIT
    /// lowers it to an UNCHECKED array load (no bounds branch, no deopt).
    /// Only listy collections with a stable length earn this; the compiler
    /// emits it solely behind `index_provably_in_bounds`.
    IndexUnchecked { dst: Reg, collection: Reg, index: Reg },
    /// REGION-ENTRY bounds-check hoist (V8 TurboFan loop bound-check
    /// elimination). For a loop `while iv </<= bound` reading/writing
    /// `R[array]` at affine indices, this asserts ONCE — at native region
    /// entry — that the array is long enough for the whole loop:
    /// `length(R[array]) >= R[bound] + add_max` and `R[iv] + add_min >= 1`.
    /// If it holds, the region runs every covered access UNCHECKED; if not,
    /// the VM declines the region and replays on bytecode (where the accesses
    /// are checked). A pure no-op in the interpreter and the function tier —
    /// speculation is region-only, made safe solely by this entry guard.
    RegionBoundsGuard { array: Reg, bound: Reg, iv: Reg, add_max: i32, add_min: i32 },
    /// `R[dst] = length of R[collection]`.
    Length { dst: Reg, collection: Reg },
    /// `R[dst] = R[collection] contains R[value]`.
    Contains { dst: Reg, collection: Reg, value: Reg },

    // ---- Strings / slices / tuples / sets / temporal ----
    /// `R[dst] = Text(debug_prefix? + format(R[src], spec?))` — one
    /// interpolated-string segment. `u32::MAX` = no spec / no prefix.
    FormatValue { dst: Reg, src: Reg, spec: ConstIdx, debug_prefix: ConstIdx },
    /// `R[dst] = R[collection][R[start]..=R[end]]` (1-indexed inclusive).
    SliceOp { dst: Reg, collection: Reg, start: Reg, end: Reg },
    /// `R[dst] = deep clone of R[src]`.
    DeepClone { dst: Reg, src: Reg },
    /// `R[dst] = (R[start], …, R[start+count-1])` (immutable tuple).
    NewTuple { dst: Reg, start: Reg, count: u16 },
    UnionOp { dst: Reg, lhs: Reg, rhs: Reg },
    IntersectOp { dst: Reg, lhs: Reg, rhs: Reg },
    /// `R[dst] = today` (Date; honors the test fixed-clock).
    LoadToday { dst: Reg },
    /// `R[dst] = now` (Moment; honors the test fixed-clock).
    LoadNow { dst: Reg },

    // ---- Structs / enums / Inspect / CRDT ----
    /// `R[dst] = Struct { type_name: constants[type_name], fields: {} }`.
    NewStruct { dst: Reg, type_name: ConstIdx },
    /// Insert `R[value]` under field `constants[field]` into the struct in
    /// `R[obj]` (construction and SetField — structs have VALUE semantics, so
    /// in-place mutation of the register equals the tree-walker's
    /// clone-mutate-reassign).
    StructInsert { obj: Reg, field: ConstIdx, value: Reg },
    /// `R[dst] = R[obj].field` — "Field '{f}' not found" / "Cannot access
    /// field on {type}".
    GetField { dst: Reg, obj: Reg, field: ConstIdx },
    /// `R[dst] = Inductive { type, constructor, args: R[args_start..+count] }`.
    NewInductive { dst: Reg, type_name: ConstIdx, ctor: ConstIdx, args_start: Reg, count: u16 },
    /// `R[dst] = Bool(struct type-name or inductive constructor == constants[variant])`;
    /// false for any other value.
    TestArm { dst: Reg, target: Reg, variant: ConstIdx },
    /// Inspect-arm binding: a Struct target binds `fields[constants[field]]`,
    /// an Inductive target binds `args[index]`. A missing field/index leaves
    /// `R[dst]` unwritten (the tree-walker skips the bind — unreachable for
    /// parsed programs, whose fields always exist after default-fill).
    BindArm { dst: Reg, target: Reg, field: ConstIdx, index: u16 },
    /// GCounter/PNCounter bump of `R[obj].field` by `R[amount]` (negated for
    /// Decrease — also selects the tree-walker's increment/decrement wording).
    CrdtBump { obj: Reg, field: ConstIdx, amount: Reg, negate: bool },
    /// GCounter merge: fold every field of `R[source]` into `R[target]`.
    CrdtMerge { target: Reg, source: Reg },
    /// `R[dst]` = a fresh, empty rich CRDT — `kind` 0 = SharedSet (OR-Set),
    /// 1 = SharedSequence (RGA), 2 = Divergent (MV-register). Used to default-fill a
    /// `Shared` struct's CRDT fields, mirroring the tree-walker's `new`-struct init.
    NewCrdt { dst: Reg, kind: u8 },
    /// RGA append: push `R[value]` onto the replicated sequence in `R[seq]` (mutates the
    /// shared CRDT in place, so a field access propagates).
    CrdtAppend { seq: Reg, value: Reg },
    /// Resolve `R[obj].field` to `R[value]`: a real MV-register resolves in place, a plain
    /// field is overwritten — the same fallback the tree-walker's `Resolve` takes.
    CrdtResolve { obj: Reg, field: ConstIdx, value: Reg },

    // ---- Repeat (snapshot iteration) ----
    /// Snapshot `R[iterable]` (List/Set items, Text chars, Map (k,v) tuples)
    /// and push it onto the iterator stack.
    IterPrepare { iterable: Reg },
    /// Load the next snapshot element into `R[dst]` and advance; jump to
    /// `exit` when exhausted (the iterator stays pushed — `IterPop` at the
    /// exit point drops it).
    IterNext { dst: Reg, exit: usize },
    /// Drop the top iterator.
    IterPop,
    /// `R[dst] = R[list].pop()` — Nothing when empty (not an error).
    ListPop { list: Reg, dst: Reg },
    /// Sleep for `R[nanos]` (Duration nanos, or Int milliseconds).
    Sleep { duration: Reg },
    /// Tuple-pattern binding: `R[start..start+count] = tuple elements` with
    /// zip semantics (stops at the shorter side, like the tree-walker).
    /// Errors when `R[src]` is not a Tuple.
    DestructureTuple { src: Reg, start: Reg, count: u16 },

    /// Emit `R[src].to_display_string()` to the output stream.
    Show { src: Reg },
    /// `R[dst] = the program argument vector` as a `Seq of Text` (the
    /// interpreter's `args()` system native, mirroring the compiled binary's
    /// `env::args()`: index 0 is the program name). Outside the JIT integer
    /// subset, so the adapters bail on it and it always runs in the VM.
    Args { dst: Reg },
    // ─── Go-like concurrency (Phase 54 / T10) ───────────────────────────────
    // Args travel through register ranges (`Op` is `Copy`). Channel/task handles
    // are ordinary `Value`s (`RuntimeValue::Chan` / `::TaskHandle`). Every op that
    // can block suspends the resumable VM (`run_until_block`) and is serviced by
    // the deterministic scheduler, exactly as the tree-walker's `yield_request`.

    /// `R[dst] = a new channel`; `cap < 0` ⇒ the scheduler's default capacity. `elem` is the
    /// declared `Pipe of T` element type (a hint for a typed-queue consumer; the scheduler ignores it).
    ChanNew { dst: Reg, cap: i32, elem: ChanElem },
    /// Send `R[val]` into channel `R[chan]` (blocks if the channel is full).
    ChanSend { chan: Reg, val: Reg },
    /// `R[dst] = receive from channel R[chan]` (blocks if the channel is empty).
    ChanRecv { dst: Reg, chan: Reg },
    /// `R[dst] = bool` — non-blocking send of `R[val]` into `R[chan]`.
    ChanTrySend { dst: Reg, chan: Reg, val: Reg },
    /// `R[dst] = received value, or Nothing` — non-blocking receive from `R[chan]`.
    ChanTryRecv { dst: Reg, chan: Reg },
    /// Close channel `R[chan]`.
    ChanClose { chan: Reg },
    /// Spawn `functions[func]` with args in `R[args_start..+arg_count]` (fire-and-forget).
    Spawn { func: FuncIdx, args_start: Reg, arg_count: u16 },
    /// `R[dst] = task handle` of a spawned `functions[func]` (same arg convention).
    SpawnHandle { dst: Reg, func: FuncIdx, args_start: Reg, arg_count: u16 },
    /// `R[dst] = result of awaiting task R[handle]` (Nothing if it was aborted).
    TaskAwait { dst: Reg, handle: Reg },
    /// Abort task `R[handle]`.
    TaskAbort { handle: Reg },
    /// Register a `Receive var from chan` arm for the next `SelectWait`.
    SelectArmRecv { chan: Reg, var: Reg },
    /// Register an `After ticks` timeout arm for the next `SelectWait`.
    SelectArmTimeout { ticks: Reg },
    /// Block on the registered select arms; `R[dst_arm] = the winning arm index`
    /// (a recv arm's received value is already in its `var` register).
    SelectWait { dst_arm: Reg },

    // ─── peer networking over the relay (async-tier). Each suspends the resumable VM via a
    // `VmBlock::Net*` request the async VM driver services with the shared `NetInbox` — the same
    // inbox the tree-walker uses, so a `Send`/`Await` runs byte-identically on both tiers. ───
    /// Dial the relay at `R[url]` (async); resume when connected.
    NetConnect { url: Reg },
    /// Subscribe this node's inbox to `R[topic]` (async); resume when subscribed.
    NetListen { topic: Reg },
    /// Encode `R[msg]` and publish it to peer `R[to]`.
    NetSend { to: Reg, msg: Reg },
    /// Batch-stream the list `R[values]` to peer `R[to]`.
    NetStream { to: Reg, values: Reg },
    /// `R[dst] = await a message (or a batch stream, if `stream`) from peer `R\[from\]`` (blocks).
    NetAwait { dst: Reg, from: Reg, stream: bool },
    /// `R[dst] = a PeerAgent handle for address `R\[addr\]`` (its canonical relay topic). Pure.
    NetMakePeer { dst: Reg, addr: Reg },
    /// CRDT sync point on topic `R[topic]`: publish `R[dst]`'s counter, merge what has arrived,
    /// and write the merged value back to `R[dst]`.
    NetSync { dst: Reg, topic: Reg },

    /// Fail with the Text constant at `msg` — used for constructs whose
    /// tree-walker semantics are "error WHEN EXECUTED" (an unbound `Set`, an
    /// unsupported statement). Never fails at compile time: dead branches must
    /// stay free.
    FailWith { msg: ConstIdx },
    /// Stop execution.
    Halt,
}

/// A parameter's (or struct field's) declared type, RESOLVED at compile time into a self-contained
/// form a STATICALLY-typed consumer can use without the AST or the interner. The dynamically-typed
/// tree-walker/VM and the scalar-only native JIT never needed static types, so they were not put in
/// the bytecode before; they are now a first-class part of the compiled program (a struct is
/// referenced by name, its layout living in [`CompiledProgram::struct_types`]).
#[derive(Clone, Debug, PartialEq)]
pub enum BoundaryType {
    Int,
    Float,
    Bool,
    Text,
    Date,
    Moment,
    /// A `Word32`/`Word64` wrapping-integer (ℤ/2ⁿ) — a native `i32`/`i64` scalar, so a `Seq of Word32`
    /// parameter (a crypto state array) resolves its element kind cross-region.
    Word32,
    Word64,
    /// `Seq of <elem>`.
    Seq(Box<BoundaryType>),
    /// A user struct, by name — its field layout is in [`CompiledProgram::struct_types`].
    Struct(String),
    /// A user enum (sum type), by name. Carried as one i32 handle whose word 0 is the constructor
    /// tag; `Inspect`/`TestArm` read the tag directly, so no separate layout is needed.
    Enum(String),
    /// `Map of <key> to <value>` — one i32 handle. The key kind is recovered at each access from the
    /// key expression's own kind (no map metadata needed); the VALUE kind is carried here so a
    /// parameter map's `item k of m` resolves its result kind cross-region.
    Map(Box<BoundaryType>, Box<BoundaryType>),
    /// A HETEROGENEOUS tuple (`Pair`/`Triple` of mixed element types) — one i32 handle to a buffer of
    /// 8-byte slots, each holding its element at its own kind. The per-position element types are
    /// carried so a `item N of t` (constant `N`) resolves its result kind cross-region. (A homogeneous
    /// tuple resolves to [`BoundaryType::Seq`] instead — it shares the list buffer layout.)
    Tuple(Vec<BoundaryType>),
    /// A first-class BUILTIN value type by name (`Uuid`, `Duration`, `Time`, `Span`, `Money`,
    /// `Quantity`, `Rational`, `Complex`, `Decimal`, `Modular`) — a scalar/handle type whose wasm
    /// register kind is fixed by its name, not by a user-defined layout. Carried by name (like
    /// [`BoundaryType::Struct`]/[`BoundaryType::Enum`]) so a cross-region parameter/return of one of
    /// these types (`uuidV5(namespace: Uuid) -> Uuid`) seeds the correct handle kind; a name with no
    /// wasm kind resolves to `None` and is left soundly unmodeled.
    Builtin(String),
}

/// A struct type's resolved field layout (fields in declaration / slot order), carried in the
/// bytecode so any consumer can address a struct's fields without re-deriving them from the AST.
#[derive(Clone, Debug)]
pub struct StructTypeDef {
    pub name: String,
    pub fields: Vec<(String, BoundaryType)>,
}

/// One variant of an enum: its constructor name and the resolved types of its payload fields, in
/// position order (empty for a nullary variant). A `BindArm` extracts payload position `index`.
#[derive(Clone, Debug)]
pub struct EnumVariantDef {
    pub name: String,
    pub field_types: Vec<BoundaryType>,
}

/// An enum type's resolved per-variant payload layout, carried in the bytecode so a consumer can
/// type a `When V (binds)` payload extraction on a value whose construction isn't visible (an enum
/// PARAMETER) — the sum-type analog of [`StructTypeDef`].
#[derive(Clone, Debug)]
pub struct EnumTypeDef {
    pub name: String,
    pub variants: Vec<EnumVariantDef>,
}

/// A compiled user function (or closure body). All bodies share the program's
/// single `code` vector; `entry_pc` is where this one begins. A closure body's
/// frame layout is `[params… , capture values… , capture-present flags…]`.
#[derive(Clone, Debug)]
pub struct CompiledFunction {
    pub name: Symbol,
    pub entry_pc: usize,
    pub param_count: u16,
    pub register_count: usize,
    /// Capture list for a closure body (empty for plain functions), in frame
    /// order. Each entry: the captured name and, when that name is a promoted
    /// global, its global index (the live-fallback source).
    pub captures: Vec<(Symbol, Option<u16>)>,
    /// Which of THIS frame's registers carry a user-visible name (params,
    /// captures, Let targets, loop variables) — the region JIT's
    /// observability map for loops tiering up inside this function.
    pub named_regs: Vec<bool>,
    /// DECLARED parameter kinds: scalars (`x: Float`) ride i64 slots,
    /// `Seq of <scalar>` pins at the boundary; `None` for types native
    /// code cannot represent (Map, Text, nested Seq, …). Closures (no
    /// declarations) default to all-Int — exactly the old entry-guard
    /// contract.
    pub param_kinds: Vec<Option<super::native_tier::ParamKind>>,
    /// Declared return kind; `None` falls back to the adapter's Int/Bool
    /// return inference.
    pub ret_kind: Option<super::native_tier::SlotKind>,
    /// Each parameter's FULL declared type, resolved at compile time (`None` for a closure body —
    /// no declarations — or a type the resolver does not model). Unlike `param_kinds` (a scalar/list
    /// performance hint for the native tier), this carries struct/Text/etc. so a statically-typed
    /// consumer can type a `f(p: Point)` / `f(s: Text)` parameter from the bytecode alone.
    pub param_types: Vec<Option<BoundaryType>>,
    /// The function's FULL declared RETURN type, resolved at compile time (`None` if undeclared or
    /// unmodeled). Carries struct/map/enum/etc. so a caller can type the inline use of a returned
    /// composite (`item k of f()`, `Inspect f()`) the same way a parameter of that type is typed.
    pub return_type: Option<BoundaryType>,
    /// Frame registers (parameter positions) of `mutable` parameters. A `mutable`
    /// parameter passes BY REFERENCE under value semantics, so mutating it in
    /// place must propagate to the caller — copy-on-write is suppressed for these
    /// registers (mirrors the tree-walker's `is_mutable_param` skip). Empty for
    /// closures and functions with no `mutable` parameters.
    pub mutable_param_regs: Vec<Reg>,
}

/// A compiled program: the constant pool, the linear bytecode (Main first, then
/// every function body), the size of Main's register frame, and the function
/// table (indexed by `FuncIdx`, with a name → index map for call resolution).
#[derive(Clone, Debug, Default)]
pub struct CompiledProgram {
    pub constants: Vec<Constant>,
    pub code: Vec<Op>,
    pub register_count: usize,
    pub functions: Vec<CompiledFunction>,
    pub fn_index: HashMap<Symbol, FuncIdx>,
    /// Names of the promoted globals (Main top-level bindings referenced from
    /// function/closure bodies), for "Undefined variable" errors.
    pub globals: Vec<String>,
    /// Which Main-frame registers carry a user-visible NAME (Let targets,
    /// loop variables). Everything else is a statement-local scratch — dead
    /// at every statement boundary by the allocator's recycling discipline —
    /// so the region JIT neither writes it back nor preserves its pre-state.
    pub named_regs: Vec<bool>,
    /// `loop_locals[head]` = the registers bound INSIDE the loop whose region
    /// head (back-edge target) is the absolute pc `head` — names lexically dead
    /// the moment that loop exits. The region JIT subtracts these from the
    /// write-back set, so copy-prop/CSE/fusion can treat them as true scratch.
    /// Keyed by absolute pc (one code array); valued by the head's OWNING frame's
    /// register indices (Main or the enclosing function).
    pub loop_locals: HashMap<usize, Vec<bool>>,
    /// DEBUG-ONLY: Main-frame register index → source variable name, populated only
    /// by the debugger's compile path ([`crate::vm::Compiler::compile_for_debug`]).
    /// Empty on every production build, so the runtime pays nothing; it just lets the
    /// Studio debug drawer show `x` instead of `R0`.
    pub reg_names: Vec<(u16, String)>,
    /// The program's struct type definitions (name → field layout), resolved at compile time. The
    /// bytecode's static type registry — the dynamically-typed tree-walker/VM never needed it, but a
    /// statically-typed consumer (the AOT backend) addresses a struct's fields through it.
    pub struct_types: Vec<StructTypeDef>,
    /// The program's enum type definitions (name → per-variant payload layout), resolved at compile
    /// time — the sum-type companion to `struct_types`. Lets the AOT backend type a `When V (binds)`
    /// payload extraction on an enum whose construction isn't in scope (an enum PARAMETER).
    pub enum_types: Vec<EnumTypeDef>,
    /// Each promoted GLOBAL's resolved composite type (by global index; `None` for scalar /
    /// self-describing / unmodeled). Lets the AOT type a closure that CAPTURES a composite global with
    /// the value's shape — the capture analog of a parameter's declared type.
    pub global_types: Vec<Option<BoundaryType>>,
}

#[cfg(test)]
mod concurrency_op_tests {
    use super::*;

    /// Every concurrency op must survive the tier cache's `serde_json` roundtrip
    /// (`CompiledProgram`/`FnBytecode` are cached as JSON), or a cached program
    /// containing one would fail to reload. `Op` is `Copy` but not `PartialEq`,
    /// so we compare its `Debug` form.
    #[test]
    fn concurrency_ops_serde_roundtrip() {
        let ops = [
            Op::ChanNew { dst: 1, cap: -1, elem: ChanElem::Int },
            Op::ChanNew { dst: 2, cap: 8, elem: ChanElem::Text },
            Op::ChanSend { chan: 3, val: 4 },
            Op::ChanRecv { dst: 5, chan: 6 },
            Op::ChanTrySend { dst: 7, chan: 8, val: 9 },
            Op::ChanTryRecv { dst: 10, chan: 11 },
            Op::ChanClose { chan: 12 },
            Op::Spawn { func: 1, args_start: 13, arg_count: 2 },
            Op::SpawnHandle { dst: 14, func: 2, args_start: 15, arg_count: 0 },
            Op::TaskAwait { dst: 16, handle: 17 },
            Op::TaskAbort { handle: 18 },
            Op::SelectArmRecv { chan: 19, var: 20 },
            Op::SelectArmTimeout { ticks: 21 },
            Op::SelectWait { dst_arm: 22 },
        ];
        for op in ops {
            let json = serde_json::to_string(&op).expect("serialize");
            let back: Op = serde_json::from_str(&json).expect("deserialize");
            assert_eq!(format!("{op:?}"), format!("{back:?}"), "roundtrip {op:?} via {json}");
        }
    }
}