summaryrefslogtreecommitdiff
path: root/yjit/src/backend/ir.rs
diff options
context:
space:
mode:
Diffstat (limited to 'yjit/src/backend/ir.rs')
-rw-r--r--yjit/src/backend/ir.rs496
1 files changed, 365 insertions, 131 deletions
diff --git a/yjit/src/backend/ir.rs b/yjit/src/backend/ir.rs
index 8c4a6c1b6d..edc0eaf390 100644
--- a/yjit/src/backend/ir.rs
+++ b/yjit/src/backend/ir.rs
@@ -1,26 +1,16 @@
-#![allow(dead_code)]
-#![allow(unused_variables)]
-#![allow(unused_imports)]
-
-use std::cell::Cell;
use std::collections::HashMap;
use std::fmt;
use std::convert::From;
-use std::io::Write;
use std::mem::take;
use crate::codegen::{gen_outlined_exit, gen_counted_exit};
-use crate::cruby::{VALUE, SIZEOF_VALUE_I32};
-use crate::virtualmem::{CodePtr};
-use crate::asm::{CodeBlock, uimm_num_bits, imm_num_bits, OutlinedCb};
-use crate::core::{Context, Type, TempMapping, RegTemps, MAX_REG_TEMPS, MAX_TEMP_TYPES};
+use crate::cruby::{vm_stack_canary, SIZEOF_VALUE_I32, VALUE};
+use crate::virtualmem::CodePtr;
+use crate::asm::{CodeBlock, OutlinedCb};
+use crate::core::{Context, RegTemps, MAX_REG_TEMPS};
use crate::options::*;
use crate::stats::*;
-#[cfg(target_arch = "x86_64")]
-use crate::backend::x86_64::*;
-
-#[cfg(target_arch = "aarch64")]
-use crate::backend::arm64::*;
+use crate::backend::current::*;
pub const EC: Opnd = _EC;
pub const CFP: Opnd = _CFP;
@@ -28,6 +18,7 @@ pub const SP: Opnd = _SP;
pub const C_ARG_OPNDS: [Opnd; 6] = _C_ARG_OPNDS;
pub const C_RET_OPND: Opnd = _C_RET_OPND;
+pub use crate::backend::current::{Reg, C_RET_REG};
// Memory operand base
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
@@ -72,6 +63,9 @@ pub enum Opnd
// Immediate Ruby value, may be GC'd, movable
Value(VALUE),
+ /// C argument register. The alloc_regs resolves its register dependencies.
+ CArg(Reg),
+
// Output of a preceding instruction in this block
InsnOut{ idx: usize, num_bits: u8 },
@@ -102,6 +96,7 @@ impl fmt::Debug for Opnd {
match self {
Self::None => write!(fmt, "None"),
Value(val) => write!(fmt, "Value({val:?})"),
+ CArg(reg) => write!(fmt, "CArg({reg:?})"),
Stack { idx, sp_offset, .. } => write!(fmt, "SP[{}]", *sp_offset as i32 - idx - 1),
InsnOut { idx, num_bits } => write!(fmt, "Out{num_bits}({idx})"),
Imm(signed) => write!(fmt, "{signed:x}_i64"),
@@ -145,10 +140,11 @@ impl Opnd
Opnd::UImm(ptr as u64)
}
- pub fn is_some(&self) -> bool {
- match *self {
- Opnd::None => false,
- _ => true,
+ /// Constructor for a C argument operand
+ pub fn c_arg(reg_opnd: Opnd) -> Self {
+ match reg_opnd {
+ Opnd::Reg(reg) => Opnd::CArg(reg),
+ _ => unreachable!(),
}
}
@@ -233,11 +229,16 @@ impl Opnd
/// Calculate Opnd::Stack's index from the stack bottom.
pub fn stack_idx(&self) -> u8 {
+ self.get_stack_idx().unwrap()
+ }
+
+ /// Calculate Opnd::Stack's index from the stack bottom if it's Opnd::Stack.
+ pub fn get_stack_idx(&self) -> Option<u8> {
match self {
Opnd::Stack { idx, stack_size, .. } => {
- (*stack_size as isize - *idx as isize - 1) as u8
+ Some((*stack_size as isize - *idx as isize - 1) as u8)
},
- _ => unreachable!(),
+ _ => None
}
}
@@ -331,7 +332,7 @@ impl From<CodePtr> for Target {
}
}
-type PosMarkerFn = Box<dyn Fn(CodePtr)>;
+type PosMarkerFn = Box<dyn Fn(CodePtr, &CodeBlock)>;
/// YJIT IR instruction
pub enum Insn {
@@ -346,6 +347,7 @@ pub enum Insn {
BakeString(String),
// Trigger a debugger breakpoint
+ #[allow(dead_code)]
Breakpoint,
/// Add a comment into the IR at the point that this instruction is added.
@@ -411,15 +413,24 @@ pub enum Insn {
// Produces no output
IncrCounter { mem: Opnd, value: Opnd },
- /// Jump if below or equal
+ /// Jump if below or equal (unsigned)
Jbe(Target),
+ /// Jump if below (unsigned)
+ Jb(Target),
+
/// Jump if equal
Je(Target),
/// Jump if lower
Jl(Target),
+ /// Jump if greater
+ Jg(Target),
+
+ /// Jump if greater or equal
+ Jge(Target),
+
// Unconditional jump to a branch target
Jmp(Target),
@@ -435,15 +446,23 @@ pub enum Insn {
/// Jump if overflow
Jo(Target),
+ /// Jump if overflow in multiplication
+ JoMul(Target),
+
/// Jump if zero
Jz(Target),
+ /// Jump if operand is zero (only used during lowering at the moment)
+ Joz(Opnd, Target),
+
+ /// Jump if operand is non-zero (only used during lowering at the moment)
+ Jonz(Opnd, Target),
+
// Add a label into the IR at the point that this instruction is added.
Label(Target),
- // Load effective address relative to the current instruction pointer. It
- // accepts a single signed immediate operand.
- LeaLabel { target: Target, out: Opnd },
+ /// Get the code address of a jump target
+ LeaJumpTarget { target: Target, out: Opnd },
// Load effective address
Lea { opnd: Opnd, out: Opnd },
@@ -489,9 +508,12 @@ pub enum Insn {
// Low-level instruction to store a value to memory.
Store { dest: Opnd, src: Opnd },
- // This is the same as the OP_ADD instruction, except for subtraction.
+ // This is the same as the add instruction, except for subtraction.
Sub { left: Opnd, right: Opnd, out: Opnd },
+ // Integer multiplication
+ Mul { left: Opnd, right: Opnd, out: Opnd },
+
// Bitwise AND test instruction
Test { left: Opnd, right: Opnd },
@@ -520,15 +542,21 @@ impl Insn {
pub(super) fn target_mut(&mut self) -> Option<&mut Target> {
match self {
Insn::Jbe(target) |
+ Insn::Jb(target) |
Insn::Je(target) |
Insn::Jl(target) |
+ Insn::Jg(target) |
+ Insn::Jge(target) |
Insn::Jmp(target) |
Insn::Jne(target) |
Insn::Jnz(target) |
Insn::Jo(target) |
Insn::Jz(target) |
Insn::Label(target) |
- Insn::LeaLabel { target, .. } => {
+ Insn::JoMul(target) |
+ Insn::Joz(_, target) |
+ Insn::Jonz(_, target) |
+ Insn::LeaJumpTarget { target, .. } => {
Some(target)
}
_ => None,
@@ -564,16 +592,22 @@ impl Insn {
Insn::FrameTeardown => "FrameTeardown",
Insn::IncrCounter { .. } => "IncrCounter",
Insn::Jbe(_) => "Jbe",
+ Insn::Jb(_) => "Jb",
Insn::Je(_) => "Je",
Insn::Jl(_) => "Jl",
+ Insn::Jg(_) => "Jg",
+ Insn::Jge(_) => "Jge",
Insn::Jmp(_) => "Jmp",
Insn::JmpOpnd(_) => "JmpOpnd",
Insn::Jne(_) => "Jne",
Insn::Jnz(_) => "Jnz",
Insn::Jo(_) => "Jo",
+ Insn::JoMul(_) => "JoMul",
Insn::Jz(_) => "Jz",
+ Insn::Joz(..) => "Joz",
+ Insn::Jonz(..) => "Jonz",
Insn::Label(_) => "Label",
- Insn::LeaLabel { .. } => "LeaLabel",
+ Insn::LeaJumpTarget { .. } => "LeaJumpTarget",
Insn::Lea { .. } => "Lea",
Insn::LiveReg { .. } => "LiveReg",
Insn::Load { .. } => "Load",
@@ -588,6 +622,7 @@ impl Insn {
Insn::RShift { .. } => "RShift",
Insn::Store { .. } => "Store",
Insn::Sub { .. } => "Sub",
+ Insn::Mul { .. } => "Mul",
Insn::Test { .. } => "Test",
Insn::URShift { .. } => "URShift",
Insn::Xor { .. } => "Xor"
@@ -611,7 +646,7 @@ impl Insn {
Insn::CSelNZ { out, .. } |
Insn::CSelZ { out, .. } |
Insn::Lea { out, .. } |
- Insn::LeaLabel { out, .. } |
+ Insn::LeaJumpTarget { out, .. } |
Insn::LiveReg { out, .. } |
Insn::Load { out, .. } |
Insn::LoadSExt { out, .. } |
@@ -620,6 +655,7 @@ impl Insn {
Insn::Or { out, .. } |
Insn::RShift { out, .. } |
Insn::Sub { out, .. } |
+ Insn::Mul { out, .. } |
Insn::URShift { out, .. } |
Insn::Xor { out, .. } => Some(out),
_ => None
@@ -643,7 +679,7 @@ impl Insn {
Insn::CSelNZ { out, .. } |
Insn::CSelZ { out, .. } |
Insn::Lea { out, .. } |
- Insn::LeaLabel { out, .. } |
+ Insn::LeaJumpTarget { out, .. } |
Insn::LiveReg { out, .. } |
Insn::Load { out, .. } |
Insn::LoadSExt { out, .. } |
@@ -652,6 +688,7 @@ impl Insn {
Insn::Or { out, .. } |
Insn::RShift { out, .. } |
Insn::Sub { out, .. } |
+ Insn::Mul { out, .. } |
Insn::URShift { out, .. } |
Insn::Xor { out, .. } => Some(out),
_ => None
@@ -662,14 +699,17 @@ impl Insn {
pub fn target(&self) -> Option<&Target> {
match self {
Insn::Jbe(target) |
+ Insn::Jb(target) |
Insn::Je(target) |
Insn::Jl(target) |
+ Insn::Jg(target) |
+ Insn::Jge(target) |
Insn::Jmp(target) |
Insn::Jne(target) |
Insn::Jnz(target) |
Insn::Jo(target) |
Insn::Jz(target) |
- Insn::LeaLabel { target, .. } => Some(target),
+ Insn::LeaJumpTarget { target, .. } => Some(target),
_ => None
}
}
@@ -711,17 +751,22 @@ impl<'a> Iterator for InsnOpndIterator<'a> {
Insn::FrameSetup |
Insn::FrameTeardown |
Insn::Jbe(_) |
+ Insn::Jb(_) |
Insn::Je(_) |
Insn::Jl(_) |
+ Insn::Jg(_) |
+ Insn::Jge(_) |
Insn::Jmp(_) |
Insn::Jne(_) |
Insn::Jnz(_) |
Insn::Jo(_) |
+ Insn::JoMul(_) |
Insn::Jz(_) |
Insn::Label(_) |
- Insn::LeaLabel { .. } |
+ Insn::LeaJumpTarget { .. } |
Insn::PadInvalPatch |
Insn::PosMarker(_) => None,
+
Insn::CPopInto(opnd) |
Insn::CPush(opnd) |
Insn::CRet(opnd) |
@@ -730,6 +775,8 @@ impl<'a> Iterator for InsnOpndIterator<'a> {
Insn::LiveReg { opnd, .. } |
Insn::Load { opnd, .. } |
Insn::LoadSExt { opnd, .. } |
+ Insn::Joz(opnd, _) |
+ Insn::Jonz(opnd, _) |
Insn::Not { opnd, .. } => {
match self.idx {
0 => {
@@ -758,6 +805,7 @@ impl<'a> Iterator for InsnOpndIterator<'a> {
Insn::RShift { opnd: opnd0, shift: opnd1, .. } |
Insn::Store { dest: opnd0, src: opnd1 } |
Insn::Sub { left: opnd0, right: opnd1, .. } |
+ Insn::Mul { left: opnd0, right: opnd1, .. } |
Insn::Test { left: opnd0, right: opnd1 } |
Insn::URShift { opnd: opnd0, shift: opnd1, .. } |
Insn::Xor { left: opnd0, right: opnd1, .. } => {
@@ -808,17 +856,22 @@ impl<'a> InsnOpndMutIterator<'a> {
Insn::FrameSetup |
Insn::FrameTeardown |
Insn::Jbe(_) |
+ Insn::Jb(_) |
Insn::Je(_) |
Insn::Jl(_) |
+ Insn::Jg(_) |
+ Insn::Jge(_) |
Insn::Jmp(_) |
Insn::Jne(_) |
Insn::Jnz(_) |
Insn::Jo(_) |
+ Insn::JoMul(_) |
Insn::Jz(_) |
Insn::Label(_) |
- Insn::LeaLabel { .. } |
+ Insn::LeaJumpTarget { .. } |
Insn::PadInvalPatch |
Insn::PosMarker(_) => None,
+
Insn::CPopInto(opnd) |
Insn::CPush(opnd) |
Insn::CRet(opnd) |
@@ -827,6 +880,8 @@ impl<'a> InsnOpndMutIterator<'a> {
Insn::LiveReg { opnd, .. } |
Insn::Load { opnd, .. } |
Insn::LoadSExt { opnd, .. } |
+ Insn::Joz(opnd, _) |
+ Insn::Jonz(opnd, _) |
Insn::Not { opnd, .. } => {
match self.idx {
0 => {
@@ -855,6 +910,7 @@ impl<'a> InsnOpndMutIterator<'a> {
Insn::RShift { opnd: opnd0, shift: opnd1, .. } |
Insn::Store { dest: opnd0, src: opnd1 } |
Insn::Sub { left: opnd0, right: opnd1, .. } |
+ Insn::Mul { left: opnd0, right: opnd1, .. } |
Insn::Test { left: opnd0, right: opnd1 } |
Insn::URShift { opnd: opnd0, shift: opnd1, .. } |
Insn::Xor { left: opnd0, right: opnd1, .. } => {
@@ -915,10 +971,51 @@ pub struct SideExitContext {
/// PC of the instruction being compiled
pub pc: *mut VALUE,
- /// Context when it started to compile the instruction
- pub ctx: Context,
+ /// Context fields used by get_generic_ctx()
+ pub stack_size: u8,
+ pub sp_offset: i8,
+ pub reg_temps: RegTemps,
+ pub is_return_landing: bool,
+ pub is_deferred: bool,
+}
+
+impl SideExitContext {
+ /// Convert PC and Context into SideExitContext
+ pub fn new(pc: *mut VALUE, ctx: Context) -> Self {
+ let exit_ctx = SideExitContext {
+ pc,
+ stack_size: ctx.get_stack_size(),
+ sp_offset: ctx.get_sp_offset(),
+ reg_temps: ctx.get_reg_temps(),
+ is_return_landing: ctx.is_return_landing(),
+ is_deferred: ctx.is_deferred(),
+ };
+ if cfg!(debug_assertions) {
+ // Assert that we're not losing any mandatory metadata
+ assert_eq!(exit_ctx.get_ctx(), ctx.get_generic_ctx());
+ }
+ exit_ctx
+ }
+
+ /// Convert SideExitContext to Context
+ fn get_ctx(&self) -> Context {
+ let mut ctx = Context::default();
+ ctx.set_stack_size(self.stack_size);
+ ctx.set_sp_offset(self.sp_offset);
+ ctx.set_reg_temps(self.reg_temps);
+ if self.is_return_landing {
+ ctx.set_as_return_landing();
+ }
+ if self.is_deferred {
+ ctx.mark_as_deferred();
+ }
+ ctx
+ }
}
+/// Initial capacity for asm.insns vector
+const ASSEMBLER_INSNS_CAPACITY: usize = 256;
+
/// Object into which we assemble instructions to be
/// optimized and lowered
pub struct Assembler {
@@ -942,6 +1039,9 @@ pub struct Assembler {
/// Stack size for Target::SideExit
side_exit_stack_size: Option<u8>,
+
+ /// If true, the next ccall() should verify its leafness
+ leaf_ccall: bool,
}
impl Assembler
@@ -952,21 +1052,21 @@ impl Assembler
pub fn new_with_label_names(label_names: Vec<String>, side_exits: HashMap<SideExitContext, CodePtr>) -> Self {
Self {
- insns: Vec::default(),
- live_ranges: Vec::default(),
+ insns: Vec::with_capacity(ASSEMBLER_INSNS_CAPACITY),
+ live_ranges: Vec::with_capacity(ASSEMBLER_INSNS_CAPACITY),
label_names,
ctx: Context::default(),
side_exits,
side_exit_pc: None,
side_exit_stack_size: None,
+ leaf_ccall: false,
}
}
/// Get the list of registers that can be used for stack temps.
- pub fn get_temp_regs() -> Vec<Reg> {
+ pub fn get_temp_regs() -> &'static [Reg] {
let num_regs = get_option!(num_temp_regs);
- let mut regs = Self::TEMP_REGS.to_vec();
- regs.drain(0..num_regs).collect()
+ &TEMP_REGS[0..num_regs]
}
/// Set a context for generating side exits
@@ -984,11 +1084,10 @@ impl Assembler
/// Append an instruction onto the current list of instructions and update
/// the live ranges of any instructions whose outputs are being used as
/// operands to this instruction.
- pub(super) fn push_insn(&mut self, insn: Insn) {
+ pub fn push_insn(&mut self, mut insn: Insn) {
// Index of this instruction
let insn_idx = self.insns.len();
- let mut insn = insn;
let mut opnd_iter = insn.opnd_iter_mut();
while let Some(opnd) = opnd_iter.next() {
match opnd {
@@ -1004,6 +1103,12 @@ impl Assembler
}
// Set current ctx.reg_temps to Opnd::Stack.
Opnd::Stack { idx, num_bits, stack_size, sp_offset, reg_temps: None } => {
+ assert_eq!(
+ self.ctx.get_stack_size() as i16 - self.ctx.get_sp_offset() as i16,
+ *stack_size as i16 - *sp_offset as i16,
+ "Opnd::Stack (stack_size: {}, sp_offset: {}) expects a different SP position from asm.ctx (stack_size: {}, sp_offset: {})",
+ *stack_size, *sp_offset, self.ctx.get_stack_size(), self.ctx.get_sp_offset(),
+ );
*opnd = Opnd::Stack {
idx: *idx,
num_bits: *num_bits,
@@ -1017,14 +1122,13 @@ impl Assembler
}
// Set a side exit context to Target::SideExit
- let mut insn = insn;
if let Some(Target::SideExit { context, .. }) = insn.target_mut() {
// We should skip this when this instruction is being copied from another Assembler.
if context.is_none() {
- *context = Some(SideExitContext {
- pc: self.side_exit_pc.unwrap(),
- ctx: self.ctx.with_stack_size(self.side_exit_stack_size.unwrap()),
- });
+ *context = Some(SideExitContext::new(
+ self.side_exit_pc.unwrap(),
+ self.ctx.with_stack_size(self.side_exit_stack_size.unwrap()),
+ ));
}
}
@@ -1033,23 +1137,19 @@ impl Assembler
}
/// Get a cached side exit, wrapping a counter if specified
- pub fn get_side_exit(&mut self, side_exit_context: &SideExitContext, counter: Option<Counter>, ocb: &mut OutlinedCb) -> CodePtr {
- // Drop type information from a cache key
- let mut side_exit_context = side_exit_context.clone();
- side_exit_context.ctx = side_exit_context.ctx.get_generic_ctx();
-
+ pub fn get_side_exit(&mut self, side_exit_context: &SideExitContext, counter: Option<Counter>, ocb: &mut OutlinedCb) -> Option<CodePtr> {
// Get a cached side exit
let side_exit = match self.side_exits.get(&side_exit_context) {
None => {
- let exit_code = gen_outlined_exit(side_exit_context.pc, &side_exit_context.ctx, ocb);
- self.side_exits.insert(side_exit_context.clone(), exit_code);
+ let exit_code = gen_outlined_exit(side_exit_context.pc, &side_exit_context.get_ctx(), ocb)?;
+ self.side_exits.insert(*side_exit_context, exit_code);
exit_code
}
Some(code_ptr) => *code_ptr,
};
// Wrap a counter if needed
- gen_counted_exit(side_exit, ocb, counter)
+ gen_counted_exit(side_exit_context.pc, side_exit, ocb, counter)
}
/// Create a new label instance that we can jump to
@@ -1086,7 +1186,7 @@ impl Assembler
}
match opnd {
- Opnd::Stack { idx, num_bits, stack_size, sp_offset, reg_temps } => {
+ Opnd::Stack { reg_temps, .. } => {
if opnd.stack_idx() < MAX_REG_TEMPS && reg_temps.unwrap().get(opnd.stack_idx()) {
reg_opnd(opnd)
} else {
@@ -1113,6 +1213,13 @@ impl Assembler
}
}
+ /// Erase local variable type information
+ /// eg: because of a call we can't track
+ pub fn clear_local_types(&mut self) {
+ asm_comment!(self, "clear local variable types");
+ self.ctx.clear_local_types();
+ }
+
/// Spill all live stack temps from registers to the stack
pub fn spill_temps(&mut self) {
// Forget registers above the stack top
@@ -1124,7 +1231,7 @@ impl Assembler
// Spill live stack temps
if self.ctx.get_reg_temps() != RegTemps::default() {
- self.comment(&format!("spill_temps: {:08b} -> {:08b}", self.ctx.get_reg_temps().as_u8(), RegTemps::default().as_u8()));
+ asm_comment!(self, "spill_temps: {:08b} -> {:08b}", self.ctx.get_reg_temps().as_u8(), RegTemps::default().as_u8());
for stack_idx in 0..u8::min(MAX_REG_TEMPS, self.ctx.get_stack_size()) {
if self.ctx.get_reg_temps().get(stack_idx) {
let idx = self.ctx.get_stack_size() - 1 - stack_idx;
@@ -1164,7 +1271,7 @@ impl Assembler
/// Update which stack temps are in a register
pub fn set_reg_temps(&mut self, reg_temps: RegTemps) {
if self.ctx.get_reg_temps() != reg_temps {
- self.comment(&format!("reg_temps: {:08b} -> {:08b}", self.ctx.get_reg_temps().as_u8(), reg_temps.as_u8()));
+ asm_comment!(self, "reg_temps: {:08b} -> {:08b}", self.ctx.get_reg_temps().as_u8(), reg_temps.as_u8());
self.ctx.set_reg_temps(reg_temps);
self.verify_reg_temps();
}
@@ -1224,6 +1331,55 @@ impl Assembler
}
}
+ // Reorder C argument moves, sometimes adding extra moves using SCRATCH_REG,
+ // so that they will not rewrite each other before they are used.
+ fn reorder_c_args(c_args: &Vec<(Reg, Opnd)>) -> Vec<(Reg, Opnd)> {
+ // Return the index of a move whose destination is not used as a source if any.
+ fn find_safe_arg(c_args: &Vec<(Reg, Opnd)>) -> Option<usize> {
+ c_args.iter().enumerate().find(|(_, &(dest_reg, _))| {
+ c_args.iter().all(|&(_, src_opnd)| src_opnd != Opnd::Reg(dest_reg))
+ }).map(|(index, _)| index)
+ }
+
+ // Remove moves whose source and destination are the same
+ let mut c_args: Vec<(Reg, Opnd)> = c_args.clone().into_iter()
+ .filter(|&(reg, opnd)| Opnd::Reg(reg) != opnd).collect();
+
+ let mut moves = vec![];
+ while c_args.len() > 0 {
+ // Keep taking safe moves
+ while let Some(index) = find_safe_arg(&c_args) {
+ moves.push(c_args.remove(index));
+ }
+
+ // No safe move. Load the source of one move into SCRATCH_REG, and
+ // then load SCRATCH_REG into the destination when it's safe.
+ if c_args.len() > 0 {
+ // Make sure it's safe to use SCRATCH_REG
+ assert!(c_args.iter().all(|&(_, opnd)| opnd != Opnd::Reg(Assembler::SCRATCH_REG)));
+
+ // Move SCRATCH <- opnd, and delay reg <- SCRATCH
+ let (reg, opnd) = c_args.remove(0);
+ moves.push((Assembler::SCRATCH_REG, opnd));
+ c_args.push((reg, Opnd::Reg(Assembler::SCRATCH_REG)));
+ }
+ }
+ moves
+ }
+
+ // Adjust the number of entries in live_ranges so that it can be indexed by mapped indexes.
+ fn shift_live_ranges(live_ranges: &mut Vec<usize>, start_index: usize, shift_offset: isize) {
+ if shift_offset >= 0 {
+ for index in 0..(shift_offset as usize) {
+ live_ranges.insert(start_index + index, start_index + index);
+ }
+ } else {
+ for _ in 0..-shift_offset {
+ live_ranges.remove(start_index);
+ }
+ }
+ }
+
// Dump live registers for register spill debugging.
fn dump_live_regs(insns: Vec<Insn>, live_ranges: Vec<usize>, num_regs: usize, spill_index: usize) {
// Convert live_ranges to live_regs: the number of live registers at each index
@@ -1247,11 +1403,18 @@ impl Assembler
}
}
+ // We may need to reorder LoadInto instructions with a C argument operand.
+ // This buffers the operands of such instructions to process them in batches.
+ let mut c_args: Vec<(Reg, Opnd)> = vec![];
+
+ // live_ranges is indexed by original `index` given by the iterator.
let live_ranges: Vec<usize> = take(&mut self.live_ranges);
+ // shifted_live_ranges is indexed by mapped indexes in insn operands.
+ let mut shifted_live_ranges: Vec<usize> = live_ranges.clone();
let mut asm = Assembler::new_with_label_names(take(&mut self.label_names), take(&mut self.side_exits));
let mut iterator = self.into_draining_iter();
- while let Some((index, mut insn)) = iterator.next_unmapped() {
+ while let Some((index, mut insn)) = iterator.next_mapped() {
// Check if this is the last instruction that uses an operand that
// spans more than one instruction. In that case, return the
// allocated register to the pool.
@@ -1262,12 +1425,11 @@ impl Assembler
// Since we have an InsnOut, we know it spans more that one
// instruction.
let start_index = *idx;
- assert!(start_index < index);
// We're going to check if this is the last instruction that
// uses this operand. If it is, we can return the allocated
// register to the pool.
- if live_ranges[start_index] == index {
+ if shifted_live_ranges[start_index] == index {
if let Some(Opnd::Reg(reg)) = asm.insns[start_index].out_opnd() {
dealloc_reg(&mut pool, &regs, reg);
} else {
@@ -1314,7 +1476,7 @@ impl Assembler
let mut opnd_iter = insn.opnd_iter();
if let Some(Opnd::InsnOut{ idx, .. }) = opnd_iter.next() {
- if live_ranges[*idx] == index {
+ if shifted_live_ranges[*idx] == index {
if let Some(Opnd::Reg(reg)) = asm.insns[*idx].out_opnd() {
out_reg = Some(take_reg(&mut pool, &regs, reg));
}
@@ -1371,23 +1533,43 @@ impl Assembler
}
}
- asm.push_insn(insn);
+ // Push instruction(s). Batch and reorder C argument operations if needed.
+ if let Insn::LoadInto { dest: Opnd::CArg(reg), opnd } = insn {
+ // Buffer C arguments
+ c_args.push((reg, opnd));
+ } else {
+ // C arguments are buffered until CCall
+ if c_args.len() > 0 {
+ // Resolve C argument dependencies
+ let c_args_len = c_args.len() as isize;
+ let moves = reorder_c_args(&c_args.drain(..).into_iter().collect());
+ shift_live_ranges(&mut shifted_live_ranges, asm.insns.len(), moves.len() as isize - c_args_len);
+
+ // Push batched C arguments
+ for (reg, opnd) in moves {
+ asm.load_into(Opnd::Reg(reg), opnd);
+ }
+ }
+ // Other instructions are pushed as is
+ asm.push_insn(insn);
+ }
+ iterator.map_insn_index(&mut asm);
}
assert_eq!(pool, 0, "Expected all registers to be returned to the pool");
asm
}
- /// Compile the instructions down to machine code
- /// NOTE: should compile return a list of block labels to enable
- /// compiling multiple blocks at a time?
- pub fn compile(self, cb: &mut CodeBlock, ocb: Option<&mut OutlinedCb>) -> Vec<u32>
+ /// Compile the instructions down to machine code.
+ /// Can fail due to lack of code memory and inopportune code placement, among other reasons.
+ #[must_use]
+ pub fn compile(self, cb: &mut CodeBlock, ocb: Option<&mut OutlinedCb>) -> Option<(CodePtr, Vec<u32>)>
{
#[cfg(feature = "disasm")]
let start_addr = cb.get_write_ptr();
let alloc_regs = Self::get_alloc_regs();
- let gc_offsets = self.compile_with_regs(cb, ocb, alloc_regs);
+ let ret = self.compile_with_regs(cb, ocb, alloc_regs);
#[cfg(feature = "disasm")]
if let Some(dump_disasm) = get_option_ref!(dump_disasm) {
@@ -1395,15 +1577,16 @@ impl Assembler
let end_addr = cb.get_write_ptr();
dump_disasm_addr_range(cb, start_addr, end_addr, dump_disasm)
}
- gc_offsets
+ ret
}
/// Compile with a limited number of registers. Used only for unit tests.
- pub fn compile_with_num_regs(self, cb: &mut CodeBlock, num_regs: usize) -> Vec<u32>
+ #[cfg(test)]
+ pub fn compile_with_num_regs(self, cb: &mut CodeBlock, num_regs: usize) -> (CodePtr, Vec<u32>)
{
let mut alloc_regs = Self::get_alloc_regs();
let alloc_regs = alloc_regs.drain(0..num_regs).collect();
- self.compile_with_regs(cb, None, alloc_regs)
+ self.compile_with_regs(cb, None, alloc_regs).unwrap()
}
/// Consume the assembler by creating a new draining iterator.
@@ -1411,9 +1594,14 @@ impl Assembler
AssemblerDrainingIterator::new(self)
}
- /// Consume the assembler by creating a new lookback iterator.
- pub fn into_lookback_iter(self) -> AssemblerLookbackIterator {
- AssemblerLookbackIterator::new(self)
+ /// Return true if the next ccall() is expected to be leaf.
+ pub fn get_leaf_ccall(&mut self) -> bool {
+ self.leaf_ccall
+ }
+
+ /// Assert that the next ccall() is going to be leaf.
+ pub fn expect_leaf_ccall(&mut self) {
+ self.leaf_ccall = true;
}
}
@@ -1430,7 +1618,7 @@ impl AssemblerDrainingIterator {
Self {
insns: asm.insns.into_iter().peekable(),
index: 0,
- indices: Vec::default()
+ indices: Vec::with_capacity(ASSEMBLER_INSNS_CAPACITY),
}
}
@@ -1442,10 +1630,11 @@ impl AssemblerDrainingIterator {
/// end of the current list of instructions in order to maintain that
/// alignment.
pub fn map_insn_index(&mut self, asm: &mut Assembler) {
- self.indices.push(asm.insns.len() - 1);
+ self.indices.push(asm.insns.len().saturating_sub(1));
}
/// Map an operand by using this iterator's list of mapped indices.
+ #[cfg(target_arch = "x86_64")]
pub fn map_opnd(&self, opnd: Opnd) -> Opnd {
opnd.map_index(&self.indices)
}
@@ -1477,52 +1666,6 @@ impl AssemblerDrainingIterator {
}
}
-/// A struct that allows iterating through references to an assembler's
-/// instructions without consuming them.
-pub struct AssemblerLookbackIterator {
- asm: Assembler,
- index: Cell<usize>
-}
-
-impl AssemblerLookbackIterator {
- fn new(asm: Assembler) -> Self {
- Self { asm, index: Cell::new(0) }
- }
-
- /// Fetches a reference to an instruction at a specific index.
- pub fn get(&self, index: usize) -> Option<&Insn> {
- self.asm.insns.get(index)
- }
-
- /// Fetches a reference to an instruction in the list relative to the
- /// current cursor location of this iterator.
- pub fn get_relative(&self, difference: i32) -> Option<&Insn> {
- let index: Result<i32, _> = self.index.get().try_into();
- let relative: Result<usize, _> = index.and_then(|value| (value + difference).try_into());
- relative.ok().and_then(|value| self.asm.insns.get(value))
- }
-
- /// Fetches the previous instruction relative to the current cursor location
- /// of this iterator.
- pub fn get_previous(&self) -> Option<&Insn> {
- self.get_relative(-1)
- }
-
- /// Fetches the next instruction relative to the current cursor location of
- /// this iterator.
- pub fn get_next(&self) -> Option<&Insn> {
- self.get_relative(1)
- }
-
- /// Returns the next instruction in the list with the indices corresponding
- /// to the previous list of instructions.
- pub fn next_unmapped(&self) -> Option<(usize, &Insn)> {
- let index = self.index.get();
- self.index.set(index + 1);
- self.asm.insns.get(index).map(|insn| (index, insn))
- }
-}
-
impl fmt::Debug for Assembler {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
writeln!(fmt, "Assembler")?;
@@ -1554,23 +1697,67 @@ impl Assembler {
self.push_insn(Insn::BakeString(text.to_string()));
}
+ #[allow(dead_code)]
pub fn breakpoint(&mut self) {
self.push_insn(Insn::Breakpoint);
}
pub fn ccall(&mut self, fptr: *const u8, opnds: Vec<Opnd>) -> Opnd {
- assert_eq!(self.ctx.get_reg_temps(), RegTemps::default(), "temps must be spilled before ccall");
+ // Let vm_check_canary() assert this ccall's leafness if leaf_ccall is set
+ let canary_opnd = self.set_stack_canary(&opnds);
+
+ let old_temps = self.ctx.get_reg_temps(); // with registers
+ // Spill stack temp registers since they are caller-saved registers.
+ // Note that this doesn't spill stack temps that are already popped
+ // but may still be used in the C arguments.
+ self.spill_temps();
+ let new_temps = self.ctx.get_reg_temps(); // all spilled
+
+ // Temporarily manipulate RegTemps so that we can use registers
+ // to pass stack operands that are already spilled above.
+ self.ctx.set_reg_temps(old_temps);
+
+ // Call a C function
let out = self.next_opnd_out(Opnd::match_num_bits(&opnds));
self.push_insn(Insn::CCall { fptr, opnds, out });
+
+ // Registers in old_temps may be clobbered by the above C call,
+ // so rollback the manipulated RegTemps to a spilled version.
+ self.ctx.set_reg_temps(new_temps);
+
+ // Clear the canary after use
+ if let Some(canary_opnd) = canary_opnd {
+ self.mov(canary_opnd, 0.into());
+ }
+
out
}
- pub fn cmp(&mut self, left: Opnd, right: Opnd) {
- self.push_insn(Insn::Cmp { left, right });
+ /// Let vm_check_canary() assert the leafness of this ccall if leaf_ccall is set
+ fn set_stack_canary(&mut self, opnds: &Vec<Opnd>) -> Option<Opnd> {
+ // Use the slot right above the stack top for verifying leafness.
+ let canary_opnd = self.stack_opnd(-1);
+
+ // If the slot is already used, which is a valid optimization to avoid spills,
+ // give up the verification.
+ let canary_opnd = if cfg!(debug_assertions) && self.leaf_ccall && opnds.iter().all(|opnd|
+ opnd.get_stack_idx() != canary_opnd.get_stack_idx()
+ ) {
+ asm_comment!(self, "set stack canary");
+ self.mov(canary_opnd, vm_stack_canary().into());
+ Some(canary_opnd)
+ } else {
+ None
+ };
+
+ // Avoid carrying the flag to the next instruction whether we verified it or not.
+ self.leaf_ccall = false;
+
+ canary_opnd
}
- pub fn comment(&mut self, text: &str) {
- self.push_insn(Insn::Comment(text.to_string()));
+ pub fn cmp(&mut self, left: Opnd, right: Opnd) {
+ self.push_insn(Insn::Cmp { left, right });
}
#[must_use]
@@ -1682,6 +1869,10 @@ impl Assembler {
self.push_insn(Insn::Jbe(target));
}
+ pub fn jb(&mut self, target: Target) {
+ self.push_insn(Insn::Jb(target));
+ }
+
pub fn je(&mut self, target: Target) {
self.push_insn(Insn::Je(target));
}
@@ -1690,6 +1881,16 @@ impl Assembler {
self.push_insn(Insn::Jl(target));
}
+ #[allow(dead_code)]
+ pub fn jg(&mut self, target: Target) {
+ self.push_insn(Insn::Jg(target));
+ }
+
+ #[allow(dead_code)]
+ pub fn jge(&mut self, target: Target) {
+ self.push_insn(Insn::Jge(target));
+ }
+
pub fn jmp(&mut self, target: Target) {
self.push_insn(Insn::Jmp(target));
}
@@ -1710,6 +1911,10 @@ impl Assembler {
self.push_insn(Insn::Jo(target));
}
+ pub fn jo_mul(&mut self, target: Target) {
+ self.push_insn(Insn::JoMul(target));
+ }
+
pub fn jz(&mut self, target: Target) {
self.push_insn(Insn::Jz(target));
}
@@ -1722,9 +1927,9 @@ impl Assembler {
}
#[must_use]
- pub fn lea_label(&mut self, target: Target) -> Opnd {
+ pub fn lea_jump_target(&mut self, target: Target) -> Opnd {
let out = self.next_opnd_out(Opnd::DEFAULT_NUM_BITS);
- self.push_insn(Insn::LeaLabel { target, out });
+ self.push_insn(Insn::LeaJumpTarget { target, out });
out
}
@@ -1786,7 +1991,7 @@ impl Assembler {
}
//pub fn pos_marker<F: FnMut(CodePtr)>(&mut self, marker_fn: F)
- pub fn pos_marker(&mut self, marker_fn: impl Fn(CodePtr) + 'static) {
+ pub fn pos_marker(&mut self, marker_fn: impl Fn(CodePtr, &CodeBlock) + 'static) {
self.push_insn(Insn::PosMarker(Box::new(marker_fn)));
}
@@ -1808,17 +2013,35 @@ impl Assembler {
out
}
+ #[must_use]
+ pub fn mul(&mut self, left: Opnd, right: Opnd) -> Opnd {
+ let out = self.next_opnd_out(Opnd::match_num_bits(&[left, right]));
+ self.push_insn(Insn::Mul { left, right, out });
+ out
+ }
+
pub fn test(&mut self, left: Opnd, right: Opnd) {
self.push_insn(Insn::Test { left, right });
}
#[must_use]
+ #[allow(dead_code)]
pub fn urshift(&mut self, opnd: Opnd, shift: Opnd) -> Opnd {
let out = self.next_opnd_out(Opnd::match_num_bits(&[opnd, shift]));
self.push_insn(Insn::URShift { opnd, shift, out });
out
}
+ /// Verify the leafness of the given block
+ pub fn with_leaf_ccall<F, R>(&mut self, mut block: F) -> R
+ where F: FnMut(&mut Self) -> R {
+ let old_leaf_ccall = self.leaf_ccall;
+ self.leaf_ccall = true;
+ let ret = block(self);
+ self.leaf_ccall = old_leaf_ccall;
+ ret
+ }
+
/// Add a label at the current position
pub fn write_label(&mut self, target: Target) {
assert!(target.unwrap_label_idx() < self.label_names.len());
@@ -1833,6 +2056,17 @@ impl Assembler {
}
}
+/// Macro to use format! for Insn::Comment, which skips a format! call
+/// when disasm is not supported.
+macro_rules! asm_comment {
+ ($asm:expr, $($fmt:tt)*) => {
+ if cfg!(feature = "disasm") {
+ $asm.push_insn(Insn::Comment(format!($($fmt)*)));
+ }
+ };
+}
+pub(crate) use asm_comment;
+
#[cfg(test)]
mod tests {
use super::*;