Skip to main content

logicaffeine_forge/
buffer.rs

1//! The copy-and-patch assembler: glue stencils into executable code.
2//!
3//! [`JitBuffer`] collects stencil instances with their hole values, then
4//! `finish()` lays everything out — stencil code, an 8-byte-aligned literal
5//! pool for constant holes, and pointer slots for GOT-indirect sites — maps a
6//! page (learning the final base address), patches every relocation against
7//! that base, writes ONCE, and seals. No mutable window ever escapes; W^X
8//! toggling stays confined to the constructing thread.
9//!
10//! Layout:
11//! ```text
12//! [piece 0][piece 1]…[piece N]  [pointer slots…]  [value slots…]
13//!  ^code, 16-byte aligned        ^8-byte aligned GOT-style slots
14//! ```
15
16use crate::patch;
17use crate::stencil_model::{HoleId, RelocKind, Stencil};
18use crate::{JitError, JitPage};
19
20/// A placed stencil instance, identifying a continuation target.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct Label(usize);
23
24/// What to plug into a stencil's holes, by hole id.
25#[derive(Clone, Copy, Debug)]
26pub enum HoleValue {
27    /// Continuation hole N jumps to this piece.
28    Cont(u8, Label),
29    /// Constant hole N reads this value.
30    Const(u8, i64),
31}
32
33/// Assembly errors.
34#[derive(Debug)]
35pub enum BufferError {
36    /// A hole had no value supplied.
37    MissingHoleValue {
38        /// The stencil whose hole went unfilled.
39        stencil: &'static str,
40        /// The unfilled hole.
41        hole: HoleId,
42    },
43    /// A label referenced a piece that does not exist.
44    UnresolvedLabel(Label),
45    /// A relocation could not be patched.
46    Patch(patch::PatchError),
47    /// The page could not be created.
48    Jit(JitError),
49    /// The buffer has no pieces.
50    Empty,
51}
52
53impl std::fmt::Display for BufferError {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            BufferError::MissingHoleValue { stencil, hole } => {
57                write!(f, "stencil '{stencil}': no value for hole {hole:?}")
58            }
59            BufferError::UnresolvedLabel(l) => write!(f, "unresolved label {l:?}"),
60            BufferError::Patch(e) => write!(f, "patch failed: {e}"),
61            BufferError::Jit(e) => write!(f, "page failed: {e}"),
62            BufferError::Empty => write!(f, "empty JitBuffer"),
63        }
64    }
65}
66
67impl std::error::Error for BufferError {}
68
69struct Piece {
70    stencil: &'static Stencil,
71    holes: Vec<HoleValue>,
72}
73
74/// The staged assembly.
75#[derive(Default)]
76pub struct JitBuffer {
77    pieces: Vec<Piece>,
78    patch_marks: Vec<(usize, u8)>,
79}
80
81/// A finished, executable chain.
82#[derive(Debug)]
83pub struct JitChain {
84    page: JitPage,
85    pieces: usize,
86    /// Literal-pool offsets awaiting the post-finish self-entry patch.
87    patch_slots: Vec<usize>,
88}
89
90impl JitChain {
91    /// Wrap a finished, self-contained block of machine code (no stencil
92    /// pieces, no post-seal patch slots) as a runnable chain. The contiguous
93    /// register-allocated region backend (`compile_region_regalloc`) emits its
94    /// whole function as one such block, then runs it through the SAME
95    /// `run_with_frame` ABI as a stencil chain.
96    ///
97    /// `pieces` is the logical op count (diagnostics only); the executable is
98    /// one contiguous function regardless.
99    pub fn from_code(code: &[u8], pieces: usize) -> Result<JitChain, crate::JitError> {
100        let page = JitPage::new(code)?;
101        Ok(JitChain { page, pieces, patch_slots: Vec::new() })
102    }
103
104    /// Write `value` into every marked literal-pool slot (the self-call
105    /// entry address, known only after layout). Errs on targets without
106    /// post-seal patching (Apple Silicon MAP_JIT) — callers fall back to
107    /// the table-indirect call.
108    pub fn patch_marked(&self, value: u64) -> Result<(), crate::JitError> {
109        for &slot in &self.patch_slots {
110            self.page.patch_word(slot, value)?;
111        }
112        Ok(())
113    }
114
115    /// Whether any slots await the self-entry patch.
116    pub fn has_patch_marks(&self) -> bool {
117        !self.patch_slots.is_empty()
118    }
119
120    /// The CPS entry point: `fn(base, sp) -> i64` — `base` is the frame (the
121    /// compiled function's register slots), `sp` the operand-stack top.
122    ///
123    /// # Safety
124    /// `base` must point at a frame at least as large as every patched slot
125    /// index; `sp` must point into an operand stack with capacity for the
126    /// chain's pushes; the page must outlive every call.
127    pub unsafe fn entry(
128        &self,
129    ) -> unsafe extern "C" fn(*mut i64, *mut i64, i64, i64, i64, i64, f64, f64, f64, f64, f64, f64) -> i64 {
130        std::mem::transmute::<
131            *const u8,
132            unsafe extern "C" fn(*mut i64, *mut i64, i64, i64, i64, i64, f64, f64, f64, f64, f64, f64) -> i64,
133        >(
134            self.page.as_ptr(),
135        )
136    }
137
138    /// Run the chain with an empty frame and a fresh operand stack.
139    pub fn run(&self) -> i64 {
140        let mut frame = vec![0i64; 64];
141        self.run_with_frame(&mut frame)
142    }
143
144    /// Run the chain over the given frame (slot 0 = VM register 0, …), with a
145    /// fresh operand stack. The frame is read AND written by slot stencils.
146    ///
147    /// `LOGOS_JIT_CANARY=1` guards the operand stack with a sentinel canary
148    /// past its end: any stencil that pushes beyond the 256-slot stack (a
149    /// pc/sp miscompile) trips it loudly at the source instead of silently
150    /// corrupting adjacent memory. It is env-gated rather than always-on
151    /// because this runs per chain — millions of times for hot recursion —
152    /// so the default (and every release build) pays nothing.
153    pub fn run_with_frame(&self, frame: &mut [i64]) -> i64 {
154        const STACK: usize = 256;
155        const CANARY: usize = 64;
156        const SENTINEL: i64 = 0x5151_5151_5151_5151u64 as i64;
157        if jit_canary_enabled() {
158            let mut stack = vec![0i64; STACK + CANARY];
159            for c in &mut stack[STACK..] {
160                *c = SENTINEL;
161            }
162            let r = unsafe {
163                (self.entry())(frame.as_mut_ptr(), stack.as_mut_ptr(), 0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
164            };
165            for (k, c) in stack[STACK..].iter().enumerate() {
166                assert_eq!(
167                    *c, SENTINEL,
168                    "OPERAND STACK OVERFLOW: canary slot {k} (stack base + {}) clobbered",
169                    STACK + k
170                );
171            }
172            return r;
173        }
174        let mut stack = vec![0i64; STACK];
175        // Threaded registers enter zeroed; a pinned chain's prologue
176        // reloads them from the frame before any use.
177        unsafe {
178            (self.entry())(frame.as_mut_ptr(), stack.as_mut_ptr(), 0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
179        }
180    }
181
182    /// The mapped code+pool bytes (diagnostics/tests).
183    pub fn bytes(&self) -> &[u8] {
184        unsafe { std::slice::from_raw_parts(self.page.as_ptr(), self.page.len()) }
185    }
186
187    /// The runtime base address (diagnostics/tests).
188    pub fn base(&self) -> u64 {
189        self.page.as_ptr() as u64
190    }
191
192    /// How many stencil pieces were glued (diagnostics/tests — the
193    /// tail-jump economics of a lowering).
194    pub fn piece_count(&self) -> usize {
195        self.pieces
196    }
197}
198
199/// Whether `LOGOS_JIT_CANARY=1` armed the operand-stack / region-frame
200/// sentinel guards (read once; the per-chain path stays branch-cheap).
201pub fn jit_canary_enabled() -> bool {
202    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
203    *ON.get_or_init(|| std::env::var("LOGOS_JIT_CANARY").is_ok_and(|v| v == "1"))
204}
205
206const PIECE_ALIGN: usize = 16;
207const SLOT_SIZE: usize = 8;
208
209fn align_up(v: usize, a: usize) -> usize {
210    v.div_ceil(a) * a
211}
212
213impl JitBuffer {
214    /// An empty buffer.
215    pub fn new() -> Self {
216        JitBuffer::default()
217    }
218
219    /// Append a stencil instance; the returned [`Label`] is its address for
220    /// continuation holes (including back-edges to earlier pieces).
221    /// Mark hole `n` of the LAST pushed piece for post-finish patching
222    /// (the self-call entry slot).
223    pub fn mark_patch_hole(&mut self, hole_n: u8) {
224        let pi = self.pieces.len().checked_sub(1).expect("mark after a push");
225        self.patch_marks.push((pi, hole_n));
226    }
227
228    /// How many stencil pieces have been pushed so far (the index the next
229    /// `push_stencil` will occupy). Used to assert PASS-1/PASS-2 agreement.
230    pub fn pieces_pushed(&self) -> usize {
231        self.pieces.len()
232    }
233
234    pub fn push_stencil(&mut self, stencil: &'static Stencil, holes: &[HoleValue]) -> Label {
235        self.pieces.push(Piece { stencil, holes: holes.to_vec() });
236        Label(self.pieces.len() - 1)
237    }
238
239    /// The label of the piece at `index` — usable for FORWARD references
240    /// (validated when `finish` runs).
241    pub fn label(&self, index: usize) -> Label {
242        Label(index)
243    }
244
245    /// Lay out, map, patch against the final base, write once, seal.
246    pub fn finish(self) -> Result<JitChain, BufferError> {
247        if self.pieces.is_empty() {
248            return Err(BufferError::Empty);
249        }
250
251        // Piece offsets.
252        let mut piece_off: Vec<usize> = Vec::with_capacity(self.pieces.len());
253        let mut cursor = 0usize;
254        for piece in &self.pieces {
255            cursor = align_up(cursor, PIECE_ALIGN);
256            piece_off.push(cursor);
257            cursor += piece.stencil.code.len();
258        }
259
260        // Slot assignment: one VALUE slot per Const hole instance; one POINTER
261        // slot per (piece, hole) among indirect relocs — SHARED by every
262        // indirect reloc of that hole. An arm64 GOT load is an ADRP+LDR PAIR
263        // (two relocs, one hole): the ADRP contributes the 4 KiB page and the
264        // LDR the low 12 bits, so both MUST resolve to the same slot or the
265        // composed address breaks as soon as the pool straddles a page
266        // boundary.
267        let mut value_slot: Vec<Vec<Option<usize>>> = Vec::new(); // [piece][hole-n] -> slot off
268        let mut pointer_slot: std::collections::HashMap<(usize, HoleId), usize> = std::collections::HashMap::new();
269        let mut slot_cursor = align_up(cursor, SLOT_SIZE);
270
271        for (pi, piece) in self.pieces.iter().enumerate() {
272            let mut per_hole: Vec<Option<usize>> = vec![None; 9];
273            for hv in &piece.holes {
274                if let HoleValue::Const(n, _) = hv {
275                    if per_hole[*n as usize].is_none() {
276                        per_hole[*n as usize] = Some(slot_cursor);
277                        slot_cursor += SLOT_SIZE;
278                    }
279                }
280            }
281            value_slot.push(per_hole);
282            for reloc in piece.stencil.relocs.iter() {
283                if patch::is_indirect(reloc.kind) {
284                    pointer_slot.entry((pi, reloc.target)).or_insert_with(|| {
285                        let s = slot_cursor;
286                        slot_cursor += SLOT_SIZE;
287                        s
288                    });
289                }
290            }
291        }
292        let total_len = slot_cursor;
293
294        // Resolve a hole on a piece to (target address fn of base, is_const_value).
295        let find_hole = |piece: &Piece, hole: HoleId| -> Result<HoleValue, BufferError> {
296            for hv in &piece.holes {
297                match (hv, hole) {
298                    (HoleValue::Cont(n, _), HoleId::Cont(m)) if *n == m => return Ok(*hv),
299                    (HoleValue::Const(n, _), HoleId::ConstI64(m)) if *n == m => return Ok(*hv),
300                    _ => {}
301                }
302            }
303            Err(BufferError::MissingHoleValue { stencil: piece.stencil.name, hole })
304        };
305
306        // Validate labels up front.
307        for piece in &self.pieces {
308            for hv in &piece.holes {
309                if let HoleValue::Cont(_, Label(t)) = hv {
310                    if *t >= self.pieces.len() {
311                        return Err(BufferError::UnresolvedLabel(Label(*t)));
312                    }
313                }
314            }
315        }
316
317        let pieces = self.pieces;
318        let patch_marks = self.patch_marks;
319        let mut patch_err: Option<BufferError> = None;
320        let page = JitPage::with_layout(total_len, |base| {
321            let mut buf = vec![0u8; total_len];
322
323            // Copy stencil code.
324            for (pi, piece) in pieces.iter().enumerate() {
325                let off = piece_off[pi];
326                buf[off..off + piece.stencil.code.len()].copy_from_slice(piece.stencil.code);
327            }
328
329            let mut do_patch = || -> Result<(), BufferError> {
330                // Fill value slots.
331                for (pi, piece) in pieces.iter().enumerate() {
332                    for hv in &piece.holes {
333                        if let HoleValue::Const(n, v) = hv {
334                            if let Some(slot) = value_slot[pi][*n as usize] {
335                                buf[slot..slot + 8].copy_from_slice(&v.to_le_bytes());
336                            }
337                        }
338                    }
339                }
340                // Fill pointer slots (one per (piece, hole)).
341                for (&(pi, hole), &slot) in &pointer_slot {
342                    let piece = &pieces[pi];
343                    let content: u64 = match find_hole(piece, hole)? {
344                        HoleValue::Cont(_, Label(t)) => base + piece_off[t] as u64,
345                        HoleValue::Const(n, _) => {
346                            let vslot = value_slot[pi][n as usize]
347                                .expect("const hole has a value slot");
348                            base + vslot as u64
349                        }
350                    };
351                    buf[slot..slot + 8].copy_from_slice(&content.to_le_bytes());
352                }
353
354                // Patch every relocation site.
355                for (pi, piece) in pieces.iter().enumerate() {
356                    for reloc in piece.stencil.relocs.iter() {
357                        let site_off = piece_off[pi] + reloc.offset as usize;
358                        let site_addr = base + site_off as u64;
359                        // The address the SITE refers to: a pointer slot for
360                        // indirect kinds, else the direct target.
361                        let target_addr: u64 = if patch::is_indirect(reloc.kind) {
362                            base + pointer_slot[&(pi, reloc.target)] as u64
363                        } else {
364                            match find_hole(piece, reloc.target)? {
365                                HoleValue::Cont(_, Label(t)) => base + piece_off[t] as u64,
366                                HoleValue::Const(n, _) => {
367                                    let vslot = value_slot[pi][n as usize]
368                                        .expect("const hole has a value slot");
369                                    base + vslot as u64
370                                }
371                            }
372                        };
373                        let r = match reloc.kind {
374                            RelocKind::Branch26 => {
375                                patch::patch_aarch64_branch26(&mut buf, site_off, site_addr, target_addr)
376                            }
377                            RelocKind::Page21 | RelocKind::GotPage21 => {
378                                patch::patch_aarch64_page21(&mut buf, site_off, site_addr, target_addr)
379                            }
380                            RelocKind::PageOff12 { scale } => {
381                                patch::patch_aarch64_pageoff12(&mut buf, site_off, target_addr, scale)
382                            }
383                            RelocKind::GotPageOff12 => {
384                                patch::patch_aarch64_pageoff12(&mut buf, site_off, target_addr, 3)
385                            }
386                            RelocKind::Rel32 | RelocKind::GotRel32 => {
387                                patch::patch_x64_rel32(&mut buf, site_off, site_addr, target_addr, reloc.addend)
388                            }
389                            RelocKind::Abs64 => {
390                                patch::patch_abs64(&mut buf, site_off, target_addr, reloc.addend);
391                                Ok(())
392                            }
393                        };
394                        r.map_err(BufferError::Patch)?;
395                    }
396                }
397                Ok(())
398            };
399            if let Err(e) = do_patch() {
400                patch_err = Some(e);
401            }
402            buf
403        })
404        .map_err(BufferError::Jit)?;
405
406        if let Some(e) = patch_err {
407            return Err(e);
408        }
409        let patch_slots: Vec<usize> = patch_marks
410            .iter()
411            .map(|&(pi, n)| {
412                value_slot[pi][n as usize].expect("marked hole has a const value slot")
413            })
414            .collect();
415        Ok(JitChain { page, pieces: pieces.len(), patch_slots })
416    }
417}