Skip to main content

logicaffeine_compile/vm/
binisa.rs

1//! The **binary Silicon ISA** — the register bytecode as a stable, self-contained,
2//! chip-decodable byte stream. The text sibling is [`crate::vm::isa`] (`emit_isa`, the
3//! human-readable assembly listing); this is the machine form of the SAME opcode set.
4//!
5//! Two roles, one artifact:
6//!
7//! 1. **Wire transport.** [`encode_program`] / [`decode_program`] turn a whole
8//!    [`CompiledProgram`] into a versioned, checksummed, SELF-CONTAINED frame. The frame
9//!    carries its own symbol string-table, so a receiver with a *fresh* [`Interner`]
10//!    re-interns and runs it byte-identically. This is what makes "send the language
11//!    bytecode over the wire and execute it" real: the raw bytecode's `Symbol`s are
12//!    interner-relative (see [`crate::vm::tier_cache`]) and thus NOT portable across
13//!    machines; the frame lowers every symbol to a wire-local id into the string table
14//!    and the decoder re-interns them on the far side.
15//!
16//! 2. **Chip ISA.** [`opcode`] is the STABLE opcode number a hardware decoder shares.
17//!    It is keyed on [`Op::name`] through an APPEND-ONLY table ([`ISA_OPCODES`]), so the
18//!    number for a given op can never drift when the `Op` enum is reordered — the exact
19//!    property a fabricated decoder needs (its wired constants outlive the source order).
20//!    The reversible-core opcodes ([`RevAdd`](Op::RevAdd) … [`RevSwap`](Op::RevSwap)) sit
21//!    at pinned low numbers so the adiabatic datapath's control decode is fixed.
22
23use std::collections::HashMap;
24
25use serde::{Deserialize, Serialize};
26
27use crate::intern::{Interner, Symbol};
28use crate::vm::instruction::{
29    BoundaryType, CompiledFunction, CompiledProgram, Constant, EnumTypeDef, FuncIdx, Op,
30    StructTypeDef,
31};
32use crate::vm::native_tier::{ParamKind, SlotKind};
33
34/// The frame magic — `LOGOS Bytecode, format 1`. A receiver rejects any buffer that does
35/// not open with these four bytes (a foreign or truncated frame is not a program).
36pub const ISA_MAGIC: &[u8; 4] = b"LGB1";
37
38/// The wire-format version. Bumped when the frame layout changes incompatibly; a decoder
39/// rejects a version it does not understand rather than mis-parsing it.
40pub const ISA_VERSION: u32 = 3;
41
42/// The APPEND-ONLY opcode table: `opcode(op) == position of op.name() in this slice`.
43/// Order IS the ABI — a new op appends at the end and keeps every prior number fixed; the
44/// totality lock (this module's unit test `opcode_map_is_total_over_the_isa`) forces a newly-added `Op` to
45/// be registered here, and the append-only lock pins the reversible-core numbers so a chip
46/// decoder built against them can never silently mismatch. Never reorder or delete an entry.
47pub const ISA_OPCODES: &[&str] = &[
48    // --- reversible / adiabatic core (pinned low, the charge-recovery datapath) ---
49    "RevAdd",  // 0
50    "RevSub",  // 1
51    "RevXor",  // 2
52    "RevNeg",  // 3
53    "RevSwap", // 4
54    // --- data movement ---
55    "LoadConst",
56    "Move",
57    "EnsureOwned",
58    // --- integer / arithmetic ALU ---
59    "Add",
60    "AddAssign",
61    "Sub",
62    "Mul",
63    "Div",
64    "ExactDiv",
65    "FloorDiv",
66    "Mod",
67    "DivPow2",
68    "MagicDivU",
69    "Pow",
70    // --- bitwise ---
71    "BitXor",
72    "BitAnd",
73    "BitOr",
74    "Shl",
75    "Shr",
76    "Not",
77    // --- comparison ---
78    "Lt",
79    "Gt",
80    "LtEq",
81    "GtEq",
82    "Eq",
83    "ApproxEq",
84    "NotEq",
85    // --- text / sequence value ops ---
86    "Concat",
87    "SeqConcat",
88    // --- control flow ---
89    "Jump",
90    "JumpIfFalse",
91    "JumpIfTrue",
92    "Call",
93    "CallBuiltin",
94    "CallValue",
95    "MakeClosure",
96    "Return",
97    "ReturnNothing",
98    "Halt",
99    // --- policy / capability ---
100    "CheckPolicy",
101    // --- globals ---
102    "GlobalGet",
103    "GlobalSet",
104    // --- collections ---
105    "ListPushField",
106    "NewList",
107    "NewEmptyList",
108    "NewEmptyListI32",
109    "NewEmptySet",
110    "NewEmptyMap",
111    "NewRange",
112    "ListPush",
113    "SetAdd",
114    "RemoveFrom",
115    "SetIndex",
116    "SetIndexUnchecked",
117    "Index",
118    "IndexUnchecked",
119    "RegionBoundsGuard",
120    "Length",
121    "Contains",
122    "FormatValue",
123    "SliceOp",
124    "DeepClone",
125    "NewTuple",
126    "UnionOp",
127    "IntersectOp",
128    "ListPop",
129    "DestructureTuple",
130    // --- temporal ---
131    "LoadToday",
132    "LoadNow",
133    // --- structs / enums ---
134    "NewStruct",
135    "StructInsert",
136    "GetField",
137    "NewInductive",
138    "TestArm",
139    "BindArm",
140    // --- CRDTs ---
141    "CrdtBump",
142    "CrdtMerge",
143    "NewCrdt",
144    "CrdtAppend",
145    "CrdtResolve",
146    // --- iteration ---
147    "IterPrepare",
148    "IterNext",
149    "IterPop",
150    // --- misc runtime ---
151    "Sleep",
152    "Show",
153    "Args",
154    "FailWith",
155    // --- concurrency ---
156    "ChanNew",
157    "ChanSend",
158    "ChanRecv",
159    "ChanTrySend",
160    "ChanTryRecv",
161    "ChanClose",
162    "Spawn",
163    "SpawnHandle",
164    "TaskAwait",
165    "TaskAbort",
166    "SelectArmRecv",
167    "SelectArmTimeout",
168    "SelectWait",
169    // --- networking ---
170    "NetConnect",
171    "NetListen",
172    "NetSend",
173    "NetStream",
174    "NetAwait",
175    "NetMakePeer",
176    "NetSync",
177    "Obs",
178    // --- CRDT lattice join (appended: the wire map is append-only, so existing opcodes never shift) ---
179    "CrdtJoin",
180    // --- 256-bit BigInt wide lane (one opcode; the operation rides the chip word's dst high nibble) ---
181    "BigIntWide",
182    // --- tcp.lg client sockets (one opcode; the operation rides the `kind` field) — an off-chip
183    //     coprocessor service like the Net ops ---
184    "Tcp",
185    // --- the `Serve` HTTP keystone (async-tier): the accept loop lives in the async io driver, which
186    //     re-enters the handler function as bytecode; an off-host service like the Net/Tcp ops ---
187    "Serve",
188    // --- fixed-capacity list allocation (append-only; carries the shared sequence proof) ---
189    "NewEmptyListFixed",
190    // --- runtime capacity hints (append-only; the hint remains visible to every backend) ---
191    "NewEmptyListWithCapacity",
192    "NewEmptyMapWithCapacity",
193];
194
195/// The stable opcode number for an op — the value a hardware decoder keys on. Total over the
196/// ISA (a new op that forgets to register in [`ISA_OPCODES`] panics here, and the totality
197/// lock catches it in CI first).
198pub fn opcode(op: &Op) -> u16 {
199    opcode_of_name(op.name())
200        .unwrap_or_else(|| panic!("op {:?} is not registered in ISA_OPCODES", op.name()))
201}
202
203/// The stable opcode number for an op *name*, or `None` if the name is not in the ISA table.
204pub fn opcode_of_name(name: &str) -> Option<u16> {
205    ISA_OPCODES
206        .iter()
207        .position(|&n| n == name)
208        .map(|i| i as u16)
209}
210
211/// One compiled function, with every interner-relative [`Symbol`] lowered to a wire-local id
212/// into [`WireProgram::symbols`]. The mirror of [`CompiledFunction`] that is serde-clean and
213/// portable (the real struct's `name`/`captures` symbols cannot be moved between interners).
214#[derive(Serialize, Deserialize)]
215struct WireFunction {
216    name: u32,
217    entry_pc: u64,
218    param_count: u16,
219    register_count: u64,
220    captures: Vec<(u32, Option<u16>)>,
221    named_regs: Vec<bool>,
222    param_kinds: Vec<Option<ParamKind>>,
223    ret_kind: Option<SlotKind>,
224    param_types: Vec<Option<BoundaryType>>,
225    return_type: Option<BoundaryType>,
226    mutable_param_regs: Vec<u16>,
227    cow_exempt_regs: Vec<u16>,
228}
229
230/// The serde-serialized body of an ISA frame: a whole [`CompiledProgram`] with a self-contained
231/// symbol string-table. Every `Symbol` anywhere in the program is replaced by an index into
232/// `symbols`; the decoder re-interns those strings against the target interner and rewrites the
233/// indices back. Debug-only fields (`reg_names`) are dropped — they never affect execution.
234#[derive(Serialize, Deserialize)]
235struct WireProgram {
236    symbols: Vec<String>,
237    constants: Vec<Constant>,
238    code: Vec<Op>,
239    register_count: u64,
240    functions: Vec<WireFunction>,
241    fn_index: Vec<(u32, u16)>,
242    globals: Vec<String>,
243    named_regs: Vec<bool>,
244    loop_locals: Vec<(u64, Vec<bool>)>,
245    struct_types: Vec<StructTypeDef>,
246    enum_types: Vec<EnumTypeDef>,
247    global_types: Vec<Option<BoundaryType>>,
248}
249
250/// Accumulates the wire symbol table during encoding: each distinct interner symbol gets one
251/// dense wire-local id, its string captured once from the source interner.
252struct SymTab<'i> {
253    interner: &'i Interner,
254    strings: Vec<String>,
255    map: HashMap<Symbol, u32>,
256}
257
258impl<'i> SymTab<'i> {
259    fn new(interner: &'i Interner) -> Self {
260        SymTab {
261            interner,
262            strings: Vec::new(),
263            map: HashMap::new(),
264        }
265    }
266
267    fn lower(&mut self, s: Symbol) -> u32 {
268        if let Some(&id) = self.map.get(&s) {
269            return id;
270        }
271        let id = self.strings.len() as u32;
272        self.strings.push(self.interner.resolve(s).to_string());
273        self.map.insert(s, id);
274        id
275    }
276}
277
278/// FNV-1a 64 over the payload — a corruption tripwire, not a cryptographic seal. A decoder
279/// rejects a frame whose stored digest does not match its recomputed one.
280fn checksum(bytes: &[u8]) -> u64 {
281    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
282    for &b in bytes {
283        h ^= b as u64;
284        h = h.wrapping_mul(0x0000_0100_0000_01b3);
285    }
286    h
287}
288
289/// Serialize a whole compiled program into a self-contained, portable ISA frame:
290/// `MAGIC ‖ version ‖ checksum ‖ bincode(WireProgram)`. The `interner` is the one the program
291/// was compiled against — every symbol string is captured from it so the frame stands alone.
292pub fn encode_program(program: &CompiledProgram, interner: &Interner) -> Vec<u8> {
293    let mut tab = SymTab::new(interner);
294
295    // Code: the ONLY symbol-carrying op is `CheckPolicy` (its policy predicate). Lower it to a
296    // wire id; every other op references names through constants / function indices already.
297    let mut code = program.code.clone();
298    for op in &mut code {
299        if let Op::CheckPolicy { predicate, .. } = op {
300            *predicate = Symbol::from_index(tab.lower(*predicate) as usize);
301        }
302    }
303
304    let functions: Vec<WireFunction> = program
305        .functions
306        .iter()
307        .map(|f| WireFunction {
308            name: tab.lower(f.name),
309            entry_pc: f.entry_pc as u64,
310            param_count: f.param_count,
311            register_count: f.register_count as u64,
312            captures: f
313                .captures
314                .iter()
315                .map(|(s, g)| (tab.lower(*s), *g))
316                .collect(),
317            named_regs: f.named_regs.clone(),
318            param_kinds: f.param_kinds.clone(),
319            ret_kind: f.ret_kind,
320            param_types: f.param_types.clone(),
321            return_type: f.return_type.clone(),
322            mutable_param_regs: f.mutable_param_regs.clone(),
323            cow_exempt_regs: f.cow_exempt_regs.clone(),
324        })
325        .collect();
326
327    // `fn_index` is a HashMap — sort by function index so the emitted frame is byte-deterministic
328    // regardless of hash iteration order (a chip / cache wants a reproducible artifact).
329    let mut fn_index: Vec<(u32, u16)> = program
330        .fn_index
331        .iter()
332        .map(|(s, fi)| (tab.lower(*s), *fi))
333        .collect();
334    fn_index.sort_unstable_by_key(|&(_, fi)| fi);
335
336    // `loop_locals` is ALSO a HashMap (keyed by absolute pc) — same treatment, sorted by that key.
337    let mut loop_locals: Vec<(u64, Vec<bool>)> = program
338        .loop_locals
339        .iter()
340        .map(|(k, v)| (*k as u64, v.clone()))
341        .collect();
342    loop_locals.sort_unstable_by_key(|&(pc, _)| pc);
343
344    // `struct_types`/`enum_types` are `Vec`s on `CompiledProgram`, but the discovery-pass registry
345    // they are built FROM (`TypeRegistry::iter_types`) is itself a `HashMap<Symbol, TypeDef>` whose
346    // iteration order is reseeded (and so reordered) every process run — the exact hazard
347    // `TypeRegistry`'s own variant-lookup helper already documents and sorts around. Two encodes of
348    // the identical source therefore end up with two DIFFERENT type-definition orders unless this
349    // boundary re-canonicalizes them: sort by name, since every consumer (the AOT backend, `vm/wasm/
350    // kind.rs`'s named-layout resolution) addresses a struct/enum by NAME, never by its position in
351    // this list — reordering the list changes nothing else.
352    let mut struct_types = program.struct_types.clone();
353    struct_types.sort_by(|a, b| a.name.cmp(&b.name));
354    let mut enum_types = program.enum_types.clone();
355    enum_types.sort_by(|a, b| a.name.cmp(&b.name));
356
357    let wire = WireProgram {
358        symbols: tab.strings,
359        constants: program.constants.clone(),
360        code,
361        register_count: program.register_count as u64,
362        functions,
363        fn_index,
364        globals: program.globals.clone(),
365        named_regs: program.named_regs.clone(),
366        loop_locals,
367        struct_types,
368        enum_types,
369        global_types: program.global_types.clone(),
370    };
371
372    let payload = bincode::serialize(&wire).expect("WireProgram serializes");
373    let mut out = Vec::with_capacity(payload.len() + 16);
374    out.extend_from_slice(ISA_MAGIC);
375    out.extend_from_slice(&ISA_VERSION.to_le_bytes());
376    out.extend_from_slice(&checksum(&payload).to_le_bytes());
377    out.extend_from_slice(&payload);
378    out
379}
380
381/// Decode an ISA frame back into an executable [`CompiledProgram`], re-interning its symbol
382/// table against `interner` (which may be BRAND NEW — the frame is self-contained). Returns
383/// `None` on any corruption: bad magic, wrong version, checksum mismatch, or malformed payload
384/// — a corrupt frame is not a program and is rejected, never half-executed.
385pub fn decode_program(bytes: &[u8], interner: &mut Interner) -> Option<CompiledProgram> {
386    if bytes.len() < 16 || &bytes[0..4] != ISA_MAGIC {
387        return None;
388    }
389    let version = u32::from_le_bytes(bytes[4..8].try_into().ok()?);
390    if version != ISA_VERSION {
391        return None;
392    }
393    let stored = u64::from_le_bytes(bytes[8..16].try_into().ok()?);
394    let payload = &bytes[16..];
395    if checksum(payload) != stored {
396        return None;
397    }
398    let wire: WireProgram = bincode::deserialize(payload).ok()?;
399
400    // Re-intern the wire string table → real symbols in the target interner.
401    let real: Vec<Symbol> = wire.symbols.iter().map(|s| interner.intern(s)).collect();
402    let remap = |wid: usize| -> Symbol { real.get(wid).copied().unwrap_or(Symbol::EMPTY) };
403
404    let mut code = wire.code;
405    for op in &mut code {
406        if let Op::CheckPolicy { predicate, .. } = op {
407            *predicate = remap(predicate.index());
408        }
409    }
410
411    let functions: Vec<CompiledFunction> = wire
412        .functions
413        .into_iter()
414        .map(|wf| CompiledFunction {
415            name: remap(wf.name as usize),
416            entry_pc: wf.entry_pc as usize,
417            param_count: wf.param_count,
418            register_count: wf.register_count as usize,
419            captures: wf
420                .captures
421                .into_iter()
422                .map(|(w, g)| (remap(w as usize), g))
423                .collect(),
424            named_regs: wf.named_regs,
425            param_kinds: wf.param_kinds,
426            ret_kind: wf.ret_kind,
427            param_types: wf.param_types,
428            return_type: wf.return_type,
429            mutable_param_regs: wf.mutable_param_regs,
430            cow_exempt_regs: wf.cow_exempt_regs,
431        })
432        .collect();
433
434    let fn_index: HashMap<Symbol, FuncIdx> = wire
435        .fn_index
436        .into_iter()
437        .map(|(w, fi)| (remap(w as usize), fi))
438        .collect();
439
440    Some(CompiledProgram {
441        // Not serialized: a wire form that predates the table has none, and a backend that finds
442        // none falls back to tracing a construction exactly as it always did.
443        declared_types: Vec::new(),
444        constants: wire.constants,
445        code,
446        register_count: wire.register_count as usize,
447        functions,
448        fn_index,
449        globals: wire.globals,
450        named_regs: wire.named_regs,
451        loop_locals: wire
452            .loop_locals
453            .into_iter()
454            .map(|(k, v)| (k as usize, v))
455            .collect(),
456        reg_names: Vec::new(),
457        fn_debug: Vec::new(),
458        struct_types: wire.struct_types,
459        enum_types: wire.enum_types,
460        global_types: wire.global_types,
461        // Observation is elided before wire serialization (the wire frame carries
462        // only the compiled/release program), so a decoded program observes nothing.
463        observation_sites: Vec::new(),
464        // Not serialized: a wire form that predates the table simply has none.
465        site_at: Vec::new(),
466    })
467}
468
469#[cfg(test)]
470#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/vm/binisa/tests.rs"]
471mod tests;