Skip to main content

logicaffeine_compile/vm/
fn_bytecode.rs

1//! A relocatable, self-contained per-function bytecode unit (HOTSWAP §7).
2//!
3//! A [`CompiledProgram`] holds every function body in ONE flat `code` vector with
4//! ABSOLUTE jump targets; `entry_pc` marks where each begins. [`slice_function`]
5//! lifts one function out of that vector into a [`FnBytecode`] whose jump targets are
6//! 0-relative to its own first op, carrying the frame/param/return metadata needed to
7//! run or compile it independently of the program it came from.
8//!
9//! This is the producer the Axis-1 hot-swap consumes: the WASM-portable warm-bytecode
10//! side-table (P11) installs an `FnBytecode` per function, and the OPFS tier cache
11//! (P12) serializes it. The native path (P10) feeds the same body to forge (which
12//! rebases by `entry_pc` itself, so it takes the program slice directly).
13
14#[allow(unused_imports)]
15use super::instruction::{CompiledProgram, Constant, Op};
16use super::native_tier::{ParamKind, SlotKind};
17
18/// One function lifted out of a [`CompiledProgram`]: a self-contained, relocatable
19/// body with 0-relative jump targets plus the metadata to execute or compile it.
20#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
21#[allow(dead_code)] // consumed by P10 (native swap), P11 (warm side-table), P12 (cache)
22pub struct FnBytecode {
23    /// The function body, with every jump target rebased to be relative to `code[0]`.
24    pub code: Vec<Op>,
25    /// The constant pool the code indexes into — the program's pool, copied so the
26    /// unit is self-contained; constant indices are preserved unchanged.
27    pub constants: Vec<Constant>,
28    pub register_count: usize,
29    pub param_count: u16,
30    pub param_kinds: Vec<Option<ParamKind>>,
31    pub ret_kind: Option<SlotKind>,
32    /// Which frame registers carry a user-visible name (the region JIT's
33    /// observability map; preserved from the source function).
34    pub named_regs: Vec<bool>,
35}
36
37impl FnBytecode {
38    /// Structural self-consistency check (HOTSWAP §P12/P11 robustness). Every jump target
39    /// must land inside the body and the body must end in a control-leaving op, so an
40    /// installed body can neither fetch past the warm buffer (panic) nor fall through into
41    /// the next installed body (silent miscompile). A `slice_function` body always passes;
42    /// this rejects a corrupt / foreign / mis-decoded body so the VM declines the install
43    /// and falls back to baseline instead of trusting it. `n_funcs` bounds any `Call`
44    /// target so a body can't dispatch to a non-existent function index.
45    pub fn is_well_formed(&self, n_funcs: usize) -> bool {
46        if self.code.is_empty() {
47            return false;
48        }
49        for op in &self.code {
50            match *op {
51                Op::Jump { target }
52                | Op::JumpIfFalse { target, .. }
53                | Op::JumpIfTrue { target, .. } => {
54                    if target >= self.code.len() {
55                        return false;
56                    }
57                }
58                Op::Call { func, .. } => {
59                    if func as usize >= n_funcs {
60                        return false;
61                    }
62                }
63                _ => {}
64            }
65        }
66        matches!(
67            self.code.last(),
68            Some(Op::Return { .. } | Op::ReturnNothing | Op::Halt)
69        )
70    }
71}
72
73/// Shift one op's jump target by `delta` (signed). Only the three control-flow ops
74/// carry an absolute pc target; the `target: Reg` ops (`TestArm`/`BindArm`/…) name a
75/// register, not a pc, and are left untouched.
76pub(crate) fn rebase(op: Op, delta: isize) -> Op {
77    let shift = |t: usize| (t as isize + delta) as usize;
78    match op {
79        Op::Jump { target } => Op::Jump { target: shift(target) },
80        Op::JumpIfFalse { cond, target } => Op::JumpIfFalse { cond, target: shift(target) },
81        Op::JumpIfTrue { cond, target } => Op::JumpIfTrue { cond, target: shift(target) },
82        other => other,
83    }
84}
85
86/// Lift function `fi` out of `program` into a self-contained [`FnBytecode`] with
87/// 0-relative jumps. The body is `program.code[entry_pc..end]` (where `end` is the
88/// next function's entry, or the end of code); its absolute jump targets are rebased
89/// by `-entry_pc`, so they index into the returned `code` directly.
90#[allow(dead_code)]
91pub fn slice_function(program: &CompiledProgram, fi: usize) -> FnBytecode {
92    let f = &program.functions[fi];
93    let entry = f.entry_pc;
94    let end = program
95        .functions
96        .iter()
97        .map(|g| g.entry_pc)
98        .filter(|&e| e > entry)
99        .min()
100        .unwrap_or(program.code.len());
101    let code = program.code[entry..end]
102        .iter()
103        .map(|&op| rebase(op, -(entry as isize)))
104        .collect();
105    FnBytecode {
106        code,
107        constants: program.constants.clone(),
108        register_count: f.register_count,
109        param_count: f.param_count,
110        param_kinds: f.param_kinds.clone(),
111        ret_kind: f.ret_kind,
112        named_regs: f.named_regs.clone(),
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::intern::Interner;
120    use crate::vm::instruction::CompiledFunction;
121
122    fn jump_target(op: &Op) -> Option<usize> {
123        match *op {
124            Op::Jump { target }
125            | Op::JumpIfFalse { target, .. }
126            | Op::JumpIfTrue { target, .. } => Some(target),
127            _ => None,
128        }
129    }
130
131    #[test]
132    fn slice_rebases_jumps_zero_relative_and_round_trips() {
133        let mut it = Interner::new();
134        let name = it.intern("f");
135        // Main = [Halt]@0 ; function `f` @1 = a self-loop with a forward exit:
136        //   @1 JumpIfFalse cond0 -> abs 3   (exit)      -> rel target 2
137        //   @2 Jump        -> abs 1          (loop back) -> rel target 0
138        //   @3 Halt
139        let code = vec![
140            Op::Halt,
141            Op::JumpIfFalse { cond: 0, target: 3 },
142            Op::Jump { target: 1 },
143            Op::Halt,
144        ];
145        let prog = CompiledProgram {
146            code,
147            functions: vec![CompiledFunction {
148                name,
149                entry_pc: 1,
150                param_count: 0,
151                register_count: 1,
152                captures: vec![],
153                named_regs: vec![true],
154                param_kinds: vec![],
155                ret_kind: None,
156                param_types: vec![],
157                return_type: None,
158                mutable_param_regs: vec![],
159            }],
160            ..Default::default()
161        };
162
163        let fnbc = slice_function(&prog, 0);
164        assert_eq!(fnbc.code.len(), 3, "body is entry..next-entry");
165
166        // Jumps are now 0-relative to the slice.
167        assert!(matches!(fnbc.code[0], Op::JumpIfFalse { target: 2, .. }));
168        assert!(matches!(fnbc.code[1], Op::Jump { target: 0 }));
169
170        // 0-relative validity: every jump target lands inside the slice.
171        for op in &fnbc.code {
172            if let Some(t) = jump_target(op) {
173                assert!(t < fnbc.code.len(), "jump target {t} out of slice bounds");
174            }
175        }
176
177        // Round-trip: re-absolutizing by +entry_pc reproduces the original span
178        // exactly (Op has no PartialEq, so compare its Debug form).
179        let reabs: Vec<String> = fnbc.code.iter().map(|&op| format!("{:?}", rebase(op, 1))).collect();
180        let orig: Vec<String> = prog.code[1..4].iter().map(|op| format!("{:?}", op)).collect();
181        assert_eq!(reabs, orig, "slice must invert back to the original body");
182
183        // Metadata is carried through.
184        assert_eq!(fnbc.register_count, 1);
185        assert_eq!(fnbc.named_regs, vec![true]);
186    }
187
188    fn body(code: Vec<Op>) -> FnBytecode {
189        FnBytecode {
190            code,
191            constants: vec![],
192            register_count: 1,
193            param_count: 0,
194            param_kinds: vec![],
195            ret_kind: None,
196            named_regs: vec![],
197        }
198    }
199
200    #[test]
201    fn is_well_formed_accepts_a_valid_body() {
202        // JumpIfFalse exits forward, Jump loops back, ends in ReturnNothing — all in range.
203        let good = body(vec![
204            Op::JumpIfFalse { cond: 0, target: 2 },
205            Op::Jump { target: 0 },
206            Op::ReturnNothing,
207        ]);
208        assert!(good.is_well_formed(1));
209    }
210
211    #[test]
212    fn is_well_formed_rejects_malformed_bodies() {
213        // Empty body.
214        assert!(!body(vec![]).is_well_formed(1));
215        // No terminal op (would fall through into the next warm body / past the buffer).
216        assert!(!body(vec![Op::Jump { target: 0 }]).is_well_formed(1));
217        // Out-of-range jump target.
218        assert!(!body(vec![Op::Jump { target: 99 }, Op::ReturnNothing]).is_well_formed(1));
219        // Call to a non-existent function index (func 5 ≥ n_funcs 1).
220        assert!(!body(vec![
221            Op::Call { dst: 0, func: 5, args_start: 0, arg_count: 0 },
222            Op::ReturnNothing,
223        ])
224        .is_well_formed(1));
225        // Same call IS in range when there are enough functions.
226        assert!(body(vec![
227            Op::Call { dst: 0, func: 5, args_start: 0, arg_count: 0 },
228            Op::ReturnNothing,
229        ])
230        .is_well_formed(6));
231    }
232}