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}