Skip to main content

rucc_codegen/
finish.rs

1//! The prologue, the epilogue, and the moves the allocator asked for.
2//!
3//! Design: `spec/10-backend.md` sections 10.4 and 10.7.
4//!
5//! [`crate::frame`] works out what a function's stack looks like and writes nothing. This is what
6//! writes it. Three things are still missing from a function the allocator has finished with, and
7//! all three of them are instructions no lowering rule chose:
8//!
9//! ```text
10//!   the prologue     takes the frame the layout worked out, and puts away the registers a call
11//!                    leaves alone that this function writes anyway
12//!   the moves        every spill, every reload and every copy the allocator handed back as an
13//!                    edit, in the place it said and in the order it said
14//!   the epilogue     gives the frame back and puts the registers back, at the end of every block
15//!                    the function returns from
16//! ```
17//!
18//! There is a fourth thing and it is not an instruction but a number. The lowering wrote an
19//! instruction for every `alloca` that computes the address of the memory it asked for, and could
20//! not write how far into the frame that memory is, because when it ran there was no frame. So
21//! the displacement of each of those is filled in here, out of the same [`Frame`] everything else
22//! here reads, and off the same stack pointer every other offset in it is from.
23//!
24//! After this the function is one an encoder can read: every register is physical, every offset
25//! into the frame is a constant, and the stack pointer is where the convention says it should be
26//! at every instruction that could look.
27//!
28//! # Why the moves go in first
29//!
30//! Every offset the frame reports is from the stack pointer as it stands in the body of the
31//! function. A spill written before the prologue exists would be written in front of the
32//! instruction it belongs to and behind nothing, which is where the prologue then goes, so the
33//! prologue ends up in front of it and the offsets stay true. Writing them the other way round
34//! would put the first reload above the instruction that takes the frame, and it would read from
35//! an address that is one frame out.
36//!
37//! # Where a return is
38//!
39//! A block that goes nowhere is a block the function returns from. There is no other kind: a
40//! block with no successors and no return would be one that falls off the end of the function,
41//! which is a function that was mis-lowered rather than one this has an opinion about. So the
42//! epilogue goes at the end of every block with an empty successor list, and there may be several,
43//! because nothing here insists a function has one exit.
44//!
45//! # What is target-specific here
46//!
47//! The names, and only the names. Which instruction pushes a register and which one moves the
48//! stack pointer is [`rucc_target::FrameInsts`], which the target says and this reads, so what
49//! is written below is the shape of a prologue rather than any particular machine's. That is
50//! `spec/10-backend.md` section 10.8 as it applies to the one pass that would otherwise be full
51//! of `x64.` by hand.
52
53use rucc_base::Interner;
54use rucc_mir::{Block, Func, Inst, Mem, Opcode, Operand, Reg};
55use rucc_regalloc::Allocation;
56use rucc_regalloc::assign::Place;
57use rucc_regalloc::rewrite::{At, Edit};
58use rucc_target::{CallRegs, FrameInsts, PhysReg, RegClass};
59
60use crate::frame::Frame;
61
62/// Writes the moves, the prologue and the epilogue into a function the allocator has finished
63/// with.
64///
65/// # Panics
66///
67/// Panics on a function with no blocks in it, on a frame whose slots or locals the allocation and
68/// the lowering do not match, and on a move of a class the target did not say how to move. All of
69/// them are the caller handing it a frame and a function that were not worked out from each other.
70pub fn finish(
71    func: &mut Func,
72    allocation: &Allocation,
73    frame: &Frame,
74    locals: &[(Inst, usize)],
75    conv: &CallRegs,
76    insts: &FrameInsts,
77    names: &mut Interner,
78) {
79    let entry = func.entry().expect("a function with a block in it");
80    let returns: Vec<Block> = func.blocks().filter(|&block| func[block].succs.is_empty()).collect();
81
82    // Before anything is written, because these are instructions the lowering already put in the
83    // function and every one of them is somewhere the prologue is about to go in front of, which
84    // is what makes an offset from the stack pointer the right thing to write into them.
85    for &(inst, local) in locals {
86        let at = frame.local(local).expect("a local the frame was worked out from");
87        let mem = func[inst].mem.expect("the address of a local is an address");
88        func[mem].disp = at;
89    }
90
91    let mut writer = Writer { func, conv, insts, names };
92
93    let mut cursors: Vec<(At, Inst)> = Vec::new();
94    for edit in &allocation.edits {
95        let inst = writer.mov(edit, frame);
96        writer.put(&mut cursors, edit.at, inst);
97    }
98
99    let prologue = writer.prologue(frame);
100    for &inst in prologue.iter().rev() {
101        writer.func.prepend_inst(entry, inst);
102    }
103    for block in returns {
104        let epilogue = writer.epilogue(frame);
105        for inst in epilogue {
106            writer.func.append_inst(block, inst);
107        }
108    }
109}
110
111/// One function having its frame written into it.
112struct Writer<'a> {
113    func: &'a mut Func,
114    conv: &'a CallRegs,
115    insts: &'a FrameInsts,
116    names: &'a mut Interner,
117}
118
119impl Writer<'_> {
120    /// The instructions the prologue is, in the order they run.
121    ///
122    /// The order is the one the epilogue undoes and it is not free. The frame pointer is saved
123    /// before anything else, so that it points at a fixed place whatever else happens. The
124    /// registers are pushed before the alignment is forced, so that the epilogue can find them
125    /// again from the frame pointer, since after the alignment is forced nothing else can. And the
126    /// vector registers are stored last, because until the frame has been taken there is nowhere
127    /// to store them.
128    fn prologue(&mut self, frame: &Frame) -> Vec<Inst> {
129        let sp = self.conv.stack_pointer;
130        let fp = self.conv.frame_pointer;
131        let mut out = Vec::new();
132        if frame.frame_pointer() {
133            out.push(self.push(fp));
134            let mov = self.opcode(self.insts.moves(self.conv.int_class).expect("a move").mov);
135            out.push(self.two(mov, fp, sp));
136        }
137        for &reg in frame.saved_int() {
138            out.push(self.push(reg));
139        }
140        if let Some(to) = frame.realign() {
141            let and = self.opcode(self.insts.align);
142            out.push(self.arith(and, -i64::from(to)));
143        }
144        if frame.size() > 0 {
145            let sub = self.opcode(self.insts.sub);
146            out.push(self.arith(sub, i64::from(frame.size())));
147        }
148        for save in frame.saved_sse() {
149            out.push(self.store(self.conv.sse_class, save.reg, save.at));
150        }
151        out
152    }
153
154    /// The instructions the epilogue is, in the order they run.
155    ///
156    /// The vector registers are read back while the stack pointer is still where the body left it,
157    /// because that is what their offsets are from. Then the stack pointer goes back to the last
158    /// register the prologue pushed, which is arithmetic when the prologue knew how far it had
159    /// moved and a read of the frame pointer when it did not.
160    fn epilogue(&mut self, frame: &Frame) -> Vec<Inst> {
161        let sp = self.conv.stack_pointer;
162        let fp = self.conv.frame_pointer;
163        let word = self.conv.word;
164        let mut out = Vec::new();
165        for save in frame.saved_sse() {
166            out.push(self.load(self.conv.sse_class, save.reg, save.at));
167        }
168        let pushed = u32::try_from(frame.saved_int().len()).expect("a frame");
169        if frame.frame_pointer() {
170            if pushed == 0 {
171                let mov = self.opcode(self.insts.moves(self.conv.int_class).expect("a move").mov);
172                out.push(self.two(mov, sp, fp));
173            } else {
174                let lea = self.opcode(self.insts.lea);
175                let back = -offset(word * pushed);
176                out.push(self.address(lea, sp, fp, back));
177            }
178        } else if frame.size() > 0 {
179            let add = self.opcode(self.insts.add);
180            out.push(self.arith(add, i64::from(frame.size())));
181        }
182        for &reg in frame.saved_int().iter().rev() {
183            out.push(self.pop(reg));
184        }
185        if frame.frame_pointer() {
186            out.push(self.pop(fp));
187        }
188        let ret = self.opcode(self.insts.ret);
189        out.push(self.func.build_loose(ret).finish());
190        out
191    }
192
193    /// One edit as the instruction that makes it true.
194    fn mov(&mut self, edit: &Edit, frame: &Frame) -> Inst {
195        let moves = self.insts.moves(edit.class).expect("a class the target says how to move");
196        match (edit.mov.to, edit.mov.from) {
197            (Place::Reg(to), Place::Reg(from)) => {
198                let mov = self.opcode(moves.mov);
199                self.func
200                    .build_loose(mov)
201                    .def(Reg::physical(to), edit.class)
202                    .uses(Reg::physical(from), edit.class)
203                    .finish()
204            }
205            (Place::Reg(to), Place::Slot(slot)) => {
206                let at = self.slot(frame, slot);
207                self.load(edit.class, to, at)
208            }
209            (Place::Slot(slot), Place::Reg(from)) => {
210                let at = self.slot(frame, slot);
211                self.store(edit.class, from, at)
212            }
213            // The allocator expands this into two moves through a register of its own, because a
214            // machine that could do it in one is not a machine any of this is written for.
215            (Place::Slot(_), Place::Slot(_)) => {
216                unreachable!("a move from one stack slot straight into another")
217            }
218        }
219    }
220
221    /// Puts an instruction where an edit says it goes, after whatever earlier edits went there.
222    ///
223    /// The edits at one place are in the order they have to be made in, so each one goes behind
224    /// the last, and the first of them is what the place itself means.
225    fn put(&mut self, cursors: &mut Vec<(At, Inst)>, at: At, inst: Inst) {
226        if let Some(cursor) = cursors.iter_mut().find(|(place, _)| *place == at) {
227            self.func.insert_after(cursor.1, inst);
228            cursor.1 = inst;
229            return;
230        }
231        match at {
232            At::Before(before) => self.func.insert_before(before, inst),
233            At::After(after) => self.func.insert_after(after, inst),
234            At::StartOf(block) => self.func.prepend_inst(block, inst),
235            // Behind everything in the block. A block the allocator puts an edge's moves at the
236            // end of is one with a single edge out of it, and an edge like that is not an
237            // instruction here: [`crate::layout`] writes the jump it becomes after this has run.
238            // So the last instruction is an ordinary one, which may still be waiting on moves of
239            // its own that have to be made before the edge's are.
240            At::EndOf(block) => self.func.append_inst(block, inst),
241        }
242        cursors.push((at, inst));
243    }
244
245    /// Where a spill slot is, from the stack pointer in the body of the function.
246    fn slot(&self, frame: &Frame, slot: u32) -> i32 {
247        frame.slot(slot).expect("a slot the frame was worked out from")
248    }
249
250    /// Reads a register out of the frame.
251    fn load(&mut self, class: RegClass, reg: PhysReg, at: i32) -> Inst {
252        let load = self.opcode(self.insts.moves(class).expect("a class to load").load);
253        let base = Operand::read(Reg::physical(self.conv.stack_pointer), self.conv.int_class);
254        self.func
255            .build_loose(load)
256            .def(Reg::physical(reg), class)
257            .mem(Mem::at(base).plus(at))
258            .finish()
259    }
260
261    /// Writes a register into the frame.
262    fn store(&mut self, class: RegClass, reg: PhysReg, at: i32) -> Inst {
263        let store = self.opcode(self.insts.moves(class).expect("a class to store").store);
264        let base = Operand::read(Reg::physical(self.conv.stack_pointer), self.conv.int_class);
265        self.func
266            .build_loose(store)
267            .uses(Reg::physical(reg), class)
268            .mem(Mem::at(base).plus(at))
269            .finish()
270    }
271
272    /// Puts a general purpose register on the stack.
273    fn push(&mut self, reg: PhysReg) -> Inst {
274        let push = self.opcode(self.insts.push);
275        self.func.build_loose(push).uses(Reg::physical(reg), self.conv.int_class).finish()
276    }
277
278    /// Takes a general purpose register back off the stack.
279    fn pop(&mut self, reg: PhysReg) -> Inst {
280        let pop = self.opcode(self.insts.pop);
281        self.func.build_loose(pop).def(Reg::physical(reg), self.conv.int_class).finish()
282    }
283
284    /// One general purpose register written with another.
285    fn two(&mut self, opcode: Opcode, to: PhysReg, from: PhysReg) -> Inst {
286        let class = self.conv.int_class;
287        self.func
288            .build_loose(opcode)
289            .def(Reg::physical(to), class)
290            .uses(Reg::physical(from), class)
291            .finish()
292    }
293
294    /// Two-address arithmetic on the stack pointer, which reads it and writes it back.
295    fn arith(&mut self, opcode: Opcode, value: i64) -> Inst {
296        let class = self.conv.int_class;
297        let sp = Reg::physical(self.conv.stack_pointer);
298        self.func.build_loose(opcode).def(sp, class).uses(sp, class).imm(value).finish()
299    }
300
301    /// One register written with an address rather than with what is at it.
302    fn address(&mut self, opcode: Opcode, to: PhysReg, base: PhysReg, disp: i32) -> Inst {
303        let class = self.conv.int_class;
304        let base = Operand::read(Reg::physical(base), class);
305        self.func
306            .build_loose(opcode)
307            .def(Reg::physical(to), class)
308            .mem(Mem::at(base).plus(disp))
309            .finish()
310    }
311
312    /// The opcode of that name, in the machine IR's spelling, which is the target's prefix and
313    /// then the name the target gave.
314    fn opcode(&mut self, name: &str) -> Opcode {
315        Opcode::new(self.names.intern(&format!("{}{name}", self.insts.prefix)))
316    }
317}
318
319/// A distance in a frame, as the signed number every offset is.
320fn offset(bytes: u32) -> i32 {
321    i32::try_from(bytes).expect("a frame under two gigabytes")
322}
323
324#[cfg(test)]
325mod tests {
326    use rucc_base::Interner;
327    use rucc_mir::{BlockCall, print_func};
328    use rucc_regalloc::assign::Env;
329    use rucc_target::x86_64::{FRAME, GPR, REGS, SYSV, WIN64, XMM, xmm};
330
331    use super::*;
332    use crate::frame::{Layout, Local};
333
334    /// An environment offering that many of the convention's registers, with everything after
335    /// them held back as scratch.
336    fn env(conv: &CallRegs, count: usize) -> Env {
337        Env::new().with(GPR, &conv.int_order[..count], &conv.int_order[count..])
338    }
339
340    /// A function of that many values, every one written before any is read, allocated with that
341    /// many registers to hand out. The same shape the frame layout's own tests are written
342    /// against, so that a frame here is one that has already been checked there.
343    fn pressure(conv: &CallRegs, values: usize, count: usize) -> (Func, Allocation, Interner) {
344        let mut names = Interner::new();
345        let mut func = Func::new(names.intern("f"));
346        let opcode = Opcode::new(names.intern("x64.nop"));
347        let block = func.create_block();
348        let regs: Vec<Reg> = (0..values).map(|_| func.new_vreg(GPR)).collect();
349        for &reg in &regs {
350            func.build(block, opcode).def(reg, GPR).finish();
351        }
352        for &reg in &regs {
353            func.build(block, opcode).uses(reg, GPR).finish();
354        }
355        let allocation = rucc_regalloc::run(&mut func, &env(conv, count));
356        (func, allocation, names)
357    }
358
359    /// The function with its frame written into it, as the lines a dump would show.
360    fn written(
361        func: &mut Func,
362        allocation: &Allocation,
363        layout: &Layout<'_>,
364        names: &mut Interner,
365    ) -> Vec<String> {
366        let frame = Frame::of(func, allocation, layout);
367        finish(func, allocation, &frame, &[], layout.conv, &FRAME, names);
368        print_func(func, names, &REGS)
369            .lines()
370            .filter(|line| !line.is_empty())
371            .map(|line| line.trim().to_string())
372            .collect()
373    }
374
375    /// Just the lines the frame put in, which is every line that is not the function it was
376    /// given and not the shape of the dump around it.
377    fn added(lines: &[String]) -> Vec<&str> {
378        lines
379            .iter()
380            .map(String::as_str)
381            .filter(|line| !line.contains("x64.nop"))
382            .filter(|line| !line.starts_with("mfunc") && !line.starts_with("block") && *line != "}")
383            .collect()
384    }
385
386    #[test]
387    fn a_function_that_needs_no_frame_is_given_a_return_and_nothing_else() {
388        let (mut func, allocation, mut names) = pressure(&SYSV, 2, 4);
389        let lines = written(&mut func, &allocation, &Layout::new(&SYSV, REGS), &mut names);
390
391        // Two values and four registers, so nothing is spilled, nothing is saved and the stack
392        // pointer never moves. A prologue of nothing is the right prologue for that.
393        assert_eq!(added(&lines), ["x64.ret"]);
394    }
395
396    #[test]
397    fn a_spill_is_a_store_and_a_reload_is_a_load() {
398        let (mut func, allocation, mut names) = pressure(&SYSV, 4, 2);
399        let lines = written(&mut func, &allocation, &Layout::new(&SYSV, REGS), &mut names);
400
401        // Two registers for four values, so two of them go to the stack. The store goes behind the
402        // instruction that wrote the value and the load in front of the one that wants it, both at
403        // the offsets the frame gave, which are below the stack pointer because a small leaf
404        // function is entitled to the red zone.
405        assert_eq!(
406            lines,
407            [
408                "mfunc @f {",
409                "block0:",
410                "$rax = x64.nop",
411                "$rcx = x64.nop",
412                "$rdx = x64.nop",
413                "x64.mov_mr_64 $rdx, [$rsp - 16]",
414                "$rdx = x64.nop",
415                "x64.mov_mr_64 $rdx, [$rsp - 8]",
416                "x64.nop $rax",
417                "x64.nop $rcx",
418                "$rdx = x64.mov_rm_64 [$rsp - 16]",
419                "x64.nop $rdx",
420                "$rdx = x64.mov_rm_64 [$rsp - 8]",
421                "x64.nop $rdx",
422                "x64.ret",
423                "}",
424            ]
425        );
426    }
427
428    #[test]
429    fn the_frame_the_prologue_takes_is_the_frame_the_epilogue_gives_back() {
430        let (mut func, allocation, mut names) = pressure(&SYSV, 4, 2);
431        let base = Layout::new(&SYSV, REGS);
432        let layout = Layout { red_zone: false, ..base };
433        let lines = written(&mut func, &allocation, &layout, &mut names);
434
435        // The same function told it may not use the red zone takes sixteen bytes instead, and
436        // every offset moves above the stack pointer to match.
437        assert_eq!(
438            added(&lines),
439            [
440                "$rsp = x64.sub_ri_64 $rsp, 16",
441                "x64.mov_mr_64 $rdx, [$rsp]",
442                "x64.mov_mr_64 $rdx, [$rsp + 8]",
443                "$rdx = x64.mov_rm_64 [$rsp]",
444                "$rdx = x64.mov_rm_64 [$rsp + 8]",
445                "$rsp = x64.add_ri_64 $rsp, 16",
446                "x64.ret",
447            ]
448        );
449    }
450
451    #[test]
452    fn the_registers_the_prologue_pushes_come_back_in_the_opposite_order() {
453        let (mut func, allocation, mut names) = pressure(&SYSV, 13, 13);
454        let lines = written(&mut func, &allocation, &Layout::new(&SYSV, REGS), &mut names);
455
456        // Four registers a call leaves alone, pushed in the convention's order and popped in the
457        // other one, which is the only order that gets each of them its own value back.
458        assert_eq!(
459            added(&lines),
460            [
461                "x64.push_64 $rbx",
462                "x64.push_64 $r12",
463                "x64.push_64 $r13",
464                "x64.push_64 $r14",
465                "$r14 = x64.pop_64",
466                "$r13 = x64.pop_64",
467                "$r12 = x64.pop_64",
468                "$rbx = x64.pop_64",
469                "x64.ret",
470            ]
471        );
472    }
473
474    #[test]
475    fn a_function_that_keeps_a_frame_pointer_sets_it_up_and_leaves_by_it() {
476        let (mut func, allocation, mut names) = pressure(&SYSV, 4, 2);
477        let base = Layout::new(&SYSV, REGS);
478        let layout = Layout { frame_pointer: true, red_zone: false, ..base };
479        let lines = written(&mut func, &allocation, &layout, &mut names);
480
481        // The frame pointer is saved before anything else and points at where it was saved, so the
482        // epilogue reaches the stack pointer through it rather than by counting the frame back.
483        assert_eq!(
484            added(&lines),
485            [
486                "x64.push_64 $rbp",
487                "$rbp = x64.mov_rr_64 $rsp",
488                "$rsp = x64.sub_ri_64 $rsp, 16",
489                "x64.mov_mr_64 $rdx, [$rsp]",
490                "x64.mov_mr_64 $rdx, [$rsp + 8]",
491                "$rdx = x64.mov_rm_64 [$rsp]",
492                "$rdx = x64.mov_rm_64 [$rsp + 8]",
493                "$rsp = x64.mov_rr_64 $rbp",
494                "$rbp = x64.pop_64",
495                "x64.ret",
496            ]
497        );
498    }
499
500    #[test]
501    fn a_realigned_frame_forces_the_alignment_after_it_has_pushed_what_it_saves() {
502        let (mut func, allocation, mut names) = pressure(&SYSV, 13, 13);
503        let locals = [Local { size: 64, align: 32 }];
504        let base = Layout::new(&SYSV, REGS);
505        let layout = Layout { locals: &locals, ..base };
506        let lines = written(&mut func, &allocation, &layout, &mut names);
507
508        // Forcing the alignment throws away how far the stack pointer had moved, so the registers
509        // are pushed before it happens and the epilogue counts back from the frame pointer to find
510        // them. The frame pointer is required here whatever the flags said.
511        assert_eq!(
512            added(&lines),
513            [
514                "x64.push_64 $rbp",
515                "$rbp = x64.mov_rr_64 $rsp",
516                "x64.push_64 $rbx",
517                "x64.push_64 $r12",
518                "x64.push_64 $r13",
519                "x64.push_64 $r14",
520                "$rsp = x64.and_ri_64 $rsp, -32",
521                "$rsp = x64.sub_ri_64 $rsp, 64",
522                "$rsp = x64.lea_64 [$rbp - 32]",
523                "$r14 = x64.pop_64",
524                "$r13 = x64.pop_64",
525                "$r12 = x64.pop_64",
526                "$rbx = x64.pop_64",
527                "$rbp = x64.pop_64",
528                "x64.ret",
529            ]
530        );
531    }
532
533    #[test]
534    fn every_block_the_function_returns_from_gets_an_epilogue() {
535        let mut names = Interner::new();
536        let mut func = Func::new(names.intern("f"));
537        let opcode = Opcode::new(names.intern("x64.nop"));
538        let head = func.create_block();
539        let left = func.create_block();
540        let right = func.create_block();
541        func.build(head, opcode).finish();
542        *func.succs_mut(head) = vec![BlockCall::to(left), BlockCall::to(right)];
543        func.build(left, opcode).finish();
544        func.build(right, opcode).finish();
545        let allocation = rucc_regalloc::run(&mut func, &env(&SYSV, 4));
546        let base = Layout::new(&SYSV, REGS);
547        let layout = Layout { leaf: false, ..base };
548        let lines = written(&mut func, &allocation, &layout, &mut names);
549
550        // Both ways out get the frame given back, and the block that goes somewhere gets nothing,
551        // because a block with an edge out of it is not a block anything returns from.
552        assert_eq!(
553            lines,
554            [
555                "mfunc @f {",
556                "block0:",
557                "$rsp = x64.sub_ri_64 $rsp, 8",
558                "x64.nop block1, block2",
559                "block1:",
560                "x64.nop",
561                "$rsp = x64.add_ri_64 $rsp, 8",
562                "x64.ret",
563                "block2:",
564                "x64.nop",
565                "$rsp = x64.add_ri_64 $rsp, 8",
566                "x64.ret",
567                "}",
568            ]
569        );
570    }
571
572    #[test]
573    fn a_vector_register_a_windows_call_preserves_is_stored_and_read_back() {
574        let mut names = Interner::new();
575        let mut func = Func::new(names.intern("f"));
576        let opcode = Opcode::new(names.intern("x64.nop"));
577        let block = func.create_block();
578        // An instruction that writes one of the vector registers Windows preserves, which is what
579        // a rule for something that has to use it produces.
580        func.build(block, opcode).operand(Operand::write(Reg::physical(xmm(6)), XMM)).finish();
581        let allocation = rucc_regalloc::run(&mut func, &env(&WIN64, 4));
582        let lines = written(&mut func, &allocation, &Layout::new(&WIN64, REGS), &mut names);
583
584        // No machine here pushes a vector register, so it is stored into the frame rather than
585        // pushed, and the frame has to be taken before there is anywhere to put it.
586        assert_eq!(
587            added(&lines),
588            [
589                "$rsp = x64.sub_ri_64 $rsp, 24",
590                "x64.movaps_mr $xmm6, [$rsp]",
591                "$xmm6 = x64.movaps_rm [$rsp]",
592                "$rsp = x64.add_ri_64 $rsp, 24",
593                "x64.ret",
594            ]
595        );
596    }
597}