#![allow(non_upper_case_globals, dead_code)]
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::mem;
use crate::codegen::{
MachineBasicBlock, MachineFunction, MachineInstr, MachineOperand, PhysReg, VirtReg,
};
use crate::x86::x86_calling_convention::X86CallingConvention;
use crate::x86::x86_instr_info::{X86InstrDesc, X86InstrInfo, X86Opcode};
use crate::x86::x86_register_info::*;
pub const X86_TC_MAX_ARGS: usize = 64;
pub const X86_RET_MAX_REGS: usize = 4;
pub const X86_TC_SYSV_STACK_ALIGN: i64 = 16;
pub const X86_TC_32_STACK_ALIGN: i64 = 4;
pub const X86_TC_WIN64_SHADOW_SPACE: i64 = 32;
pub const X86_TC_RED_ZONE_SIZE: i64 = 128;
pub const X86_TC_NEST_REG_SYSV: u16 = R10;
pub const X86_TC_NEST_REG_WIN64: u16 = R11;
pub const X86_TC_SWIFT_ASYNC_CTX_REG: u16 = R14;
pub const X86_TC_SWIFT_ERROR_REG: u16 = R12;
const RAX_CONST: u16 = 0;
const RCX_CONST: u16 = 1;
const RDX_CONST: u16 = 2;
const RBX_CONST: u16 = 3;
const RSP_CONST: u16 = 4;
const RBP_CONST: u16 = 5;
const RSI_CONST: u16 = 6;
const RDI_CONST: u16 = 7;
const R8_CONST: u16 = 8;
const R9_CONST: u16 = 9;
const R10_CONST: u16 = 10;
const R11_CONST: u16 = 11;
const R12_CONST: u16 = 12;
const R13_CONST: u16 = 13;
const R14_CONST: u16 = 14;
const R15_CONST: u16 = 15;
const XMM0_CONST: u16 = 64;
const XMM1_CONST: u16 = 65;
const XMM2_CONST: u16 = 66;
const XMM3_CONST: u16 = 67;
const XMM4_CONST: u16 = 68;
const XMM5_CONST: u16 = 69;
const XMM6_CONST: u16 = 70;
const XMM7_CONST: u16 = 71;
const XMM8_CONST: u16 = 72;
const XMM9_CONST: u16 = 73;
const XMM10_CONST: u16 = 74;
const XMM11_CONST: u16 = 75;
const XMM12_CONST: u16 = 76;
const XMM13_CONST: u16 = 77;
const XMM14_CONST: u16 = 78;
const XMM15_CONST: u16 = 79;
const EAX_CONST: u16 = 16;
const ECX_CONST: u16 = 17;
const EDX_CONST: u16 = 18;
const EBX_CONST: u16 = 19;
const ESP_CONST: u16 = 20;
const EBP_CONST: u16 = 21;
const ESI_CONST: u16 = 22;
const EDI_CONST: u16 = 23;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TailCallKind {
MustTail,
Tail,
NoTail,
}
impl TailCallKind {
pub fn is_musttail(&self) -> bool {
matches!(self, TailCallKind::MustTail)
}
pub fn is_tail_call(&self) -> bool {
matches!(self, TailCallKind::MustTail | TailCallKind::Tail)
}
pub fn name(&self) -> &'static str {
match self {
TailCallKind::MustTail => "musttail",
TailCallKind::Tail => "tail",
TailCallKind::NoTail => "notail",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TailCallArgInfo {
pub arg_index: usize,
pub reg: Option<u16>,
pub stack_offset: Option<i64>,
pub size: u32,
pub alignment: u32,
pub is_byval: bool,
pub is_inalloca: bool,
pub is_sret: bool,
pub is_nest: bool,
pub is_swift_async_context: bool,
pub is_swift_error: bool,
pub is_sse: bool,
pub is_variable_sized: bool,
pub vreg: Option<VirtReg>,
}
impl TailCallArgInfo {
pub fn new(arg_index: usize, size: u32, alignment: u32) -> Self {
TailCallArgInfo {
arg_index,
reg: None,
stack_offset: None,
size,
alignment,
is_byval: false,
is_inalloca: false,
is_sret: false,
is_nest: false,
is_swift_async_context: false,
is_swift_error: false,
is_sse: false,
is_variable_sized: false,
vreg: None,
}
}
pub fn is_in_reg(&self) -> bool {
self.reg.is_some()
}
pub fn is_on_stack(&self) -> bool {
self.stack_offset.is_some()
}
pub fn describe(&self) -> String {
let loc = if let Some(r) = self.reg {
format!("reg({})", r)
} else if let Some(off) = self.stack_offset {
format!("stack({})", off)
} else {
"unknown".to_string()
};
format!(
"arg[{}]: size={} align={} loc={}",
self.arg_index, self.size, self.alignment, loc
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TailCallEligibilityResult {
pub eligible: bool,
pub kind: TailCallKind,
pub reason: String,
pub caller_args: Vec<TailCallArgInfo>,
pub callee_args: Vec<TailCallArgInfo>,
pub cc_compatible: bool,
pub stack_compatible: bool,
pub has_lifetime_markers: bool,
pub has_dynamic_alloca: bool,
pub has_byval_conflict: bool,
pub has_variable_sized_objects: bool,
pub stack_adjust_delta: i64,
pub reg_moves: Vec<RegMove>,
pub needs_thunk: bool,
}
impl TailCallEligibilityResult {
pub fn eligible_default(kind: TailCallKind) -> Self {
TailCallEligibilityResult {
eligible: true,
kind,
reason: String::new(),
caller_args: Vec::new(),
callee_args: Vec::new(),
cc_compatible: true,
stack_compatible: true,
has_lifetime_markers: false,
has_dynamic_alloca: false,
has_byval_conflict: false,
has_variable_sized_objects: false,
stack_adjust_delta: 0,
reg_moves: Vec::new(),
needs_thunk: false,
}
}
pub fn ineligible(kind: TailCallKind, reason: String) -> Self {
TailCallEligibilityResult {
eligible: false,
kind,
reason,
caller_args: Vec::new(),
callee_args: Vec::new(),
cc_compatible: false,
stack_compatible: false,
has_lifetime_markers: false,
has_dynamic_alloca: false,
has_byval_conflict: false,
has_variable_sized_objects: false,
stack_adjust_delta: 0,
reg_moves: Vec::new(),
needs_thunk: false,
}
}
pub fn is_musttail_failure(&self) -> bool {
self.kind.is_musttail() && !self.eligible
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegMove {
pub src_reg: u16,
pub dst_reg: u16,
pub src_is_stack: bool,
pub src_stack_offset: i64,
pub size: u32,
pub is_sse: bool,
}
impl RegMove {
pub fn reg_to_reg(src: u16, dst: u16, size: u32) -> Self {
RegMove {
src_reg: src,
dst_reg: dst,
src_is_stack: false,
src_stack_offset: 0,
size,
is_sse: false,
}
}
pub fn stack_to_reg(src_offset: i64, dst: u16, size: u32) -> Self {
RegMove {
src_reg: 0,
dst_reg: dst,
src_is_stack: true,
src_stack_offset: src_offset,
size,
is_sse: false,
}
}
pub fn xmm_to_xmm(src: u16, dst: u16) -> Self {
RegMove {
src_reg: src,
dst_reg: dst,
src_is_stack: false,
src_stack_offset: 0,
size: 16,
is_sse: true,
}
}
pub fn describe(&self) -> String {
if self.src_is_stack {
format!(
"stack[{}] -> r{} ({} bytes)",
self.src_stack_offset, self.dst_reg, self.size
)
} else if self.is_sse {
format!("xmm{} -> xmm{}", self.src_reg, self.dst_reg)
} else {
format!(
"r{} -> r{} ({} bytes)",
self.src_reg, self.dst_reg, self.size
)
}
}
}
#[derive(Debug, Clone)]
pub struct CallSiteInfo {
pub call_instr_idx: usize,
pub block_idx: usize,
pub tail_kind: TailCallKind,
pub callee_name: String,
pub caller_cc: X86CallingConvention,
pub callee_cc: X86CallingConvention,
pub num_args: usize,
pub result_used: bool,
pub is_varargs: bool,
pub caller_frame_size: i64,
pub callee_frame_size: i64,
}
impl CallSiteInfo {
pub fn new(
call_instr_idx: usize,
block_idx: usize,
callee_name: String,
caller_cc: X86CallingConvention,
callee_cc: X86CallingConvention,
) -> Self {
CallSiteInfo {
call_instr_idx,
block_idx,
tail_kind: TailCallKind::NoTail,
callee_name,
caller_cc,
callee_cc,
num_args: 0,
result_used: false,
is_varargs: false,
caller_frame_size: 0,
callee_frame_size: 0,
}
}
pub fn is_tail_call_requested(&self) -> bool {
self.tail_kind.is_tail_call()
}
pub fn is_musttail(&self) -> bool {
self.tail_kind.is_musttail()
}
}
pub struct X86TailCall {
pub enabled: bool,
pub enable_musttail: bool,
pub enable_tail: bool,
pub is_64bit: bool,
pub default_cc: X86CallingConvention,
pub needs_stack_realignment: bool,
pub max_tail_call_args: usize,
pub stats: X86TailCallStats,
}
#[derive(Debug, Clone, Default)]
pub struct X86TailCallStats {
pub analyzed: u64,
pub eligible: u64,
pub lowered: u64,
pub musttail_failures: u64,
pub tail_fallbacks: u64,
pub rejected_cc_mismatch: u64,
pub rejected_stack: u64,
pub rejected_alloca: u64,
pub rejected_byval: u64,
pub rejected_variable_sized: u64,
pub rejected_lifetime: u64,
}
impl X86TailCallStats {
pub fn new() -> Self {
Self::default()
}
pub fn report(&self) -> String {
format!(
"X86TailCall: analyzed={} eligible={} lowered={} failures={} fallbacks={}",
self.analyzed, self.eligible, self.lowered, self.musttail_failures, self.tail_fallbacks
)
}
}
impl X86TailCall {
pub fn new_sysv64() -> Self {
X86TailCall {
enabled: true,
enable_musttail: true,
enable_tail: true,
is_64bit: true,
default_cc: X86CallingConvention::X86_64_SysV,
needs_stack_realignment: false,
max_tail_call_args: X86_TC_MAX_ARGS,
stats: X86TailCallStats::new(),
}
}
pub fn new_win64() -> Self {
X86TailCall {
enabled: true,
enable_musttail: true,
enable_tail: true,
is_64bit: true,
default_cc: X86CallingConvention::Win64,
needs_stack_realignment: false,
max_tail_call_args: X86_TC_MAX_ARGS,
stats: X86TailCallStats::new(),
}
}
pub fn new_32bit(cc: X86CallingConvention) -> Self {
X86TailCall {
enabled: true,
enable_musttail: true,
enable_tail: true,
is_64bit: false,
default_cc: cc,
needs_stack_realignment: false,
max_tail_call_args: X86_TC_MAX_ARGS,
stats: X86TailCallStats::new(),
}
}
pub fn run(&mut self, mf: &mut MachineFunction) -> Vec<String> {
if !self.enabled {
return Vec::new();
}
let mut diagnostics: Vec<String> = Vec::new();
let call_sites = self.scan_call_sites(mf);
let results: Vec<(usize, TailCallEligibilityResult)> = call_sites
.iter()
.map(|site| {
self.stats.analyzed += 1;
let result = self.analyze_tail_call_eligibility(mf, site);
if result.eligible {
self.stats.eligible += 1;
}
(site.call_instr_idx, result)
})
.collect();
for (call_idx, result) in &results {
if result.eligible {
match self.lower_tail_call(mf, *call_idx, result) {
Ok(()) => {
self.stats.lowered += 1;
}
Err(msg) => {
if result.kind.is_musttail() {
self.stats.musttail_failures += 1;
diagnostics.push(format!(
"musttail lowering failed for call at instruction {}: {}",
call_idx, msg
));
} else {
self.stats.tail_fallbacks += 1;
}
}
}
} else if result.kind.is_musttail() {
self.stats.musttail_failures += 1;
diagnostics.push(format!(
"musttail ineligible for call at instruction {}: {}",
call_idx, result.reason
));
} else if result.kind == TailCallKind::Tail {
self.stats.tail_fallbacks += 1;
}
}
diagnostics
}
pub fn scan_call_sites(&self, mf: &MachineFunction) -> Vec<CallSiteInfo> {
let mut sites = Vec::new();
for (block_idx, block) in mf.blocks.iter().enumerate() {
let instrs = &block.instructions;
for (instr_idx, instr) in instrs.iter().enumerate() {
if !self.is_call_instr(instr) {
continue;
}
let tail_kind = self.detect_tail_kind(instrs, instr_idx);
let callee_name = self.extract_callee_name(instr);
let mut site = CallSiteInfo::new(
instr_idx,
block_idx,
callee_name,
self.default_cc,
self.default_cc,
);
site.tail_kind = tail_kind;
site.num_args = self.count_args(instrs, instr_idx);
site.result_used = self.is_call_result_used(instrs, instr_idx);
site.is_varargs = self.is_varargs_call(instr);
site.caller_frame_size = self.estimate_frame_size(mf);
site.callee_frame_size = site.caller_frame_size;
sites.push(site);
}
}
sites
}
pub fn analyze_tail_call_eligibility(
&mut self,
mf: &MachineFunction,
site: &CallSiteInfo,
) -> TailCallEligibilityResult {
let mut result = TailCallEligibilityResult::eligible_default(site.tail_kind);
if !self.check_calling_convention_compatibility(site) {
result.cc_compatible = false;
result.eligible = false;
result.reason = format!(
"calling convention mismatch: caller={:?} callee={:?}",
site.caller_cc, site.callee_cc
);
self.stats.rejected_cc_mismatch += 1;
return result;
}
result.stack_compatible = self.check_stack_argument_compatibility(mf, site);
if !result.stack_compatible {
result.eligible = false;
result.reason =
"stack arguments incompatible: cannot forward in same position".to_string();
self.stats.rejected_stack += 1;
return result;
}
result.has_lifetime_markers = self.check_lifetime_markers(mf);
if result.has_lifetime_markers {
result.eligible = false;
result.reason =
"lifetime markers present: cannot deallocate caller frame early".to_string();
self.stats.rejected_lifetime += 1;
return result;
}
result.has_dynamic_alloca = self.check_dynamic_alloca(mf);
if result.has_dynamic_alloca {
result.eligible = false;
result.reason = "dynamic alloca in caller prevents tail call".to_string();
self.stats.rejected_alloca += 1;
return result;
}
result.has_byval_conflict = self.check_byval_conflicts(mf, site);
if result.has_byval_conflict {
result.eligible = false;
result.reason = "byval argument prevents tail call forwarding".to_string();
self.stats.rejected_byval += 1;
return result;
}
result.has_variable_sized_objects = self.check_variable_sized_objects(mf);
if result.has_variable_sized_objects {
result.eligible = false;
result.reason =
"variable-sized objects between caller and callee prevent tail call".to_string();
self.stats.rejected_variable_sized += 1;
return result;
}
if !self.check_argument_position_compatibility(mf, site) {
result.eligible = false;
result.reason = "argument position incompatibility prevents tail call".to_string();
return result;
}
result.caller_args = self.build_caller_arg_info(mf, site);
result.callee_args = self.build_callee_arg_info(mf, site);
result.reg_moves = self.compute_reg_moves(&result.caller_args, &result.callee_args);
result.stack_adjust_delta = self.compute_stack_adjust_delta(mf, &result);
result.needs_thunk = self.check_thunk_necessity(&result);
result
}
pub fn check_calling_convention_compatibility(&self, site: &CallSiteInfo) -> bool {
let caller = site.caller_cc;
let callee = site.callee_cc;
if caller == callee {
return true;
}
if caller.is_64bit() && callee.is_64bit() {
return false;
}
let caller_cleans = caller.callee_cleans_stack();
let callee_cleans = callee.callee_cleans_stack();
if caller_cleans != callee_cleans {
return false;
}
true
}
pub fn check_stack_argument_compatibility(
&self,
_mf: &MachineFunction,
site: &CallSiteInfo,
) -> bool {
if site.caller_cc == site.callee_cc {
return true;
}
if site.is_musttail() {
return true;
}
false
}
pub fn check_lifetime_markers(&self, _mf: &MachineFunction) -> bool {
false
}
pub fn check_dynamic_alloca(&self, _mf: &MachineFunction) -> bool {
false
}
pub fn check_byval_conflicts(&self, _mf: &MachineFunction, site: &CallSiteInfo) -> bool {
if site.is_musttail() {
return false;
}
false
}
pub fn check_variable_sized_objects(&self, _mf: &MachineFunction) -> bool {
false
}
pub fn check_argument_position_compatibility(
&self,
_mf: &MachineFunction,
site: &CallSiteInfo,
) -> bool {
if site.is_musttail() {
return true;
}
if site.caller_cc == site.callee_cc {
return true;
}
false
}
pub fn build_caller_arg_info(
&self,
_mf: &MachineFunction,
site: &CallSiteInfo,
) -> Vec<TailCallArgInfo> {
let mut args = Vec::with_capacity(site.num_args);
for i in 0..site.num_args {
let mut info = TailCallArgInfo::new(i, 8, 8);
let int_regs = site.caller_cc.get_int_param_regs();
let sse_regs = site.caller_cc.get_sse_param_regs();
if i < int_regs.len() {
info.reg = Some(int_regs[i]);
} else if i < sse_regs.len() + int_regs.len() {
info.reg = Some(sse_regs[i - int_regs.len()]);
info.is_sse = true;
} else {
let stack_arg_index = i - int_regs.len() - sse_regs.len();
info.stack_offset = Some((stack_arg_index as i64) * 8);
}
args.push(info);
}
args
}
pub fn build_callee_arg_info(
&self,
_mf: &MachineFunction,
site: &CallSiteInfo,
) -> Vec<TailCallArgInfo> {
self.build_caller_arg_info(_mf, site)
}
pub fn compute_reg_moves(
&self,
caller_args: &[TailCallArgInfo],
callee_args: &[TailCallArgInfo],
) -> Vec<RegMove> {
let mut moves = Vec::new();
let max_args = caller_args.len().min(callee_args.len());
for i in 0..max_args {
let caller = &caller_args[i];
let callee = &callee_args[i];
match (caller.reg, callee.reg) {
(Some(src), Some(dst)) if src != dst => {
let m = if caller.is_sse || callee.is_sse {
RegMove::xmm_to_xmm(src, dst)
} else {
RegMove::reg_to_reg(src, dst, caller.size)
};
moves.push(m);
}
(Some(_), Some(_)) => {
}
(None, Some(dst)) if caller.stack_offset.is_some() => {
let m = RegMove::stack_to_reg(caller.stack_offset.unwrap(), dst, caller.size);
moves.push(m);
}
(Some(src), None) if callee.stack_offset.is_some() => {
let m = RegMove {
src_reg: src,
dst_reg: 0,
src_is_stack: false,
src_stack_offset: 0,
size: caller.size,
is_sse: caller.is_sse,
};
moves.push(m);
}
_ => {}
}
}
moves
}
pub fn compute_stack_adjust_delta(
&self,
_mf: &MachineFunction,
result: &TailCallEligibilityResult,
) -> i64 {
let caller_stack_args = result
.caller_args
.iter()
.filter(|a| a.is_on_stack())
.count();
let callee_stack_args = result
.callee_args
.iter()
.filter(|a| a.is_on_stack())
.count();
if callee_stack_args <= caller_stack_args {
0
} else {
((callee_stack_args - caller_stack_args) as i64) * 8
}
}
pub fn check_thunk_necessity(&self, _result: &TailCallEligibilityResult) -> bool {
false
}
pub fn lower_tail_call(
&mut self,
mf: &mut MachineFunction,
call_idx: usize,
result: &TailCallEligibilityResult,
) -> Result<(), String> {
if !result.eligible {
return Err(format!("not eligible: {}", result.reason));
}
if result.kind == TailCallKind::Tail {
if result.reg_moves.len() > X86_TC_MAX_ARGS {
return Err("too many register moves required".to_string());
}
}
let move_instrs = self.emit_reg_moves(result);
let adj_instr = if result.stack_adjust_delta != 0 {
Some(self.emit_stack_adjust(result.stack_adjust_delta))
} else {
None
};
let block = Self::find_block_mut(mf, call_idx)?;
let call_pos = block
.instructions
.iter()
.position(|instr| instr.opcode == X86Opcode::CALL.as_u32())
.ok_or_else(|| "CALL instruction not found".to_string())?;
let mut offset = 0;
for move_instr in move_instrs.iter() {
block
.instructions
.insert(call_pos + offset, move_instr.clone());
offset += 1;
}
if let Some(adj) = adj_instr {
block.instructions.insert(call_pos + offset, adj);
offset += 1;
}
let adjusted_call_pos = call_pos + offset;
self.replace_call_with_jmp(block, adjusted_call_pos, result)?;
self.remove_trailing_ret(block, adjusted_call_pos + 1)?;
Ok(())
}
fn find_block_mut<'a>(
mf: &'a mut MachineFunction,
_call_idx: usize,
) -> Result<&'a mut MachineBasicBlock, String> {
mf.blocks
.last_mut()
.ok_or_else(|| "no basic blocks in function".to_string())
}
pub fn emit_reg_moves(&self, result: &TailCallEligibilityResult) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
for rm in &result.reg_moves {
let mut instr = if rm.is_sse {
let mut mi = MachineInstr::new(X86Opcode::MOVSD.as_u32());
mi.operands.push(MachineOperand::PhysReg(rm.dst_reg as u32));
mi.operands.push(MachineOperand::PhysReg(rm.src_reg as u32));
mi
} else if rm.size == 8 {
let mut mi = MachineInstr::new(X86Opcode::MOV.as_u32());
mi.operands.push(MachineOperand::PhysReg(rm.dst_reg as u32));
mi.operands.push(MachineOperand::PhysReg(rm.src_reg as u32));
mi
} else {
let mut mi = MachineInstr::new(X86Opcode::MOV.as_u32());
mi.operands.push(MachineOperand::PhysReg(rm.dst_reg as u32));
mi.operands.push(MachineOperand::PhysReg(rm.src_reg as u32));
mi
};
instrs.push(instr);
}
instrs
}
pub fn emit_stack_adjust(&self, delta: i64) -> MachineInstr {
if delta == 0 {
let mut instr = MachineInstr::new(X86Opcode::ADD.as_u32());
instr
.operands
.push(MachineOperand::PhysReg(RSP_CONST as u32));
instr.operands.push(MachineOperand::Imm(0));
return instr;
}
if delta > 0 {
let mut instr = MachineInstr::new(X86Opcode::SUB.as_u32());
instr
.operands
.push(MachineOperand::PhysReg(RSP_CONST as u32));
instr.operands.push(MachineOperand::Imm(delta));
instr
} else {
let mut instr = MachineInstr::new(X86Opcode::ADD.as_u32());
instr
.operands
.push(MachineOperand::PhysReg(RSP_CONST as u32));
instr.operands.push(MachineOperand::Imm(-delta));
instr
}
}
pub fn replace_call_with_jmp(
&self,
block: &mut MachineBasicBlock,
call_pos: usize,
result: &TailCallEligibilityResult,
) -> Result<(), String> {
if call_pos >= block.instructions.len() {
return Err("call position out of bounds".to_string());
}
let callee = self.extract_callee_name(&block.instructions[call_pos]);
let mut jmp = MachineInstr::new(X86Opcode::JMP.as_u32());
jmp.operands.push(MachineOperand::Label(callee));
block.instructions[call_pos] = jmp;
Ok(())
}
pub fn remove_trailing_ret(
&self,
block: &mut MachineBasicBlock,
start_pos: usize,
) -> Result<(), String> {
let mut pos = start_pos;
while pos < block.instructions.len() {
if block.instructions[pos].opcode == X86Opcode::RET.as_u32() {
block.instructions.remove(pos);
return Ok(());
}
pos += 1;
}
Ok(())
}
pub fn is_call_instr(&self, instr: &MachineInstr) -> bool {
instr.opcode == X86Opcode::CALL.as_u32()
}
pub fn is_ret_instr(&self, instr: &MachineInstr) -> bool {
instr.opcode == X86Opcode::RET.as_u32()
}
pub fn is_jmp_instr(&self, instr: &MachineInstr) -> bool {
instr.opcode == X86Opcode::JMP.as_u32()
}
pub fn detect_tail_kind(&self, instrs: &[MachineInstr], call_idx: usize) -> TailCallKind {
if call_idx + 1 < instrs.len() && self.is_ret_instr(&instrs[call_idx + 1]) {
return TailCallKind::NoTail;
}
if call_idx < instrs.len() {
let instr = &instrs[call_idx];
for op in &instr.operands {
if let MachineOperand::Label(label) = op {
if label.starts_with("musttail.") {
return TailCallKind::MustTail;
}
if label.starts_with("tail.") {
return TailCallKind::Tail;
}
}
}
}
TailCallKind::NoTail
}
pub fn extract_callee_name(&self, instr: &MachineInstr) -> String {
for op in &instr.operands {
if let MachineOperand::Label(label) = op {
return label.clone();
}
if let MachineOperand::Global(name) = op {
return name.clone();
}
}
"unknown".to_string()
}
pub fn count_args(&self, instrs: &[MachineInstr], call_idx: usize) -> usize {
let mut count = 0;
let mut i = call_idx;
while i > 0 {
i -= 1;
let instr = &instrs[i];
match instr.opcode {
_ if instr.opcode == X86Opcode::MOV.as_u32() => {
count += 1;
}
_ if instr.opcode == X86Opcode::PUSH.as_u32() => {
count += 1;
}
_ => break, }
}
count.min(X86_TC_MAX_ARGS)
}
pub fn is_call_result_used(&self, instrs: &[MachineInstr], call_idx: usize) -> bool {
if call_idx + 1 < instrs.len() {
let next = &instrs[call_idx + 1];
if next.opcode == X86Opcode::RET.as_u32() {
return false;
}
if next.def.is_some() {
for op in &next.operands {
if let MachineOperand::PhysReg(r) = op {
if *r as u16 == RAX_CONST {
return true;
}
}
}
}
}
false
}
pub fn is_varargs_call(&self, instr: &MachineInstr) -> bool {
false
}
pub fn estimate_frame_size(&self, _mf: &MachineFunction) -> i64 {
0
}
pub fn sp_reg(&self) -> u16 {
if self.is_64bit {
RSP_CONST
} else {
ESP_CONST
}
}
pub fn bp_reg(&self) -> u16 {
if self.is_64bit {
RBP_CONST
} else {
EBP_CONST
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SRetInfo {
pub is_sret: bool,
pub sret_reg: u16,
pub struct_size: u64,
pub struct_align: u64,
pub in_regs: bool,
pub return_regs: Vec<u16>,
}
impl SRetInfo {
pub fn no_sret() -> Self {
SRetInfo {
is_sret: false,
sret_reg: 0,
struct_size: 0,
struct_align: 0,
in_regs: false,
return_regs: Vec::new(),
}
}
pub fn with_sret(sret_reg: u16, size: u64, align: u64) -> Self {
SRetInfo {
is_sret: true,
sret_reg,
struct_size: size,
struct_align: align,
in_regs: false,
return_regs: Vec::new(),
}
}
pub fn in_regs(regs: Vec<u16>, size: u64) -> Self {
SRetInfo {
is_sret: false,
sret_reg: 0,
struct_size: size,
struct_align: 1,
in_regs: true,
return_regs: regs,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ReturnKind {
Void,
Integer,
Integer128,
X87,
SSE,
Vector,
Vector256,
Vector512,
SRet,
MultiReg,
Dead,
}
impl ReturnKind {
pub fn name(&self) -> &'static str {
match self {
ReturnKind::Void => "void",
ReturnKind::Integer => "integer",
ReturnKind::Integer128 => "i128",
ReturnKind::X87 => "x87",
ReturnKind::SSE => "sse",
ReturnKind::Vector => "vector",
ReturnKind::Vector256 => "vector256",
ReturnKind::Vector512 => "vector512",
ReturnKind::SRet => "sret",
ReturnKind::MultiReg => "multireg",
ReturnKind::Dead => "dead",
}
}
pub fn uses_xmm(&self) -> bool {
matches!(
self,
ReturnKind::SSE | ReturnKind::Vector | ReturnKind::Vector256 | ReturnKind::Vector512
)
}
pub fn uses_x87(&self) -> bool {
matches!(self, ReturnKind::X87)
}
pub fn is_dead(&self) -> bool {
matches!(self, ReturnKind::Dead)
}
}
#[derive(Debug, Clone)]
pub struct ReturnLoweringResult {
pub instructions: Vec<MachineInstr>,
pub kind: ReturnKind,
pub dead_eliminated: bool,
pub stack_pop: u16,
pub sret_forwarded: bool,
}
impl ReturnLoweringResult {
pub fn new(kind: ReturnKind) -> Self {
ReturnLoweringResult {
instructions: Vec::new(),
kind,
dead_eliminated: false,
stack_pop: 0,
sret_forwarded: false,
}
}
}
pub struct X86ReturnLowering {
pub is_64bit: bool,
pub default_cc: X86CallingConvention,
pub sret_demotion: bool,
pub enable_dead_ret_elim: bool,
pub has_shadow_space: bool,
pub ret_stats: X86ReturnStats,
}
#[derive(Debug, Clone, Default)]
pub struct X86ReturnStats {
pub ret_lowered: u64,
pub reti_lowered: u64,
pub sret_lowered: u64,
pub multireg_lowered: u64,
pub x87_lowered: u64,
pub vector_lowered: u64,
pub dead_eliminated: u64,
}
impl X86ReturnStats {
pub fn new() -> Self {
Self::default()
}
pub fn report(&self) -> String {
format!(
"X86Return: ret={} reti={} sret={} multireg={} x87={} vec={} dead={}",
self.ret_lowered,
self.reti_lowered,
self.sret_lowered,
self.multireg_lowered,
self.x87_lowered,
self.vector_lowered,
self.dead_eliminated,
)
}
}
impl X86ReturnLowering {
pub fn new_sysv64() -> Self {
X86ReturnLowering {
is_64bit: true,
default_cc: X86CallingConvention::X86_64_SysV,
sret_demotion: false,
enable_dead_ret_elim: true,
has_shadow_space: false,
ret_stats: X86ReturnStats::new(),
}
}
pub fn new_win64() -> Self {
X86ReturnLowering {
is_64bit: true,
default_cc: X86CallingConvention::Win64,
sret_demotion: false,
enable_dead_ret_elim: true,
has_shadow_space: true,
ret_stats: X86ReturnStats::new(),
}
}
pub fn new_32bit(cc: X86CallingConvention) -> Self {
X86ReturnLowering {
is_64bit: false,
default_cc: cc,
sret_demotion: false,
enable_dead_ret_elim: true,
has_shadow_space: false,
ret_stats: X86ReturnStats::new(),
}
}
pub fn lower_return(
&mut self,
return_kind: ReturnKind,
sret_info: Option<&SRetInfo>,
stack_pop: u16,
is_dead: bool,
) -> ReturnLoweringResult {
let mut result = ReturnLoweringResult::new(return_kind);
if is_dead && self.enable_dead_ret_elim && !return_kind.is_dead() {
result.dead_eliminated = true;
self.ret_stats.dead_eliminated += 1;
result.instructions = self.emit_bare_ret(stack_pop);
return result;
}
match return_kind {
ReturnKind::Void => {
result.instructions = self.emit_void_return();
self.ret_stats.ret_lowered += 1;
}
ReturnKind::Integer => {
result.instructions = self.emit_integer_return();
self.ret_stats.ret_lowered += 1;
}
ReturnKind::Integer128 => {
result.instructions = self.emit_i128_return();
self.ret_stats.multireg_lowered += 1;
}
ReturnKind::X87 => {
result.instructions = self.emit_x87_return();
self.ret_stats.x87_lowered += 1;
}
ReturnKind::SSE | ReturnKind::Vector => {
result.instructions = self.emit_sse_return();
self.ret_stats.vector_lowered += 1;
}
ReturnKind::Vector256 => {
result.instructions = self.emit_ymm_return();
self.ret_stats.vector_lowered += 1;
}
ReturnKind::Vector512 => {
result.instructions = self.emit_zmm_return();
self.ret_stats.vector_lowered += 1;
}
ReturnKind::SRet => {
result.sret_forwarded = true;
if let Some(si) = sret_info {
result.instructions = self.emit_sret_return(si);
} else {
result.instructions = self.emit_bare_ret(stack_pop);
}
self.ret_stats.sret_lowered += 1;
}
ReturnKind::MultiReg => {
if let Some(si) = sret_info {
result.instructions = self.emit_multireg_return(si);
} else {
result.instructions = self.emit_bare_ret(stack_pop);
}
self.ret_stats.multireg_lowered += 1;
}
ReturnKind::Dead => {
result.instructions = self.emit_bare_ret(stack_pop);
self.ret_stats.dead_eliminated += 1;
}
}
if stack_pop > 0 && !result.instructions.is_empty() {
result.stack_pop = stack_pop;
result.instructions = self.apply_stack_pop(result.instructions, stack_pop);
self.ret_stats.reti_lowered += 1;
}
result
}
pub fn lower_return_with_type(
&mut self,
return_kind: ReturnKind,
sret_info: Option<&SRetInfo>,
is_dead: bool,
) -> ReturnLoweringResult {
let stack_pop = if self.default_cc.callee_cleans_stack() {
0
} else {
0
};
self.lower_return(return_kind, sret_info, stack_pop, is_dead)
}
pub fn emit_void_return(&self) -> Vec<MachineInstr> {
let mut instr = MachineInstr::new(X86Opcode::RET.as_u32());
vec![instr]
}
pub fn emit_bare_ret(&self, _stack_pop: u16) -> Vec<MachineInstr> {
let mut instr = MachineInstr::new(X86Opcode::RET.as_u32());
vec![instr]
}
pub fn emit_integer_return(&self) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
let ret = MachineInstr::new(X86Opcode::RET.as_u32());
instrs.push(ret);
instrs
}
pub fn emit_i128_return(&self) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
let ret = MachineInstr::new(X86Opcode::RET.as_u32());
instrs.push(ret);
instrs
}
pub fn emit_x87_return(&self) -> Vec<MachineInstr> {
let ret = MachineInstr::new(X86Opcode::RET.as_u32());
vec![ret]
}
pub fn emit_sse_return(&self) -> Vec<MachineInstr> {
let ret = MachineInstr::new(X86Opcode::RET.as_u32());
vec![ret]
}
pub fn emit_ymm_return(&self) -> Vec<MachineInstr> {
let ret = MachineInstr::new(X86Opcode::RET.as_u32());
vec![ret]
}
pub fn emit_zmm_return(&self) -> Vec<MachineInstr> {
let ret = MachineInstr::new(X86Opcode::RET.as_u32());
vec![ret]
}
pub fn emit_sret_return(&self, sret_info: &SRetInfo) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
let mut mov = MachineInstr::new(X86Opcode::MOV.as_u32());
mov.operands.push(MachineOperand::PhysReg(RAX_CONST as u32));
mov.operands
.push(MachineOperand::PhysReg(sret_info.sret_reg as u32));
instrs.push(mov);
let ret = MachineInstr::new(X86Opcode::RET.as_u32());
instrs.push(ret);
instrs
}
pub fn emit_multireg_return(&self, _sret_info: &SRetInfo) -> Vec<MachineInstr> {
let ret = MachineInstr::new(X86Opcode::RET.as_u32());
vec![ret]
}
pub fn emit_reti(&self, pop_bytes: u16) -> MachineInstr {
let mut instr = MachineInstr::new(X86Opcode::RET.as_u32());
instr.operands.push(MachineOperand::Imm(pop_bytes as i64));
instr
}
pub fn apply_stack_pop(
&self,
mut instrs: Vec<MachineInstr>,
pop_bytes: u16,
) -> Vec<MachineInstr> {
if let Some(last) = instrs.last_mut() {
if last.opcode == X86Opcode::RET.as_u32() {
last.operands.push(MachineOperand::Imm(pop_bytes as i64));
}
}
instrs
}
pub fn classify_return_type(
&self,
size: u64,
is_float: bool,
is_vector: bool,
vector_bits: u32,
is_struct: bool,
) -> ReturnKind {
if size == 0 {
return ReturnKind::Void;
}
if is_struct {
return ReturnKind::SRet;
}
if is_vector {
return match vector_bits {
0..=128 => ReturnKind::Vector,
129..=256 => ReturnKind::Vector256,
_ => ReturnKind::Vector512,
};
}
if is_float && size == 16 {
return ReturnKind::X87;
}
if size <= 8 {
return ReturnKind::Integer;
}
if size <= 16 {
return ReturnKind::Integer128;
}
ReturnKind::SRet
}
pub fn is_return_dead(&self, _instrs: &[MachineInstr], _ret_idx: usize) -> bool {
false
}
pub fn should_demote_sret(&self, struct_size: u64, struct_align: u64) -> bool {
if !self.sret_demotion {
return false;
}
match self.default_cc {
X86CallingConvention::X86_64_SysV => struct_size <= 16,
X86CallingConvention::Win64 => struct_size <= 8,
_ => false,
}
}
pub fn get_return_regs_for_kind(&self, kind: ReturnKind) -> Vec<u16> {
match kind {
ReturnKind::Void | ReturnKind::Dead => vec![],
ReturnKind::Integer => {
if self.is_64bit {
vec![RAX_CONST]
} else {
vec![EAX_CONST]
}
}
ReturnKind::Integer128 => vec![RAX_CONST, RDX_CONST],
ReturnKind::X87 => vec![], ReturnKind::SSE | ReturnKind::Vector => vec![XMM0_CONST],
ReturnKind::Vector256 => vec![XMM0_CONST], ReturnKind::Vector512 => vec![XMM0_CONST], ReturnKind::SRet | ReturnKind::MultiReg => vec![],
}
}
}
pub struct X86CCEdgeCases;
impl X86CCEdgeCases {
pub fn analyze_sret_demotion(
cc: X86CallingConvention,
struct_size: u64,
_struct_has_memory_class: bool,
) -> bool {
match cc {
X86CallingConvention::X86_64_SysV => {
struct_size <= 16 && !_struct_has_memory_class
}
X86CallingConvention::Win64 => {
struct_size <= 8
}
_ => false,
}
}
pub fn sret_compatible_for_tail_call(caller_has_sret: bool, callee_has_sret: bool) -> bool {
caller_has_sret == callee_has_sret
}
pub fn byval_blocks_tail_call(
has_byval: bool,
byval_offset: i64,
caller_frame_size: i64,
) -> bool {
if !has_byval {
return false;
}
byval_offset < caller_frame_size
}
pub fn emit_byval_copy_for_tail_call(
src_offset: i64,
dst_offset: i64,
size: u64,
) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
if size <= 64 {
let num_moves = (size + 7) / 8; for i in 0..num_moves {
let offset = i as i64 * 8;
let remaining = size.saturating_sub((i as u64) * 8);
if remaining >= 8 {
let mut load = MachineInstr::new(X86Opcode::MOV.as_u32());
load.operands
.push(MachineOperand::PhysReg(RAX_CONST as u32));
load.operands.push(MachineOperand::Imm(src_offset + offset));
instrs.push(load);
let mut store = MachineInstr::new(X86Opcode::MOV.as_u32());
store
.operands
.push(MachineOperand::Imm(dst_offset + offset));
store
.operands
.push(MachineOperand::PhysReg(RAX_CONST as u32));
instrs.push(store);
} else if remaining >= 4 {
let mut load = MachineInstr::new(X86Opcode::MOV.as_u32());
load.operands
.push(MachineOperand::PhysReg(EAX_CONST as u32));
load.operands.push(MachineOperand::Imm(src_offset + offset));
instrs.push(load);
let mut store = MachineInstr::new(X86Opcode::MOV.as_u32());
store
.operands
.push(MachineOperand::Imm(dst_offset + offset));
store
.operands
.push(MachineOperand::PhysReg(EAX_CONST as u32));
instrs.push(store);
}
}
}
instrs
}
pub fn inalloca_blocks_tail_call(has_inalloca: bool) -> bool {
has_inalloca
}
pub fn get_nest_register(cc: X86CallingConvention) -> u16 {
match cc {
X86CallingConvention::X86_64_SysV => X86_TC_NEST_REG_SYSV,
X86CallingConvention::Win64 => X86_TC_NEST_REG_WIN64,
_ => 0, }
}
pub fn nest_compatible_for_tail_call(caller_has_nest: bool, callee_has_nest: bool) -> bool {
caller_has_nest == callee_has_nest
}
pub fn emit_nest_forward(caller_nest_reg: u16, callee_nest_reg: u16) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
if caller_nest_reg != callee_nest_reg {
let mut mov = MachineInstr::new(X86Opcode::MOV.as_u32());
mov.operands
.push(MachineOperand::PhysReg(callee_nest_reg as u32));
mov.operands
.push(MachineOperand::PhysReg(caller_nest_reg as u32));
instrs.push(mov);
}
instrs
}
pub fn swift_async_context_blocks_tail_call(
caller_has_async_ctx: bool,
callee_has_async_ctx: bool,
) -> bool {
caller_has_async_ctx != callee_has_async_ctx
}
pub fn get_swift_async_context_register(cc: X86CallingConvention) -> u16 {
match cc {
X86CallingConvention::X86_64_SysV => X86_TC_SWIFT_ASYNC_CTX_REG,
_ => 0,
}
}
pub fn emit_swift_async_ctx_forward(src_reg: u16, dst_reg: u16) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
if src_reg != dst_reg && src_reg != 0 && dst_reg != 0 {
let mut mov = MachineInstr::new(X86Opcode::MOV.as_u32());
mov.operands.push(MachineOperand::PhysReg(dst_reg as u32));
mov.operands.push(MachineOperand::PhysReg(src_reg as u32));
instrs.push(mov);
}
instrs
}
pub fn swift_error_blocks_tail_call(
caller_has_swift_error: bool,
callee_has_swift_error: bool,
) -> bool {
caller_has_swift_error != callee_has_swift_error
}
pub fn get_swift_error_register(cc: X86CallingConvention) -> u16 {
match cc {
X86CallingConvention::X86_64_SysV => X86_TC_SWIFT_ERROR_REG,
_ => 0,
}
}
pub fn emit_swift_error_forward(src_reg: u16, dst_reg: u16) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
if src_reg != dst_reg && src_reg != 0 && dst_reg != 0 {
let mut mov = MachineInstr::new(X86Opcode::MOV.as_u32());
mov.operands.push(MachineOperand::PhysReg(dst_reg as u32));
mov.operands.push(MachineOperand::PhysReg(src_reg as u32));
instrs.push(mov);
}
instrs
}
}
#[derive(Debug, Clone)]
pub struct X86TailCallCompatibilityMatrix;
impl X86TailCallCompatibilityMatrix {
pub fn are_compatible(
caller_cc: X86CallingConvention,
callee_cc: X86CallingConvention,
) -> bool {
if caller_cc == callee_cc {
return true;
}
if caller_cc.is_64bit() && callee_cc.is_64bit() {
return false;
}
let caller_cleans = caller_cc.callee_cleans_stack();
let callee_cleans = callee_cc.callee_cleans_stack();
if caller_cleans == callee_cleans {
return true;
}
false
}
pub fn return_value_compatible(
caller_cc: X86CallingConvention,
callee_cc: X86CallingConvention,
) -> bool {
if caller_cc == callee_cc {
return true;
}
caller_cc.is_64bit() == callee_cc.is_64bit()
}
pub fn argument_passing_compatible(
caller_cc: X86CallingConvention,
callee_cc: X86CallingConvention,
) -> bool {
if caller_cc == callee_cc {
return true;
}
let caller_int = caller_cc.get_num_int_param_regs();
let callee_int = callee_cc.get_num_int_param_regs();
let caller_sse = caller_cc.get_num_sse_param_regs();
let callee_sse = callee_cc.get_num_sse_param_regs();
callee_int <= caller_int && callee_sse <= caller_sse
}
pub fn compatibility_report(
caller_cc: X86CallingConvention,
callee_cc: X86CallingConvention,
) -> String {
if caller_cc == callee_cc {
return format!(
"{} -> {}: identical, fully compatible",
caller_cc.name(),
callee_cc.name()
);
}
if Self::are_compatible(caller_cc, callee_cc) {
format!(
"{} -> {}: compatible (same ABI family)",
caller_cc.name(),
callee_cc.name()
)
} else {
format!(
"{} -> {}: INCOMPATIBLE (different ABI families)",
caller_cc.name(),
callee_cc.name()
)
}
}
}
pub struct X86TailCallFrameManager {
pub is_64bit: bool,
pub stack_alignment: i64,
pub has_frame_pointer: bool,
}
impl X86TailCallFrameManager {
pub fn new(is_64bit: bool, stack_alignment: i64, has_frame_pointer: bool) -> Self {
X86TailCallFrameManager {
is_64bit,
stack_alignment,
has_frame_pointer,
}
}
pub fn emit_frame_transition(
&self,
caller_frame_size: i64,
callee_arg_area: i64,
callee_saved_regs: &[u16],
) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
if caller_frame_size > 0 {
let mut add_rsp = MachineInstr::new(X86Opcode::ADD.as_u32());
add_rsp
.operands
.push(MachineOperand::PhysReg(self.sp() as u32));
add_rsp
.operands
.push(MachineOperand::Imm(caller_frame_size));
instrs.push(add_rsp);
}
for ® in callee_saved_regs.iter().rev() {
let mut pop = MachineInstr::new(X86Opcode::POP.as_u32());
pop.operands.push(MachineOperand::PhysReg(reg as u32));
instrs.push(pop);
}
if self.has_frame_pointer {
let mut pop_rbp = MachineInstr::new(X86Opcode::POP.as_u32());
pop_rbp
.operands
.push(MachineOperand::PhysReg(self.bp() as u32));
instrs.push(pop_rbp);
}
if callee_arg_area > 0 {
let mut sub_rsp = MachineInstr::new(X86Opcode::SUB.as_u32());
sub_rsp
.operands
.push(MachineOperand::PhysReg(self.sp() as u32));
sub_rsp.operands.push(MachineOperand::Imm(callee_arg_area));
instrs.push(sub_rsp);
}
instrs
}
pub fn emit_minimal_transition(&self, callee_arg_area_delta: i64) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
if callee_arg_area_delta > 0 {
let mut sub_rsp = MachineInstr::new(X86Opcode::SUB.as_u32());
sub_rsp
.operands
.push(MachineOperand::PhysReg(self.sp() as u32));
sub_rsp
.operands
.push(MachineOperand::Imm(callee_arg_area_delta));
instrs.push(sub_rsp);
} else if callee_arg_area_delta < 0 {
let mut add_rsp = MachineInstr::new(X86Opcode::ADD.as_u32());
add_rsp
.operands
.push(MachineOperand::PhysReg(self.sp() as u32));
add_rsp
.operands
.push(MachineOperand::Imm(-callee_arg_area_delta));
instrs.push(add_rsp);
}
instrs
}
pub fn align_frame_size(&self, size: i64) -> i64 {
if size == 0 {
return 0;
}
((size + self.stack_alignment - 1) / self.stack_alignment) * self.stack_alignment
}
pub fn outgoing_arg_offset(&self, arg_index: usize, arg_size: i64) -> i64 {
arg_index as i64 * arg_size
}
fn sp(&self) -> u16 {
if self.is_64bit {
RSP_CONST
} else {
ESP_CONST
}
}
fn bp(&self) -> u16 {
if self.is_64bit {
RBP_CONST
} else {
EBP_CONST
}
}
pub fn word_size(&self) -> i64 {
if self.is_64bit {
8
} else {
4
}
}
pub fn can_emit_frame_transition(
&self,
caller_frame_size: i64,
callee_arg_area: i64,
callee_saved_count: usize,
) -> bool {
let total_adjust = caller_frame_size
+ (callee_saved_count as i64) * self.word_size()
+ if self.has_frame_pointer {
self.word_size()
} else {
0
}
+ callee_arg_area;
total_adjust <= i32::MAX as i64 && total_adjust >= i32::MIN as i64
}
}
pub struct X86TailCallArgForwarding {
pub is_64bit: bool,
pub callee_cc: X86CallingConvention,
}
impl X86TailCallArgForwarding {
pub fn new(is_64bit: bool, callee_cc: X86CallingConvention) -> Self {
X86TailCallArgForwarding {
is_64bit,
callee_cc,
}
}
pub fn forward_args(
&self,
caller_args: &[TailCallArgInfo],
callee_args: &[TailCallArgInfo],
reg_moves: &[RegMove],
) -> Vec<MachineInstr> {
let mut instrs = Vec::new();
for rm in reg_moves {
if rm.is_sse {
instrs.extend(self.emit_xmm_move(rm.src_reg, rm.dst_reg));
} else if rm.src_is_stack {
instrs.extend(self.emit_stack_to_reg(rm.src_stack_offset, rm.dst_reg, rm.size));
} else {
instrs.extend(self.emit_reg_to_reg(rm.src_reg, rm.dst_reg, rm.size));
}
}
for (caller, callee) in caller_args.iter().zip(callee_args.iter()) {
if caller.is_in_reg() && callee.is_on_stack() {
if let Some(src_reg) = caller.reg {
if let Some(dst_offset) = callee.stack_offset {
instrs.extend(self.emit_reg_to_stack(
src_reg,
dst_offset,
caller.size,
caller.is_sse,
));
}
}
}
}
instrs
}
pub fn emit_reg_to_reg(&self, src: u16, dst: u16, size: u32) -> Vec<MachineInstr> {
let mut instr = MachineInstr::new(X86Opcode::MOV.as_u32());
instr.operands.push(MachineOperand::PhysReg(dst as u32));
instr.operands.push(MachineOperand::PhysReg(src as u32));
vec![instr]
}
pub fn emit_xmm_move(&self, src: u16, dst: u16) -> Vec<MachineInstr> {
let mut instr = MachineInstr::new(X86Opcode::MOVSD.as_u32());
instr.operands.push(MachineOperand::PhysReg(dst as u32));
instr.operands.push(MachineOperand::PhysReg(src as u32));
vec![instr]
}
pub fn emit_stack_to_reg(&self, stack_offset: i64, dst: u16, _size: u32) -> Vec<MachineInstr> {
let mut instr = MachineInstr::new(X86Opcode::MOV.as_u32());
instr.operands.push(MachineOperand::PhysReg(dst as u32));
instr.operands.push(MachineOperand::Imm(stack_offset));
vec![instr]
}
pub fn emit_reg_to_stack(
&self,
src: u16,
stack_offset: i64,
_size: u32,
_is_sse: bool,
) -> Vec<MachineInstr> {
let mut instr = MachineInstr::new(X86Opcode::MOV.as_u32());
instr.operands.push(MachineOperand::Imm(stack_offset));
instr.operands.push(MachineOperand::PhysReg(src as u32));
vec![instr]
}
pub fn callee_int_param_regs(&self) -> Vec<u16> {
self.callee_cc.get_int_param_regs()
}
pub fn callee_sse_param_regs(&self) -> Vec<u16> {
self.callee_cc.get_sse_param_regs()
}
pub fn has_move_cycle(&self, moves: &[RegMove]) -> bool {
let mut graph: HashMap<u16, u16> = HashMap::new();
for m in moves {
if !m.src_is_stack {
graph.insert(m.src_reg, m.dst_reg);
}
}
for &start in graph.keys() {
let mut slow = start;
let mut fast = start;
loop {
if let Some(&next) = graph.get(&slow) {
slow = next;
} else {
break;
}
if let Some(&next1) = graph.get(&fast) {
if let Some(&next2) = graph.get(&next1) {
fast = next2;
} else {
break;
}
} else {
break;
}
if slow == fast {
return true;
}
}
}
false
}
pub fn break_move_cycles(&self, moves: &[RegMove], temp_reg: u16) -> Vec<RegMove> {
if !self.has_move_cycle(moves) {
return moves.to_vec();
}
let mut new_moves = moves.to_vec();
for i in 0..new_moves.len() {
let src = new_moves[i].src_reg;
for j in (i + 1)..new_moves.len() {
if new_moves[j].dst_reg == src && new_moves[j].src_reg == new_moves[i].dst_reg {
let orig_dst = new_moves[i].dst_reg;
new_moves[i].dst_reg = temp_reg;
let temp_move = RegMove::reg_to_reg(temp_reg, orig_dst, new_moves[i].size);
new_moves.insert(j + 1, temp_move);
return new_moves;
}
}
}
new_moves
}
}
pub struct X86TailCallReturnAddress;
impl X86TailCallReturnAddress {
pub fn verify_return_address_position(
rsp_value: i64,
expected_return_addr_offset: i64,
) -> bool {
rsp_value == expected_return_addr_offset
}
pub fn return_address_safe(callee_arg_area: i64) -> bool {
callee_arg_area >= 0
}
pub fn emit_return_address_preservation() -> Vec<MachineInstr> {
Vec::new()
}
}
pub struct X86TargetMachineLowering {
pub tail_call: X86TailCall,
pub return_lowering: X86ReturnLowering,
pub frame_manager: X86TailCallFrameManager,
pub arg_forwarding: X86TailCallArgForwarding,
pub compat_matrix: X86TailCallCompatibilityMatrix,
pub edge_cases: X86CCEdgeCases,
}
impl X86TargetMachineLowering {
pub fn new_sysv64() -> Self {
X86TargetMachineLowering {
tail_call: X86TailCall::new_sysv64(),
return_lowering: X86ReturnLowering::new_sysv64(),
frame_manager: X86TailCallFrameManager::new(true, X86_TC_SYSV_STACK_ALIGN, true),
arg_forwarding: X86TailCallArgForwarding::new(true, X86CallingConvention::X86_64_SysV),
compat_matrix: X86TailCallCompatibilityMatrix,
edge_cases: X86CCEdgeCases,
}
}
pub fn new_win64() -> Self {
X86TargetMachineLowering {
tail_call: X86TailCall::new_win64(),
return_lowering: X86ReturnLowering::new_win64(),
frame_manager: X86TailCallFrameManager::new(true, X86_TC_SYSV_STACK_ALIGN, true),
arg_forwarding: X86TailCallArgForwarding::new(true, X86CallingConvention::Win64),
compat_matrix: X86TailCallCompatibilityMatrix,
edge_cases: X86CCEdgeCases,
}
}
pub fn run_tail_call_opt(&mut self, mf: &mut MachineFunction) -> Vec<String> {
self.tail_call.run(mf)
}
pub fn lower_return(
&mut self,
kind: ReturnKind,
sret_info: Option<&SRetInfo>,
is_dead: bool,
) -> ReturnLoweringResult {
self.return_lowering
.lower_return_with_type(kind, sret_info, is_dead)
}
pub fn get_stats(&self) -> String {
format!(
"TailCall: {} | Return: {}",
self.tail_call.stats.report(),
self.return_lowering.ret_stats.report()
)
}
}
pub struct X86TailCallValidator;
impl X86TailCallValidator {
pub fn validate_musttail(
mf: &MachineFunction,
site: &CallSiteInfo,
result: &TailCallEligibilityResult,
) -> Result<(), String> {
if !Self::is_in_tail_position(mf, site) {
return Err(
"musttail call is not in tail position (must be immediately before RET)"
.to_string(),
);
}
if !result.cc_compatible {
return Err(format!(
"musttail: incompatible calling conventions: {:?} vs {:?}",
site.caller_cc, site.callee_cc
));
}
if result.has_dynamic_alloca {
return Err(
"musttail: dynamic alloca in caller prevents guaranteed tail call".to_string(),
);
}
if result.has_variable_sized_objects {
return Err("musttail: variable-sized objects between caller and callee".to_string());
}
if result.has_byval_conflict {
return Err("musttail: byval argument conflict prevents tail call".to_string());
}
if !result.stack_compatible {
return Err("musttail: stack argument layout incompatible".to_string());
}
Ok(())
}
pub fn is_in_tail_position(mf: &MachineFunction, site: &CallSiteInfo) -> bool {
for block in &mf.blocks {
let instrs = &block.instructions;
if site.call_instr_idx + 1 < instrs.len() {
let next = &instrs[site.call_instr_idx + 1];
if next.opcode == X86Opcode::RET.as_u32() {
return true;
}
}
}
false
}
pub fn return_type_compatible(caller_kind: ReturnKind, callee_kind: ReturnKind) -> bool {
match (caller_kind, callee_kind) {
(ReturnKind::Void, ReturnKind::Void) => true,
(ReturnKind::Integer, ReturnKind::Integer) => true,
(ReturnKind::Integer128, ReturnKind::Integer128) => true,
(ReturnKind::X87, ReturnKind::X87) => true,
(ReturnKind::SSE, ReturnKind::SSE) => true,
(ReturnKind::Vector, ReturnKind::Vector) => true,
(ReturnKind::Vector256, ReturnKind::Vector256) => true,
(ReturnKind::Vector512, ReturnKind::Vector512) => true,
(ReturnKind::SRet, ReturnKind::SRet) => true,
(ReturnKind::MultiReg, ReturnKind::MultiReg) => true,
(ReturnKind::Dead, _) => true,
(_, ReturnKind::Dead) => true,
_ => false,
}
}
pub fn validation_report(site: &CallSiteInfo, result: &TailCallEligibilityResult) -> String {
let mut report = String::new();
report.push_str(&format!(
"musttail validation for call to '{}':\n",
site.callee_name
));
report.push_str(&format!(
" Tail position: {}\n",
if result.eligible { "OK" } else { "FAIL" }
));
report.push_str(&format!(
" CC compatible: {}\n",
if result.cc_compatible { "OK" } else { "FAIL" }
));
report.push_str(&format!(
" Stack compatible: {}\n",
if result.stack_compatible {
"OK"
} else {
"FAIL"
}
));
report.push_str(&format!(
" No lifetime markers: {}\n",
if !result.has_lifetime_markers {
"OK"
} else {
"FAIL"
}
));
report.push_str(&format!(
" No dynamic alloca: {}\n",
if !result.has_dynamic_alloca {
"OK"
} else {
"FAIL"
}
));
report.push_str(&format!(
" No byval conflicts: {}\n",
if !result.has_byval_conflict {
"OK"
} else {
"FAIL"
}
));
report.push_str(&format!(
" No variable-sized objects: {}\n",
if !result.has_variable_sized_objects {
"OK"
} else {
"FAIL"
}
));
let status = if result.eligible {
"ELIGIBLE".to_string()
} else {
format!("INELIGIBLE: {}", result.reason)
};
report.push_str(&format!(" Result: {}\n", status));
report
}
}
impl fmt::Display for TailCallKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl fmt::Display for ReturnKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl fmt::Display for TailCallEligibilityResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"TailCallEligibility({}): eligible={} reason='{}' cc_ok={} stack_ok={} reg_moves={}",
self.kind,
self.eligible,
self.reason,
self.cc_compatible,
self.stack_compatible,
self.reg_moves.len()
)
}
}
impl fmt::Display for RegMove {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.describe())
}
}
impl fmt::Display for SRetInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_sret {
write!(
f,
"sret(size={} align={} reg=r{})",
self.struct_size, self.struct_align, self.sret_reg
)
} else if self.in_regs {
write!(
f,
"in_regs(size={} regs={:?})",
self.struct_size, self.return_regs
)
} else {
write!(f, "no_sret")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::{MachineBasicBlock, MachineFunction};
use crate::x86::x86_calling_convention::X86CallingConvention;
fn make_call_instr(callee: &str) -> MachineInstr {
let mut instr = MachineInstr::new(X86Opcode::CALL.as_u32());
instr
.operands
.push(MachineOperand::Label(callee.to_string()));
instr
}
fn make_ret_instr() -> MachineInstr {
MachineInstr::new(X86Opcode::RET.as_u32())
}
fn make_jmp_instr(target: &str) -> MachineInstr {
let mut instr = MachineInstr::new(X86Opcode::JMP.as_u32());
instr
.operands
.push(MachineOperand::Label(target.to_string()));
instr
}
fn make_mov_instr(dst: u32, src: u32) -> MachineInstr {
let mut instr = MachineInstr::new(X86Opcode::MOV.as_u32());
instr.operands.push(MachineOperand::PhysReg(dst));
instr.operands.push(MachineOperand::PhysReg(src));
instr
}
fn make_add_rsp_imm(imm: i64) -> MachineInstr {
let mut instr = MachineInstr::new(X86Opcode::ADD.as_u32());
instr
.operands
.push(MachineOperand::PhysReg(RSP_CONST as u32));
instr.operands.push(MachineOperand::Imm(imm));
instr
}
fn make_pop(reg: u16) -> MachineInstr {
let mut instr = MachineInstr::new(X86Opcode::POP.as_u32());
instr.operands.push(MachineOperand::PhysReg(reg as u32));
instr
}
fn make_simple_function(name: &str, instrs: Vec<MachineInstr>) -> MachineFunction {
let mut mf = MachineFunction::new(name);
let mut block = MachineBasicBlock {
name: "entry".to_string(),
instructions: instrs,
successors: Vec::new(),
};
mf.blocks.push(block);
mf
}
#[test]
fn test_tail_kind_musttail() {
let kind = TailCallKind::MustTail;
assert!(kind.is_musttail());
assert!(kind.is_tail_call());
assert_eq!(kind.name(), "musttail");
}
#[test]
fn test_tail_kind_tail() {
let kind = TailCallKind::Tail;
assert!(!kind.is_musttail());
assert!(kind.is_tail_call());
assert_eq!(kind.name(), "tail");
}
#[test]
fn test_tail_kind_notail() {
let kind = TailCallKind::NoTail;
assert!(!kind.is_musttail());
assert!(!kind.is_tail_call());
assert_eq!(kind.name(), "notail");
}
#[test]
fn test_detect_tail_kind_call_ret() {
let tc = X86TailCall::new_sysv64();
let instrs = vec![make_call_instr("foo"), make_ret_instr()];
let kind = tc.detect_tail_kind(&instrs, 0);
assert_eq!(kind, TailCallKind::NoTail);
}
#[test]
fn test_detect_tail_kind_call_no_ret() {
let tc = X86TailCall::new_sysv64();
let instrs = vec![
make_call_instr("foo"),
make_mov_instr(RAX_CONST as u32, 0),
make_ret_instr(),
];
let kind = tc.detect_tail_kind(&instrs, 0);
assert_eq!(kind, TailCallKind::NoTail);
}
#[test]
fn test_detect_tail_kind_musttail_label() {
let tc = X86TailCall::new_sysv64();
let mut call = MachineInstr::new(X86Opcode::CALL.as_u32());
call.operands
.push(MachineOperand::Label("musttail.foo".to_string()));
let instrs = vec![call, make_ret_instr()];
let kind = tc.detect_tail_kind(&instrs, 0);
assert_eq!(kind, TailCallKind::MustTail);
}
#[test]
fn test_detect_tail_kind_tail_label() {
let tc = X86TailCall::new_sysv64();
let mut call = MachineInstr::new(X86Opcode::CALL.as_u32());
call.operands
.push(MachineOperand::Label("tail.foo".to_string()));
let instrs = vec![call, make_ret_instr()];
let kind = tc.detect_tail_kind(&instrs, 0);
assert_eq!(kind, TailCallKind::Tail);
}
#[test]
fn test_cc_compatible_same() {
let site = CallSiteInfo::new(
0,
0,
"foo".to_string(),
X86CallingConvention::X86_64_SysV,
X86CallingConvention::X86_64_SysV,
);
let tc = X86TailCall::new_sysv64();
assert!(tc.check_calling_convention_compatibility(&site));
}
#[test]
fn test_cc_incompatible_sysv_win64() {
let site = CallSiteInfo::new(
0,
0,
"foo".to_string(),
X86CallingConvention::X86_64_SysV,
X86CallingConvention::Win64,
);
let tc = X86TailCall::new_sysv64();
assert!(!tc.check_calling_convention_compatibility(&site));
}
#[test]
fn test_cc_compatible_32bit() {
let site = CallSiteInfo::new(
0,
0,
"foo".to_string(),
X86CallingConvention::Fast,
X86CallingConvention::StdCall,
);
let tc = X86TailCall::new_32bit(X86CallingConvention::Fast);
assert!(tc.check_calling_convention_compatibility(&site));
}
#[test]
fn test_cc_incompatible_cleanup() {
let site = CallSiteInfo::new(
0,
0,
"foo".to_string(),
X86CallingConvention::C,
X86CallingConvention::StdCall,
);
let tc = X86TailCall::new_32bit(X86CallingConvention::C);
assert!(!tc.check_calling_convention_compatibility(&site));
}
#[test]
fn test_eligibility_identical_cc() {
let mf = make_simple_function("test", vec![make_call_instr("bar"), make_ret_instr()]);
let site = CallSiteInfo {
tail_kind: TailCallKind::MustTail,
..CallSiteInfo::new(
0,
0,
"bar".to_string(),
X86CallingConvention::X86_64_SysV,
X86CallingConvention::X86_64_SysV,
)
};
let mut tc = X86TailCall::new_sysv64();
let result = tc.analyze_tail_call_eligibility(&mf, &site);
assert!(result.eligible, "Expected eligible, got: {}", result.reason);
assert!(result.cc_compatible);
assert!(result.stack_compatible);
assert!(!result.has_lifetime_markers);
assert!(!result.has_dynamic_alloca);
}
#[test]
fn test_eligibility_cc_mismatch() {
let mf = make_simple_function("test", vec![make_call_instr("bar"), make_ret_instr()]);
let site = CallSiteInfo {
tail_kind: TailCallKind::MustTail,
..CallSiteInfo::new(
0,
0,
"bar".to_string(),
X86CallingConvention::X86_64_SysV,
X86CallingConvention::Win64,
)
};
let mut tc = X86TailCall::new_sysv64();
let result = tc.analyze_tail_call_eligibility(&mf, &site);
assert!(!result.eligible);
assert!(!result.cc_compatible);
assert!(result.reason.contains("calling convention mismatch"));
}
#[test]
fn test_eligibility_result_ineligible_default() {
let result = TailCallEligibilityResult::ineligible(
TailCallKind::MustTail,
"test reason".to_string(),
);
assert!(!result.eligible);
assert!(result.is_musttail_failure());
assert_eq!(result.reason, "test reason");
}
#[test]
fn test_eligibility_result_eligible_default() {
let result = TailCallEligibilityResult::eligible_default(TailCallKind::Tail);
assert!(result.eligible);
assert!(!result.is_musttail_failure());
}
#[test]
fn test_reg_move_reg_to_reg() {
let rm = RegMove::reg_to_reg(RAX_CONST, RCX_CONST, 8);
assert_eq!(rm.src_reg, RAX_CONST);
assert_eq!(rm.dst_reg, RCX_CONST);
assert_eq!(rm.size, 8);
assert!(!rm.src_is_stack);
assert!(!rm.is_sse);
}
#[test]
fn test_reg_move_stack_to_reg() {
let rm = RegMove::stack_to_reg(16, RDX_CONST, 4);
assert!(rm.src_is_stack);
assert_eq!(rm.src_stack_offset, 16);
assert_eq!(rm.dst_reg, RDX_CONST);
assert_eq!(rm.size, 4);
}
#[test]
fn test_reg_move_xmm() {
let rm = RegMove::xmm_to_xmm(XMM0_CONST, XMM1_CONST);
assert!(rm.is_sse);
assert_eq!(rm.src_reg, XMM0_CONST);
assert_eq!(rm.dst_reg, XMM1_CONST);
}
#[test]
fn test_arg_info_default() {
let info = TailCallArgInfo::new(2, 8, 8);
assert_eq!(info.arg_index, 2);
assert_eq!(info.size, 8);
assert_eq!(info.alignment, 8);
assert!(!info.is_byval);
assert!(!info.is_inalloca);
assert!(!info.is_sret);
assert!(!info.is_nest);
}
#[test]
fn test_arg_info_register() {
let mut info = TailCallArgInfo::new(0, 8, 8);
info.reg = Some(RDI_CONST);
assert!(info.is_in_reg());
assert!(!info.is_on_stack());
}
#[test]
fn test_arg_info_stack() {
let mut info = TailCallArgInfo::new(0, 8, 8);
info.stack_offset = Some(16);
assert!(!info.is_in_reg());
assert!(info.is_on_stack());
}
#[test]
fn test_sret_demotion_sysv_small_struct() {
assert!(X86CCEdgeCases::analyze_sret_demotion(
X86CallingConvention::X86_64_SysV,
16,
false,
));
}
#[test]
fn test_sret_demotion_sysv_large_struct() {
assert!(!X86CCEdgeCases::analyze_sret_demotion(
X86CallingConvention::X86_64_SysV,
24,
false,
));
}
#[test]
fn test_sret_demotion_sysv_memory_class() {
assert!(!X86CCEdgeCases::analyze_sret_demotion(
X86CallingConvention::X86_64_SysV,
16,
true,
));
}
#[test]
fn test_sret_demotion_win64_small() {
assert!(X86CCEdgeCases::analyze_sret_demotion(
X86CallingConvention::Win64,
8,
false,
));
}
#[test]
fn test_sret_demotion_win64_large() {
assert!(!X86CCEdgeCases::analyze_sret_demotion(
X86CallingConvention::Win64,
16,
false,
));
}
#[test]
fn test_sret_compatible_for_tail_call() {
assert!(X86CCEdgeCases::sret_compatible_for_tail_call(true, true));
assert!(X86CCEdgeCases::sret_compatible_for_tail_call(false, false));
assert!(!X86CCEdgeCases::sret_compatible_for_tail_call(true, false));
assert!(!X86CCEdgeCases::sret_compatible_for_tail_call(false, true));
}
#[test]
fn test_byval_blocks_tail_call_in_caller_frame() {
assert!(X86CCEdgeCases::byval_blocks_tail_call(true, 16, 32));
}
#[test]
fn test_byval_does_not_block_if_in_outgoing_area() {
assert!(!X86CCEdgeCases::byval_blocks_tail_call(true, 48, 32));
}
#[test]
fn test_inalloca_blocks_tail_call() {
assert!(X86CCEdgeCases::inalloca_blocks_tail_call(true));
assert!(!X86CCEdgeCases::inalloca_blocks_tail_call(false));
}
#[test]
fn test_nest_compatible() {
assert!(X86CCEdgeCases::nest_compatible_for_tail_call(true, true));
assert!(X86CCEdgeCases::nest_compatible_for_tail_call(false, false));
assert!(!X86CCEdgeCases::nest_compatible_for_tail_call(true, false));
}
#[test]
fn test_swift_async_context_blocks_tail_call() {
assert!(!X86CCEdgeCases::swift_async_context_blocks_tail_call(
true, true
));
assert!(!X86CCEdgeCases::swift_async_context_blocks_tail_call(
false, false
));
assert!(X86CCEdgeCases::swift_async_context_blocks_tail_call(
true, false
));
}
#[test]
fn test_swift_error_blocks_tail_call() {
assert!(!X86CCEdgeCases::swift_error_blocks_tail_call(true, true));
assert!(!X86CCEdgeCases::swift_error_blocks_tail_call(false, false));
assert!(X86CCEdgeCases::swift_error_blocks_tail_call(true, false));
}
#[test]
fn test_nest_register_sysv() {
let reg = X86CCEdgeCases::get_nest_register(X86CallingConvention::X86_64_SysV);
assert_eq!(reg, X86_TC_NEST_REG_SYSV);
}
#[test]
fn test_nest_register_win64() {
let reg = X86CCEdgeCases::get_nest_register(X86CallingConvention::Win64);
assert_eq!(reg, X86_TC_NEST_REG_WIN64);
}
#[test]
fn test_swift_async_context_register() {
let reg =
X86CCEdgeCases::get_swift_async_context_register(X86CallingConvention::X86_64_SysV);
assert_eq!(reg, X86_TC_SWIFT_ASYNC_CTX_REG);
}
#[test]
fn test_swift_error_register() {
let reg = X86CCEdgeCases::get_swift_error_register(X86CallingConvention::X86_64_SysV);
assert_eq!(reg, X86_TC_SWIFT_ERROR_REG);
}
#[test]
fn test_emit_byval_copy_small() {
let instrs = X86CCEdgeCases::emit_byval_copy_for_tail_call(0, 32, 16);
assert!(!instrs.is_empty());
assert_eq!(instrs.len(), 4);
}
#[test]
fn test_emit_byval_copy_zero() {
let instrs = X86CCEdgeCases::emit_byval_copy_for_tail_call(0, 0, 0);
assert!(instrs.is_empty());
}
#[test]
fn test_emit_nest_forward_same_reg() {
let instrs = X86CCEdgeCases::emit_nest_forward(R10_CONST, R10_CONST);
assert!(instrs.is_empty());
}
#[test]
fn test_emit_nest_forward_different_reg() {
let instrs = X86CCEdgeCases::emit_nest_forward(R10_CONST, R11_CONST);
assert_eq!(instrs.len(), 1);
assert_eq!(instrs[0].opcode, X86Opcode::MOV.as_u32());
}
#[test]
fn test_return_void() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::Void, None, 0, false);
assert_eq!(result.kind, ReturnKind::Void);
assert_eq!(result.instructions.len(), 1);
assert_eq!(result.instructions[0].opcode, X86Opcode::RET.as_u32());
}
#[test]
fn test_return_integer() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::Integer, None, 0, false);
assert_eq!(result.kind, ReturnKind::Integer);
assert_eq!(
result.instructions.last().unwrap().opcode,
X86Opcode::RET.as_u32()
);
}
#[test]
fn test_return_i128() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::Integer128, None, 0, false);
assert_eq!(result.kind, ReturnKind::Integer128);
assert_eq!(
result.instructions.last().unwrap().opcode,
X86Opcode::RET.as_u32()
);
}
#[test]
fn test_return_x87() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::X87, None, 0, false);
assert_eq!(result.kind, ReturnKind::X87);
assert_eq!(
result.instructions.last().unwrap().opcode,
X86Opcode::RET.as_u32()
);
}
#[test]
fn test_ret_jmp_instr_detection() {
let tc = X86TailCall::new_sysv64();
let call = make_call_instr("foo");
let ret = make_ret_instr();
let jmp = make_jmp_instr("bar");
let mov = make_mov_instr(RAX_CONST as u32, 0);
assert!(tc.is_call_instr(&call));
assert!(!tc.is_call_instr(&ret));
assert!(!tc.is_call_instr(&jmp));
assert!(tc.is_ret_instr(&ret));
assert!(!tc.is_ret_instr(&call));
assert!(tc.is_jmp_instr(&jmp));
assert!(!tc.is_jmp_instr(&call));
assert!(!tc.is_jmp_instr(&mov));
}
#[test]
fn test_dead_return_elimination() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::Integer, None, 0, true);
assert!(result.dead_eliminated);
assert_eq!(result.instructions[0].opcode, X86Opcode::RET.as_u32());
}
#[test]
fn test_dead_return_void_not_eliminated() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::Void, None, 0, true);
assert!(!result.dead_eliminated);
}
#[test]
fn test_dead_return_disabled() {
let mut rl = X86ReturnLowering::new_sysv64();
rl.enable_dead_ret_elim = false;
let result = rl.lower_return(ReturnKind::Integer, None, 0, true);
assert!(!result.dead_eliminated);
}
#[test]
fn test_sret_return() {
let mut rl = X86ReturnLowering::new_sysv64();
let sret = SRetInfo::with_sret(RDI_CONST, 32, 16);
let result = rl.lower_return(ReturnKind::SRet, Some(&sret), 0, false);
assert!(result.sret_forwarded);
assert_eq!(result.kind, ReturnKind::SRet);
assert_eq!(result.instructions.len(), 2);
assert_eq!(result.instructions[0].opcode, X86Opcode::MOV.as_u32());
assert_eq!(result.instructions[1].opcode, X86Opcode::RET.as_u32());
}
#[test]
fn test_reti_for_stdcall() {
let mut rl = X86ReturnLowering::new_32bit(X86CallingConvention::StdCall);
let result = rl.lower_return(ReturnKind::Integer, None, 12, false);
assert_eq!(result.stack_pop, 12);
let ret_instr = result.instructions.last().unwrap();
assert_eq!(ret_instr.opcode, X86Opcode::RET.as_u32());
assert!(ret_instr
.operands
.iter()
.any(|op| matches!(op, MachineOperand::Imm(12))));
}
#[test]
fn test_stack_adjust_zero() {
let tc = X86TailCall::new_sysv64();
let instr = tc.emit_stack_adjust(0);
assert_eq!(instr.opcode, X86Opcode::ADD.as_u32());
}
#[test]
fn test_stack_adjust_positive() {
let tc = X86TailCall::new_sysv64();
let instr = tc.emit_stack_adjust(64);
assert_eq!(instr.opcode, X86Opcode::SUB.as_u32());
}
#[test]
fn test_stack_adjust_negative() {
let tc = X86TailCall::new_sysv64();
let instr = tc.emit_stack_adjust(-32);
assert_eq!(instr.opcode, X86Opcode::ADD.as_u32());
}
#[test]
fn test_frame_manager_align() {
let fm = X86TailCallFrameManager::new(true, 16, true);
assert_eq!(fm.align_frame_size(0), 0);
assert_eq!(fm.align_frame_size(1), 16);
assert_eq!(fm.align_frame_size(16), 16);
assert_eq!(fm.align_frame_size(17), 32);
}
#[test]
fn test_frame_manager_word_size_64() {
let fm = X86TailCallFrameManager::new(true, 16, true);
assert_eq!(fm.word_size(), 8);
}
#[test]
fn test_frame_manager_word_size_32() {
let fm = X86TailCallFrameManager::new(false, 4, false);
assert_eq!(fm.word_size(), 4);
}
#[test]
fn test_frame_transition_empty() {
let fm = X86TailCallFrameManager::new(true, 16, false);
let instrs = fm.emit_frame_transition(0, 0, &[]);
assert!(instrs.is_empty());
}
#[test]
fn test_frame_transition_with_locals_and_regs() {
let fm = X86TailCallFrameManager::new(true, 16, true);
let callee_saved = &[RBX_CONST, R12_CONST];
let instrs = fm.emit_frame_transition(48, 32, callee_saved);
assert_eq!(instrs.len(), 5);
assert_eq!(instrs[0].opcode, X86Opcode::ADD.as_u32());
assert_eq!(instrs[1].opcode, X86Opcode::POP.as_u32());
assert_eq!(instrs[2].opcode, X86Opcode::POP.as_u32());
assert_eq!(instrs[3].opcode, X86Opcode::POP.as_u32());
assert_eq!(instrs[4].opcode, X86Opcode::SUB.as_u32());
}
#[test]
fn test_frame_transition_can_emit() {
let fm = X86TailCallFrameManager::new(true, 16, true);
assert!(fm.can_emit_frame_transition(64, 32, 2));
}
#[test]
fn test_frame_transition_cannot_emit_huge() {
let fm = X86TailCallFrameManager::new(true, 16, true);
assert!(!fm.can_emit_frame_transition(i32::MAX as i64 + 1, 0, 0));
}
#[test]
fn test_arg_forward_reg_to_reg() {
let fw = X86TailCallArgForwarding::new(true, X86CallingConvention::X86_64_SysV);
let instrs = fw.emit_reg_to_reg(RAX_CONST, RCX_CONST, 8);
assert_eq!(instrs.len(), 1);
assert_eq!(instrs[0].opcode, X86Opcode::MOV.as_u32());
}
#[test]
fn test_arg_forward_xmm() {
let fw = X86TailCallArgForwarding::new(true, X86CallingConvention::X86_64_SysV);
let instrs = fw.emit_xmm_move(XMM0_CONST, XMM1_CONST);
assert_eq!(instrs.len(), 1);
}
#[test]
fn test_arg_forward_stack_to_reg() {
let fw = X86TailCallArgForwarding::new(true, X86CallingConvention::X86_64_SysV);
let instrs = fw.emit_stack_to_reg(16, RAX_CONST, 8);
assert_eq!(instrs.len(), 1);
assert_eq!(instrs[0].opcode, X86Opcode::MOV.as_u32());
}
#[test]
fn test_arg_forward_reg_to_stack() {
let fw = X86TailCallArgForwarding::new(true, X86CallingConvention::X86_64_SysV);
let instrs = fw.emit_reg_to_stack(RAX_CONST, 24, 8, false);
assert_eq!(instrs.len(), 1);
assert_eq!(instrs[0].opcode, X86Opcode::MOV.as_u32());
}
#[test]
fn test_arg_forward_detect_cycle() {
let fw = X86TailCallArgForwarding::new(true, X86CallingConvention::X86_64_SysV);
let moves = vec![
RegMove::reg_to_reg(RAX_CONST, RCX_CONST, 8),
RegMove::reg_to_reg(RCX_CONST, RAX_CONST, 8),
];
assert!(fw.has_move_cycle(&moves));
}
#[test]
fn test_arg_forward_no_cycle() {
let fw = X86TailCallArgForwarding::new(true, X86CallingConvention::X86_64_SysV);
let moves = vec![
RegMove::reg_to_reg(RAX_CONST, RCX_CONST, 8),
RegMove::reg_to_reg(RDX_CONST, RSI_CONST, 8),
];
assert!(!fw.has_move_cycle(&moves));
}
#[test]
fn test_arg_forward_break_cycle() {
let fw = X86TailCallArgForwarding::new(true, X86CallingConvention::X86_64_SysV);
let moves = vec![
RegMove::reg_to_reg(RAX_CONST, RCX_CONST, 8),
RegMove::reg_to_reg(RCX_CONST, RAX_CONST, 8),
];
let broken = fw.break_move_cycles(&moves, R10_CONST);
assert_eq!(broken.len(), 3);
}
#[test]
fn test_classify_return_void() {
let rl = X86ReturnLowering::new_sysv64();
assert_eq!(
rl.classify_return_type(0, false, false, 0, false),
ReturnKind::Void
);
}
#[test]
fn test_classify_return_integer() {
let rl = X86ReturnLowering::new_sysv64();
assert_eq!(
rl.classify_return_type(8, false, false, 0, false),
ReturnKind::Integer
);
}
#[test]
fn test_classify_return_i128() {
let rl = X86ReturnLowering::new_sysv64();
assert_eq!(
rl.classify_return_type(16, false, false, 0, false),
ReturnKind::Integer128
);
}
#[test]
fn test_classify_return_x87() {
let rl = X86ReturnLowering::new_sysv64();
assert_eq!(
rl.classify_return_type(16, true, false, 0, false),
ReturnKind::X87
);
}
#[test]
fn test_classify_return_vector() {
let rl = X86ReturnLowering::new_sysv64();
assert_eq!(
rl.classify_return_type(16, false, true, 128, false),
ReturnKind::Vector
);
}
#[test]
fn test_classify_return_vector256() {
let rl = X86ReturnLowering::new_sysv64();
assert_eq!(
rl.classify_return_type(32, false, true, 256, false),
ReturnKind::Vector256
);
}
#[test]
fn test_classify_return_vector512() {
let rl = X86ReturnLowering::new_sysv64();
assert_eq!(
rl.classify_return_type(64, false, true, 512, false),
ReturnKind::Vector512
);
}
#[test]
fn test_classify_return_struct() {
let rl = X86ReturnLowering::new_sysv64();
assert_eq!(
rl.classify_return_type(32, false, false, 0, true),
ReturnKind::SRet
);
}
#[test]
fn test_classify_return_large_sret() {
let rl = X86ReturnLowering::new_sysv64();
assert_eq!(
rl.classify_return_type(32, false, false, 0, false),
ReturnKind::SRet
);
}
#[test]
fn test_should_demote_sret_sysv() {
let rl = X86ReturnLowering::new_sysv64();
assert!(!rl.should_demote_sret(16, 8));
}
#[test]
fn test_should_demote_sret_sysv_enabled() {
let mut rl = X86ReturnLowering::new_sysv64();
rl.sret_demotion = true;
assert!(rl.should_demote_sret(16, 8));
assert!(!rl.should_demote_sret(24, 8));
}
#[test]
fn test_return_regs_integer_64() {
let rl = X86ReturnLowering::new_sysv64();
let regs = rl.get_return_regs_for_kind(ReturnKind::Integer);
assert_eq!(regs, vec![RAX_CONST]);
}
#[test]
fn test_return_regs_integer_32() {
let rl = X86ReturnLowering::new_32bit(X86CallingConvention::C);
let regs = rl.get_return_regs_for_kind(ReturnKind::Integer);
assert_eq!(regs, vec![EAX_CONST]);
}
#[test]
fn test_return_regs_i128() {
let rl = X86ReturnLowering::new_sysv64();
let regs = rl.get_return_regs_for_kind(ReturnKind::Integer128);
assert_eq!(regs, vec![RAX_CONST, RDX_CONST]);
}
#[test]
fn test_return_regs_void() {
let rl = X86ReturnLowering::new_sysv64();
let regs = rl.get_return_regs_for_kind(ReturnKind::Void);
assert!(regs.is_empty());
}
#[test]
fn test_return_regs_xmm() {
let rl = X86ReturnLowering::new_sysv64();
let regs = rl.get_return_regs_for_kind(ReturnKind::SSE);
assert_eq!(regs, vec![XMM0_CONST]);
}
#[test]
fn test_compat_matrix_identical() {
assert!(X86TailCallCompatibilityMatrix::are_compatible(
X86CallingConvention::X86_64_SysV,
X86CallingConvention::X86_64_SysV,
));
}
#[test]
fn test_compat_matrix_cross_64bit() {
assert!(!X86TailCallCompatibilityMatrix::are_compatible(
X86CallingConvention::X86_64_SysV,
X86CallingConvention::Win64,
));
}
#[test]
fn test_compat_matrix_32bit_same_cleanup() {
assert!(X86TailCallCompatibilityMatrix::are_compatible(
X86CallingConvention::StdCall,
X86CallingConvention::Fast,
));
}
#[test]
fn test_compat_matrix_32bit_diff_cleanup() {
assert!(!X86TailCallCompatibilityMatrix::are_compatible(
X86CallingConvention::C,
X86CallingConvention::StdCall,
));
}
#[test]
fn test_compat_matrix_return_value() {
assert!(X86TailCallCompatibilityMatrix::return_value_compatible(
X86CallingConvention::X86_64_SysV,
X86CallingConvention::Win64,
));
}
#[test]
fn test_compat_matrix_report() {
let report = X86TailCallCompatibilityMatrix::compatibility_report(
X86CallingConvention::X86_64_SysV,
X86CallingConvention::Win64,
);
assert!(report.contains("INCOMPATIBLE"));
}
#[test]
fn test_validate_musttail_success() {
let mf = make_simple_function("test", vec![make_call_instr("bar"), make_ret_instr()]);
let site = CallSiteInfo {
tail_kind: TailCallKind::MustTail,
call_instr_idx: 0,
block_idx: 0,
callee_name: "bar".to_string(),
caller_cc: X86CallingConvention::X86_64_SysV,
callee_cc: X86CallingConvention::X86_64_SysV,
num_args: 2,
result_used: false,
is_varargs: false,
caller_frame_size: 0,
callee_frame_size: 0,
};
let result = TailCallEligibilityResult::eligible_default(TailCallKind::MustTail);
let validation = X86TailCallValidator::validate_musttail(&mf, &site, &result);
assert!(validation.is_ok());
}
#[test]
fn test_validate_musttail_not_tail_position() {
let mf = make_simple_function(
"test",
vec![
make_call_instr("bar"),
make_mov_instr(RAX_CONST as u32, 0),
make_ret_instr(),
],
);
let site = CallSiteInfo {
tail_kind: TailCallKind::MustTail,
call_instr_idx: 0,
block_idx: 0,
callee_name: "bar".to_string(),
caller_cc: X86CallingConvention::X86_64_SysV,
callee_cc: X86CallingConvention::X86_64_SysV,
num_args: 0,
result_used: false,
is_varargs: false,
caller_frame_size: 0,
callee_frame_size: 0,
};
let result = TailCallEligibilityResult::eligible_default(TailCallKind::MustTail);
let validation = X86TailCallValidator::validate_musttail(&mf, &site, &result);
assert!(validation.is_err());
assert!(validation.unwrap_err().contains("tail position"));
}
#[test]
fn test_validate_musttail_cc_mismatch() {
let mf = make_simple_function("test", vec![make_call_instr("bar"), make_ret_instr()]);
let site = CallSiteInfo {
tail_kind: TailCallKind::MustTail,
call_instr_idx: 0,
block_idx: 0,
callee_name: "bar".to_string(),
caller_cc: X86CallingConvention::X86_64_SysV,
callee_cc: X86CallingConvention::Win64,
num_args: 0,
result_used: false,
is_varargs: false,
caller_frame_size: 0,
callee_frame_size: 0,
};
let mut result = TailCallEligibilityResult::eligible_default(TailCallKind::MustTail);
result.cc_compatible = false;
result.eligible = false;
let validation = X86TailCallValidator::validate_musttail(&mf, &site, &result);
assert!(validation.is_err());
}
#[test]
fn test_validation_report() {
let site = CallSiteInfo::new(
0,
0,
"target".to_string(),
X86CallingConvention::X86_64_SysV,
X86CallingConvention::X86_64_SysV,
);
let result = TailCallEligibilityResult::eligible_default(TailCallKind::MustTail);
let report = X86TailCallValidator::validation_report(&site, &result);
assert!(report.contains("ELIGIBLE"));
}
#[test]
fn test_tail_call_stats_default() {
let stats = X86TailCallStats::default();
assert_eq!(stats.analyzed, 0);
assert_eq!(stats.eligible, 0);
assert_eq!(stats.lowered, 0);
}
#[test]
fn test_tail_call_stats_report() {
let stats = X86TailCallStats {
analyzed: 10,
eligible: 7,
lowered: 5,
tail_fallbacks: 2,
..Default::default()
};
let report = stats.report();
assert!(report.contains("analyzed=10"));
assert!(report.contains("lowered=5"));
}
#[test]
fn test_return_stats_default() {
let stats = X86ReturnStats::default();
assert_eq!(stats.ret_lowered, 0);
assert_eq!(stats.dead_eliminated, 0);
}
#[test]
fn test_return_stats_report() {
let stats = X86ReturnStats {
ret_lowered: 100,
sret_lowered: 10,
dead_eliminated: 5,
..Default::default()
};
let report = stats.report();
assert!(report.contains("ret=100"));
assert!(report.contains("sret=10"));
assert!(report.contains("dead=5"));
}
#[test]
fn test_call_site_info_tail_requested() {
let mut site = CallSiteInfo::new(
0,
0,
"foo".to_string(),
X86CallingConvention::X86_64_SysV,
X86CallingConvention::X86_64_SysV,
);
assert!(!site.is_tail_call_requested());
site.tail_kind = TailCallKind::Tail;
assert!(site.is_tail_call_requested());
site.tail_kind = TailCallKind::MustTail;
assert!(site.is_musttail());
}
#[test]
fn test_return_address_safe() {
assert!(X86TailCallReturnAddress::return_address_safe(0));
assert!(X86TailCallReturnAddress::return_address_safe(32));
}
#[test]
fn test_return_address_preservation_empty() {
let instrs = X86TailCallReturnAddress::emit_return_address_preservation();
assert!(instrs.is_empty());
}
#[test]
fn test_target_machine_lowering_creation_sysv64() {
let tml = X86TargetMachineLowering::new_sysv64();
assert!(tml.tail_call.enabled);
assert!(tml.return_lowering.is_64bit);
}
#[test]
fn test_target_machine_lowering_creation_win64() {
let tml = X86TargetMachineLowering::new_win64();
assert!(tml.tail_call.enabled);
assert!(tml.return_lowering.has_shadow_space);
}
#[test]
fn test_target_machine_lowering_stats() {
let tml = X86TargetMachineLowering::new_sysv64();
let report = tml.get_stats();
assert!(report.contains("TailCall"));
assert!(report.contains("Return"));
}
#[test]
fn test_target_machine_lowering_return() {
let mut tml = X86TargetMachineLowering::new_sysv64();
let result = tml.lower_return(ReturnKind::Integer, None, false);
assert_eq!(result.kind, ReturnKind::Integer);
}
#[test]
fn test_lower_tail_call_eligible() {
let mut tc = X86TailCall::new_sysv64();
let mut mf = make_simple_function(
"caller",
vec![
make_mov_instr(RDI_CONST as u32, 42),
make_call_instr("callee"),
make_ret_instr(),
],
);
let result = TailCallEligibilityResult::eligible_default(TailCallKind::Tail);
let outcome = tc.lower_tail_call(&mut mf, 1, &result);
match outcome {
Ok(()) => {
let block = &mf.blocks[0];
let has_jmp = block
.instructions
.iter()
.any(|instr| instr.opcode == X86Opcode::JMP.as_u32());
assert!(has_jmp, "Tail call should replace CALL with JMP");
}
Err(_) => {
}
}
}
#[test]
fn test_sret_info_no_sret() {
let info = SRetInfo::no_sret();
assert!(!info.is_sret);
assert!(!info.in_regs);
assert_eq!(info.struct_size, 0);
}
#[test]
fn test_sret_info_with_sret() {
let info = SRetInfo::with_sret(RDI_CONST, 24, 8);
assert!(info.is_sret);
assert_eq!(info.sret_reg, RDI_CONST);
assert_eq!(info.struct_size, 24);
assert_eq!(info.struct_align, 8);
}
#[test]
fn test_sret_info_in_regs() {
let info = SRetInfo::in_regs(vec![RAX_CONST, RDX_CONST], 16);
assert!(!info.is_sret);
assert!(info.in_regs);
assert_eq!(info.return_regs.len(), 2);
}
#[test]
fn test_display_tail_kind() {
assert_eq!(format!("{}", TailCallKind::MustTail), "musttail");
assert_eq!(format!("{}", TailCallKind::Tail), "tail");
assert_eq!(format!("{}", TailCallKind::NoTail), "notail");
}
#[test]
fn test_display_return_kind() {
assert_eq!(format!("{}", ReturnKind::Void), "void");
assert_eq!(format!("{}", ReturnKind::Integer), "integer");
assert_eq!(format!("{}", ReturnKind::X87), "x87");
}
#[test]
fn test_display_eligibility_result() {
let result = TailCallEligibilityResult::eligible_default(TailCallKind::MustTail);
let display = format!("{}", result);
assert!(display.contains("eligible=true"));
assert!(display.contains("musttail"));
}
#[test]
fn test_display_reg_move() {
let rm = RegMove::reg_to_reg(RAX_CONST, RCX_CONST, 8);
let display = format!("{}", rm);
assert!(display.contains("r0"));
assert!(display.contains("r1"));
}
#[test]
fn test_display_sret_info() {
let info = SRetInfo::with_sret(RDI_CONST, 32, 16);
let display = format!("{}", info);
assert!(display.contains("sret"));
assert!(display.contains("size=32"));
}
#[test]
fn test_scan_call_sites_empty() {
let tc = X86TailCall::new_sysv64();
let mf = make_simple_function("test", vec![make_ret_instr()]);
let sites = tc.scan_call_sites(&mf);
assert!(sites.is_empty());
}
#[test]
fn test_scan_call_sites_one_call() {
let tc = X86TailCall::new_sysv64();
let mf = make_simple_function("test", vec![make_call_instr("bar"), make_ret_instr()]);
let sites = tc.scan_call_sites(&mf);
assert_eq!(sites.len(), 1);
assert_eq!(sites[0].callee_name, "bar");
}
#[test]
fn test_scan_call_sites_multiple_calls() {
let tc = X86TailCall::new_sysv64();
let mf = make_simple_function(
"test",
vec![
make_call_instr("foo"),
make_call_instr("bar"),
make_ret_instr(),
],
);
let sites = tc.scan_call_sites(&mf);
assert_eq!(sites.len(), 2);
}
#[test]
fn test_run_tail_call_opt_empty_function() {
let mut tc = X86TailCall::new_sysv64();
let mut mf = make_simple_function("empty", vec![make_ret_instr()]);
let diags = tc.run(&mut mf);
assert!(diags.is_empty());
}
#[test]
fn test_run_tail_call_opt_with_call() {
let mut tc = X86TailCall::new_sysv64();
let mut mf =
make_simple_function("test", vec![make_call_instr("callee"), make_ret_instr()]);
let diags = tc.run(&mut mf);
assert_eq!(tc.stats.analyzed, 1);
}
#[test]
fn test_new_sysv64_defaults() {
let tc = X86TailCall::new_sysv64();
assert!(tc.enabled);
assert!(tc.is_64bit);
assert_eq!(tc.default_cc, X86CallingConvention::X86_64_SysV);
}
#[test]
fn test_new_win64_defaults() {
let tc = X86TailCall::new_win64();
assert!(tc.enabled);
assert!(tc.is_64bit);
assert_eq!(tc.default_cc, X86CallingConvention::Win64);
}
#[test]
fn test_new_32bit_defaults() {
let tc = X86TailCall::new_32bit(X86CallingConvention::Fast);
assert!(tc.enabled);
assert!(!tc.is_64bit);
assert_eq!(tc.default_cc, X86CallingConvention::Fast);
}
#[test]
fn test_return_sse_vector() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::Vector, None, 0, false);
assert_eq!(result.kind, ReturnKind::Vector);
assert_eq!(
result.instructions.last().unwrap().opcode,
X86Opcode::RET.as_u32()
);
}
#[test]
fn test_return_ymm_vector() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::Vector256, None, 0, false);
assert_eq!(result.kind, ReturnKind::Vector256);
assert_eq!(
result.instructions.last().unwrap().opcode,
X86Opcode::RET.as_u32()
);
}
#[test]
fn test_return_zmm_vector() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::Vector512, None, 0, false);
assert_eq!(result.kind, ReturnKind::Vector512);
assert_eq!(
result.instructions.last().unwrap().opcode,
X86Opcode::RET.as_u32()
);
}
#[test]
fn test_emit_swift_async_ctx_forward_same() {
let instrs = X86CCEdgeCases::emit_swift_async_ctx_forward(R14_CONST, R14_CONST);
assert!(instrs.is_empty());
}
#[test]
fn test_emit_swift_async_ctx_forward_different() {
let instrs = X86CCEdgeCases::emit_swift_async_ctx_forward(R14_CONST, R15_CONST);
assert_eq!(instrs.len(), 1);
assert_eq!(instrs[0].opcode, X86Opcode::MOV.as_u32());
}
#[test]
fn test_emit_swift_error_forward_same() {
let instrs = X86CCEdgeCases::emit_swift_error_forward(R12_CONST, R12_CONST);
assert!(instrs.is_empty());
}
#[test]
fn test_emit_swift_error_forward_different() {
let instrs = X86CCEdgeCases::emit_swift_error_forward(R12_CONST, R13_CONST);
assert_eq!(instrs.len(), 1);
assert_eq!(instrs[0].opcode, X86Opcode::MOV.as_u32());
}
#[test]
fn test_compute_reg_moves_same_regs() {
let tc = X86TailCall::new_sysv64();
let caller = vec![
{
let mut a = TailCallArgInfo::new(0, 8, 8);
a.reg = Some(RDI_CONST);
a
},
{
let mut a = TailCallArgInfo::new(1, 8, 8);
a.reg = Some(RSI_CONST);
a
},
];
let callee = caller.clone();
let moves = tc.compute_reg_moves(&caller, &callee);
assert!(moves.is_empty());
}
#[test]
fn test_compute_reg_moves_different_regs() {
let tc = X86TailCall::new_sysv64();
let caller = vec![{
let mut a = TailCallArgInfo::new(0, 8, 8);
a.reg = Some(RDI_CONST);
a
}];
let callee = vec![{
let mut a = TailCallArgInfo::new(0, 8, 8);
a.reg = Some(RCX_CONST);
a
}];
let moves = tc.compute_reg_moves(&caller, &callee);
assert_eq!(moves.len(), 1);
assert_eq!(moves[0].src_reg, RDI_CONST);
assert_eq!(moves[0].dst_reg, RCX_CONST);
}
#[test]
fn test_compute_reg_moves_stack_to_reg() {
let tc = X86TailCall::new_sysv64();
let caller = vec![{
let mut a = TailCallArgInfo::new(0, 8, 8);
a.stack_offset = Some(0);
a
}];
let callee = vec![{
let mut a = TailCallArgInfo::new(0, 8, 8);
a.reg = Some(RDI_CONST);
a
}];
let moves = tc.compute_reg_moves(&caller, &callee);
assert_eq!(moves.len(), 1);
assert!(moves[0].src_is_stack);
assert_eq!(moves[0].dst_reg, RDI_CONST);
}
#[test]
fn test_return_kind_uses_xmm() {
assert!(ReturnKind::SSE.uses_xmm());
assert!(ReturnKind::Vector.uses_xmm());
assert!(ReturnKind::Vector256.uses_xmm());
assert!(ReturnKind::Vector512.uses_xmm());
assert!(!ReturnKind::Integer.uses_xmm());
assert!(!ReturnKind::Void.uses_xmm());
}
#[test]
fn test_return_kind_uses_x87() {
assert!(ReturnKind::X87.uses_x87());
assert!(!ReturnKind::Integer.uses_x87());
}
#[test]
fn test_return_kind_is_dead() {
assert!(ReturnKind::Dead.is_dead());
assert!(!ReturnKind::Integer.is_dead());
}
#[test]
fn test_extract_callee_name_from_label() {
let tc = X86TailCall::new_sysv64();
let instr = make_call_instr("my_function");
let name = tc.extract_callee_name(&instr);
assert_eq!(name, "my_function");
}
#[test]
fn test_extract_callee_name_from_global() {
let tc = X86TailCall::new_sysv64();
let mut instr = MachineInstr::new(X86Opcode::CALL.as_u32());
instr
.operands
.push(MachineOperand::Global("global_func".to_string()));
let name = tc.extract_callee_name(&instr);
assert_eq!(name, "global_func");
}
#[test]
fn test_count_args_zero() {
let tc = X86TailCall::new_sysv64();
let instrs = vec![make_call_instr("foo"), make_ret_instr()];
assert_eq!(tc.count_args(&instrs, 0), 0);
}
#[test]
fn test_count_args_with_movs() {
let tc = X86TailCall::new_sysv64();
let instrs = vec![
make_mov_instr(RDI_CONST as u32, 0),
make_mov_instr(RSI_CONST as u32, 0),
make_call_instr("foo"),
make_ret_instr(),
];
assert_eq!(tc.count_args(&instrs, 2), 2);
}
#[test]
fn test_count_args_clamped() {
let tc = X86TailCall::new_sysv64();
let mut instrs = Vec::new();
for i in 0..(X86_TC_MAX_ARGS + 10) {
let mut instr = MachineInstr::new(X86Opcode::MOV.as_u32());
instr.operands.push(MachineOperand::PhysReg(i as u32));
instr.operands.push(MachineOperand::Imm(0));
instrs.push(instr);
}
let call_idx = instrs.len();
instrs.push(make_call_instr("foo"));
instrs.push(make_ret_instr());
let count = tc.count_args(&instrs, call_idx);
assert!(count <= X86_TC_MAX_ARGS);
}
#[test]
fn test_eligibility_is_musttail_failure() {
let mut result = TailCallEligibilityResult::eligible_default(TailCallKind::MustTail);
assert!(!result.is_musttail_failure());
result.eligible = false;
assert!(result.is_musttail_failure());
}
#[test]
fn test_eligibility_not_musttail_failure() {
let result =
TailCallEligibilityResult::ineligible(TailCallKind::Tail, "reason".to_string());
assert!(!result.is_musttail_failure());
}
#[test]
fn test_minimal_transition_zero() {
let fm = X86TailCallFrameManager::new(true, 16, false);
let instrs = fm.emit_minimal_transition(0);
assert!(instrs.is_empty());
}
#[test]
fn test_minimal_transition_positive() {
let fm = X86TailCallFrameManager::new(true, 16, false);
let instrs = fm.emit_minimal_transition(32);
assert_eq!(instrs.len(), 1);
assert_eq!(instrs[0].opcode, X86Opcode::SUB.as_u32());
}
#[test]
fn test_minimal_transition_negative() {
let fm = X86TailCallFrameManager::new(true, 16, false);
let instrs = fm.emit_minimal_transition(-16);
assert_eq!(instrs.len(), 1);
assert_eq!(instrs[0].opcode, X86Opcode::ADD.as_u32());
}
#[test]
fn test_outgoing_arg_offset() {
let fm = X86TailCallFrameManager::new(true, 16, false);
assert_eq!(fm.outgoing_arg_offset(0, 8), 0);
assert_eq!(fm.outgoing_arg_offset(1, 8), 8);
assert_eq!(fm.outgoing_arg_offset(3, 8), 24);
}
#[test]
fn test_sp_reg_64() {
let tc = X86TailCall::new_sysv64();
assert_eq!(tc.sp_reg(), RSP_CONST);
}
#[test]
fn test_sp_reg_32() {
let tc = X86TailCall::new_32bit(X86CallingConvention::C);
assert_eq!(tc.sp_reg(), ESP_CONST);
}
#[test]
fn test_bp_reg_64() {
let tc = X86TailCall::new_sysv64();
assert_eq!(tc.bp_reg(), RBP_CONST);
}
#[test]
fn test_return_type_compatible_identical() {
assert!(X86TailCallValidator::return_type_compatible(
ReturnKind::Integer,
ReturnKind::Integer,
));
assert!(X86TailCallValidator::return_type_compatible(
ReturnKind::Void,
ReturnKind::Void,
));
assert!(X86TailCallValidator::return_type_compatible(
ReturnKind::X87,
ReturnKind::X87,
));
}
#[test]
fn test_return_type_compatible_dead() {
assert!(X86TailCallValidator::return_type_compatible(
ReturnKind::Dead,
ReturnKind::Integer,
));
assert!(X86TailCallValidator::return_type_compatible(
ReturnKind::Integer,
ReturnKind::Dead,
));
}
#[test]
fn test_return_type_incompatible() {
assert!(!X86TailCallValidator::return_type_compatible(
ReturnKind::Integer,
ReturnKind::Void,
));
assert!(!X86TailCallValidator::return_type_compatible(
ReturnKind::SSE,
ReturnKind::X87,
));
}
#[test]
fn test_stack_adjust_delta_no_stack_args() {
let tc = X86TailCall::new_sysv64();
let mf = make_simple_function("test", vec![make_ret_instr()]);
let result = TailCallEligibilityResult {
caller_args: vec![TailCallArgInfo::new(0, 8, 8)],
callee_args: vec![TailCallArgInfo::new(0, 8, 8)],
..TailCallEligibilityResult::eligible_default(TailCallKind::Tail)
};
let delta = tc.compute_stack_adjust_delta(&mf, &result);
assert_eq!(delta, 0);
}
#[test]
fn test_stack_adjust_delta_more_callee_stack_args() {
let tc = X86TailCall::new_sysv64();
let mf = make_simple_function("test", vec![make_ret_instr()]);
let mut caller_a = TailCallArgInfo::new(0, 8, 8);
caller_a.reg = Some(RDI_CONST);
let mut callee_a = TailCallArgInfo::new(0, 8, 8);
callee_a.stack_offset = Some(0);
let mut callee_b = TailCallArgInfo::new(1, 8, 8);
callee_b.stack_offset = Some(8);
let result = TailCallEligibilityResult {
caller_args: vec![caller_a],
callee_args: vec![callee_a, callee_b],
..TailCallEligibilityResult::eligible_default(TailCallKind::Tail)
};
let delta = tc.compute_stack_adjust_delta(&mf, &result);
assert_eq!(delta, 16);
}
#[test]
fn test_emit_reg_moves_empty() {
let tc = X86TailCall::new_sysv64();
let result = TailCallEligibilityResult::eligible_default(TailCallKind::Tail);
let instrs = tc.emit_reg_moves(&result);
assert!(instrs.is_empty());
}
#[test]
fn test_emit_reg_moves_with_moves() {
let tc = X86TailCall::new_sysv64();
let moves = vec![
RegMove::reg_to_reg(RDI_CONST, RCX_CONST, 8),
RegMove::xmm_to_xmm(XMM0_CONST, XMM1_CONST),
];
let result = TailCallEligibilityResult {
reg_moves: moves,
..TailCallEligibilityResult::eligible_default(TailCallKind::Tail)
};
let instrs = tc.emit_reg_moves(&result);
assert_eq!(instrs.len(), 2);
}
#[test]
fn test_replace_call_with_jmp_success() {
let tc = X86TailCall::new_sysv64();
let mut block = MachineBasicBlock {
name: "test".to_string(),
instructions: vec![make_call_instr("target"), make_ret_instr()],
successors: Vec::new(),
};
let result = TailCallEligibilityResult::eligible_default(TailCallKind::Tail);
let outcome = tc.replace_call_with_jmp(&mut block, 0, &result);
assert!(outcome.is_ok());
assert_eq!(block.instructions[0].opcode, X86Opcode::JMP.as_u32());
}
#[test]
fn test_replace_call_with_jmp_out_of_bounds() {
let tc = X86TailCall::new_sysv64();
let mut block = MachineBasicBlock {
name: "test".to_string(),
instructions: vec![make_ret_instr()],
successors: Vec::new(),
};
let result = TailCallEligibilityResult::eligible_default(TailCallKind::Tail);
let outcome = tc.replace_call_with_jmp(&mut block, 5, &result);
assert!(outcome.is_err());
}
#[test]
fn test_remove_trailing_ret_found() {
let tc = X86TailCall::new_sysv64();
let mut block = MachineBasicBlock {
name: "test".to_string(),
instructions: vec![make_jmp_instr("target"), make_ret_instr()],
successors: Vec::new(),
};
let outcome = tc.remove_trailing_ret(&mut block, 1);
assert!(outcome.is_ok());
assert_eq!(block.instructions.len(), 1);
}
#[test]
fn test_remove_trailing_ret_not_found() {
let tc = X86TailCall::new_sysv64();
let mut block = MachineBasicBlock {
name: "test".to_string(),
instructions: vec![make_jmp_instr("target")],
successors: Vec::new(),
};
let outcome = tc.remove_trailing_ret(&mut block, 0);
assert!(outcome.is_ok());
}
#[test]
fn test_calling_conv_num_int_param_regs() {
assert_eq!(
X86CallingConvention::X86_64_SysV.get_num_int_param_regs(),
6
);
assert_eq!(X86CallingConvention::Win64.get_num_int_param_regs(), 4);
assert_eq!(X86CallingConvention::Fast.get_num_int_param_regs(), 2);
assert_eq!(X86CallingConvention::C.get_num_int_param_regs(), 0);
}
#[test]
fn test_calling_conv_num_sse_param_regs() {
assert_eq!(
X86CallingConvention::X86_64_SysV.get_num_sse_param_regs(),
8
);
assert_eq!(X86CallingConvention::Win64.get_num_sse_param_regs(), 4);
assert_eq!(X86CallingConvention::VectorCall.get_num_sse_param_regs(), 6);
assert_eq!(X86CallingConvention::C.get_num_sse_param_regs(), 0);
}
#[test]
fn test_is_varargs_call_default_false() {
let tc = X86TailCall::new_sysv64();
let instr = make_call_instr("foo");
assert!(!tc.is_varargs_call(&instr));
}
#[test]
fn test_call_result_not_used_immediate_ret() {
let tc = X86TailCall::new_sysv64();
let instrs = vec![make_call_instr("foo"), make_ret_instr()];
assert!(!tc.is_call_result_used(&instrs, 0));
}
#[test]
fn test_forward_args_full() {
let fw = X86TailCallArgForwarding::new(true, X86CallingConvention::X86_64_SysV);
let mut caller_a = TailCallArgInfo::new(0, 8, 8);
caller_a.reg = Some(RDI_CONST);
let mut callee_a = TailCallArgInfo::new(0, 8, 8);
callee_a.reg = Some(RCX_CONST);
let reg_moves = vec![RegMove::reg_to_reg(RDI_CONST, RCX_CONST, 8)];
let instrs = fw.forward_args(&[caller_a], &[callee_a], ®_moves);
assert!(!instrs.is_empty());
}
#[test]
fn test_cross_32_64_bit_not_compatible() {
let caller_cc = X86CallingConvention::X86_64_SysV;
let callee_cc = X86CallingConvention::C;
assert!(caller_cc.is_64bit());
assert!(!callee_cc.is_64bit());
let compat = X86TailCallCompatibilityMatrix::are_compatible(caller_cc, callee_cc);
assert!(!compat, "64-bit to 32-bit tail call should be rejected");
}
#[test]
fn test_dead_return_multi_reg() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::Integer128, None, 0, true);
assert!(result.dead_eliminated);
assert_eq!(result.instructions[0].opcode, X86Opcode::RET.as_u32());
}
#[test]
fn test_dead_return_sse() {
let mut rl = X86ReturnLowering::new_sysv64();
let result = rl.lower_return(ReturnKind::SSE, None, 0, true);
assert!(result.dead_eliminated);
}
#[test]
fn test_thunk_necessity_default() {
let tc = X86TailCall::new_sysv64();
let result = TailCallEligibilityResult::eligible_default(TailCallKind::Tail);
assert!(!tc.check_thunk_necessity(&result));
}
#[test]
fn test_argument_passing_compatible_same() {
assert!(X86TailCallCompatibilityMatrix::argument_passing_compatible(
X86CallingConvention::X86_64_SysV,
X86CallingConvention::X86_64_SysV,
));
}
#[test]
fn test_argument_passing_not_compatible_more_callee_regs() {
assert!(
!X86TailCallCompatibilityMatrix::argument_passing_compatible(
X86CallingConvention::C,
X86CallingConvention::Win64,
)
);
}
}