Skip to main content

lamina_codegen/powerpc/
frame.rs

1//! PowerPC64 ELFv2 stack frame management.
2//!
3//! ELFv2 frame layout:
4//!   [sp+0]   back-chain pointer (previous sp)
5//!   [sp+16]  LR save area
6//!   [sp+32]  parameter save area, which a callee may write
7//!   [sp+96+] local variables
8//!
9//! The frame is grown by subtracting from r1 and the back-chain must be
10//! written before we clobber anything.
11
12use std::io::{self, Write};
13
14/// Minimum linkage area size (back-chain + cr save + LR save + padding).
15/// Back chain, CR, LR and TOC, below the parameter save area.
16pub const LINKAGE_SIZE: usize = 32;
17
18/// Bytes the caller reserves above its linkage area for the parameter save area.
19/// It belongs to the callee, which may spill its register parameters into it; a
20/// variadic callee always does.
21pub const PARAM_SAVE_SIZE: usize = 64;
22
23/// Where local slots begin, in bytes from the new r1.
24pub const LOCALS_BASE: usize = LINKAGE_SIZE + PARAM_SAVE_SIZE;
25
26/// Range of the signed 16-bit displacement of D-form loads, stores and `addi`.
27const IMM16_MIN: i64 = -32768;
28const IMM16_MAX: i64 = 32767;
29
30pub struct Ppc64Frame;
31
32impl Ppc64Frame {
33    /// Total stack frame size (linkage + locals), 16-byte aligned.
34    pub fn aligned_frame_size(local_bytes: usize) -> usize {
35        ((LOCALS_BASE + local_bytes) + 15) & !15
36    }
37
38    /// Calculate the stack offset for local slot `n` (0-based).
39    ///
40    /// Slots start above the linkage area and grow upward in address
41    /// (i.e., at higher positive offsets from the old sp stored as
42    /// back-chain).  In the prologue we subtract `frame_size` from r1,
43    /// so slot `n` is at `LOCALS_BASE + n*8` from the new r1.
44    pub fn calculate_stack_offset(slot: usize) -> i32 {
45        (LOCALS_BASE + slot * 8) as i32
46    }
47
48    /// Emit the function prologue.
49    ///
50    /// Saves LR, creates the stack frame, stores callee-saved r14-r31
51    /// if needed (we don't track that here, so we just save LR and
52    /// update the back-chain).
53    pub fn generate_prologue<W: Write>(writer: &mut W, local_bytes: usize) -> io::Result<()> {
54        let frame_size = Self::aligned_frame_size(local_bytes) as i64;
55        writeln!(writer, "    mflr 0")?;
56        writeln!(writer, "    std 0, 16(1)")?; // Save LR to LR save slot
57        if -frame_size >= IMM16_MIN {
58            writeln!(writer, "    stdu 1, -{frame_size}(1)")?; // Allocate frame + store back-chain
59        } else {
60            // stdux stores r1 at r1 + r12 and moves r1 there, which is stdu with a register offset.
61            load_imm(writer, "12", -frame_size)?;
62            writeln!(writer, "    stdux 1, 1, 12")?;
63        }
64        Ok(())
65    }
66
67    /// Emit the function epilogue and return.
68    pub fn generate_epilogue<W: Write>(writer: &mut W, local_bytes: usize) -> io::Result<()> {
69        let frame_size = Self::aligned_frame_size(local_bytes) as i64;
70        adjust_sp(writer, frame_size)?; // Deallocate frame
71        writeln!(writer, "    ld 0, 16(1)")?; // Restore LR
72        writeln!(writer, "    mtlr 0")?;
73        writeln!(writer, "    blr")?;
74        Ok(())
75    }
76
77    /// Deallocate the frame and restore LR, then the caller must branch to the tail target
78    /// (no `blr` — that would return to this function instead of tail-calling).
79    pub fn generate_tail_epilogue<W: Write>(writer: &mut W, local_bytes: usize) -> io::Result<()> {
80        let frame_size = Self::aligned_frame_size(local_bytes) as i64;
81        adjust_sp(writer, frame_size)?;
82        writeln!(writer, "    ld 0, 16(1)")?;
83        writeln!(writer, "    mtlr 0")?;
84        Ok(())
85    }
86}
87
88/// Load `value` into the GPR `reg`. Values outside the signed 16-bit range take two
89/// instructions. The high half is added to `reg` with `addis`, which reads `r0` as zero,
90/// so `r0` is loaded with `lis` and `ori` and only takes non-negative values.
91pub fn load_imm<W: Write>(writer: &mut W, reg: &str, value: i64) -> io::Result<()> {
92    let out_of_range = || {
93        io::Error::new(
94            io::ErrorKind::InvalidInput,
95            format!("PowerPC64 immediate {value} does not fit a 32-bit load"),
96        )
97    };
98    if (IMM16_MIN..=IMM16_MAX).contains(&value) {
99        return writeln!(writer, "    li {reg}, {value}");
100    }
101    if reg == "0" {
102        if !(0..(1 << 31)).contains(&value) {
103            return Err(out_of_range());
104        }
105        writeln!(writer, "    lis 0, {}", value >> 16)?;
106        return writeln!(writer, "    ori 0, 0, {}", value & 0xffff);
107    }
108    let low = i64::from((value & 0xffff) as u16 as i16);
109    let high = (value - low) >> 16;
110    if i32::try_from(value).is_err() || !(IMM16_MIN..=IMM16_MAX).contains(&high) {
111        return Err(out_of_range());
112    }
113    writeln!(writer, "    li {reg}, {low}")?;
114    writeln!(writer, "    addis {reg}, {reg}, {high}")
115}
116
117/// Load or store `reg` at `offset(1)`. An offset past the signed 16-bit range is built in
118/// `r0` and used as the index of the X-form instruction. `r12` stands in when `reg` is `r0`.
119pub fn r1_mem<W: Write>(writer: &mut W, mnemonic: &str, reg: &str, offset: i32) -> io::Result<()> {
120    if (IMM16_MIN..=IMM16_MAX).contains(&i64::from(offset)) {
121        return writeln!(writer, "    {mnemonic} {reg}, {offset}(1)");
122    }
123    let scratch = if reg == "0" { "12" } else { "0" };
124    load_imm(writer, scratch, i64::from(offset))?;
125    writeln!(writer, "    {mnemonic}x {reg}, 1, {scratch}")
126}
127
128/// Materialise `r1 + offset` into `dst`.
129pub fn r1_address<W: Write>(writer: &mut W, dst: &str, offset: i32) -> io::Result<()> {
130    if (IMM16_MIN..=IMM16_MAX).contains(&i64::from(offset)) {
131        return writeln!(writer, "    addi {dst}, 1, {offset}");
132    }
133    load_imm(writer, dst, i64::from(offset))?;
134    writeln!(writer, "    add {dst}, 1, {dst}")
135}
136
137/// Move `r1` by `delta`. Deltas past the 16-bit range go through `r12`.
138fn adjust_sp<W: Write>(writer: &mut W, delta: i64) -> io::Result<()> {
139    if (IMM16_MIN..=IMM16_MAX).contains(&delta) {
140        writeln!(writer, "    addi 1, 1, {delta}")
141    } else {
142        load_imm(writer, "12", delta)?;
143        writeln!(writer, "    add 1, 1, 12")
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_stack_offset_slot_0() {
153        assert_eq!(Ppc64Frame::calculate_stack_offset(0), LOCALS_BASE as i32);
154    }
155
156    #[test]
157    fn test_stack_offset_slot_1() {
158        assert_eq!(
159            Ppc64Frame::calculate_stack_offset(1),
160            (LOCALS_BASE + 8) as i32
161        );
162    }
163
164    #[test]
165    fn test_prologue_emits_stdu() {
166        let mut buf = Vec::new();
167        Ppc64Frame::generate_prologue(&mut buf, 0).unwrap();
168        let s = String::from_utf8(buf).unwrap();
169        assert!(s.contains("stdu"), "Expected stdu in prologue: {s}");
170        assert!(s.contains("mflr"), "Expected mflr in prologue: {s}");
171    }
172
173    #[test]
174    fn test_large_frame_is_allocated_with_stdux() {
175        let mut buf = Vec::new();
176        Ppc64Frame::generate_prologue(&mut buf, 40000).unwrap();
177        let s = String::from_utf8(buf).unwrap();
178        assert!(!s.contains("stdu 1"), "{s}");
179        assert!(s.contains("stdux 1, 1, 12"), "{s}");
180        assert_eq!(ppc_value_of(&s, "12"), -40096, "{s}");
181    }
182
183    #[test]
184    fn test_large_frame_is_released_through_r12() {
185        let mut buf = Vec::new();
186        Ppc64Frame::generate_epilogue(&mut buf, 40000).unwrap();
187        let s = String::from_utf8(buf).unwrap();
188        assert!(s.contains("add 1, 1, 12"), "{s}");
189        assert_eq!(ppc_value_of(&s, "12"), 40096, "{s}");
190    }
191
192    /// Replays the `li`, `lis`, `addis` and `ori` lines that load `reg`.
193    fn ppc_value_of(asm: &str, reg: &str) -> i64 {
194        let mut value = 0i64;
195        for line in asm.lines().map(str::trim) {
196            let (op, args) = line.split_once(' ').unwrap_or((line, ""));
197            let parts: Vec<&str> = args.split(", ").collect();
198            match (op, parts.as_slice()) {
199                ("li", [r, v]) if *r == reg => value = v.parse().unwrap(),
200                ("addis", [r, _, v]) if *r == reg => value += v.parse::<i64>().unwrap() << 16,
201                _ => {}
202            }
203        }
204        value
205    }
206
207    #[test]
208    fn test_load_imm_covers_the_32_bit_range() {
209        for value in [
210            0i64,
211            1,
212            -1,
213            32767,
214            -32768,
215            32768,
216            40096,
217            65536,
218            0x7FFF_0000,
219            -40096,
220        ] {
221            let mut buf = Vec::new();
222            load_imm(&mut buf, "12", value).unwrap();
223            let s = String::from_utf8(buf).unwrap();
224            assert_eq!(ppc_value_of(&s, "12"), value, "{s}");
225        }
226    }
227
228    #[test]
229    fn test_epilogue_emits_blr() {
230        let mut buf = Vec::new();
231        Ppc64Frame::generate_epilogue(&mut buf, 0).unwrap();
232        let s = String::from_utf8(buf).unwrap();
233        assert!(s.contains("blr"), "Expected blr in epilogue: {s}");
234        assert!(s.contains("mtlr"), "Expected mtlr in epilogue: {s}");
235    }
236
237    #[test]
238    fn test_tail_epilogue_has_no_blr() {
239        let mut buf = Vec::new();
240        Ppc64Frame::generate_tail_epilogue(&mut buf, 0).unwrap();
241        let s = String::from_utf8(buf).unwrap();
242        assert!(!s.contains("blr"), "tail epilogue must not return: {s}");
243        assert!(s.contains("mtlr"), "Expected mtlr: {s}");
244    }
245}