ruby/zjit/src/hir.rs

6238 lines
231 KiB
Rust

//! High-level intermediary representation (IR) in static single-assignment (SSA) form.
// We use the YARV bytecode constants which have a CRuby-style name
#![allow(non_upper_case_globals)]
use crate::{
cast::IntoUsize, cruby::*, options::{get_option, DumpHIR}, profile::{get_or_create_iseq_payload, IseqPayload}, state::ZJITState
};
use std::{
cell::RefCell,
collections::{HashMap, HashSet, VecDeque},
ffi::{c_int, c_void, CStr},
mem::{align_of, size_of},
num::NonZeroU32,
ptr,
slice::Iter
};
use crate::hir_type::{Type, types};
/// An index of an [`Insn`] in a [`Function`]. This is a popular
/// type since this effectively acts as a pointer to an [`Insn`].
/// See also: [`Function::find`].
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
pub struct InsnId(pub usize);
impl Into<usize> for InsnId {
fn into(self) -> usize {
self.0
}
}
impl std::fmt::Display for InsnId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "v{}", self.0)
}
}
/// The index of a [`Block`], which effectively acts like a pointer.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct BlockId(pub usize);
impl std::fmt::Display for BlockId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "bb{}", self.0)
}
}
fn write_vec<T: std::fmt::Display>(f: &mut std::fmt::Formatter, objs: &Vec<T>) -> std::fmt::Result {
write!(f, "[")?;
let mut prefix = "";
for obj in objs {
write!(f, "{prefix}{obj}")?;
prefix = ", ";
}
write!(f, "]")
}
impl std::fmt::Display for VALUE {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.print(&PtrPrintMap::identity()).fmt(f)
}
}
impl VALUE {
pub fn print(self, ptr_map: &PtrPrintMap) -> VALUEPrinter {
VALUEPrinter { inner: self, ptr_map }
}
}
/// Print adaptor for [`VALUE`]. See [`PtrPrintMap`].
pub struct VALUEPrinter<'a> {
inner: VALUE,
ptr_map: &'a PtrPrintMap,
}
impl<'a> std::fmt::Display for VALUEPrinter<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.inner {
val if val.fixnum_p() => write!(f, "{}", val.as_fixnum()),
Qnil => write!(f, "nil"),
Qtrue => write!(f, "true"),
Qfalse => write!(f, "false"),
val => write!(f, "VALUE({:p})", self.ptr_map.map_ptr(val.as_ptr::<VALUE>())),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct BranchEdge {
pub target: BlockId,
pub args: Vec<InsnId>,
}
impl std::fmt::Display for BranchEdge {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}(", self.target)?;
let mut prefix = "";
for arg in &self.args {
write!(f, "{prefix}{arg}")?;
prefix = ", ";
}
write!(f, ")")
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct CallInfo {
pub method_name: String,
}
/// Invalidation reasons
#[derive(Debug, Clone, Copy)]
pub enum Invariant {
/// Basic operation is redefined
BOPRedefined {
/// {klass}_REDEFINED_OP_FLAG
klass: RedefinitionFlag,
/// BOP_{bop}
bop: ruby_basic_operators,
},
MethodRedefined {
/// The class object whose method we want to assume unchanged
klass: VALUE,
/// The method ID of the method we want to assume unchanged
method: ID,
},
/// A list of constant expression path segments that must have not been written to for the
/// following code to be valid.
StableConstantNames {
idlist: *const ID,
},
/// There is one ractor running. If a non-root ractor gets spawned, this is invalidated.
SingleRactorMode,
}
impl Invariant {
pub fn print(self, ptr_map: &PtrPrintMap) -> InvariantPrinter {
InvariantPrinter { inner: self, ptr_map }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpecialObjectType {
VMCore = 1,
CBase = 2,
ConstBase = 3,
}
impl From<u32> for SpecialObjectType {
fn from(value: u32) -> Self {
match value {
VM_SPECIAL_OBJECT_VMCORE => SpecialObjectType::VMCore,
VM_SPECIAL_OBJECT_CBASE => SpecialObjectType::CBase,
VM_SPECIAL_OBJECT_CONST_BASE => SpecialObjectType::ConstBase,
_ => panic!("Invalid special object type: {}", value),
}
}
}
impl From<SpecialObjectType> for u64 {
fn from(special_type: SpecialObjectType) -> Self {
special_type as u64
}
}
impl std::fmt::Display for SpecialObjectType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
SpecialObjectType::VMCore => write!(f, "VMCore"),
SpecialObjectType::CBase => write!(f, "CBase"),
SpecialObjectType::ConstBase => write!(f, "ConstBase"),
}
}
}
/// Print adaptor for [`Invariant`]. See [`PtrPrintMap`].
pub struct InvariantPrinter<'a> {
inner: Invariant,
ptr_map: &'a PtrPrintMap,
}
impl<'a> std::fmt::Display for InvariantPrinter<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.inner {
Invariant::BOPRedefined { klass, bop } => {
write!(f, "BOPRedefined(")?;
match klass {
INTEGER_REDEFINED_OP_FLAG => write!(f, "INTEGER_REDEFINED_OP_FLAG")?,
STRING_REDEFINED_OP_FLAG => write!(f, "STRING_REDEFINED_OP_FLAG")?,
ARRAY_REDEFINED_OP_FLAG => write!(f, "ARRAY_REDEFINED_OP_FLAG")?,
HASH_REDEFINED_OP_FLAG => write!(f, "HASH_REDEFINED_OP_FLAG")?,
_ => write!(f, "{klass}")?,
}
write!(f, ", ")?;
match bop {
BOP_PLUS => write!(f, "BOP_PLUS")?,
BOP_MINUS => write!(f, "BOP_MINUS")?,
BOP_MULT => write!(f, "BOP_MULT")?,
BOP_DIV => write!(f, "BOP_DIV")?,
BOP_MOD => write!(f, "BOP_MOD")?,
BOP_EQ => write!(f, "BOP_EQ")?,
BOP_NEQ => write!(f, "BOP_NEQ")?,
BOP_LT => write!(f, "BOP_LT")?,
BOP_LE => write!(f, "BOP_LE")?,
BOP_GT => write!(f, "BOP_GT")?,
BOP_GE => write!(f, "BOP_GE")?,
BOP_FREEZE => write!(f, "BOP_FREEZE")?,
BOP_UMINUS => write!(f, "BOP_UMINUS")?,
BOP_MAX => write!(f, "BOP_MAX")?,
BOP_AREF => write!(f, "BOP_AREF")?,
_ => write!(f, "{bop}")?,
}
write!(f, ")")
}
Invariant::MethodRedefined { klass, method } => {
let class_name = get_class_name(klass);
write!(f, "MethodRedefined({class_name}@{:p}, {}@{:p})",
self.ptr_map.map_ptr(klass.as_ptr::<VALUE>()),
method.contents_lossy(),
self.ptr_map.map_id(method.0)
)
}
Invariant::StableConstantNames { idlist } => {
write!(f, "StableConstantNames({:p}, ", self.ptr_map.map_ptr(idlist))?;
let mut idx = 0;
let mut sep = "";
loop {
let id = unsafe { *idlist.wrapping_add(idx) };
if id.0 == 0 {
break;
}
write!(f, "{sep}{}", id.contents_lossy())?;
sep = "::";
idx += 1;
}
write!(f, ")")
}
Invariant::SingleRactorMode => write!(f, "SingleRactorMode"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Const {
Value(VALUE),
CBool(bool),
CInt8(i8),
CInt16(i16),
CInt32(i32),
CInt64(i64),
CUInt8(u8),
CUInt16(u16),
CUInt32(u32),
CUInt64(u64),
CPtr(*mut u8),
CDouble(f64),
}
impl std::fmt::Display for Const {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.print(&PtrPrintMap::identity()).fmt(f)
}
}
impl Const {
fn print<'a>(&'a self, ptr_map: &'a PtrPrintMap) -> ConstPrinter<'a> {
ConstPrinter { inner: self, ptr_map }
}
}
pub enum RangeType {
Inclusive = 0, // include the end value
Exclusive = 1, // exclude the end value
}
impl std::fmt::Display for RangeType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", match self {
RangeType::Inclusive => "NewRangeInclusive",
RangeType::Exclusive => "NewRangeExclusive",
})
}
}
impl std::fmt::Debug for RangeType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.to_string())
}
}
impl Clone for RangeType {
fn clone(&self) -> Self {
*self
}
}
impl Copy for RangeType {}
impl From<u32> for RangeType {
fn from(flag: u32) -> Self {
match flag {
0 => RangeType::Inclusive,
1 => RangeType::Exclusive,
_ => panic!("Invalid range flag: {}", flag),
}
}
}
impl From<RangeType> for u32 {
fn from(range_type: RangeType) -> Self {
range_type as u32
}
}
/// Print adaptor for [`Const`]. See [`PtrPrintMap`].
struct ConstPrinter<'a> {
inner: &'a Const,
ptr_map: &'a PtrPrintMap,
}
impl<'a> std::fmt::Display for ConstPrinter<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.inner {
Const::Value(val) => write!(f, "Value({})", val.print(self.ptr_map)),
Const::CPtr(val) => write!(f, "CPtr({:p})", self.ptr_map.map_ptr(val)),
_ => write!(f, "{:?}", self.inner),
}
}
}
/// For output stability in tests, we assign each pointer with a stable
/// address the first time we see it. This mapping is off by default;
/// set [`PtrPrintMap::map_ptrs`] to switch it on.
///
/// Because this is extra state external to any pointer being printed, a
/// printing adapter struct that wraps the pointer along with this map is
/// required to make use of this effectly. The [`std::fmt::Display`]
/// implementation on the adapter struct can then be reused to implement
/// `Display` on the inner type with a default [`PtrPrintMap`], which
/// does not perform any mapping.
pub struct PtrPrintMap {
inner: RefCell<PtrPrintMapInner>,
map_ptrs: bool,
}
struct PtrPrintMapInner {
map: HashMap<*const c_void, *const c_void>,
next_ptr: *const c_void,
}
impl PtrPrintMap {
/// Return a mapper that maps the pointer to itself.
pub fn identity() -> Self {
Self {
map_ptrs: false,
inner: RefCell::new(PtrPrintMapInner {
map: HashMap::default(), next_ptr:
ptr::without_provenance(0x1000) // Simulate 4 KiB zero page
})
}
}
}
impl PtrPrintMap {
/// Map a pointer for printing
fn map_ptr<T>(&self, ptr: *const T) -> *const T {
// When testing, address stability is not a concern so print real address to enable code
// reuse
if !self.map_ptrs {
return ptr;
}
use std::collections::hash_map::Entry::*;
let ptr = ptr.cast();
let inner = &mut *self.inner.borrow_mut();
match inner.map.entry(ptr) {
Occupied(entry) => entry.get().cast(),
Vacant(entry) => {
// Pick a fake address that is suitably aligns for T and remember it in the map
let mapped = inner.next_ptr.wrapping_add(inner.next_ptr.align_offset(align_of::<T>()));
entry.insert(mapped);
// Bump for the next pointer
inner.next_ptr = mapped.wrapping_add(size_of::<T>());
mapped.cast()
}
}
}
/// Map a Ruby ID (index into intern table) for printing
fn map_id(&self, id: u64) -> *const c_void {
self.map_ptr(id as *const c_void)
}
}
/// An instruction in the SSA IR. The output of an instruction is referred to by the index of
/// the instruction ([`InsnId`]). SSA form enables this, and [`UnionFind`] ([`Function::find`])
/// helps with editing.
#[derive(Debug, Clone)]
pub enum Insn {
Const { val: Const },
/// SSA block parameter. Also used for function parameters in the function's entry block.
Param { idx: usize },
StringCopy { val: InsnId, chilled: bool },
StringIntern { val: InsnId },
/// Put special object (VMCORE, CBASE, etc.) based on value_type
PutSpecialObject { value_type: SpecialObjectType },
/// Call `to_a` on `val` if the method is defined, or make a new array `[val]` otherwise.
ToArray { val: InsnId, state: InsnId },
/// Call `to_a` on `val` if the method is defined, or make a new array `[val]` otherwise. If we
/// called `to_a`, duplicate the returned array.
ToNewArray { val: InsnId, state: InsnId },
NewArray { elements: Vec<InsnId>, state: InsnId },
/// NewHash contains a vec of (key, value) pairs
NewHash { elements: Vec<(InsnId,InsnId)>, state: InsnId },
NewRange { low: InsnId, high: InsnId, flag: RangeType, state: InsnId },
ArraySet { array: InsnId, idx: usize, val: InsnId },
ArrayDup { val: InsnId, state: InsnId },
ArrayMax { elements: Vec<InsnId>, state: InsnId },
/// Extend `left` with the elements from `right`. `left` and `right` must both be `Array`.
ArrayExtend { left: InsnId, right: InsnId, state: InsnId },
/// Push `val` onto `array`, where `array` is already `Array`.
ArrayPush { array: InsnId, val: InsnId, state: InsnId },
HashDup { val: InsnId, state: InsnId },
/// Check if the value is truthy and "return" a C boolean. In reality, we will likely fuse this
/// with IfTrue/IfFalse in the backend to generate jcc.
Test { val: InsnId },
/// Return C `true` if `val` is `Qnil`, else `false`.
IsNil { val: InsnId },
Defined { op_type: usize, obj: VALUE, pushval: VALUE, v: InsnId },
GetConstantPath { ic: *const iseq_inline_constant_cache, state: InsnId },
/// Get a global variable named `id`
GetGlobal { id: ID, state: InsnId },
/// Set a global variable named `id` to `val`
SetGlobal { id: ID, val: InsnId, state: InsnId },
//NewObject?
/// Get an instance variable `id` from `self_val`
GetIvar { self_val: InsnId, id: ID, state: InsnId },
/// Set `self_val`'s instance variable `id` to `val`
SetIvar { self_val: InsnId, id: ID, val: InsnId, state: InsnId },
/// Check whether an instance variable exists on `self_val`
DefinedIvar { self_val: InsnId, id: ID, pushval: VALUE, state: InsnId },
/// Get a local variable from a higher scope
GetLocal { level: NonZeroU32, ep_offset: u32 },
/// Set a local variable in a higher scope
SetLocal { level: NonZeroU32, ep_offset: u32, val: InsnId },
/// Own a FrameState so that instructions can look up their dominating FrameState when
/// generating deopt side-exits and frame reconstruction metadata. Does not directly generate
/// any code.
Snapshot { state: FrameState },
/// Unconditional jump
Jump(BranchEdge),
/// Conditional branch instructions
IfTrue { val: InsnId, target: BranchEdge },
IfFalse { val: InsnId, target: BranchEdge },
/// Call a C function
/// `name` is for printing purposes only
CCall { cfun: *const u8, args: Vec<InsnId>, name: ID, return_type: Type, elidable: bool },
/// Send without block with dynamic dispatch
/// Ignoring keyword arguments etc for now
SendWithoutBlock { self_val: InsnId, call_info: CallInfo, cd: *const rb_call_data, args: Vec<InsnId>, state: InsnId },
Send { self_val: InsnId, call_info: CallInfo, cd: *const rb_call_data, blockiseq: IseqPtr, args: Vec<InsnId>, state: InsnId },
SendWithoutBlockDirect {
self_val: InsnId,
call_info: CallInfo,
cd: *const rb_call_data,
cme: *const rb_callable_method_entry_t,
iseq: IseqPtr,
args: Vec<InsnId>,
state: InsnId,
},
// Invoke a builtin function
InvokeBuiltin { bf: rb_builtin_function, args: Vec<InsnId>, state: InsnId },
/// Control flow instructions
Return { val: InsnId },
/// Fixnum +, -, *, /, %, ==, !=, <, <=, >, >=
FixnumAdd { left: InsnId, right: InsnId, state: InsnId },
FixnumSub { left: InsnId, right: InsnId, state: InsnId },
FixnumMult { left: InsnId, right: InsnId, state: InsnId },
FixnumDiv { left: InsnId, right: InsnId, state: InsnId },
FixnumMod { left: InsnId, right: InsnId, state: InsnId },
FixnumEq { left: InsnId, right: InsnId },
FixnumNeq { left: InsnId, right: InsnId },
FixnumLt { left: InsnId, right: InsnId },
FixnumLe { left: InsnId, right: InsnId },
FixnumGt { left: InsnId, right: InsnId },
FixnumGe { left: InsnId, right: InsnId },
// Distinct from `SendWithoutBlock` with `mid:to_s` because does not have a patch point for String to_s being redefined
ObjToString { val: InsnId, call_info: CallInfo, cd: *const rb_call_data, state: InsnId },
AnyToString { val: InsnId, str: InsnId, state: InsnId },
/// Side-exit if val doesn't have the expected type.
GuardType { val: InsnId, guard_type: Type, state: InsnId },
/// Side-exit if val is not the expected VALUE.
GuardBitEquals { val: InsnId, expected: VALUE, state: InsnId },
/// Generate no code (or padding if necessary) and insert a patch point
/// that can be rewritten to a side exit when the Invariant is broken.
PatchPoint(Invariant),
/// Side-exit into the interpreter.
SideExit { state: InsnId },
}
impl Insn {
/// Not every instruction returns a value. Return true if the instruction does and false otherwise.
pub fn has_output(&self) -> bool {
match self {
Insn::ArraySet { .. } | Insn::Snapshot { .. } | Insn::Jump(_)
| Insn::IfTrue { .. } | Insn::IfFalse { .. } | Insn::Return { .. }
| Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::ArrayExtend { .. }
| Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetGlobal { .. }
| Insn::SetLocal { .. } => false,
_ => true,
}
}
/// Return true if the instruction ends a basic block and false otherwise.
pub fn is_terminator(&self) -> bool {
match self {
Insn::Jump(_) | Insn::Return { .. } | Insn::SideExit { .. } => true,
_ => false,
}
}
pub fn print<'a>(&self, ptr_map: &'a PtrPrintMap) -> InsnPrinter<'a> {
InsnPrinter { inner: self.clone(), ptr_map }
}
/// Return true if the instruction needs to be kept around. For example, if the instruction
/// might have a side effect, or if the instruction may raise an exception.
fn has_effects(&self) -> bool {
match self {
Insn::Const { .. } => false,
Insn::Param { .. } => false,
Insn::StringCopy { .. } => false,
Insn::NewArray { .. } => false,
Insn::NewHash { .. } => false,
Insn::NewRange { .. } => false,
Insn::ArrayDup { .. } => false,
Insn::HashDup { .. } => false,
Insn::Test { .. } => false,
Insn::Snapshot { .. } => false,
Insn::FixnumAdd { .. } => false,
Insn::FixnumSub { .. } => false,
Insn::FixnumMult { .. } => false,
// TODO(max): Consider adding a Guard that the rhs is non-zero before Div and Mod
// Div *is* critical unless we can prove the right hand side != 0
// Mod *is* critical unless we can prove the right hand side != 0
Insn::FixnumEq { .. } => false,
Insn::FixnumNeq { .. } => false,
Insn::FixnumLt { .. } => false,
Insn::FixnumLe { .. } => false,
Insn::FixnumGt { .. } => false,
Insn::FixnumGe { .. } => false,
Insn::CCall { elidable, .. } => !elidable,
_ => true,
}
}
}
/// Print adaptor for [`Insn`]. See [`PtrPrintMap`].
pub struct InsnPrinter<'a> {
inner: Insn,
ptr_map: &'a PtrPrintMap,
}
impl<'a> std::fmt::Display for InsnPrinter<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.inner {
Insn::Const { val } => { write!(f, "Const {}", val.print(self.ptr_map)) }
Insn::Param { idx } => { write!(f, "Param {idx}") }
Insn::NewArray { elements, .. } => {
write!(f, "NewArray")?;
let mut prefix = " ";
for element in elements {
write!(f, "{prefix}{element}")?;
prefix = ", ";
}
Ok(())
}
Insn::NewHash { elements, .. } => {
write!(f, "NewHash")?;
let mut prefix = " ";
for (key, value) in elements {
write!(f, "{prefix}{key}: {value}")?;
prefix = ", ";
}
Ok(())
}
Insn::NewRange { low, high, flag, .. } => {
write!(f, "NewRange {low} {flag} {high}")
}
Insn::ArrayMax { elements, .. } => {
write!(f, "ArrayMax")?;
let mut prefix = " ";
for element in elements {
write!(f, "{prefix}{element}")?;
prefix = ", ";
}
Ok(())
}
Insn::ArraySet { array, idx, val } => { write!(f, "ArraySet {array}, {idx}, {val}") }
Insn::ArrayDup { val, .. } => { write!(f, "ArrayDup {val}") }
Insn::HashDup { val, .. } => { write!(f, "HashDup {val}") }
Insn::StringCopy { val, .. } => { write!(f, "StringCopy {val}") }
Insn::Test { val } => { write!(f, "Test {val}") }
Insn::IsNil { val } => { write!(f, "IsNil {val}") }
Insn::Jump(target) => { write!(f, "Jump {target}") }
Insn::IfTrue { val, target } => { write!(f, "IfTrue {val}, {target}") }
Insn::IfFalse { val, target } => { write!(f, "IfFalse {val}, {target}") }
Insn::SendWithoutBlock { self_val, call_info, args, .. } => {
write!(f, "SendWithoutBlock {self_val}, :{}", call_info.method_name)?;
for arg in args {
write!(f, ", {arg}")?;
}
Ok(())
}
Insn::SendWithoutBlockDirect { self_val, call_info, iseq, args, .. } => {
write!(f, "SendWithoutBlockDirect {self_val}, :{} ({:?})", call_info.method_name, self.ptr_map.map_ptr(iseq))?;
for arg in args {
write!(f, ", {arg}")?;
}
Ok(())
}
Insn::Send { self_val, call_info, args, blockiseq, .. } => {
// For tests, we want to check HIR snippets textually. Addresses change
// between runs, making tests fail. Instead, pick an arbitrary hex value to
// use as a "pointer" so we can check the rest of the HIR.
write!(f, "Send {self_val}, {:p}, :{}", self.ptr_map.map_ptr(blockiseq), call_info.method_name)?;
for arg in args {
write!(f, ", {arg}")?;
}
Ok(())
}
Insn::InvokeBuiltin { bf, args, .. } => {
write!(f, "InvokeBuiltin {}", unsafe { CStr::from_ptr(bf.name) }.to_str().unwrap())?;
for arg in args {
write!(f, ", {arg}")?;
}
Ok(())
}
Insn::Return { val } => { write!(f, "Return {val}") }
Insn::FixnumAdd { left, right, .. } => { write!(f, "FixnumAdd {left}, {right}") },
Insn::FixnumSub { left, right, .. } => { write!(f, "FixnumSub {left}, {right}") },
Insn::FixnumMult { left, right, .. } => { write!(f, "FixnumMult {left}, {right}") },
Insn::FixnumDiv { left, right, .. } => { write!(f, "FixnumDiv {left}, {right}") },
Insn::FixnumMod { left, right, .. } => { write!(f, "FixnumMod {left}, {right}") },
Insn::FixnumEq { left, right, .. } => { write!(f, "FixnumEq {left}, {right}") },
Insn::FixnumNeq { left, right, .. } => { write!(f, "FixnumNeq {left}, {right}") },
Insn::FixnumLt { left, right, .. } => { write!(f, "FixnumLt {left}, {right}") },
Insn::FixnumLe { left, right, .. } => { write!(f, "FixnumLe {left}, {right}") },
Insn::FixnumGt { left, right, .. } => { write!(f, "FixnumGt {left}, {right}") },
Insn::FixnumGe { left, right, .. } => { write!(f, "FixnumGe {left}, {right}") },
Insn::GuardType { val, guard_type, .. } => { write!(f, "GuardType {val}, {}", guard_type.print(self.ptr_map)) },
Insn::GuardBitEquals { val, expected, .. } => { write!(f, "GuardBitEquals {val}, {}", expected.print(self.ptr_map)) },
Insn::PatchPoint(invariant) => { write!(f, "PatchPoint {}", invariant.print(self.ptr_map)) },
Insn::GetConstantPath { ic, .. } => { write!(f, "GetConstantPath {:p}", self.ptr_map.map_ptr(ic)) },
Insn::CCall { cfun, args, name, return_type: _, elidable: _ } => {
write!(f, "CCall {}@{:p}", name.contents_lossy(), self.ptr_map.map_ptr(cfun))?;
for arg in args {
write!(f, ", {arg}")?;
}
Ok(())
},
Insn::Snapshot { state } => write!(f, "Snapshot {}", state),
Insn::Defined { op_type, v, .. } => {
// op_type (enum defined_type) printing logic from iseq.c.
// Not sure why rb_iseq_defined_string() isn't exhaustive.
use std::borrow::Cow;
let op_type = *op_type as u32;
let op_type = if op_type == DEFINED_FUNC {
Cow::Borrowed("func")
} else if op_type == DEFINED_REF {
Cow::Borrowed("ref")
} else if op_type == DEFINED_CONST_FROM {
Cow::Borrowed("constant-from")
} else {
String::from_utf8_lossy(unsafe { rb_iseq_defined_string(op_type).as_rstring_byte_slice().unwrap() })
};
write!(f, "Defined {op_type}, {v}")
}
Insn::DefinedIvar { self_val, id, .. } => write!(f, "DefinedIvar {self_val}, :{}", id.contents_lossy().into_owned()),
Insn::GetIvar { self_val, id, .. } => write!(f, "GetIvar {self_val}, :{}", id.contents_lossy().into_owned()),
Insn::SetIvar { self_val, id, val, .. } => write!(f, "SetIvar {self_val}, :{}, {val}", id.contents_lossy().into_owned()),
Insn::GetGlobal { id, .. } => write!(f, "GetGlobal :{}", id.contents_lossy().into_owned()),
Insn::SetGlobal { id, val, .. } => write!(f, "SetGlobal :{}, {val}", id.contents_lossy().into_owned()),
Insn::GetLocal { level, ep_offset } => write!(f, "GetLocal l{level}, EP@{ep_offset}"),
Insn::SetLocal { val, level, ep_offset } => write!(f, "SetLocal l{level}, EP@{ep_offset}, {val}"),
Insn::ToArray { val, .. } => write!(f, "ToArray {val}"),
Insn::ToNewArray { val, .. } => write!(f, "ToNewArray {val}"),
Insn::ArrayExtend { left, right, .. } => write!(f, "ArrayExtend {left}, {right}"),
Insn::ArrayPush { array, val, .. } => write!(f, "ArrayPush {array}, {val}"),
Insn::ObjToString { val, .. } => { write!(f, "ObjToString {val}") },
Insn::AnyToString { val, str, .. } => { write!(f, "AnyToString {val}, str: {str}") },
Insn::SideExit { .. } => write!(f, "SideExit"),
Insn::PutSpecialObject { value_type } => {
write!(f, "PutSpecialObject {}", value_type)
}
insn => { write!(f, "{insn:?}") }
}
}
}
impl std::fmt::Display for Insn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.print(&PtrPrintMap::identity()).fmt(f)
}
}
/// An extended basic block in a [`Function`].
#[derive(Default, Debug)]
pub struct Block {
params: Vec<InsnId>,
insns: Vec<InsnId>,
}
impl Block {
/// Return an iterator over params
pub fn params(&self) -> Iter<InsnId> {
self.params.iter()
}
/// Return an iterator over insns
pub fn insns(&self) -> Iter<InsnId> {
self.insns.iter()
}
}
/// Pretty printer for [`Function`].
pub struct FunctionPrinter<'a> {
fun: &'a Function,
display_snapshot: bool,
ptr_map: PtrPrintMap,
}
impl<'a> FunctionPrinter<'a> {
pub fn without_snapshot(fun: &'a Function) -> Self {
let mut ptr_map = PtrPrintMap::identity();
if cfg!(test) {
ptr_map.map_ptrs = true;
}
Self { fun, display_snapshot: false, ptr_map }
}
pub fn with_snapshot(fun: &'a Function) -> FunctionPrinter<'a> {
let mut printer = Self::without_snapshot(fun);
printer.display_snapshot = true;
printer
}
}
/// Union-Find (Disjoint-Set) is a data structure for managing disjoint sets that has an interface
/// of two operations:
///
/// * find (what set is this item part of?)
/// * union (join these two sets)
///
/// Union-Find identifies sets by their *representative*, which is some chosen element of the set.
/// This is implemented by structuring each set as its own graph component with the representative
/// pointing at nothing. For example:
///
/// * A -> B -> C
/// * D -> E
///
/// This represents two sets `C` and `E`, with three and two members, respectively. In this
/// example, `find(A)=C`, `find(C)=C`, `find(D)=E`, and so on.
///
/// To union sets, call `make_equal_to` on any set element. That is, `make_equal_to(A, D)` and
/// `make_equal_to(B, E)` have the same result: the two sets are joined into the same graph
/// component. After this operation, calling `find` on any element will return `E`.
///
/// This is a useful data structure in compilers because it allows in-place rewriting without
/// linking/unlinking instructions and without replacing all uses. When calling `make_equal_to` on
/// any instruction, all of its uses now implicitly point to the replacement.
///
/// This does mean that pattern matching and analysis of the instruction graph must be careful to
/// call `find` whenever it is inspecting an instruction (or its operands). If not, this may result
/// in missing optimizations.
#[derive(Debug)]
struct UnionFind<T: Copy + Into<usize>> {
forwarded: Vec<Option<T>>,
}
impl<T: Copy + Into<usize> + PartialEq> UnionFind<T> {
fn new() -> UnionFind<T> {
UnionFind { forwarded: vec![] }
}
/// Private. Return the internal representation of the forwarding pointer for a given element.
fn at(&self, idx: T) -> Option<T> {
self.forwarded.get(idx.into()).map(|x| *x).flatten()
}
/// Private. Set the internal representation of the forwarding pointer for the given element
/// `idx`. Extend the internal vector if necessary.
fn set(&mut self, idx: T, value: T) {
if idx.into() >= self.forwarded.len() {
self.forwarded.resize(idx.into()+1, None);
}
self.forwarded[idx.into()] = Some(value);
}
/// Find the set representative for `insn`. Perform path compression at the same time to speed
/// up further find operations. For example, before:
///
/// `A -> B -> C`
///
/// and after `find(A)`:
///
/// ```
/// A -> C
/// B ---^
/// ```
pub fn find(&mut self, insn: T) -> T {
let result = self.find_const(insn);
if result != insn {
// Path compression
self.set(insn, result);
}
result
}
/// Find the set representative for `insn` without doing path compression.
fn find_const(&self, insn: T) -> T {
let mut result = insn;
loop {
match self.at(result) {
None => return result,
Some(insn) => result = insn,
}
}
}
/// Union the two sets containing `insn` and `target` such that every element in `insn`s set is
/// now part of `target`'s. Neither argument must be the representative in its set.
pub fn make_equal_to(&mut self, insn: T, target: T) {
let found = self.find(insn);
self.set(found, target);
}
}
/// A [`Function`], which is analogous to a Ruby ISeq, is a control-flow graph of [`Block`]s
/// containing instructions.
#[derive(Debug)]
pub struct Function {
// ISEQ this function refers to
iseq: *const rb_iseq_t,
// The types for the parameters of this function
param_types: Vec<Type>,
// TODO: get method name and source location from the ISEQ
insns: Vec<Insn>,
union_find: std::cell::RefCell<UnionFind<InsnId>>,
insn_types: Vec<Type>,
blocks: Vec<Block>,
entry_block: BlockId,
profiles: Option<ProfileOracle>,
}
impl Function {
fn new(iseq: *const rb_iseq_t) -> Function {
Function {
iseq,
insns: vec![],
insn_types: vec![],
union_find: UnionFind::new().into(),
blocks: vec![Block::default()],
entry_block: BlockId(0),
param_types: vec![],
profiles: None,
}
}
// Add an instruction to the function without adding it to any block
fn new_insn(&mut self, insn: Insn) -> InsnId {
let id = InsnId(self.insns.len());
if insn.has_output() {
self.insn_types.push(types::Any);
} else {
self.insn_types.push(types::Empty);
}
self.insns.push(insn);
id
}
// Add an instruction to an SSA block
fn push_insn(&mut self, block: BlockId, insn: Insn) -> InsnId {
let is_param = matches!(insn, Insn::Param { .. });
let id = self.new_insn(insn);
if is_param {
self.blocks[block.0].params.push(id);
} else {
self.blocks[block.0].insns.push(id);
}
id
}
// Add an instruction to an SSA block
fn push_insn_id(&mut self, block: BlockId, insn_id: InsnId) -> InsnId {
self.blocks[block.0].insns.push(insn_id);
insn_id
}
/// Return the number of instructions
pub fn num_insns(&self) -> usize {
self.insns.len()
}
/// Return a FrameState at the given instruction index.
pub fn frame_state(&self, insn_id: InsnId) -> FrameState {
match self.find(insn_id) {
Insn::Snapshot { state } => state,
insn => panic!("Unexpected non-Snapshot {insn} when looking up FrameState"),
}
}
fn new_block(&mut self) -> BlockId {
let id = BlockId(self.blocks.len());
self.blocks.push(Block::default());
id
}
/// Return a reference to the Block at the given index.
pub fn block(&self, block_id: BlockId) -> &Block {
&self.blocks[block_id.0]
}
/// Return the number of blocks
pub fn num_blocks(&self) -> usize {
self.blocks.len()
}
/// Return a copy of the instruction where the instruction and its operands have been read from
/// the union-find table (to find the current most-optimized version of this instruction). See
/// [`UnionFind`] for more.
///
/// This is _the_ function for reading [`Insn`]. Use frequently. Example:
///
/// ```rust
/// match func.find(insn_id) {
/// IfTrue { val, target } if func.is_truthy(val) => {
/// let jump = self.new_insn(Insn::Jump(target));
/// func.make_equal_to(insn_id, jump);
/// }
/// _ => {}
/// }
/// ```
pub fn find(&self, insn_id: InsnId) -> Insn {
macro_rules! find {
( $x:expr ) => {
{
// TODO(max): Figure out why borrow_mut().find() causes `already borrowed:
// BorrowMutError`
self.union_find.borrow().find_const($x)
}
};
}
macro_rules! find_vec {
( $x:expr ) => {
{
$x.iter().map(|arg| find!(*arg)).collect()
}
};
}
macro_rules! find_branch_edge {
( $edge:ident ) => {
{
BranchEdge {
target: $edge.target,
args: find_vec!($edge.args),
}
}
};
}
let insn_id = find!(insn_id);
use Insn::*;
match &self.insns[insn_id.0] {
result@(Const {..}
| Param {..}
| GetConstantPath {..}
| PatchPoint {..}
| PutSpecialObject {..}
| GetGlobal {..}
| GetLocal {..}
| SideExit {..}) => result.clone(),
Snapshot { state: FrameState { iseq, insn_idx, pc, stack, locals } } =>
Snapshot {
state: FrameState {
iseq: *iseq,
insn_idx: *insn_idx,
pc: *pc,
stack: find_vec!(stack),
locals: find_vec!(locals),
}
},
Return { val } => Return { val: find!(*val) },
StringCopy { val, chilled } => StringCopy { val: find!(*val), chilled: *chilled },
StringIntern { val } => StringIntern { val: find!(*val) },
Test { val } => Test { val: find!(*val) },
&IsNil { val } => IsNil { val: find!(val) },
Jump(target) => Jump(find_branch_edge!(target)),
IfTrue { val, target } => IfTrue { val: find!(*val), target: find_branch_edge!(target) },
IfFalse { val, target } => IfFalse { val: find!(*val), target: find_branch_edge!(target) },
GuardType { val, guard_type, state } => GuardType { val: find!(*val), guard_type: *guard_type, state: *state },
GuardBitEquals { val, expected, state } => GuardBitEquals { val: find!(*val), expected: *expected, state: *state },
FixnumAdd { left, right, state } => FixnumAdd { left: find!(*left), right: find!(*right), state: *state },
FixnumSub { left, right, state } => FixnumSub { left: find!(*left), right: find!(*right), state: *state },
FixnumMult { left, right, state } => FixnumMult { left: find!(*left), right: find!(*right), state: *state },
FixnumDiv { left, right, state } => FixnumDiv { left: find!(*left), right: find!(*right), state: *state },
FixnumMod { left, right, state } => FixnumMod { left: find!(*left), right: find!(*right), state: *state },
FixnumNeq { left, right } => FixnumNeq { left: find!(*left), right: find!(*right) },
FixnumEq { left, right } => FixnumEq { left: find!(*left), right: find!(*right) },
FixnumGt { left, right } => FixnumGt { left: find!(*left), right: find!(*right) },
FixnumGe { left, right } => FixnumGe { left: find!(*left), right: find!(*right) },
FixnumLt { left, right } => FixnumLt { left: find!(*left), right: find!(*right) },
FixnumLe { left, right } => FixnumLe { left: find!(*left), right: find!(*right) },
ObjToString { val, call_info, cd, state } => ObjToString {
val: find!(*val),
call_info: call_info.clone(),
cd: *cd,
state: *state,
},
AnyToString { val, str, state } => AnyToString {
val: find!(*val),
str: find!(*str),
state: *state,
},
SendWithoutBlock { self_val, call_info, cd, args, state } => SendWithoutBlock {
self_val: find!(*self_val),
call_info: call_info.clone(),
cd: *cd,
args: find_vec!(args),
state: *state,
},
SendWithoutBlockDirect { self_val, call_info, cd, cme, iseq, args, state } => SendWithoutBlockDirect {
self_val: find!(*self_val),
call_info: call_info.clone(),
cd: *cd,
cme: *cme,
iseq: *iseq,
args: find_vec!(args),
state: *state,
},
Send { self_val, call_info, cd, blockiseq, args, state } => Send {
self_val: find!(*self_val),
call_info: call_info.clone(),
cd: *cd,
blockiseq: *blockiseq,
args: find_vec!(args),
state: *state,
},
InvokeBuiltin { bf, args, state } => InvokeBuiltin { bf: *bf, args: find_vec!(*args), state: *state },
ArraySet { array, idx, val } => ArraySet { array: find!(*array), idx: *idx, val: find!(*val) },
ArrayDup { val , state } => ArrayDup { val: find!(*val), state: *state },
&HashDup { val , state } => HashDup { val: find!(val), state },
&CCall { cfun, ref args, name, return_type, elidable } => CCall { cfun: cfun, args: find_vec!(args), name: name, return_type: return_type, elidable },
&Defined { op_type, obj, pushval, v } => Defined { op_type, obj, pushval, v: find!(v) },
&DefinedIvar { self_val, pushval, id, state } => DefinedIvar { self_val: find!(self_val), pushval, id, state },
NewArray { elements, state } => NewArray { elements: find_vec!(*elements), state: find!(*state) },
&NewHash { ref elements, state } => {
let mut found_elements = vec![];
for &(key, value) in elements {
found_elements.push((find!(key), find!(value)));
}
NewHash { elements: found_elements, state: find!(state) }
}
&NewRange { low, high, flag, state } => NewRange { low: find!(low), high: find!(high), flag, state: find!(state) },
ArrayMax { elements, state } => ArrayMax { elements: find_vec!(*elements), state: find!(*state) },
&SetGlobal { id, val, state } => SetGlobal { id, val: find!(val), state },
&GetIvar { self_val, id, state } => GetIvar { self_val: find!(self_val), id, state },
&SetIvar { self_val, id, val, state } => SetIvar { self_val: find!(self_val), id, val, state },
&SetLocal { val, ep_offset, level } => SetLocal { val: find!(val), ep_offset, level },
&ToArray { val, state } => ToArray { val: find!(val), state },
&ToNewArray { val, state } => ToNewArray { val: find!(val), state },
&ArrayExtend { left, right, state } => ArrayExtend { left: find!(left), right: find!(right), state },
&ArrayPush { array, val, state } => ArrayPush { array: find!(array), val: find!(val), state },
}
}
/// Replace `insn` with the new instruction `replacement`, which will get appended to `insns`.
fn make_equal_to(&mut self, insn: InsnId, replacement: InsnId) {
// Don't push it to the block
self.union_find.borrow_mut().make_equal_to(insn, replacement);
}
fn type_of(&self, insn: InsnId) -> Type {
assert!(self.insns[insn.0].has_output());
self.insn_types[self.union_find.borrow_mut().find(insn).0]
}
/// Check if the type of `insn` is a subtype of `ty`.
fn is_a(&self, insn: InsnId, ty: Type) -> bool {
self.type_of(insn).is_subtype(ty)
}
fn infer_type(&self, insn: InsnId) -> Type {
assert!(self.insns[insn.0].has_output());
match &self.insns[insn.0] {
Insn::Param { .. } => unimplemented!("params should not be present in block.insns"),
Insn::SetGlobal { .. } | Insn::ArraySet { .. } | Insn::Snapshot { .. } | Insn::Jump(_)
| Insn::IfTrue { .. } | Insn::IfFalse { .. } | Insn::Return { .. }
| Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::ArrayExtend { .. }
| Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetLocal { .. } =>
panic!("Cannot infer type of instruction with no output"),
Insn::Const { val: Const::Value(val) } => Type::from_value(*val),
Insn::Const { val: Const::CBool(val) } => Type::from_cbool(*val),
Insn::Const { val: Const::CInt8(val) } => Type::from_cint(types::CInt8, *val as i64),
Insn::Const { val: Const::CInt16(val) } => Type::from_cint(types::CInt16, *val as i64),
Insn::Const { val: Const::CInt32(val) } => Type::from_cint(types::CInt32, *val as i64),
Insn::Const { val: Const::CInt64(val) } => Type::from_cint(types::CInt64, *val),
Insn::Const { val: Const::CUInt8(val) } => Type::from_cint(types::CUInt8, *val as i64),
Insn::Const { val: Const::CUInt16(val) } => Type::from_cint(types::CUInt16, *val as i64),
Insn::Const { val: Const::CUInt32(val) } => Type::from_cint(types::CUInt32, *val as i64),
Insn::Const { val: Const::CUInt64(val) } => Type::from_cint(types::CUInt64, *val as i64),
Insn::Const { val: Const::CPtr(val) } => Type::from_cint(types::CPtr, *val as i64),
Insn::Const { val: Const::CDouble(val) } => Type::from_double(*val),
Insn::Test { val } if self.type_of(*val).is_known_falsy() => Type::from_cbool(false),
Insn::Test { val } if self.type_of(*val).is_known_truthy() => Type::from_cbool(true),
Insn::Test { .. } => types::CBool,
Insn::IsNil { val } if self.is_a(*val, types::NilClassExact) => Type::from_cbool(true),
Insn::IsNil { val } if !self.type_of(*val).could_be(types::NilClassExact) => Type::from_cbool(false),
Insn::IsNil { .. } => types::CBool,
Insn::StringCopy { .. } => types::StringExact,
Insn::StringIntern { .. } => types::StringExact,
Insn::NewArray { .. } => types::ArrayExact,
Insn::ArrayDup { .. } => types::ArrayExact,
Insn::NewHash { .. } => types::HashExact,
Insn::HashDup { .. } => types::HashExact,
Insn::NewRange { .. } => types::RangeExact,
Insn::CCall { return_type, .. } => *return_type,
Insn::GuardType { val, guard_type, .. } => self.type_of(*val).intersection(*guard_type),
Insn::GuardBitEquals { val, expected, .. } => self.type_of(*val).intersection(Type::from_value(*expected)),
Insn::FixnumAdd { .. } => types::Fixnum,
Insn::FixnumSub { .. } => types::Fixnum,
Insn::FixnumMult { .. } => types::Fixnum,
Insn::FixnumDiv { .. } => types::Fixnum,
Insn::FixnumMod { .. } => types::Fixnum,
Insn::FixnumEq { .. } => types::BoolExact,
Insn::FixnumNeq { .. } => types::BoolExact,
Insn::FixnumLt { .. } => types::BoolExact,
Insn::FixnumLe { .. } => types::BoolExact,
Insn::FixnumGt { .. } => types::BoolExact,
Insn::FixnumGe { .. } => types::BoolExact,
Insn::PutSpecialObject { .. } => types::BasicObject,
Insn::SendWithoutBlock { .. } => types::BasicObject,
Insn::SendWithoutBlockDirect { .. } => types::BasicObject,
Insn::Send { .. } => types::BasicObject,
Insn::InvokeBuiltin { .. } => types::BasicObject,
Insn::Defined { .. } => types::BasicObject,
Insn::DefinedIvar { .. } => types::BasicObject,
Insn::GetConstantPath { .. } => types::BasicObject,
Insn::ArrayMax { .. } => types::BasicObject,
Insn::GetGlobal { .. } => types::BasicObject,
Insn::GetIvar { .. } => types::BasicObject,
Insn::ToNewArray { .. } => types::ArrayExact,
Insn::ToArray { .. } => types::ArrayExact,
Insn::ObjToString { .. } => types::BasicObject,
Insn::AnyToString { .. } => types::String,
Insn::GetLocal { .. } => types::BasicObject,
}
}
fn infer_types(&mut self) {
// Reset all types
self.insn_types.fill(types::Empty);
// Fill parameter types
let entry_params = self.blocks[self.entry_block.0].params.iter();
let param_types = self.param_types.iter();
assert_eq!(
entry_params.len(),
entry_params.len(),
"param types should be initialized before type inference"
);
for (param, param_type) in std::iter::zip(entry_params, param_types) {
// We know that function parameters are BasicObject or some subclass
self.insn_types[param.0] = *param_type;
}
let rpo = self.rpo();
// Walk the graph, computing types until fixpoint
let mut reachable = vec![false; self.blocks.len()];
reachable[self.entry_block.0] = true;
loop {
let mut changed = false;
for block in &rpo {
if !reachable[block.0] { continue; }
for insn_id in &self.blocks[block.0].insns {
let insn = self.find(*insn_id);
let insn_type = match insn {
Insn::IfTrue { val, target: BranchEdge { target, args } } => {
assert!(!self.type_of(val).bit_equal(types::Empty));
if self.type_of(val).could_be(Type::from_cbool(true)) {
reachable[target.0] = true;
for (idx, arg) in args.iter().enumerate() {
let param = self.blocks[target.0].params[idx];
self.insn_types[param.0] = self.type_of(param).union(self.type_of(*arg));
}
}
continue;
}
Insn::IfFalse { val, target: BranchEdge { target, args } } => {
assert!(!self.type_of(val).bit_equal(types::Empty));
if self.type_of(val).could_be(Type::from_cbool(false)) {
reachable[target.0] = true;
for (idx, arg) in args.iter().enumerate() {
let param = self.blocks[target.0].params[idx];
self.insn_types[param.0] = self.type_of(param).union(self.type_of(*arg));
}
}
continue;
}
Insn::Jump(BranchEdge { target, args }) => {
reachable[target.0] = true;
for (idx, arg) in args.iter().enumerate() {
let param = self.blocks[target.0].params[idx];
self.insn_types[param.0] = self.type_of(param).union(self.type_of(*arg));
}
continue;
}
_ if insn.has_output() => self.infer_type(*insn_id),
_ => continue,
};
if !self.type_of(*insn_id).bit_equal(insn_type) {
self.insn_types[insn_id.0] = insn_type;
changed = true;
}
}
}
if !changed {
break;
}
}
}
/// Return the interpreter-profiled type of the HIR instruction at the given ISEQ instruction
/// index, if it is known. This historical type record is not a guarantee and must be checked
/// with a GuardType or similar.
fn profiled_type_of_at(&self, insn: InsnId, iseq_insn_idx: usize) -> Option<Type> {
let Some(ref profiles) = self.profiles else { return None };
let Some(entries) = profiles.types.get(&iseq_insn_idx) else { return None };
for &(entry_insn, entry_type) in entries {
if self.union_find.borrow().find_const(entry_insn) == self.union_find.borrow().find_const(insn) {
return Some(entry_type);
}
}
None
}
fn likely_is_fixnum(&self, val: InsnId, profiled_type: Type) -> bool {
return self.is_a(val, types::Fixnum) || profiled_type.is_subtype(types::Fixnum);
}
fn coerce_to_fixnum(&mut self, block: BlockId, val: InsnId, state: InsnId) -> InsnId {
if self.is_a(val, types::Fixnum) { return val; }
return self.push_insn(block, Insn::GuardType { val, guard_type: types::Fixnum, state });
}
fn arguments_likely_fixnums(&mut self, left: InsnId, right: InsnId, state: InsnId) -> bool {
let frame_state = self.frame_state(state);
let iseq_insn_idx = frame_state.insn_idx as usize;
let left_profiled_type = self.profiled_type_of_at(left, iseq_insn_idx).unwrap_or(types::BasicObject);
let right_profiled_type = self.profiled_type_of_at(right, iseq_insn_idx).unwrap_or(types::BasicObject);
self.likely_is_fixnum(left, left_profiled_type) && self.likely_is_fixnum(right, right_profiled_type)
}
fn try_rewrite_fixnum_op(&mut self, block: BlockId, orig_insn_id: InsnId, f: &dyn Fn(InsnId, InsnId) -> Insn, bop: u32, left: InsnId, right: InsnId, state: InsnId) {
if self.arguments_likely_fixnums(left, right, state) {
if bop == BOP_NEQ {
// For opt_neq, the interpreter checks that both neq and eq are unchanged.
self.push_insn(block, Insn::PatchPoint(Invariant::BOPRedefined { klass: INTEGER_REDEFINED_OP_FLAG, bop: BOP_EQ }));
}
self.push_insn(block, Insn::PatchPoint(Invariant::BOPRedefined { klass: INTEGER_REDEFINED_OP_FLAG, bop }));
let left = self.coerce_to_fixnum(block, left, state);
let right = self.coerce_to_fixnum(block, right, state);
let result = self.push_insn(block, f(left, right));
self.make_equal_to(orig_insn_id, result);
self.insn_types[result.0] = self.infer_type(result);
} else {
self.push_insn_id(block, orig_insn_id);
}
}
fn rewrite_if_frozen(&mut self, block: BlockId, orig_insn_id: InsnId, self_val: InsnId, klass: u32, bop: u32) {
let self_type = self.type_of(self_val);
if let Some(obj) = self_type.ruby_object() {
if obj.is_frozen() {
self.push_insn(block, Insn::PatchPoint(Invariant::BOPRedefined { klass, bop }));
self.make_equal_to(orig_insn_id, self_val);
return;
}
}
self.push_insn_id(block, orig_insn_id);
}
fn try_rewrite_freeze(&mut self, block: BlockId, orig_insn_id: InsnId, self_val: InsnId) {
if self.is_a(self_val, types::StringExact) {
self.rewrite_if_frozen(block, orig_insn_id, self_val, STRING_REDEFINED_OP_FLAG, BOP_FREEZE);
} else if self.is_a(self_val, types::ArrayExact) {
self.rewrite_if_frozen(block, orig_insn_id, self_val, ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE);
} else if self.is_a(self_val, types::HashExact) {
self.rewrite_if_frozen(block, orig_insn_id, self_val, HASH_REDEFINED_OP_FLAG, BOP_FREEZE);
} else {
self.push_insn_id(block, orig_insn_id);
}
}
fn try_rewrite_uminus(&mut self, block: BlockId, orig_insn_id: InsnId, self_val: InsnId) {
if self.is_a(self_val, types::StringExact) {
self.rewrite_if_frozen(block, orig_insn_id, self_val, STRING_REDEFINED_OP_FLAG, BOP_UMINUS);
} else {
self.push_insn_id(block, orig_insn_id);
}
}
fn try_rewrite_aref(&mut self, block: BlockId, orig_insn_id: InsnId, self_val: InsnId, idx_val: InsnId) {
let self_type = self.type_of(self_val);
let idx_type = self.type_of(idx_val);
if self_type.is_subtype(types::ArrayExact) {
if let Some(array_obj) = self_type.ruby_object() {
if array_obj.is_frozen() {
if let Some(idx) = idx_type.fixnum_value() {
self.push_insn(block, Insn::PatchPoint(Invariant::BOPRedefined { klass: ARRAY_REDEFINED_OP_FLAG, bop: BOP_AREF }));
let val = unsafe { rb_yarv_ary_entry_internal(array_obj, idx) };
let const_insn = self.push_insn(block, Insn::Const { val: Const::Value(val) });
self.make_equal_to(orig_insn_id, const_insn);
return;
}
}
}
}
self.push_insn_id(block, orig_insn_id);
}
/// Rewrite SendWithoutBlock opcodes into SendWithoutBlockDirect opcodes if we know the target
/// ISEQ statically. This removes run-time method lookups and opens the door for inlining.
fn optimize_direct_sends(&mut self) {
for block in self.rpo() {
let old_insns = std::mem::take(&mut self.blocks[block.0].insns);
assert!(self.blocks[block.0].insns.is_empty());
for insn_id in old_insns {
match self.find(insn_id) {
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == "+" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumAdd { left, right, state }, BOP_PLUS, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == "-" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumSub { left, right, state }, BOP_MINUS, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == "*" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumMult { left, right, state }, BOP_MULT, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == "/" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumDiv { left, right, state }, BOP_DIV, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == "%" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumMod { left, right, state }, BOP_MOD, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == "==" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumEq { left, right }, BOP_EQ, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == "!=" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumNeq { left, right }, BOP_NEQ, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == "<" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumLt { left, right }, BOP_LT, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == "<=" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumLe { left, right }, BOP_LE, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == ">" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumGt { left, right }, BOP_GT, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, state, .. } if method_name == ">=" && args.len() == 1 =>
self.try_rewrite_fixnum_op(block, insn_id, &|left, right| Insn::FixnumGe { left, right }, BOP_GE, self_val, args[0], state),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, .. } if method_name == "freeze" && args.len() == 0 =>
self.try_rewrite_freeze(block, insn_id, self_val),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, .. } if method_name == "-@" && args.len() == 0 =>
self.try_rewrite_uminus(block, insn_id, self_val),
Insn::SendWithoutBlock { self_val, call_info: CallInfo { method_name }, args, .. } if method_name == "[]" && args.len() == 1 =>
self.try_rewrite_aref(block, insn_id, self_val, args[0]),
Insn::SendWithoutBlock { mut self_val, call_info, cd, args, state } => {
let frame_state = self.frame_state(state);
let (klass, guard_equal_to) = if let Some(klass) = self.type_of(self_val).runtime_exact_ruby_class() {
// If we know the class statically, use it to fold the lookup at compile-time.
(klass, None)
} else {
// If we know that self is top-self from profile information, guard and use it to fold the lookup at compile-time.
match self.profiled_type_of_at(self_val, frame_state.insn_idx) {
Some(self_type) if self_type.is_top_self() => (self_type.exact_ruby_class().unwrap(), self_type.ruby_object()),
_ => { self.push_insn_id(block, insn_id); continue; }
}
};
let ci = unsafe { get_call_data_ci(cd) }; // info about the call site
let mid = unsafe { vm_ci_mid(ci) };
// Do method lookup
let mut cme = unsafe { rb_callable_method_entry(klass, mid) };
if cme.is_null() {
self.push_insn_id(block, insn_id); continue;
}
// Load an overloaded cme if applicable. See vm_search_cc().
// It allows you to use a faster ISEQ if possible.
cme = unsafe { rb_check_overloaded_cme(cme, ci) };
let def_type = unsafe { get_cme_def_type(cme) };
if def_type != VM_METHOD_TYPE_ISEQ {
// TODO(max): Allow non-iseq; cache cme
self.push_insn_id(block, insn_id); continue;
}
self.push_insn(block, Insn::PatchPoint(Invariant::MethodRedefined { klass, method: mid }));
let iseq = unsafe { get_def_iseq_ptr((*cme).def) };
if let Some(expected) = guard_equal_to {
self_val = self.push_insn(block, Insn::GuardBitEquals { val: self_val, expected, state });
}
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { self_val, call_info, cd, cme, iseq, args, state });
self.make_equal_to(insn_id, send_direct);
}
Insn::GetConstantPath { ic, .. } => {
let idlist: *const ID = unsafe { (*ic).segments };
let ice = unsafe { (*ic).entry };
if ice.is_null() {
self.push_insn_id(block, insn_id); continue;
}
let cref_sensitive = !unsafe { (*ice).ic_cref }.is_null();
let multi_ractor_mode = unsafe { rb_zjit_multi_ractor_p() };
if cref_sensitive || multi_ractor_mode {
self.push_insn_id(block, insn_id); continue;
}
// Assume single-ractor mode.
self.push_insn(block, Insn::PatchPoint(Invariant::SingleRactorMode));
// Invalidate output code on any constant writes associated with constants
// referenced after the PatchPoint.
self.push_insn(block, Insn::PatchPoint(Invariant::StableConstantNames { idlist }));
let replacement = self.push_insn(block, Insn::Const { val: Const::Value(unsafe { (*ice).value }) });
self.make_equal_to(insn_id, replacement);
}
Insn::ObjToString { val, call_info, cd, state, .. } => {
if self.is_a(val, types::String) {
// behaves differently from `SendWithoutBlock` with `mid:to_s` because ObjToString should not have a patch point for String to_s being redefined
self.make_equal_to(insn_id, val);
} else {
let replacement = self.push_insn(block, Insn::SendWithoutBlock { self_val: val, call_info, cd, args: vec![], state });
self.make_equal_to(insn_id, replacement)
}
}
Insn::AnyToString { str, .. } => {
if self.is_a(str, types::String) {
self.make_equal_to(insn_id, str);
} else {
self.push_insn_id(block, insn_id);
}
}
_ => { self.push_insn_id(block, insn_id); }
}
}
}
self.infer_types();
}
/// Optimize SendWithoutBlock that land in a C method to a direct CCall without
/// runtime lookup.
fn optimize_c_calls(&mut self) {
// Try to reduce one SendWithoutBlock to a CCall
fn reduce_to_ccall(
fun: &mut Function,
block: BlockId,
self_type: Type,
send: Insn,
send_insn_id: InsnId,
) -> Result<(), ()> {
let Insn::SendWithoutBlock { mut self_val, cd, mut args, state, .. } = send else {
return Err(());
};
let call_info = unsafe { (*cd).ci };
let argc = unsafe { vm_ci_argc(call_info) };
let method_id = unsafe { rb_vm_ci_mid(call_info) };
// If we have info about the class of the receiver
//
// TODO(alan): there was a seemingly a miscomp here if you swap with
// `inexact_ruby_class`. Theoretically it can call a method too general
// for the receiver. Confirm and add a test.
let (recv_class, guard_type) = if let Some(klass) = self_type.runtime_exact_ruby_class() {
(klass, None)
} else {
let iseq_insn_idx = fun.frame_state(state).insn_idx;
let Some(recv_type) = fun.profiled_type_of_at(self_val, iseq_insn_idx) else { return Err(()) };
let Some(recv_class) = recv_type.exact_ruby_class() else { return Err(()) };
(recv_class, Some(recv_type.unspecialized()))
};
// Do method lookup
let method = unsafe { rb_callable_method_entry(recv_class, method_id) };
if method.is_null() {
return Err(());
}
// Filter for C methods
let def_type = unsafe { get_cme_def_type(method) };
if def_type != VM_METHOD_TYPE_CFUNC {
return Err(());
}
// Find the `argc` (arity) of the C method, which describes the parameters it expects
let cfunc = unsafe { get_cme_def_body_cfunc(method) };
let cfunc_argc = unsafe { get_mct_argc(cfunc) };
match cfunc_argc {
0.. => {
// (self, arg0, arg1, ..., argc) form
//
// Bail on argc mismatch
if argc != cfunc_argc as u32 {
return Err(());
}
// Filter for a leaf and GC free function
use crate::cruby_methods::FnProperties;
let Some(FnProperties { leaf: true, no_gc: true, return_type, elidable }) =
ZJITState::get_method_annotations().get_cfunc_properties(method)
else {
return Err(());
};
let ci_flags = unsafe { vm_ci_flag(call_info) };
// Filter for simple call sites (i.e. no splats etc.)
if ci_flags & VM_CALL_ARGS_SIMPLE != 0 {
// Commit to the replacement. Put PatchPoint.
fun.push_insn(block, Insn::PatchPoint(Invariant::MethodRedefined { klass: recv_class, method: method_id }));
if let Some(guard_type) = guard_type {
// Guard receiver class
self_val = fun.push_insn(block, Insn::GuardType { val: self_val, guard_type, state });
}
let cfun = unsafe { get_mct_func(cfunc) }.cast();
let mut cfunc_args = vec![self_val];
cfunc_args.append(&mut args);
let ccall = fun.push_insn(block, Insn::CCall { cfun, args: cfunc_args, name: method_id, return_type, elidable });
fun.make_equal_to(send_insn_id, ccall);
return Ok(());
}
}
-1 => {
// (argc, argv, self) parameter form
// Falling through for now
}
-2 => {
// (self, args_ruby_array) parameter form
// Falling through for now
}
_ => unreachable!("unknown cfunc kind: argc={argc}")
}
Err(())
}
for block in self.rpo() {
let old_insns = std::mem::take(&mut self.blocks[block.0].insns);
assert!(self.blocks[block.0].insns.is_empty());
for insn_id in old_insns {
if let send @ Insn::SendWithoutBlock { self_val, .. } = self.find(insn_id) {
let self_type = self.type_of(self_val);
if reduce_to_ccall(self, block, self_type, send, insn_id).is_ok() {
continue;
}
}
self.push_insn_id(block, insn_id);
}
}
self.infer_types();
}
/// Fold a binary operator on fixnums.
fn fold_fixnum_bop(&mut self, insn_id: InsnId, left: InsnId, right: InsnId, f: impl FnOnce(Option<i64>, Option<i64>) -> Option<i64>) -> InsnId {
f(self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value())
.filter(|&n| n >= (RUBY_FIXNUM_MIN as i64) && n <= RUBY_FIXNUM_MAX as i64)
.map(|n| self.new_insn(Insn::Const { val: Const::Value(VALUE::fixnum_from_usize(n as usize)) }))
.unwrap_or(insn_id)
}
/// Fold a binary predicate on fixnums.
fn fold_fixnum_pred(&mut self, insn_id: InsnId, left: InsnId, right: InsnId, f: impl FnOnce(Option<i64>, Option<i64>) -> Option<bool>) -> InsnId {
f(self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value())
.map(|b| if b { Qtrue } else { Qfalse })
.map(|b| self.new_insn(Insn::Const { val: Const::Value(b) }))
.unwrap_or(insn_id)
}
/// Use type information left by `infer_types` to fold away operations that can be evaluated at compile-time.
///
/// It can fold fixnum math, truthiness tests, and branches with constant conditionals.
fn fold_constants(&mut self) {
// TODO(max): Determine if it's worth it for us to reflow types after each branch
// simplification. This means that we can have nice cascading optimizations if what used to
// be a union of two different basic block arguments now has a single value.
//
// This would require 1) fixpointing, 2) worklist, or 3) (slightly less powerful) calling a
// function-level infer_types after each pruned branch.
for block in self.rpo() {
let old_insns = std::mem::take(&mut self.blocks[block.0].insns);
let mut new_insns = vec![];
for insn_id in old_insns {
let replacement_id = match self.find(insn_id) {
Insn::GuardType { val, guard_type, .. } if self.is_a(val, guard_type) => {
self.make_equal_to(insn_id, val);
// Don't bother re-inferring the type of val; we already know it.
continue;
}
Insn::FixnumAdd { left, right, .. } => {
self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) {
(Some(l), Some(r)) => l.checked_add(r),
_ => None,
})
}
Insn::FixnumSub { left, right, .. } => {
self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) {
(Some(l), Some(r)) => l.checked_sub(r),
_ => None,
})
}
Insn::FixnumMult { left, right, .. } => {
self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) {
(Some(l), Some(r)) => l.checked_mul(r),
(Some(0), _) | (_, Some(0)) => Some(0),
_ => None,
})
}
Insn::FixnumEq { left, right, .. } => {
self.fold_fixnum_pred(insn_id, left, right, |l, r| match (l, r) {
(Some(l), Some(r)) => Some(l == r),
_ => None,
})
}
Insn::FixnumNeq { left, right, .. } => {
self.fold_fixnum_pred(insn_id, left, right, |l, r| match (l, r) {
(Some(l), Some(r)) => Some(l != r),
_ => None,
})
}
Insn::FixnumLt { left, right, .. } => {
self.fold_fixnum_pred(insn_id, left, right, |l, r| match (l, r) {
(Some(l), Some(r)) => Some(l < r),
_ => None,
})
}
Insn::FixnumLe { left, right, .. } => {
self.fold_fixnum_pred(insn_id, left, right, |l, r| match (l, r) {
(Some(l), Some(r)) => Some(l <= r),
_ => None,
})
}
Insn::FixnumGt { left, right, .. } => {
self.fold_fixnum_pred(insn_id, left, right, |l, r| match (l, r) {
(Some(l), Some(r)) => Some(l > r),
_ => None,
})
}
Insn::FixnumGe { left, right, .. } => {
self.fold_fixnum_pred(insn_id, left, right, |l, r| match (l, r) {
(Some(l), Some(r)) => Some(l >= r),
_ => None,
})
}
Insn::Test { val } if self.type_of(val).is_known_falsy() => {
self.new_insn(Insn::Const { val: Const::CBool(false) })
}
Insn::Test { val } if self.type_of(val).is_known_truthy() => {
self.new_insn(Insn::Const { val: Const::CBool(true) })
}
Insn::IfTrue { val, target } if self.is_a(val, Type::from_cbool(true)) => {
self.new_insn(Insn::Jump(target))
}
Insn::IfFalse { val, target } if self.is_a(val, Type::from_cbool(false)) => {
self.new_insn(Insn::Jump(target))
}
// If we know that the branch condition is never going to cause a branch,
// completely drop the branch from the block.
Insn::IfTrue { val, .. } if self.is_a(val, Type::from_cbool(false)) => continue,
Insn::IfFalse { val, .. } if self.is_a(val, Type::from_cbool(true)) => continue,
_ => insn_id,
};
// If we're adding a new instruction, mark the two equivalent in the union-find and
// do an incremental flow typing of the new instruction.
if insn_id != replacement_id {
self.make_equal_to(insn_id, replacement_id);
if self.insns[replacement_id.0].has_output() {
self.insn_types[replacement_id.0] = self.infer_type(replacement_id);
}
}
new_insns.push(replacement_id);
// If we've just folded an IfTrue into a Jump, for example, don't bother copying
// over unreachable instructions afterward.
if self.insns[replacement_id.0].is_terminator() {
break;
}
}
self.blocks[block.0].insns = new_insns;
}
}
/// Remove instructions that do not have side effects and are not referenced by any other
/// instruction.
fn eliminate_dead_code(&mut self) {
let rpo = self.rpo();
let mut worklist = VecDeque::new();
// Find all of the instructions that have side effects, are control instructions, or are
// otherwise necessary to keep around
for block_id in &rpo {
for insn_id in &self.blocks[block_id.0].insns {
let insn = &self.insns[insn_id.0];
if insn.has_effects() {
worklist.push_back(*insn_id);
}
}
}
let mut necessary = vec![false; self.insns.len()];
// Now recursively traverse their data dependencies and mark those as necessary
while let Some(insn_id) = worklist.pop_front() {
if necessary[insn_id.0] { continue; }
necessary[insn_id.0] = true;
match self.find(insn_id) {
Insn::Const { .. }
| Insn::Param { .. }
| Insn::PatchPoint(..)
| Insn::GetLocal { .. }
| Insn::PutSpecialObject { .. } =>
{}
Insn::GetConstantPath { ic: _, state } => {
worklist.push_back(state);
}
Insn::ArrayMax { elements, state }
| Insn::NewArray { elements, state } => {
worklist.extend(elements);
worklist.push_back(state);
}
Insn::NewHash { elements, state } => {
for (key, value) in elements {
worklist.push_back(key);
worklist.push_back(value);
}
worklist.push_back(state);
}
Insn::NewRange { low, high, state, .. } => {
worklist.push_back(low);
worklist.push_back(high);
worklist.push_back(state);
}
Insn::StringCopy { val, .. }
| Insn::StringIntern { val }
| Insn::Return { val }
| Insn::Defined { v: val, .. }
| Insn::Test { val }
| Insn::SetLocal { val, .. }
| Insn::IsNil { val } =>
worklist.push_back(val),
Insn::SetGlobal { val, state, .. }
| Insn::GuardType { val, state, .. }
| Insn::GuardBitEquals { val, state, .. }
| Insn::ToArray { val, state }
| Insn::ToNewArray { val, state } => {
worklist.push_back(val);
worklist.push_back(state);
}
Insn::ArraySet { array, val, .. } => {
worklist.push_back(array);
worklist.push_back(val);
}
Insn::Snapshot { state } => {
worklist.extend(&state.stack);
worklist.extend(&state.locals);
}
Insn::FixnumAdd { left, right, state }
| Insn::FixnumSub { left, right, state }
| Insn::FixnumMult { left, right, state }
| Insn::FixnumDiv { left, right, state }
| Insn::FixnumMod { left, right, state }
| Insn::ArrayExtend { left, right, state }
=> {
worklist.push_back(left);
worklist.push_back(right);
worklist.push_back(state);
}
Insn::FixnumLt { left, right }
| Insn::FixnumLe { left, right }
| Insn::FixnumGt { left, right }
| Insn::FixnumGe { left, right }
| Insn::FixnumEq { left, right }
| Insn::FixnumNeq { left, right }
=> {
worklist.push_back(left);
worklist.push_back(right);
}
Insn::Jump(BranchEdge { args, .. }) => worklist.extend(args),
Insn::IfTrue { val, target: BranchEdge { args, .. } } | Insn::IfFalse { val, target: BranchEdge { args, .. } } => {
worklist.push_back(val);
worklist.extend(args);
}
Insn::ArrayDup { val, state } | Insn::HashDup { val, state } => {
worklist.push_back(val);
worklist.push_back(state);
}
Insn::Send { self_val, args, state, .. }
| Insn::SendWithoutBlock { self_val, args, state, .. }
| Insn::SendWithoutBlockDirect { self_val, args, state, .. } => {
worklist.push_back(self_val);
worklist.extend(args);
worklist.push_back(state);
}
Insn::InvokeBuiltin { args, state, .. } => {
worklist.extend(args);
worklist.push_back(state)
}
Insn::CCall { args, .. } => worklist.extend(args),
Insn::GetIvar { self_val, state, .. } | Insn::DefinedIvar { self_val, state, .. } => {
worklist.push_back(self_val);
worklist.push_back(state);
}
Insn::SetIvar { self_val, val, state, .. } => {
worklist.push_back(self_val);
worklist.push_back(val);
worklist.push_back(state);
}
Insn::ArrayPush { array, val, state } => {
worklist.push_back(array);
worklist.push_back(val);
worklist.push_back(state);
}
Insn::ObjToString { val, state, .. } => {
worklist.push_back(val);
worklist.push_back(state);
}
Insn::AnyToString { val, str, state, .. } => {
worklist.push_back(val);
worklist.push_back(str);
worklist.push_back(state);
}
Insn::GetGlobal { state, .. } |
Insn::SideExit { state } => worklist.push_back(state),
}
}
// Now remove all unnecessary instructions
for block_id in &rpo {
self.blocks[block_id.0].insns.retain(|insn_id| necessary[insn_id.0]);
}
}
fn absorb_dst_block(&mut self, num_in_edges: &Vec<u32>, block: BlockId) -> bool {
let Some(terminator_id) = self.blocks[block.0].insns.last()
else { return false };
let Insn::Jump(BranchEdge { target, args }) = self.find(*terminator_id)
else { return false };
if target == block {
// Can't absorb self
return false;
}
if num_in_edges[target.0] != 1 {
// Can't absorb block if it's the target of more than one branch
return false;
}
// Link up params with block args
let params = std::mem::take(&mut self.blocks[target.0].params);
assert_eq!(args.len(), params.len());
for (arg, param) in args.iter().zip(params) {
self.make_equal_to(param, *arg);
}
// Remove branch instruction
self.blocks[block.0].insns.pop();
// Move target instructions into block
let target_insns = std::mem::take(&mut self.blocks[target.0].insns);
self.blocks[block.0].insns.extend(target_insns);
true
}
/// Clean up linked lists of blocks A -> B -> C into A (with B's and C's instructions).
fn clean_cfg(&mut self) {
// num_in_edges is invariant throughout cleaning the CFG:
// * we don't allocate new blocks
// * blocks that get absorbed are not in RPO anymore
// * blocks pointed to by blocks that get absorbed retain the same number of in-edges
let mut num_in_edges = vec![0; self.blocks.len()];
for block in self.rpo() {
for &insn in &self.blocks[block.0].insns {
if let Insn::IfTrue { target, .. } | Insn::IfFalse { target, .. } | Insn::Jump(target) = self.find(insn) {
num_in_edges[target.target.0] += 1;
}
}
}
let mut changed = false;
loop {
let mut iter_changed = false;
for block in self.rpo() {
// Ignore transient empty blocks
if self.blocks[block.0].insns.is_empty() { continue; }
loop {
let absorbed = self.absorb_dst_block(&num_in_edges, block);
if !absorbed { break; }
iter_changed = true;
}
}
if !iter_changed { break; }
changed = true;
}
if changed {
self.infer_types();
}
}
/// Return a traversal of the `Function`'s `BlockId`s in reverse post-order.
pub fn rpo(&self) -> Vec<BlockId> {
let mut result = self.po_from(self.entry_block);
result.reverse();
result
}
fn po_from(&self, start: BlockId) -> Vec<BlockId> {
#[derive(PartialEq)]
enum Action {
VisitEdges,
VisitSelf,
}
let mut result = vec![];
let mut seen = HashSet::new();
let mut stack = vec![(start, Action::VisitEdges)];
while let Some((block, action)) = stack.pop() {
if action == Action::VisitSelf {
result.push(block);
continue;
}
if !seen.insert(block) { continue; }
stack.push((block, Action::VisitSelf));
for insn_id in &self.blocks[block.0].insns {
let insn = self.find(*insn_id);
if let Insn::IfTrue { target, .. } | Insn::IfFalse { target, .. } | Insn::Jump(target) = insn {
stack.push((target.target, Action::VisitEdges));
}
}
}
result
}
/// Run all the optimization passes we have.
pub fn optimize(&mut self) {
// Function is assumed to have types inferred already
self.optimize_direct_sends();
self.optimize_c_calls();
self.fold_constants();
self.clean_cfg();
self.eliminate_dead_code();
// Dump HIR after optimization
match get_option!(dump_hir_opt) {
Some(DumpHIR::WithoutSnapshot) => println!("HIR:\n{}", FunctionPrinter::without_snapshot(&self)),
Some(DumpHIR::All) => println!("HIR:\n{}", FunctionPrinter::with_snapshot(&self)),
Some(DumpHIR::Debug) => println!("HIR:\n{:#?}", &self),
None => {},
}
}
}
impl<'a> std::fmt::Display for FunctionPrinter<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let fun = &self.fun;
let iseq_name = iseq_name(fun.iseq);
writeln!(f, "fn {iseq_name}:")?;
for block_id in fun.rpo() {
write!(f, "{block_id}(")?;
if !fun.blocks[block_id.0].params.is_empty() {
let mut sep = "";
for param in &fun.blocks[block_id.0].params {
write!(f, "{sep}{param}")?;
let insn_type = fun.type_of(*param);
if !insn_type.is_subtype(types::Empty) {
write!(f, ":{}", insn_type.print(&self.ptr_map))?;
}
sep = ", ";
}
}
writeln!(f, "):")?;
for insn_id in &fun.blocks[block_id.0].insns {
let insn = fun.find(*insn_id);
if !self.display_snapshot && matches!(insn, Insn::Snapshot {..}) {
continue;
}
write!(f, " ")?;
if insn.has_output() {
let insn_type = fun.type_of(*insn_id);
if insn_type.is_subtype(types::Empty) {
write!(f, "{insn_id} = ")?;
} else {
write!(f, "{insn_id}:{} = ", insn_type.print(&self.ptr_map))?;
}
}
writeln!(f, "{}", insn.print(&self.ptr_map))?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FrameState {
iseq: IseqPtr,
insn_idx: usize,
// Ruby bytecode instruction pointer
pub pc: *const VALUE,
stack: Vec<InsnId>,
locals: Vec<InsnId>,
}
impl FrameState {
/// Get the opcode for the current instruction
pub fn get_opcode(&self) -> i32 {
unsafe { rb_iseq_opcode_at_pc(self.iseq, self.pc) }
}
}
/// Compute the index of a local variable from its slot index
fn ep_offset_to_local_idx(iseq: IseqPtr, ep_offset: u32) -> usize {
// Layout illustration
// This is an array of VALUE
// | VM_ENV_DATA_SIZE |
// v v
// low addr <+-------+-------+-------+-------+------------------+
// |local 0|local 1| ... |local n| .... |
// +-------+-------+-------+-------+------------------+
// ^ ^ ^ ^
// +-------+---local_table_size----+ cfp->ep--+
// | |
// +------------------ep_offset---------------+
//
// See usages of local_var_name() from iseq.c for similar calculation.
// Equivalent of iseq->body->local_table_size
let local_table_size: i32 = unsafe { get_iseq_body_local_table_size(iseq) }
.try_into()
.unwrap();
let op = (ep_offset - VM_ENV_DATA_SIZE) as i32;
let local_idx = local_table_size - op - 1;
assert!(local_idx >= 0 && local_idx < local_table_size);
local_idx.try_into().unwrap()
}
impl FrameState {
fn new(iseq: IseqPtr) -> FrameState {
FrameState { iseq, pc: std::ptr::null::<VALUE>(), insn_idx: 0, stack: vec![], locals: vec![] }
}
/// Get the number of stack operands
pub fn stack_size(&self) -> usize {
self.stack.len()
}
/// Iterate over all stack slots
pub fn stack(&self) -> Iter<InsnId> {
self.stack.iter()
}
/// Iterate over all local variables
pub fn locals(&self) -> Iter<InsnId> {
self.locals.iter()
}
/// Push a stack operand
fn stack_push(&mut self, opnd: InsnId) {
self.stack.push(opnd);
}
/// Pop a stack operand
fn stack_pop(&mut self) -> Result<InsnId, ParseError> {
self.stack.pop().ok_or_else(|| ParseError::StackUnderflow(self.clone()))
}
/// Get a stack-top operand
fn stack_top(&self) -> Result<InsnId, ParseError> {
self.stack.last().ok_or_else(|| ParseError::StackUnderflow(self.clone())).copied()
}
/// Set a stack operand at idx
fn stack_setn(&mut self, idx: usize, opnd: InsnId) {
let idx = self.stack.len() - idx - 1;
self.stack[idx] = opnd;
}
/// Get a stack operand at idx
fn stack_topn(&self, idx: usize) -> Result<InsnId, ParseError> {
let idx = self.stack.len() - idx - 1;
self.stack.get(idx).ok_or_else(|| ParseError::StackUnderflow(self.clone())).copied()
}
fn setlocal(&mut self, ep_offset: u32, opnd: InsnId) {
let idx = ep_offset_to_local_idx(self.iseq, ep_offset);
self.locals[idx] = opnd;
}
fn getlocal(&mut self, ep_offset: u32) -> InsnId {
let idx = ep_offset_to_local_idx(self.iseq, ep_offset);
self.locals[idx]
}
fn as_args(&self, self_param: InsnId) -> Vec<InsnId> {
// We're currently passing around the self parameter as a basic block
// argument because the register allocator uses a fixed register based
// on the basic block argument index, which would cause a conflict if
// we reuse an argument from another basic block.
// TODO: Modify the register allocator to allow reusing an argument
// of another basic block.
let mut args = vec![self_param];
args.extend(self.locals.iter().chain(self.stack.iter()).map(|op| *op));
args
}
}
impl std::fmt::Display for FrameState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "FrameState {{ pc: {:?}, stack: ", self.pc)?;
write_vec(f, &self.stack)?;
write!(f, ", locals: ")?;
write_vec(f, &self.locals)?;
write!(f, " }}")
}
}
/// Get YARV instruction argument
fn get_arg(pc: *const VALUE, arg_idx: isize) -> VALUE {
unsafe { *(pc.offset(arg_idx + 1)) }
}
/// Compute YARV instruction index at relative offset
fn insn_idx_at_offset(idx: u32, offset: i64) -> u32 {
((idx as isize) + (offset as isize)) as u32
}
fn compute_jump_targets(iseq: *const rb_iseq_t) -> Vec<u32> {
let iseq_size = unsafe { get_iseq_encoded_size(iseq) };
let mut insn_idx = 0;
let mut jump_targets = HashSet::new();
while insn_idx < iseq_size {
// Get the current pc and opcode
let pc = unsafe { rb_iseq_pc_at_idx(iseq, insn_idx) };
// try_into() call below is unfortunate. Maybe pick i32 instead of usize for opcodes.
let opcode: u32 = unsafe { rb_iseq_opcode_at_pc(iseq, pc) }
.try_into()
.unwrap();
insn_idx += insn_len(opcode as usize);
match opcode {
YARVINSN_branchunless | YARVINSN_jump | YARVINSN_branchif | YARVINSN_branchnil => {
let offset = get_arg(pc, 0).as_i64();
jump_targets.insert(insn_idx_at_offset(insn_idx, offset));
}
YARVINSN_opt_new => {
let offset = get_arg(pc, 1).as_i64();
jump_targets.insert(insn_idx_at_offset(insn_idx, offset));
}
YARVINSN_leave | YARVINSN_opt_invokebuiltin_delegate_leave => {
if insn_idx < iseq_size {
jump_targets.insert(insn_idx);
}
}
_ => {}
}
}
let mut result = jump_targets.into_iter().collect::<Vec<_>>();
result.sort();
result
}
#[derive(Debug, PartialEq)]
pub enum CallType {
Splat,
BlockArg,
Kwarg,
KwSplat,
Tailcall,
Super,
Zsuper,
OptSend,
KwSplatMut,
SplatMut,
Forwarding,
}
#[derive(Debug, PartialEq)]
pub enum ParseError {
StackUnderflow(FrameState),
MalformedIseq(u32), // insn_idx into iseq_encoded
}
/// Return the number of locals in the current ISEQ (includes parameters)
fn num_locals(iseq: *const rb_iseq_t) -> usize {
(unsafe { get_iseq_body_local_table_size(iseq) }).as_usize()
}
/// If we can't handle the type of send (yet), bail out.
fn unknown_call_type(flag: u32) -> bool {
if (flag & VM_CALL_KW_SPLAT_MUT) != 0 { return true; }
if (flag & VM_CALL_ARGS_SPLAT_MUT) != 0 { return true; }
if (flag & VM_CALL_ARGS_SPLAT) != 0 { return true; }
if (flag & VM_CALL_KW_SPLAT) != 0 { return true; }
if (flag & VM_CALL_ARGS_BLOCKARG) != 0 { return true; }
if (flag & VM_CALL_KWARG) != 0 { return true; }
if (flag & VM_CALL_TAILCALL) != 0 { return true; }
if (flag & VM_CALL_SUPER) != 0 { return true; }
if (flag & VM_CALL_ZSUPER) != 0 { return true; }
if (flag & VM_CALL_OPT_SEND) != 0 { return true; }
if (flag & VM_CALL_FORWARDING) != 0 { return true; }
false
}
/// We have IseqPayload, which keeps track of HIR Types in the interpreter, but this is not useful
/// or correct to query from inside the optimizer. Instead, ProfileOracle provides an API to look
/// up profiled type information by HIR InsnId at a given ISEQ instruction.
#[derive(Debug)]
struct ProfileOracle {
payload: &'static IseqPayload,
/// types is a map from ISEQ instruction indices -> profiled type information at that ISEQ
/// instruction index. At a given ISEQ instruction, the interpreter has profiled the stack
/// operands to a given ISEQ instruction, and this list of pairs of (InsnId, Type) map that
/// profiling information into HIR instructions.
types: HashMap<usize, Vec<(InsnId, Type)>>,
}
impl ProfileOracle {
fn new(payload: &'static IseqPayload) -> Self {
Self { payload, types: Default::default() }
}
/// Map the interpreter-recorded types of the stack onto the HIR operands on our compile-time virtual stack
fn profile_stack(&mut self, state: &FrameState) {
let iseq_insn_idx = state.insn_idx;
let Some(operand_types) = self.payload.get_operand_types(iseq_insn_idx) else { return };
let entry = self.types.entry(iseq_insn_idx).or_insert_with(|| vec![]);
// operand_types is always going to be <= stack size (otherwise it would have an underflow
// at run-time) so use that to drive iteration.
for (idx, &insn_type) in operand_types.iter().rev().enumerate() {
let insn = state.stack_topn(idx).expect("Unexpected stack underflow in profiling");
entry.push((insn, insn_type))
}
}
}
/// The index of the self parameter in the HIR function
pub const SELF_PARAM_IDX: usize = 0;
/// Compile ISEQ into High-level IR
pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
let payload = get_or_create_iseq_payload(iseq);
let mut profiles = ProfileOracle::new(payload);
let mut fun = Function::new(iseq);
// Compute a map of PC->Block by finding jump targets
let jump_targets = compute_jump_targets(iseq);
let mut insn_idx_to_block = HashMap::new();
for insn_idx in jump_targets {
if insn_idx == 0 {
todo!("Separate entry block for param/self/...");
}
insn_idx_to_block.insert(insn_idx, fun.new_block());
}
// Iteratively fill out basic blocks using a queue
// TODO(max): Basic block arguments at edges
let mut queue = std::collections::VecDeque::new();
// Index of the rest parameter for comparison below
let rest_param_idx = if !iseq.is_null() && unsafe { get_iseq_flags_has_rest(iseq) } {
let opt_num = unsafe { get_iseq_body_param_opt_num(iseq) };
let lead_num = unsafe { get_iseq_body_param_lead_num(iseq) };
opt_num + lead_num
} else {
-1
};
// The HIR function will have the same number of parameter as the iseq so
// we properly handle calls from the interpreter. Roughly speaking, each
// item between commas in the source increase the parameter count by one,
// regardless of parameter kind.
let mut entry_state = FrameState::new(iseq);
fun.push_insn(fun.entry_block, Insn::Param { idx: SELF_PARAM_IDX });
fun.param_types.push(types::BasicObject); // self
for local_idx in 0..num_locals(iseq) {
if local_idx < unsafe { get_iseq_body_param_size(iseq) }.as_usize() {
entry_state.locals.push(fun.push_insn(fun.entry_block, Insn::Param { idx: local_idx + 1 })); // +1 for self
} else {
entry_state.locals.push(fun.push_insn(fun.entry_block, Insn::Const { val: Const::Value(Qnil) }));
}
let mut param_type = types::BasicObject;
// Rest parameters are always ArrayExact
if let Ok(true) = c_int::try_from(local_idx).map(|idx| idx == rest_param_idx) {
param_type = types::ArrayExact;
}
fun.param_types.push(param_type);
}
queue.push_back((entry_state, fun.entry_block, /*insn_idx=*/0_u32));
let mut visited = HashSet::new();
let iseq_size = unsafe { get_iseq_encoded_size(iseq) };
while let Some((incoming_state, block, mut insn_idx)) = queue.pop_front() {
if visited.contains(&block) { continue; }
visited.insert(block);
let (self_param, mut state) = if insn_idx == 0 {
(fun.blocks[fun.entry_block.0].params[SELF_PARAM_IDX], incoming_state.clone())
} else {
let self_param = fun.push_insn(block, Insn::Param { idx: SELF_PARAM_IDX });
let mut result = FrameState::new(iseq);
let mut idx = 1;
for _ in 0..incoming_state.locals.len() {
result.locals.push(fun.push_insn(block, Insn::Param { idx }));
idx += 1;
}
for _ in incoming_state.stack {
result.stack.push(fun.push_insn(block, Insn::Param { idx }));
idx += 1;
}
(self_param, result)
};
// Start the block off with a Snapshot so that if we need to insert a new Guard later on
// and we don't have a Snapshot handy, we can just iterate backward (at the earliest, to
// the beginning of the block).
fun.push_insn(block, Insn::Snapshot { state: state.clone() });
while insn_idx < iseq_size {
state.insn_idx = insn_idx as usize;
// Get the current pc and opcode
let pc = unsafe { rb_iseq_pc_at_idx(iseq, insn_idx) };
state.pc = pc;
let exit_state = state.clone();
profiles.profile_stack(&exit_state);
// try_into() call below is unfortunate. Maybe pick i32 instead of usize for opcodes.
let opcode: u32 = unsafe { rb_iseq_opcode_at_pc(iseq, pc) }
.try_into()
.unwrap();
// Move to the next instruction to compile
insn_idx += insn_len(opcode as usize);
match opcode {
YARVINSN_nop => {},
YARVINSN_putnil => { state.stack_push(fun.push_insn(block, Insn::Const { val: Const::Value(Qnil) })); },
YARVINSN_putobject => { state.stack_push(fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) })); },
YARVINSN_putspecialobject => {
let value_type = SpecialObjectType::from(get_arg(pc, 0).as_u32());
let insn = if value_type == SpecialObjectType::VMCore {
Insn::Const { val: Const::Value(unsafe { rb_mRubyVMFrozenCore }) }
} else {
Insn::PutSpecialObject { value_type }
};
state.stack_push(fun.push_insn(block, insn));
}
YARVINSN_putstring => {
let val = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
let insn_id = fun.push_insn(block, Insn::StringCopy { val, chilled: false });
state.stack_push(insn_id);
}
YARVINSN_putchilledstring => {
let val = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
let insn_id = fun.push_insn(block, Insn::StringCopy { val, chilled: true });
state.stack_push(insn_id);
}
YARVINSN_putself => { state.stack_push(self_param); }
YARVINSN_intern => {
let val = state.stack_pop()?;
let insn_id = fun.push_insn(block, Insn::StringIntern { val });
state.stack_push(insn_id);
}
YARVINSN_newarray => {
let count = get_arg(pc, 0).as_usize();
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let mut elements = vec![];
for _ in 0..count {
elements.push(state.stack_pop()?);
}
elements.reverse();
state.stack_push(fun.push_insn(block, Insn::NewArray { elements, state: exit_id }));
}
YARVINSN_opt_newarray_send => {
let count = get_arg(pc, 0).as_usize();
let method = get_arg(pc, 1).as_u32();
let mut elements = vec![];
for _ in 0..count {
elements.push(state.stack_pop()?);
}
elements.reverse();
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let (bop, insn) = match method {
VM_OPT_NEWARRAY_SEND_MAX => (BOP_MAX, Insn::ArrayMax { elements, state: exit_id }),
_ => {
// Unknown opcode; side-exit into the interpreter
fun.push_insn(block, Insn::SideExit { state: exit_id });
break; // End the block
},
};
fun.push_insn(block, Insn::PatchPoint(Invariant::BOPRedefined { klass: ARRAY_REDEFINED_OP_FLAG, bop }));
state.stack_push(fun.push_insn(block, insn));
}
YARVINSN_duparray => {
let val = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let insn_id = fun.push_insn(block, Insn::ArrayDup { val, state: exit_id });
state.stack_push(insn_id);
}
YARVINSN_newhash => {
let count = get_arg(pc, 0).as_usize();
assert!(count % 2 == 0, "newhash count should be even");
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let mut elements = vec![];
for _ in 0..(count/2) {
let value = state.stack_pop()?;
let key = state.stack_pop()?;
elements.push((key, value));
}
elements.reverse();
state.stack_push(fun.push_insn(block, Insn::NewHash { elements, state: exit_id }));
}
YARVINSN_duphash => {
let val = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let insn_id = fun.push_insn(block, Insn::HashDup { val, state: exit_id });
state.stack_push(insn_id);
}
YARVINSN_splatarray => {
let flag = get_arg(pc, 0);
let result_must_be_mutable = flag.test();
let val = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let obj = if result_must_be_mutable {
fun.push_insn(block, Insn::ToNewArray { val, state: exit_id })
} else {
fun.push_insn(block, Insn::ToArray { val, state: exit_id })
};
state.stack_push(obj);
}
YARVINSN_concattoarray => {
let right = state.stack_pop()?;
let left = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let right_array = fun.push_insn(block, Insn::ToArray { val: right, state: exit_id });
fun.push_insn(block, Insn::ArrayExtend { left, right: right_array, state: exit_id });
state.stack_push(left);
}
YARVINSN_pushtoarray => {
let count = get_arg(pc, 0).as_usize();
let mut vals = vec![];
for _ in 0..count {
vals.push(state.stack_pop()?);
}
let array = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
for val in vals.into_iter().rev() {
fun.push_insn(block, Insn::ArrayPush { array, val, state: exit_id });
}
state.stack_push(array);
}
YARVINSN_putobject_INT2FIX_0_ => {
state.stack_push(fun.push_insn(block, Insn::Const { val: Const::Value(VALUE::fixnum_from_usize(0)) }));
}
YARVINSN_putobject_INT2FIX_1_ => {
state.stack_push(fun.push_insn(block, Insn::Const { val: Const::Value(VALUE::fixnum_from_usize(1)) }));
}
YARVINSN_defined => {
// (rb_num_t op_type, VALUE obj, VALUE pushval)
let op_type = get_arg(pc, 0).as_usize();
let obj = get_arg(pc, 1);
let pushval = get_arg(pc, 2);
let v = state.stack_pop()?;
state.stack_push(fun.push_insn(block, Insn::Defined { op_type, obj, pushval, v }));
}
YARVINSN_definedivar => {
// (ID id, IVC ic, VALUE pushval)
let id = ID(get_arg(pc, 0).as_u64());
let pushval = get_arg(pc, 2);
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
state.stack_push(fun.push_insn(block, Insn::DefinedIvar { self_val: self_param, id, pushval, state: exit_id }));
}
YARVINSN_opt_getconstant_path => {
let ic = get_arg(pc, 0).as_ptr();
let snapshot = fun.push_insn(block, Insn::Snapshot { state: exit_state });
state.stack_push(fun.push_insn(block, Insn::GetConstantPath { ic, state: snapshot }));
}
YARVINSN_branchunless => {
let offset = get_arg(pc, 0).as_i64();
let val = state.stack_pop()?;
let test_id = fun.push_insn(block, Insn::Test { val });
// TODO(max): Check interrupts
let target_idx = insn_idx_at_offset(insn_idx, offset);
let target = insn_idx_to_block[&target_idx];
let _branch_id = fun.push_insn(block, Insn::IfFalse {
val: test_id,
target: BranchEdge { target, args: state.as_args(self_param) }
});
queue.push_back((state.clone(), target, target_idx));
}
YARVINSN_branchif => {
let offset = get_arg(pc, 0).as_i64();
let val = state.stack_pop()?;
let test_id = fun.push_insn(block, Insn::Test { val });
// TODO(max): Check interrupts
let target_idx = insn_idx_at_offset(insn_idx, offset);
let target = insn_idx_to_block[&target_idx];
let _branch_id = fun.push_insn(block, Insn::IfTrue {
val: test_id,
target: BranchEdge { target, args: state.as_args(self_param) }
});
queue.push_back((state.clone(), target, target_idx));
}
YARVINSN_branchnil => {
let offset = get_arg(pc, 0).as_i64();
let val = state.stack_pop()?;
let test_id = fun.push_insn(block, Insn::IsNil { val });
// TODO(max): Check interrupts
let target_idx = insn_idx_at_offset(insn_idx, offset);
let target = insn_idx_to_block[&target_idx];
let _branch_id = fun.push_insn(block, Insn::IfTrue {
val: test_id,
target: BranchEdge { target, args: state.as_args(self_param) }
});
queue.push_back((state.clone(), target, target_idx));
}
YARVINSN_opt_new => {
let offset = get_arg(pc, 1).as_i64();
// TODO(max): Check interrupts
let target_idx = insn_idx_at_offset(insn_idx, offset);
let target = insn_idx_to_block[&target_idx];
// Skip the fast-path and go straight to the fallback code. We will let the
// optimizer take care of the converting Class#new->alloc+initialize instead.
fun.push_insn(block, Insn::Jump(BranchEdge { target, args: state.as_args(self_param) }));
queue.push_back((state.clone(), target, target_idx));
break; // Don't enqueue the next block as a successor
}
YARVINSN_jump => {
let offset = get_arg(pc, 0).as_i64();
// TODO(max): Check interrupts
let target_idx = insn_idx_at_offset(insn_idx, offset);
let target = insn_idx_to_block[&target_idx];
let _branch_id = fun.push_insn(block, Insn::Jump(
BranchEdge { target, args: state.as_args(self_param) }
));
queue.push_back((state.clone(), target, target_idx));
break; // Don't enqueue the next block as a successor
}
YARVINSN_getlocal_WC_0 => {
// TODO(alan): This implementation doesn't read from EP, so will miss writes
// from nested ISeqs. This will need to be amended when we add codegen for
// Send.
let ep_offset = get_arg(pc, 0).as_u32();
let val = state.getlocal(ep_offset);
state.stack_push(val);
}
YARVINSN_setlocal_WC_0 => {
// TODO(alan): This implementation doesn't write to EP, where nested scopes
// read, so they'll miss these writes. This will need to be amended when we
// add codegen for Send.
let ep_offset = get_arg(pc, 0).as_u32();
let val = state.stack_pop()?;
state.setlocal(ep_offset, val);
}
YARVINSN_getlocal_WC_1 => {
let ep_offset = get_arg(pc, 0).as_u32();
state.stack_push(fun.push_insn(block, Insn::GetLocal { ep_offset, level: NonZeroU32::new(1).unwrap() }));
}
YARVINSN_setlocal_WC_1 => {
let ep_offset = get_arg(pc, 0).as_u32();
fun.push_insn(block, Insn::SetLocal { val: state.stack_pop()?, ep_offset, level: NonZeroU32::new(1).unwrap() });
}
YARVINSN_getlocal => {
let ep_offset = get_arg(pc, 0).as_u32();
let level = NonZeroU32::try_from(get_arg(pc, 1).as_u32()).map_err(|_| ParseError::MalformedIseq(insn_idx))?;
state.stack_push(fun.push_insn(block, Insn::GetLocal { ep_offset, level }));
}
YARVINSN_setlocal => {
let ep_offset = get_arg(pc, 0).as_u32();
let level = NonZeroU32::try_from(get_arg(pc, 1).as_u32()).map_err(|_| ParseError::MalformedIseq(insn_idx))?;
fun.push_insn(block, Insn::SetLocal { val: state.stack_pop()?, ep_offset, level });
}
YARVINSN_pop => { state.stack_pop()?; }
YARVINSN_dup => { state.stack_push(state.stack_top()?); }
YARVINSN_dupn => {
// Duplicate the top N element of the stack. As we push, n-1 naturally
// points higher in the original stack.
let n = get_arg(pc, 0).as_usize();
for _ in 0..n {
state.stack_push(state.stack_topn(n-1)?);
}
}
YARVINSN_swap => {
let right = state.stack_pop()?;
let left = state.stack_pop()?;
state.stack_push(right);
state.stack_push(left);
}
YARVINSN_setn => {
let n = get_arg(pc, 0).as_usize();
let top = state.stack_top()?;
state.stack_setn(n, top);
}
YARVINSN_topn => {
let n = get_arg(pc, 0).as_usize();
let top = state.stack_topn(n)?;
state.stack_push(top);
}
YARVINSN_adjuststack => {
let mut n = get_arg(pc, 0).as_usize();
while n > 0 {
state.stack_pop()?;
n -= 1;
}
}
YARVINSN_opt_aref_with => {
// NB: opt_aref_with has an instruction argument for the call at get_arg(0)
let cd: *const rb_call_data = get_arg(pc, 1).as_ptr();
let call_info = unsafe { rb_get_call_data_ci(cd) };
if unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) {
// Unknown call type; side-exit into the interpreter
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
fun.push_insn(block, Insn::SideExit { state: exit_id });
break; // End the block
}
let argc = unsafe { vm_ci_argc((*cd).ci) };
let method_name = unsafe {
let mid = rb_vm_ci_mid(call_info);
mid.contents_lossy().into_owned()
};
assert_eq!(1, argc, "opt_aref_with should only be emitted for argc=1");
let aref_arg = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
let args = vec![aref_arg];
let recv = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, call_info: CallInfo { method_name }, cd, args, state: exit_id });
state.stack_push(send);
}
YARVINSN_opt_neq => {
// NB: opt_neq has two cd; get_arg(0) is for eq and get_arg(1) is for neq
let cd: *const rb_call_data = get_arg(pc, 1).as_ptr();
let call_info = unsafe { rb_get_call_data_ci(cd) };
if unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) {
// Unknown call type; side-exit into the interpreter
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
fun.push_insn(block, Insn::SideExit { state: exit_id });
break; // End the block
}
let argc = unsafe { vm_ci_argc((*cd).ci) };
let method_name = unsafe {
let mid = rb_vm_ci_mid(call_info);
mid.contents_lossy().into_owned()
};
let mut args = vec![];
for _ in 0..argc {
args.push(state.stack_pop()?);
}
args.reverse();
let recv = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, call_info: CallInfo { method_name }, cd, args, state: exit_id });
state.stack_push(send);
}
YARVINSN_opt_hash_freeze |
YARVINSN_opt_ary_freeze |
YARVINSN_opt_str_freeze |
YARVINSN_opt_str_uminus => {
// NB: these instructions have the recv for the call at get_arg(0)
let cd: *const rb_call_data = get_arg(pc, 1).as_ptr();
let call_info = unsafe { rb_get_call_data_ci(cd) };
if unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) {
// Unknown call type; side-exit into the interpreter
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
fun.push_insn(block, Insn::SideExit { state: exit_id });
break; // End the block
}
let argc = unsafe { vm_ci_argc((*cd).ci) };
let name = insn_name(opcode as usize);
assert_eq!(0, argc, "{name} should not have args");
let args = vec![];
let method_name = unsafe {
let mid = rb_vm_ci_mid(call_info);
mid.contents_lossy().into_owned()
};
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let recv = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, call_info: CallInfo { method_name }, cd, args, state: exit_id });
state.stack_push(send);
}
YARVINSN_leave => {
fun.push_insn(block, Insn::Return { val: state.stack_pop()? });
break; // Don't enqueue the next block as a successor
}
// These are opt_send_without_block and all the opt_* instructions
// specialized to a certain method that could also be serviced
// using the general send implementation. The optimizer start from
// a general send for all of these later in the pipeline.
YARVINSN_opt_nil_p |
YARVINSN_opt_plus |
YARVINSN_opt_minus |
YARVINSN_opt_mult |
YARVINSN_opt_div |
YARVINSN_opt_mod |
YARVINSN_opt_eq |
YARVINSN_opt_lt |
YARVINSN_opt_le |
YARVINSN_opt_gt |
YARVINSN_opt_ge |
YARVINSN_opt_ltlt |
YARVINSN_opt_aset |
YARVINSN_opt_length |
YARVINSN_opt_size |
YARVINSN_opt_aref |
YARVINSN_opt_empty_p |
YARVINSN_opt_succ |
YARVINSN_opt_and |
YARVINSN_opt_or |
YARVINSN_opt_not |
YARVINSN_opt_regexpmatch2 |
YARVINSN_opt_send_without_block => {
let cd: *const rb_call_data = get_arg(pc, 0).as_ptr();
let call_info = unsafe { rb_get_call_data_ci(cd) };
if unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) {
// Unknown call type; side-exit into the interpreter
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
fun.push_insn(block, Insn::SideExit { state: exit_id });
break; // End the block
}
let argc = unsafe { vm_ci_argc((*cd).ci) };
let method_name = unsafe {
let mid = rb_vm_ci_mid(call_info);
mid.contents_lossy().into_owned()
};
let mut args = vec![];
for _ in 0..argc {
args.push(state.stack_pop()?);
}
args.reverse();
let recv = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, call_info: CallInfo { method_name }, cd, args, state: exit_id });
state.stack_push(send);
}
YARVINSN_send => {
let cd: *const rb_call_data = get_arg(pc, 0).as_ptr();
let blockiseq: IseqPtr = get_arg(pc, 1).as_iseq();
let call_info = unsafe { rb_get_call_data_ci(cd) };
if unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) {
// Unknown call type; side-exit into the interpreter
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
fun.push_insn(block, Insn::SideExit { state: exit_id });
break; // End the block
}
let argc = unsafe { vm_ci_argc((*cd).ci) };
let method_name = unsafe {
let mid = rb_vm_ci_mid(call_info);
mid.contents_lossy().into_owned()
};
let mut args = vec![];
for _ in 0..argc {
args.push(state.stack_pop()?);
}
args.reverse();
let recv = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let send = fun.push_insn(block, Insn::Send { self_val: recv, call_info: CallInfo { method_name }, cd, blockiseq, args, state: exit_id });
state.stack_push(send);
}
YARVINSN_getglobal => {
let id = ID(get_arg(pc, 0).as_u64());
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let result = fun.push_insn(block, Insn::GetGlobal { id, state: exit_id });
state.stack_push(result);
}
YARVINSN_setglobal => {
let id = ID(get_arg(pc, 0).as_u64());
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let val = state.stack_pop()?;
fun.push_insn(block, Insn::SetGlobal { id, val, state: exit_id });
}
YARVINSN_getinstancevariable => {
let id = ID(get_arg(pc, 0).as_u64());
// ic is in arg 1
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let result = fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, state: exit_id });
state.stack_push(result);
}
YARVINSN_setinstancevariable => {
let id = ID(get_arg(pc, 0).as_u64());
// ic is in arg 1
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let val = state.stack_pop()?;
fun.push_insn(block, Insn::SetIvar { self_val: self_param, id, val, state: exit_id });
}
YARVINSN_opt_reverse => {
// Reverse the order of the top N stack items.
let n = get_arg(pc, 0).as_usize();
for i in 0..n/2 {
let bottom = state.stack_topn(n - 1 - i)?;
let top = state.stack_topn(i)?;
state.stack_setn(i, bottom);
state.stack_setn(n - 1 - i, top);
}
}
YARVINSN_newrange => {
let flag = RangeType::from(get_arg(pc, 0).as_u32());
let high = state.stack_pop()?;
let low = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let insn_id = fun.push_insn(block, Insn::NewRange { low, high, flag, state: exit_id });
state.stack_push(insn_id);
}
YARVINSN_invokebuiltin => {
let bf: rb_builtin_function = unsafe { *get_arg(pc, 0).as_ptr() };
let mut args = vec![];
for _ in 0..bf.argc {
args.push(state.stack_pop()?);
}
args.push(self_param);
args.reverse();
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let insn_id = fun.push_insn(block, Insn::InvokeBuiltin { bf, args, state: exit_id });
state.stack_push(insn_id);
}
YARVINSN_opt_invokebuiltin_delegate |
YARVINSN_opt_invokebuiltin_delegate_leave => {
let bf: rb_builtin_function = unsafe { *get_arg(pc, 0).as_ptr() };
let index = get_arg(pc, 1).as_usize();
let argc = bf.argc as usize;
let mut args = vec![self_param];
for &local in state.locals().skip(index).take(argc) {
args.push(local);
}
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let insn_id = fun.push_insn(block, Insn::InvokeBuiltin { bf, args, state: exit_id });
state.stack_push(insn_id);
}
YARVINSN_objtostring => {
let cd: *const rb_call_data = get_arg(pc, 0).as_ptr();
let call_info = unsafe { rb_get_call_data_ci(cd) };
if unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) {
assert!(false, "objtostring should not have unknown call type");
}
let argc = unsafe { vm_ci_argc((*cd).ci) };
assert_eq!(0, argc, "objtostring should not have args");
let method_name: String = unsafe {
let mid = rb_vm_ci_mid(call_info);
mid.contents_lossy().into_owned()
};
let recv = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let objtostring = fun.push_insn(block, Insn::ObjToString { val: recv, call_info: CallInfo { method_name }, cd, state: exit_id });
state.stack_push(objtostring)
}
YARVINSN_anytostring => {
let str = state.stack_pop()?;
let val = state.stack_pop()?;
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
let anytostring = fun.push_insn(block, Insn::AnyToString { val, str, state: exit_id });
state.stack_push(anytostring);
}
_ => {
// Unknown opcode; side-exit into the interpreter
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
fun.push_insn(block, Insn::SideExit { state: exit_id });
break; // End the block
}
}
if insn_idx_to_block.contains_key(&insn_idx) {
let target = insn_idx_to_block[&insn_idx];
fun.push_insn(block, Insn::Jump(BranchEdge { target, args: state.as_args(self_param) }));
queue.push_back((state, target, insn_idx));
break; // End the block
}
}
}
fun.infer_types();
match get_option!(dump_hir_init) {
Some(DumpHIR::WithoutSnapshot) => println!("HIR:\n{}", FunctionPrinter::without_snapshot(&fun)),
Some(DumpHIR::All) => println!("HIR:\n{}", FunctionPrinter::with_snapshot(&fun)),
Some(DumpHIR::Debug) => println!("HIR:\n{:#?}", &fun),
None => {},
}
fun.profiles = Some(profiles);
Ok(fun)
}
#[cfg(test)]
mod union_find_tests {
use super::UnionFind;
#[test]
fn test_find_returns_self() {
let mut uf = UnionFind::new();
assert_eq!(uf.find(3usize), 3);
}
#[test]
fn test_find_returns_target() {
let mut uf = UnionFind::new();
uf.make_equal_to(3, 4);
assert_eq!(uf.find(3usize), 4);
}
#[test]
fn test_find_returns_transitive_target() {
let mut uf = UnionFind::new();
uf.make_equal_to(3, 4);
uf.make_equal_to(4, 5);
assert_eq!(uf.find(3usize), 5);
assert_eq!(uf.find(4usize), 5);
}
#[test]
fn test_find_compresses_path() {
let mut uf = UnionFind::new();
uf.make_equal_to(3, 4);
uf.make_equal_to(4, 5);
assert_eq!(uf.at(3usize), Some(4));
assert_eq!(uf.find(3usize), 5);
assert_eq!(uf.at(3usize), Some(5));
}
}
#[cfg(test)]
mod rpo_tests {
use super::*;
#[test]
fn one_block() {
let mut function = Function::new(std::ptr::null());
let entry = function.entry_block;
let val = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) });
function.push_insn(entry, Insn::Return { val });
assert_eq!(function.rpo(), vec![entry]);
}
#[test]
fn jump() {
let mut function = Function::new(std::ptr::null());
let entry = function.entry_block;
let exit = function.new_block();
function.push_insn(entry, Insn::Jump(BranchEdge { target: exit, args: vec![] }));
let val = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) });
function.push_insn(entry, Insn::Return { val });
assert_eq!(function.rpo(), vec![entry, exit]);
}
#[test]
fn diamond_iftrue() {
let mut function = Function::new(std::ptr::null());
let entry = function.entry_block;
let side = function.new_block();
let exit = function.new_block();
function.push_insn(side, Insn::Jump(BranchEdge { target: exit, args: vec![] }));
let val = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) });
function.push_insn(entry, Insn::IfTrue { val, target: BranchEdge { target: side, args: vec![] } });
function.push_insn(entry, Insn::Jump(BranchEdge { target: exit, args: vec![] }));
let val = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) });
function.push_insn(entry, Insn::Return { val });
assert_eq!(function.rpo(), vec![entry, side, exit]);
}
#[test]
fn diamond_iffalse() {
let mut function = Function::new(std::ptr::null());
let entry = function.entry_block;
let side = function.new_block();
let exit = function.new_block();
function.push_insn(side, Insn::Jump(BranchEdge { target: exit, args: vec![] }));
let val = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) });
function.push_insn(entry, Insn::IfFalse { val, target: BranchEdge { target: side, args: vec![] } });
function.push_insn(entry, Insn::Jump(BranchEdge { target: exit, args: vec![] }));
let val = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) });
function.push_insn(entry, Insn::Return { val });
assert_eq!(function.rpo(), vec![entry, side, exit]);
}
#[test]
fn a_loop() {
let mut function = Function::new(std::ptr::null());
let entry = function.entry_block;
function.push_insn(entry, Insn::Jump(BranchEdge { target: entry, args: vec![] }));
assert_eq!(function.rpo(), vec![entry]);
}
}
#[cfg(test)]
mod infer_tests {
use super::*;
#[track_caller]
fn assert_subtype(left: Type, right: Type) {
assert!(left.is_subtype(right), "{left} is not a subtype of {right}");
}
#[track_caller]
fn assert_bit_equal(left: Type, right: Type) {
assert!(left.bit_equal(right), "{left} != {right}");
}
#[test]
fn test_const() {
let mut function = Function::new(std::ptr::null());
let val = function.push_insn(function.entry_block, Insn::Const { val: Const::Value(Qnil) });
assert_bit_equal(function.infer_type(val), types::NilClassExact);
}
#[test]
fn test_nil() {
crate::cruby::with_rubyvm(|| {
let mut function = Function::new(std::ptr::null());
let nil = function.push_insn(function.entry_block, Insn::Const { val: Const::Value(Qnil) });
let val = function.push_insn(function.entry_block, Insn::Test { val: nil });
function.infer_types();
assert_bit_equal(function.type_of(val), Type::from_cbool(false));
});
}
#[test]
fn test_false() {
crate::cruby::with_rubyvm(|| {
let mut function = Function::new(std::ptr::null());
let false_ = function.push_insn(function.entry_block, Insn::Const { val: Const::Value(Qfalse) });
let val = function.push_insn(function.entry_block, Insn::Test { val: false_ });
function.infer_types();
assert_bit_equal(function.type_of(val), Type::from_cbool(false));
});
}
#[test]
fn test_truthy() {
crate::cruby::with_rubyvm(|| {
let mut function = Function::new(std::ptr::null());
let true_ = function.push_insn(function.entry_block, Insn::Const { val: Const::Value(Qtrue) });
let val = function.push_insn(function.entry_block, Insn::Test { val: true_ });
function.infer_types();
assert_bit_equal(function.type_of(val), Type::from_cbool(true));
});
}
#[test]
fn test_unknown() {
crate::cruby::with_rubyvm(|| {
let mut function = Function::new(std::ptr::null());
let param = function.push_insn(function.entry_block, Insn::Param { idx: SELF_PARAM_IDX });
function.param_types.push(types::BasicObject); // self
let val = function.push_insn(function.entry_block, Insn::Test { val: param });
function.infer_types();
assert_bit_equal(function.type_of(val), types::CBool);
});
}
#[test]
fn newarray() {
let mut function = Function::new(std::ptr::null());
// Fake FrameState index of 0usize
let val = function.push_insn(function.entry_block, Insn::NewArray { elements: vec![], state: InsnId(0usize) });
assert_bit_equal(function.infer_type(val), types::ArrayExact);
}
#[test]
fn arraydup() {
let mut function = Function::new(std::ptr::null());
// Fake FrameState index of 0usize
let arr = function.push_insn(function.entry_block, Insn::NewArray { elements: vec![], state: InsnId(0usize) });
let val = function.push_insn(function.entry_block, Insn::ArrayDup { val: arr, state: InsnId(0usize) });
assert_bit_equal(function.infer_type(val), types::ArrayExact);
}
#[test]
fn diamond_iffalse_merge_fixnum() {
let mut function = Function::new(std::ptr::null());
let entry = function.entry_block;
let side = function.new_block();
let exit = function.new_block();
let v0 = function.push_insn(side, Insn::Const { val: Const::Value(VALUE::fixnum_from_usize(3)) });
function.push_insn(side, Insn::Jump(BranchEdge { target: exit, args: vec![v0] }));
let val = function.push_insn(entry, Insn::Const { val: Const::CBool(false) });
function.push_insn(entry, Insn::IfFalse { val, target: BranchEdge { target: side, args: vec![] } });
let v1 = function.push_insn(entry, Insn::Const { val: Const::Value(VALUE::fixnum_from_usize(4)) });
function.push_insn(entry, Insn::Jump(BranchEdge { target: exit, args: vec![v1] }));
let param = function.push_insn(exit, Insn::Param { idx: 0 });
crate::cruby::with_rubyvm(|| {
function.infer_types();
});
assert_bit_equal(function.type_of(param), types::Fixnum);
}
#[test]
fn diamond_iffalse_merge_bool() {
let mut function = Function::new(std::ptr::null());
let entry = function.entry_block;
let side = function.new_block();
let exit = function.new_block();
let v0 = function.push_insn(side, Insn::Const { val: Const::Value(Qtrue) });
function.push_insn(side, Insn::Jump(BranchEdge { target: exit, args: vec![v0] }));
let val = function.push_insn(entry, Insn::Const { val: Const::CBool(false) });
function.push_insn(entry, Insn::IfFalse { val, target: BranchEdge { target: side, args: vec![] } });
let v1 = function.push_insn(entry, Insn::Const { val: Const::Value(Qfalse) });
function.push_insn(entry, Insn::Jump(BranchEdge { target: exit, args: vec![v1] }));
let param = function.push_insn(exit, Insn::Param { idx: 0 });
crate::cruby::with_rubyvm(|| {
function.infer_types();
assert_bit_equal(function.type_of(param), types::TrueClassExact.union(types::FalseClassExact));
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use expect_test::{expect, Expect};
#[macro_export]
macro_rules! assert_matches {
( $x:expr, $pat:pat ) => {
{
let val = $x;
if (!matches!(val, $pat)) {
eprintln!("{} ({:?}) does not match pattern {}", stringify!($x), val, stringify!($pat));
assert!(false);
}
}
};
}
#[track_caller]
fn assert_matches_value(insn: Option<&Insn>, val: VALUE) {
match insn {
Some(Insn::Const { val: Const::Value(spec) }) => {
assert_eq!(*spec, val);
}
_ => assert!(false, "Expected Const {val}, found {insn:?}"),
}
}
#[track_caller]
fn assert_matches_const(insn: Option<&Insn>, expected: Const) {
match insn {
Some(Insn::Const { val }) => {
assert_eq!(*val, expected, "{val:?} does not match {expected:?}");
}
_ => assert!(false, "Expected Const {expected:?}, found {insn:?}"),
}
}
#[track_caller]
fn assert_method_hir(method: &str, hir: Expect) {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
let function = iseq_to_hir(iseq).unwrap();
assert_function_hir(function, hir);
}
fn iseq_contains_opcode(iseq: IseqPtr, expected_opcode: u32) -> bool {
let iseq_size = unsafe { get_iseq_encoded_size(iseq) };
let mut insn_idx = 0;
while insn_idx < iseq_size {
// Get the current pc and opcode
let pc = unsafe { rb_iseq_pc_at_idx(iseq, insn_idx) };
// try_into() call below is unfortunate. Maybe pick i32 instead of usize for opcodes.
let opcode: u32 = unsafe { rb_iseq_opcode_at_pc(iseq, pc) }
.try_into()
.unwrap();
if opcode == expected_opcode {
return true;
}
insn_idx += insn_len(opcode as usize);
}
false
}
#[track_caller]
fn assert_method_hir_with_opcodes(method: &str, opcodes: &[u32], hir: Expect) {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
for &opcode in opcodes {
assert!(iseq_contains_opcode(iseq, opcode), "iseq {method} does not contain {}", insn_name(opcode as usize));
}
let function = iseq_to_hir(iseq).unwrap();
assert_function_hir(function, hir);
}
#[track_caller]
fn assert_method_hir_with_opcode(method: &str, opcode: u32, hir: Expect) {
assert_method_hir_with_opcodes(method, &[opcode], hir)
}
#[track_caller]
pub fn assert_function_hir(function: Function, expected_hir: Expect) {
let actual_hir = format!("{}", FunctionPrinter::without_snapshot(&function));
expected_hir.assert_eq(&actual_hir);
}
#[track_caller]
fn assert_compile_fails(method: &str, reason: ParseError) {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
let result = iseq_to_hir(iseq);
assert!(result.is_err(), "Expected an error but successfully compiled to HIR: {}", FunctionPrinter::without_snapshot(&result.unwrap()));
assert_eq!(result.unwrap_err(), reason);
}
#[test]
fn test_putobject() {
eval("def test = 123");
assert_method_hir_with_opcode("test", YARVINSN_putobject, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Fixnum[123] = Const Value(123)
Return v2
"#]]);
}
#[test]
fn test_new_array() {
eval("def test = []");
assert_method_hir_with_opcode("test", YARVINSN_newarray, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:ArrayExact = NewArray
Return v3
"#]]);
}
#[test]
fn test_new_array_with_element() {
eval("def test(a) = [a]");
assert_method_hir_with_opcode("test", YARVINSN_newarray, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v4:ArrayExact = NewArray v1
Return v4
"#]]);
}
#[test]
fn test_new_array_with_elements() {
eval("def test(a, b) = [a, b]");
assert_method_hir_with_opcode("test", YARVINSN_newarray, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:ArrayExact = NewArray v1, v2
Return v5
"#]]);
}
#[test]
fn test_new_range_inclusive_with_one_element() {
eval("def test(a) = (a..10)");
assert_method_hir_with_opcode("test", YARVINSN_newrange, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[10] = Const Value(10)
v5:RangeExact = NewRange v1 NewRangeInclusive v3
Return v5
"#]]);
}
#[test]
fn test_new_range_inclusive_with_two_elements() {
eval("def test(a, b) = (a..b)");
assert_method_hir_with_opcode("test", YARVINSN_newrange, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:RangeExact = NewRange v1 NewRangeInclusive v2
Return v5
"#]]);
}
#[test]
fn test_new_range_exclusive_with_one_element() {
eval("def test(a) = (a...10)");
assert_method_hir_with_opcode("test", YARVINSN_newrange, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[10] = Const Value(10)
v5:RangeExact = NewRange v1 NewRangeExclusive v3
Return v5
"#]]);
}
#[test]
fn test_new_range_exclusive_with_two_elements() {
eval("def test(a, b) = (a...b)");
assert_method_hir_with_opcode("test", YARVINSN_newrange, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:RangeExact = NewRange v1 NewRangeExclusive v2
Return v5
"#]]);
}
#[test]
fn test_array_dup() {
eval("def test = [1, 2, 3]");
assert_method_hir_with_opcode("test", YARVINSN_duparray, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v4:ArrayExact = ArrayDup v2
Return v4
"#]]);
}
#[test]
fn test_hash_dup() {
eval("def test = {a: 1, b: 2}");
assert_method_hir_with_opcode("test", YARVINSN_duphash, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v4:HashExact = HashDup v2
Return v4
"#]]);
}
#[test]
fn test_new_hash_empty() {
eval("def test = {}");
assert_method_hir_with_opcode("test", YARVINSN_newhash, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:HashExact = NewHash
Return v3
"#]]);
}
#[test]
fn test_new_hash_with_elements() {
eval("def test(aval, bval) = {a: aval, b: bval}");
assert_method_hir_with_opcode("test", YARVINSN_newhash, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v4:StaticSymbol[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v5:StaticSymbol[VALUE(0x1008)] = Const Value(VALUE(0x1008))
v7:HashExact = NewHash v4: v1, v5: v2
Return v7
"#]]);
}
#[test]
fn test_string_copy() {
eval("def test = \"hello\"");
assert_method_hir_with_opcode("test", YARVINSN_putchilledstring, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v3:StringExact = StringCopy v2
Return v3
"#]]);
}
#[test]
fn test_bignum() {
eval("def test = 999999999999999999999999999999999999");
assert_method_hir_with_opcode("test", YARVINSN_putobject, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Bignum[VALUE(0x1000)] = Const Value(VALUE(0x1000))
Return v2
"#]]);
}
#[test]
fn test_flonum() {
eval("def test = 1.5");
assert_method_hir_with_opcode("test", YARVINSN_putobject, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Flonum[VALUE(0x1000)] = Const Value(VALUE(0x1000))
Return v2
"#]]);
}
#[test]
fn test_heap_float() {
eval("def test = 1.7976931348623157e+308");
assert_method_hir_with_opcode("test", YARVINSN_putobject, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:HeapFloat[VALUE(0x1000)] = Const Value(VALUE(0x1000))
Return v2
"#]]);
}
#[test]
fn test_static_sym() {
eval("def test = :foo");
assert_method_hir_with_opcode("test", YARVINSN_putobject, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:StaticSymbol[VALUE(0x1000)] = Const Value(VALUE(0x1000))
Return v2
"#]]);
}
#[test]
fn test_opt_plus() {
eval("def test = 1+2");
assert_method_hir_with_opcode("test", YARVINSN_opt_plus, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Fixnum[1] = Const Value(1)
v3:Fixnum[2] = Const Value(2)
v5:BasicObject = SendWithoutBlock v2, :+, v3
Return v5
"#]]);
}
#[test]
fn test_opt_hash_freeze() {
eval("
def test = {}.freeze
");
assert_method_hir_with_opcode("test", YARVINSN_opt_hash_freeze, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v4:BasicObject = SendWithoutBlock v3, :freeze
Return v4
"#]]);
}
#[test]
fn test_opt_ary_freeze() {
eval("
def test = [].freeze
");
assert_method_hir_with_opcode("test", YARVINSN_opt_ary_freeze, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v4:BasicObject = SendWithoutBlock v3, :freeze
Return v4
"#]]);
}
#[test]
fn test_opt_str_freeze() {
eval("
def test = ''.freeze
");
assert_method_hir_with_opcode("test", YARVINSN_opt_str_freeze, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v4:BasicObject = SendWithoutBlock v3, :freeze
Return v4
"#]]);
}
#[test]
fn test_opt_str_uminus() {
eval("
def test = -''
");
assert_method_hir_with_opcode("test", YARVINSN_opt_str_uminus, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v4:BasicObject = SendWithoutBlock v3, :-@
Return v4
"#]]);
}
#[test]
fn test_setlocal_getlocal() {
eval("
def test
a = 1
a
end
");
assert_method_hir_with_opcodes("test", &[YARVINSN_getlocal_WC_0, YARVINSN_setlocal_WC_0], expect![[r#"
fn test:
bb0(v0:BasicObject):
v1:NilClassExact = Const Value(nil)
v3:Fixnum[1] = Const Value(1)
Return v3
"#]]);
}
#[test]
fn test_nested_setlocal_getlocal() {
eval("
l3 = 3
_unused = _unused1 = nil
1.times do |l2|
_ = nil
l2 = 2
1.times do |l1|
l1 = 1
define_method(:test) do
l1 = l2
l2 = l1 + l2
l3 = l2 + l3
end
end
end
");
assert_method_hir_with_opcodes(
"test",
&[YARVINSN_getlocal_WC_1, YARVINSN_setlocal_WC_1,
YARVINSN_getlocal, YARVINSN_setlocal],
expect![[r#"
fn block (3 levels) in <compiled>:
bb0(v0:BasicObject):
v2:BasicObject = GetLocal l2, EP@4
SetLocal l1, EP@3, v2
v4:BasicObject = GetLocal l1, EP@3
v5:BasicObject = GetLocal l2, EP@4
v7:BasicObject = SendWithoutBlock v4, :+, v5
SetLocal l2, EP@4, v7
v9:BasicObject = GetLocal l2, EP@4
v10:BasicObject = GetLocal l3, EP@5
v12:BasicObject = SendWithoutBlock v9, :+, v10
SetLocal l3, EP@5, v12
Return v12
"#]]
);
}
#[test]
fn defined_ivar() {
eval("
def test = defined?(@foo)
");
assert_method_hir_with_opcode("test", YARVINSN_definedivar, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:BasicObject = DefinedIvar v0, :@foo
Return v3
"#]]);
}
#[test]
fn defined() {
eval("
def test = return defined?(SeaChange), defined?(favourite), defined?($ruby)
");
assert_method_hir_with_opcode("test", YARVINSN_defined, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:NilClassExact = Const Value(nil)
v3:BasicObject = Defined constant, v2
v4:BasicObject = Defined func, v0
v5:NilClassExact = Const Value(nil)
v6:BasicObject = Defined global-variable, v5
v8:ArrayExact = NewArray v3, v4, v6
Return v8
"#]]);
}
#[test]
fn test_return_const() {
eval("
def test(cond)
if cond
3
else
4
end
end
");
assert_method_hir_with_opcode("test", YARVINSN_leave, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:CBool = Test v1
IfFalse v3, bb1(v0, v1)
v5:Fixnum[3] = Const Value(3)
Return v5
bb1(v7:BasicObject, v8:BasicObject):
v10:Fixnum[4] = Const Value(4)
Return v10
"#]]);
}
#[test]
fn test_merge_const() {
eval("
def test(cond)
if cond
result = 3
else
result = 4
end
result
end
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v2:NilClassExact = Const Value(nil)
v4:CBool = Test v1
IfFalse v4, bb1(v0, v1, v2)
v6:Fixnum[3] = Const Value(3)
Jump bb2(v0, v1, v6)
bb1(v8:BasicObject, v9:BasicObject, v10:NilClassExact):
v12:Fixnum[4] = Const Value(4)
Jump bb2(v8, v9, v12)
bb2(v14:BasicObject, v15:BasicObject, v16:Fixnum):
Return v16
"#]]);
}
#[test]
fn test_opt_plus_fixnum() {
eval("
def test(a, b) = a + b
test(1, 2); test(1, 2)
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :+, v2
Return v5
"#]]);
}
#[test]
fn test_opt_minus_fixnum() {
eval("
def test(a, b) = a - b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_minus, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :-, v2
Return v5
"#]]);
}
#[test]
fn test_opt_mult_fixnum() {
eval("
def test(a, b) = a * b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_mult, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :*, v2
Return v5
"#]]);
}
#[test]
fn test_opt_div_fixnum() {
eval("
def test(a, b) = a / b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_div, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :/, v2
Return v5
"#]]);
}
#[test]
fn test_opt_mod_fixnum() {
eval("
def test(a, b) = a % b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_mod, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :%, v2
Return v5
"#]]);
}
#[test]
fn test_opt_eq_fixnum() {
eval("
def test(a, b) = a == b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_eq, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :==, v2
Return v5
"#]]);
}
#[test]
fn test_opt_neq_fixnum() {
eval("
def test(a, b) = a != b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_neq, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :!=, v2
Return v5
"#]]);
}
#[test]
fn test_opt_lt_fixnum() {
eval("
def test(a, b) = a < b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_lt, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :<, v2
Return v5
"#]]);
}
#[test]
fn test_opt_le_fixnum() {
eval("
def test(a, b) = a <= b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_le, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :<=, v2
Return v5
"#]]);
}
#[test]
fn test_opt_gt_fixnum() {
eval("
def test(a, b) = a > b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_gt, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :>, v2
Return v5
"#]]);
}
#[test]
fn test_loop() {
eval("
def test
result = 0
times = 10
while times > 0
result = result + 1
times = times - 1
end
result
end
test
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v1:NilClassExact = Const Value(nil)
v2:NilClassExact = Const Value(nil)
v4:Fixnum[0] = Const Value(0)
v5:Fixnum[10] = Const Value(10)
Jump bb2(v0, v4, v5)
bb2(v7:BasicObject, v8:BasicObject, v9:BasicObject):
v11:Fixnum[0] = Const Value(0)
v13:BasicObject = SendWithoutBlock v9, :>, v11
v14:CBool = Test v13
IfTrue v14, bb1(v7, v8, v9)
v16:NilClassExact = Const Value(nil)
Return v8
bb1(v18:BasicObject, v19:BasicObject, v20:BasicObject):
v22:Fixnum[1] = Const Value(1)
v24:BasicObject = SendWithoutBlock v19, :+, v22
v25:Fixnum[1] = Const Value(1)
v27:BasicObject = SendWithoutBlock v20, :-, v25
Jump bb2(v18, v24, v27)
"#]]);
}
#[test]
fn test_opt_ge_fixnum() {
eval("
def test(a, b) = a >= b
test(1, 2); test(1, 2)
");
assert_method_hir_with_opcode("test", YARVINSN_opt_ge, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :>=, v2
Return v5
"#]]);
}
#[test]
fn test_display_types() {
eval("
def test
cond = true
if cond
3
else
4
end
end
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v1:NilClassExact = Const Value(nil)
v3:TrueClassExact = Const Value(true)
v4:CBool[true] = Test v3
IfFalse v4, bb1(v0, v3)
v6:Fixnum[3] = Const Value(3)
Return v6
bb1(v8, v9):
v11 = Const Value(4)
Return v11
"#]]);
}
#[test]
fn test_send_without_block() {
eval("
def bar(a, b)
a+b
end
def test
bar(2, 3)
end
");
assert_method_hir_with_opcode("test", YARVINSN_opt_send_without_block, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Fixnum[2] = Const Value(2)
v3:Fixnum[3] = Const Value(3)
v5:BasicObject = SendWithoutBlock v0, :bar, v2, v3
Return v5
"#]]);
}
#[test]
fn test_send_with_block() {
eval("
def test(a)
a.each {|item|
item
}
end
test([1,2,3])
");
assert_method_hir_with_opcode("test", YARVINSN_send, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v4:BasicObject = Send v1, 0x1000, :each
Return v4
"#]]);
}
#[test]
fn different_objects_get_addresses() {
eval("def test = unknown_method([0], [1], '2', '2')");
// The 2 string literals have the same address because they're deduped.
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v4:ArrayExact = ArrayDup v2
v5:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008))
v7:ArrayExact = ArrayDup v5
v8:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010))
v9:StringExact = StringCopy v8
v10:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010))
v11:StringExact = StringCopy v10
v13:BasicObject = SendWithoutBlock v0, :unknown_method, v4, v7, v9, v11
Return v13
"#]]);
}
#[test]
fn test_cant_compile_splat() {
eval("
def test(a) = foo(*a)
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v4:ArrayExact = ToArray v1
SideExit
"#]]);
}
#[test]
fn test_cant_compile_block_arg() {
eval("
def test(a) = foo(&a)
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
SideExit
"#]]);
}
#[test]
fn test_cant_compile_kwarg() {
eval("
def test(a) = foo(a: 1)
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[1] = Const Value(1)
SideExit
"#]]);
}
#[test]
fn test_cant_compile_kw_splat() {
eval("
def test(a) = foo(**a)
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
SideExit
"#]]);
}
// TODO(max): Figure out how to generate a call with TAILCALL flag
#[test]
fn test_cant_compile_super() {
eval("
def test = super()
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
SideExit
"#]]);
}
#[test]
fn test_cant_compile_zsuper() {
eval("
def test = super
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
SideExit
"#]]);
}
#[test]
fn test_cant_compile_super_forward() {
eval("
def test(...) = super(...)
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
SideExit
"#]]);
}
// TODO(max): Figure out how to generate a call with OPT_SEND flag
#[test]
fn test_cant_compile_kw_splat_mut() {
eval("
def test(a) = foo **a, b: 1
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:BasicObject[VMFrozenCore] = Const Value(VALUE(0x1000))
v5:HashExact = NewHash
v7:BasicObject = SendWithoutBlock v3, :core#hash_merge_kwd, v5, v1
v8:BasicObject[VMFrozenCore] = Const Value(VALUE(0x1000))
v9:StaticSymbol[VALUE(0x1008)] = Const Value(VALUE(0x1008))
v10:Fixnum[1] = Const Value(1)
v12:BasicObject = SendWithoutBlock v8, :core#hash_merge_ptr, v7, v9, v10
SideExit
"#]]);
}
#[test]
fn test_cant_compile_splat_mut() {
eval("
def test(*) = foo *, 1
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:ArrayExact):
v4:ArrayExact = ToNewArray v1
v5:Fixnum[1] = Const Value(1)
ArrayPush v4, v5
SideExit
"#]]);
}
#[test]
fn test_cant_compile_forwarding() {
eval("
def test(...) = foo(...)
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
SideExit
"#]]);
}
#[test]
fn test_opt_new() {
eval("
class C; end
def test = C.new
");
assert_method_hir_with_opcode("test", YARVINSN_opt_new, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:BasicObject = GetConstantPath 0x1000
v4:NilClassExact = Const Value(nil)
Jump bb1(v0, v4, v3)
bb1(v6:BasicObject, v7:NilClassExact, v8:BasicObject):
v11:BasicObject = SendWithoutBlock v8, :new
Jump bb2(v6, v11, v7)
bb2(v13:BasicObject, v14:BasicObject, v15:NilClassExact):
Return v14
"#]]);
}
#[test]
fn test_opt_newarray_send_max_no_elements() {
eval("
def test = [].max
");
// TODO(max): Rewrite to nil
assert_method_hir_with_opcode("test", YARVINSN_opt_newarray_send, expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_MAX)
v4:BasicObject = ArrayMax
Return v4
"#]]);
}
#[test]
fn test_opt_newarray_send_max() {
eval("
def test(a,b) = [a,b].max
");
assert_method_hir_with_opcode("test", YARVINSN_opt_newarray_send, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_MAX)
v6:BasicObject = ArrayMax v1, v2
Return v6
"#]]);
}
#[test]
fn test_opt_newarray_send_min() {
eval("
def test(a,b)
sum = a+b
result = [a,b].min
puts [1,2,3]
result
end
");
assert_method_hir_with_opcode("test", YARVINSN_opt_newarray_send, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v3:NilClassExact = Const Value(nil)
v4:NilClassExact = Const Value(nil)
v7:BasicObject = SendWithoutBlock v1, :+, v2
SideExit
"#]]);
}
#[test]
fn test_opt_newarray_send_hash() {
eval("
def test(a,b)
sum = a+b
result = [a,b].hash
puts [1,2,3]
result
end
");
assert_method_hir_with_opcode("test", YARVINSN_opt_newarray_send, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v3:NilClassExact = Const Value(nil)
v4:NilClassExact = Const Value(nil)
v7:BasicObject = SendWithoutBlock v1, :+, v2
SideExit
"#]]);
}
#[test]
fn test_opt_newarray_send_pack() {
eval("
def test(a,b)
sum = a+b
result = [a,b].pack 'C'
puts [1,2,3]
result
end
");
assert_method_hir_with_opcode("test", YARVINSN_opt_newarray_send, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v3:NilClassExact = Const Value(nil)
v4:NilClassExact = Const Value(nil)
v7:BasicObject = SendWithoutBlock v1, :+, v2
v8:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v9:StringExact = StringCopy v8
SideExit
"#]]);
}
// TODO(max): Add a test for VM_OPT_NEWARRAY_SEND_PACK_BUFFER
#[test]
fn test_opt_newarray_send_include_p() {
eval("
def test(a,b)
sum = a+b
result = [a,b].include? b
puts [1,2,3]
result
end
");
assert_method_hir_with_opcode("test", YARVINSN_opt_newarray_send, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v3:NilClassExact = Const Value(nil)
v4:NilClassExact = Const Value(nil)
v7:BasicObject = SendWithoutBlock v1, :+, v2
SideExit
"#]]);
}
#[test]
fn test_opt_length() {
eval("
def test(a,b) = [a,b].length
");
assert_method_hir_with_opcode("test", YARVINSN_opt_length, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:ArrayExact = NewArray v1, v2
v7:BasicObject = SendWithoutBlock v5, :length
Return v7
"#]]);
}
#[test]
fn test_opt_size() {
eval("
def test(a,b) = [a,b].size
");
assert_method_hir_with_opcode("test", YARVINSN_opt_size, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:ArrayExact = NewArray v1, v2
v7:BasicObject = SendWithoutBlock v5, :size
Return v7
"#]]);
}
#[test]
fn test_getinstancevariable() {
eval("
def test = @foo
test
");
assert_method_hir_with_opcode("test", YARVINSN_getinstancevariable, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:BasicObject = GetIvar v0, :@foo
Return v3
"#]]);
}
#[test]
fn test_setinstancevariable() {
eval("
def test = @foo = 1
test
");
assert_method_hir_with_opcode("test", YARVINSN_setinstancevariable, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Fixnum[1] = Const Value(1)
SetIvar v0, :@foo, v2
Return v2
"#]]);
}
#[test]
fn test_setglobal() {
eval("
def test = $foo = 1
test
");
assert_method_hir_with_opcode("test", YARVINSN_setglobal, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Fixnum[1] = Const Value(1)
SetGlobal :$foo, v2
Return v2
"#]]);
}
#[test]
fn test_getglobal() {
eval("
def test = $foo
test
");
assert_method_hir_with_opcode("test", YARVINSN_getglobal, expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:BasicObject = GetGlobal :$foo
Return v3
"#]]);
}
#[test]
fn test_splatarray_mut() {
eval("
def test(a) = [*a]
");
assert_method_hir_with_opcode("test", YARVINSN_splatarray, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v4:ArrayExact = ToNewArray v1
Return v4
"#]]);
}
#[test]
fn test_concattoarray() {
eval("
def test(a) = [1, *a]
");
assert_method_hir_with_opcode("test", YARVINSN_concattoarray, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[1] = Const Value(1)
v5:ArrayExact = NewArray v3
v7:ArrayExact = ToArray v1
ArrayExtend v5, v7
Return v5
"#]]);
}
#[test]
fn test_pushtoarray_one_element() {
eval("
def test(a) = [*a, 1]
");
assert_method_hir_with_opcode("test", YARVINSN_pushtoarray, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v4:ArrayExact = ToNewArray v1
v5:Fixnum[1] = Const Value(1)
ArrayPush v4, v5
Return v4
"#]]);
}
#[test]
fn test_pushtoarray_multiple_elements() {
eval("
def test(a) = [*a, 1, 2, 3]
");
assert_method_hir_with_opcode("test", YARVINSN_pushtoarray, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v4:ArrayExact = ToNewArray v1
v5:Fixnum[1] = Const Value(1)
v6:Fixnum[2] = Const Value(2)
v7:Fixnum[3] = Const Value(3)
ArrayPush v4, v5
ArrayPush v4, v6
ArrayPush v4, v7
Return v4
"#]]);
}
#[test]
fn test_aset() {
eval("
def test(a, b) = a[b] = 1
");
assert_method_hir_with_opcode("test", YARVINSN_opt_aset, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v4:NilClassExact = Const Value(nil)
v5:Fixnum[1] = Const Value(1)
v7:BasicObject = SendWithoutBlock v1, :[]=, v2, v5
Return v5
"#]]);
}
#[test]
fn test_aref() {
eval("
def test(a, b) = a[b]
");
assert_method_hir_with_opcode("test", YARVINSN_opt_aref, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :[], v2
Return v5
"#]]);
}
#[test]
fn test_aref_with() {
eval("
def test(a) = a['string lit triggers aref_with']
");
assert_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v5:BasicObject = SendWithoutBlock v1, :[], v3
Return v5
"#]]);
}
#[test]
fn opt_empty_p() {
eval("
def test(x) = x.empty?
");
assert_method_hir_with_opcode("test", YARVINSN_opt_empty_p, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v4:BasicObject = SendWithoutBlock v1, :empty?
Return v4
"#]]);
}
#[test]
fn opt_succ() {
eval("
def test(x) = x.succ
");
assert_method_hir_with_opcode("test", YARVINSN_opt_succ, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v4:BasicObject = SendWithoutBlock v1, :succ
Return v4
"#]]);
}
#[test]
fn opt_and() {
eval("
def test(x, y) = x & y
");
assert_method_hir_with_opcode("test", YARVINSN_opt_and, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :&, v2
Return v5
"#]]);
}
#[test]
fn opt_or() {
eval("
def test(x, y) = x | y
");
assert_method_hir_with_opcode("test", YARVINSN_opt_or, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :|, v2
Return v5
"#]]);
}
#[test]
fn opt_not() {
eval("
def test(x) = !x
");
assert_method_hir_with_opcode("test", YARVINSN_opt_not, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v4:BasicObject = SendWithoutBlock v1, :!
Return v4
"#]]);
}
#[test]
fn opt_regexpmatch2() {
eval("
def test(regexp, matchee) = regexp =~ matchee
");
assert_method_hir_with_opcode("test", YARVINSN_opt_regexpmatch2, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:BasicObject = SendWithoutBlock v1, :=~, v2
Return v5
"#]]);
}
#[test]
// Tests for ConstBase requires either constant or class definition, both
// of which can't be performed inside a method.
fn test_putspecialobject_vm_core_and_cbase() {
eval("
def test
alias aliased __callee__
end
");
assert_method_hir_with_opcode("test", YARVINSN_putspecialobject, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:BasicObject[VMFrozenCore] = Const Value(VALUE(0x1000))
v3:BasicObject = PutSpecialObject CBase
v4:StaticSymbol[VALUE(0x1008)] = Const Value(VALUE(0x1008))
v5:StaticSymbol[VALUE(0x1010)] = Const Value(VALUE(0x1010))
v7:BasicObject = SendWithoutBlock v2, :core#set_method_alias, v3, v4, v5
Return v7
"#]]);
}
#[test]
fn opt_reverse() {
eval("
def reverse_odd
a, b, c = @a, @b, @c
[a, b, c]
end
def reverse_even
a, b, c, d = @a, @b, @c, @d
[a, b, c, d]
end
");
assert_method_hir_with_opcode("reverse_odd", YARVINSN_opt_reverse, expect![[r#"
fn reverse_odd:
bb0(v0:BasicObject):
v1:NilClassExact = Const Value(nil)
v2:NilClassExact = Const Value(nil)
v3:NilClassExact = Const Value(nil)
v6:BasicObject = GetIvar v0, :@a
v8:BasicObject = GetIvar v0, :@b
v10:BasicObject = GetIvar v0, :@c
v12:ArrayExact = NewArray v6, v8, v10
Return v12
"#]]);
assert_method_hir_with_opcode("reverse_even", YARVINSN_opt_reverse, expect![[r#"
fn reverse_even:
bb0(v0:BasicObject):
v1:NilClassExact = Const Value(nil)
v2:NilClassExact = Const Value(nil)
v3:NilClassExact = Const Value(nil)
v4:NilClassExact = Const Value(nil)
v7:BasicObject = GetIvar v0, :@a
v9:BasicObject = GetIvar v0, :@b
v11:BasicObject = GetIvar v0, :@c
v13:BasicObject = GetIvar v0, :@d
v15:ArrayExact = NewArray v7, v9, v11, v13
Return v15
"#]]);
}
#[test]
fn test_branchnil() {
eval("
def test(x) = x&.itself
");
assert_method_hir_with_opcode("test", YARVINSN_branchnil, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:CBool = IsNil v1
IfTrue v3, bb1(v0, v1, v1)
v6:BasicObject = SendWithoutBlock v1, :itself
Jump bb1(v0, v1, v6)
bb1(v8:BasicObject, v9:BasicObject, v10:BasicObject):
Return v10
"#]]);
}
#[test]
fn test_invokebuiltin_delegate_with_args() {
assert_method_hir_with_opcode("Float", YARVINSN_opt_invokebuiltin_delegate_leave, expect![[r#"
fn Float:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject):
v6:BasicObject = InvokeBuiltin rb_f_float, v0, v1, v2
Jump bb1(v0, v1, v2, v3, v6)
bb1(v8:BasicObject, v9:BasicObject, v10:BasicObject, v11:BasicObject, v12:BasicObject):
Return v12
"#]]);
}
#[test]
fn test_invokebuiltin_delegate_without_args() {
assert_method_hir_with_opcode("class", YARVINSN_opt_invokebuiltin_delegate_leave, expect![[r#"
fn class:
bb0(v0:BasicObject):
v3:BasicObject = InvokeBuiltin _bi20, v0
Jump bb1(v0, v3)
bb1(v5:BasicObject, v6:BasicObject):
Return v6
"#]]);
}
#[test]
fn test_invokebuiltin_with_args() {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("GC", "start"));
assert!(iseq_contains_opcode(iseq, YARVINSN_invokebuiltin), "iseq GC.start does not contain invokebuiltin");
let function = iseq_to_hir(iseq).unwrap();
assert_function_hir(function, expect![[r#"
fn start:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject, v3:BasicObject, v4:BasicObject):
v6:FalseClassExact = Const Value(false)
v8:BasicObject = InvokeBuiltin gc_start_internal, v0, v1, v2, v3, v6
Return v8
"#]]);
}
#[test]
fn dupn() {
eval("
def test(x) = (x[0, 1] ||= 2)
");
assert_method_hir_with_opcode("test", YARVINSN_dupn, expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:NilClassExact = Const Value(nil)
v4:Fixnum[0] = Const Value(0)
v5:Fixnum[1] = Const Value(1)
v7:BasicObject = SendWithoutBlock v1, :[], v4, v5
v8:CBool = Test v7
IfTrue v8, bb1(v0, v1, v3, v1, v4, v5, v7)
v10:Fixnum[2] = Const Value(2)
v12:BasicObject = SendWithoutBlock v1, :[]=, v4, v5, v10
Return v10
bb1(v14:BasicObject, v15:BasicObject, v16:NilClassExact, v17:BasicObject, v18:Fixnum[0], v19:Fixnum[1], v20:BasicObject):
Return v20
"#]]);
}
#[test]
fn test_objtostring_anytostring() {
eval("
def test = \"#{1}\"
");
assert_method_hir_with_opcode("test", YARVINSN_objtostring, expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v3:Fixnum[1] = Const Value(1)
v5:BasicObject = ObjToString v3
v7:String = AnyToString v3, str: v5
SideExit
"#]]);
}
}
#[cfg(test)]
mod opt_tests {
use super::*;
use super::tests::assert_function_hir;
use expect_test::{expect, Expect};
#[track_caller]
fn assert_optimized_method_hir(method: &str, hir: Expect) {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
let mut function = iseq_to_hir(iseq).unwrap();
function.optimize();
assert_function_hir(function, hir);
}
#[test]
fn test_fold_iftrue_away() {
eval("
def test
cond = true
if cond
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v6:Fixnum[3] = Const Value(3)
Return v6
"#]]);
}
#[test]
fn test_fold_iftrue_into_jump() {
eval("
def test
cond = false
if cond
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v11:Fixnum[4] = Const Value(4)
Return v11
"#]]);
}
#[test]
fn test_fold_fixnum_add() {
eval("
def test
1 + 2 + 3
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_PLUS)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_PLUS)
v15:Fixnum[6] = Const Value(6)
Return v15
"#]]);
}
#[test]
fn test_fold_fixnum_sub() {
eval("
def test
5 - 3 - 1
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_MINUS)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_MINUS)
v15:Fixnum[1] = Const Value(1)
Return v15
"#]]);
}
#[test]
fn test_fold_fixnum_mult() {
eval("
def test
6 * 7
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_MULT)
v9:Fixnum[42] = Const Value(42)
Return v9
"#]]);
}
#[test]
fn test_fold_fixnum_mult_zero() {
eval("
def test(n)
0 * n + n * 0
end
test 1; test 2
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[0] = Const Value(0)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_MULT)
v13:Fixnum = GuardType v1, Fixnum
v20:Fixnum[0] = Const Value(0)
v6:Fixnum[0] = Const Value(0)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_MULT)
v16:Fixnum = GuardType v1, Fixnum
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_PLUS)
v22:Fixnum[0] = Const Value(0)
Return v22
"#]]);
}
#[test]
fn test_fold_fixnum_less() {
eval("
def test
if 1 < 2
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_LT)
v8:Fixnum[3] = Const Value(3)
Return v8
"#]]);
}
#[test]
fn test_fold_fixnum_less_equal() {
eval("
def test
if 1 <= 2 && 2 <= 2
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_LE)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_LE)
v14:Fixnum[3] = Const Value(3)
Return v14
"#]]);
}
#[test]
fn test_fold_fixnum_greater() {
eval("
def test
if 2 > 1
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_GT)
v8:Fixnum[3] = Const Value(3)
Return v8
"#]]);
}
#[test]
fn test_fold_fixnum_greater_equal() {
eval("
def test
if 2 >= 1 && 2 >= 2
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_GE)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_GE)
v14:Fixnum[3] = Const Value(3)
Return v14
"#]]);
}
#[test]
fn test_fold_fixnum_eq_false() {
eval("
def test
if 1 == 2
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ)
v12:Fixnum[4] = Const Value(4)
Return v12
"#]]);
}
#[test]
fn test_fold_fixnum_eq_true() {
eval("
def test
if 2 == 2
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ)
v8:Fixnum[3] = Const Value(3)
Return v8
"#]]);
}
#[test]
fn test_fold_fixnum_neq_true() {
eval("
def test
if 1 != 2
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_NEQ)
v8:Fixnum[3] = Const Value(3)
Return v8
"#]]);
}
#[test]
fn test_fold_fixnum_neq_false() {
eval("
def test
if 2 != 2
3
else
4
end
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_NEQ)
v12:Fixnum[4] = Const Value(4)
Return v12
"#]]);
}
#[test]
fn test_replace_guard_if_known_fixnum() {
eval("
def test(a)
a + 1
end
test(2); test(3)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[1] = Const Value(1)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_PLUS)
v8:Fixnum = GuardType v1, Fixnum
v9:Fixnum = FixnumAdd v8, v3
Return v9
"#]]);
}
#[test]
fn test_param_forms_get_bb_param() {
eval("
def rest(*array) = array
def kw(k:) = k
def kw_rest(**k) = k
def post(*rest, post) = post
def block(&b) = nil
def forwardable(...) = nil
");
assert_optimized_method_hir("rest", expect![[r#"
fn rest:
bb0(v0:BasicObject, v1:ArrayExact):
Return v1
"#]]);
// extra hidden param for the set of specified keywords
assert_optimized_method_hir("kw", expect![[r#"
fn kw:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
Return v1
"#]]);
assert_optimized_method_hir("kw_rest", expect![[r#"
fn kw_rest:
bb0(v0:BasicObject, v1:BasicObject):
Return v1
"#]]);
assert_optimized_method_hir("block", expect![[r#"
fn block:
bb0(v0:BasicObject, v1:BasicObject):
v3:NilClassExact = Const Value(nil)
Return v3
"#]]);
assert_optimized_method_hir("post", expect![[r#"
fn post:
bb0(v0:BasicObject, v1:ArrayExact, v2:BasicObject):
Return v2
"#]]);
assert_optimized_method_hir("forwardable", expect![[r#"
fn forwardable:
bb0(v0:BasicObject, v1:BasicObject):
v3:NilClassExact = Const Value(nil)
Return v3
"#]]);
}
#[test]
fn test_optimize_top_level_call_into_send_direct() {
eval("
def foo
end
def test
foo
end
test; test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008)
v6:BasicObject[VALUE(0x1010)] = GuardBitEquals v0, VALUE(0x1010)
v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1018)
Return v7
"#]]);
}
#[test]
fn test_optimize_nonexistent_top_level_call() {
eval("
def foo
end
def test
foo
end
test; test
undef :foo
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:BasicObject = SendWithoutBlock v0, :foo
Return v3
"#]]);
}
#[test]
fn test_optimize_private_top_level_call() {
eval("
def foo
end
private :foo
def test
foo
end
test; test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008)
v6:BasicObject[VALUE(0x1010)] = GuardBitEquals v0, VALUE(0x1010)
v7:BasicObject = SendWithoutBlockDirect v6, :foo (0x1018)
Return v7
"#]]);
}
#[test]
fn test_optimize_top_level_call_with_overloaded_cme() {
eval("
def test
Integer(3)
end
test; test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Fixnum[3] = Const Value(3)
PatchPoint MethodRedefined(Object@0x1000, Integer@0x1008)
v7:BasicObject[VALUE(0x1010)] = GuardBitEquals v0, VALUE(0x1010)
v8:BasicObject = SendWithoutBlockDirect v7, :Integer (0x1018), v2
Return v8
"#]]);
}
#[test]
fn test_optimize_top_level_call_with_args_into_send_direct() {
eval("
def foo a, b
end
def test
foo 1, 2
end
test; test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Fixnum[1] = Const Value(1)
v3:Fixnum[2] = Const Value(2)
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008)
v8:BasicObject[VALUE(0x1010)] = GuardBitEquals v0, VALUE(0x1010)
v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1018), v2, v3
Return v9
"#]]);
}
#[test]
fn test_optimize_top_level_sends_into_send_direct() {
eval("
def foo
end
def bar
end
def test
foo
bar
end
test; test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008)
v8:BasicObject[VALUE(0x1010)] = GuardBitEquals v0, VALUE(0x1010)
v9:BasicObject = SendWithoutBlockDirect v8, :foo (0x1018)
PatchPoint MethodRedefined(Object@0x1000, bar@0x1020)
v11:BasicObject[VALUE(0x1010)] = GuardBitEquals v0, VALUE(0x1010)
v12:BasicObject = SendWithoutBlockDirect v11, :bar (0x1018)
Return v12
"#]]);
}
#[test]
fn test_optimize_send_into_fixnum_add_both_profiled() {
eval("
def test(a, b) = a + b
test(1,2); test(3,4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_PLUS)
v8:Fixnum = GuardType v1, Fixnum
v9:Fixnum = GuardType v2, Fixnum
v10:Fixnum = FixnumAdd v8, v9
Return v10
"#]]);
}
#[test]
fn test_optimize_send_into_fixnum_add_left_profiled() {
eval("
def test(a) = a + 1
test(1); test(3)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[1] = Const Value(1)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_PLUS)
v8:Fixnum = GuardType v1, Fixnum
v9:Fixnum = FixnumAdd v8, v3
Return v9
"#]]);
}
#[test]
fn test_optimize_send_into_fixnum_add_right_profiled() {
eval("
def test(a) = 1 + a
test(1); test(3)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[1] = Const Value(1)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_PLUS)
v8:Fixnum = GuardType v1, Fixnum
v9:Fixnum = FixnumAdd v3, v8
Return v9
"#]]);
}
#[test]
fn test_optimize_send_into_fixnum_lt_both_profiled() {
eval("
def test(a, b) = a < b
test(1,2); test(3,4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_LT)
v8:Fixnum = GuardType v1, Fixnum
v9:Fixnum = GuardType v2, Fixnum
v10:BoolExact = FixnumLt v8, v9
Return v10
"#]]);
}
#[test]
fn test_optimize_send_into_fixnum_lt_left_profiled() {
eval("
def test(a) = a < 1
test(1); test(3)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[1] = Const Value(1)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_LT)
v8:Fixnum = GuardType v1, Fixnum
v9:BoolExact = FixnumLt v8, v3
Return v9
"#]]);
}
#[test]
fn test_optimize_send_into_fixnum_lt_right_profiled() {
eval("
def test(a) = 1 < a
test(1); test(3)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[1] = Const Value(1)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_LT)
v8:Fixnum = GuardType v1, Fixnum
v9:BoolExact = FixnumLt v3, v8
Return v9
"#]]);
}
#[test]
fn test_eliminate_new_array() {
eval("
def test()
c = []
5
end
test; test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v5:Fixnum[5] = Const Value(5)
Return v5
"#]]);
}
#[test]
fn test_eliminate_new_range() {
eval("
def test()
c = (1..2)
5
end
test; test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v4:Fixnum[5] = Const Value(5)
Return v4
"#]]);
}
#[test]
fn test_eliminate_new_array_with_elements() {
eval("
def test(a)
c = [a]
5
end
test(1); test(2)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_new_hash() {
eval("
def test()
c = {}
5
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v5:Fixnum[5] = Const Value(5)
Return v5
"#]]);
}
#[test]
fn test_eliminate_new_hash_with_elements() {
eval("
def test(aval, bval)
c = {a: aval, b: bval}
5
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v9:Fixnum[5] = Const Value(5)
Return v9
"#]]);
}
#[test]
fn test_eliminate_array_dup() {
eval("
def test
c = [1, 2]
5
end
test; test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_hash_dup() {
eval("
def test
c = {a: 1, b: 2}
5
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_putself() {
eval("
def test()
c = self
5
end
test; test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:Fixnum[5] = Const Value(5)
Return v3
"#]]);
}
#[test]
fn test_eliminate_string_copy() {
eval(r#"
def test()
c = "abc"
5
end
test; test
"#);
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v5:Fixnum[5] = Const Value(5)
Return v5
"#]]);
}
#[test]
fn test_eliminate_fixnum_add() {
eval("
def test(a, b)
a + b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_PLUS)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_fixnum_sub() {
eval("
def test(a, b)
a - b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_MINUS)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_fixnum_mul() {
eval("
def test(a, b)
a * b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_MULT)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_do_not_eliminate_fixnum_div() {
eval("
def test(a, b)
a / b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_DIV)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v11:Fixnum = FixnumDiv v9, v10
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_do_not_eliminate_fixnum_mod() {
eval("
def test(a, b)
a % b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_MOD)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v11:Fixnum = FixnumMod v9, v10
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_fixnum_lt() {
eval("
def test(a, b)
a < b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_LT)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_fixnum_le() {
eval("
def test(a, b)
a <= b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_LE)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_fixnum_gt() {
eval("
def test(a, b)
a > b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_GT)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_fixnum_ge() {
eval("
def test(a, b)
a >= b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_GE)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_fixnum_eq() {
eval("
def test(a, b)
a == b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ)
v9:Fixnum = GuardType v1, Fixnum
v10:Fixnum = GuardType v2, Fixnum
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_eliminate_fixnum_neq() {
eval("
def test(a, b)
a != b
5
end
test(1, 2); test(3, 4)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ)
PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_NEQ)
v10:Fixnum = GuardType v1, Fixnum
v11:Fixnum = GuardType v2, Fixnum
v6:Fixnum[5] = Const Value(5)
Return v6
"#]]);
}
#[test]
fn test_do_not_eliminate_get_constant_path() {
eval("
def test()
C
5
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:BasicObject = GetConstantPath 0x1000
v4:Fixnum[5] = Const Value(5)
Return v4
"#]]);
}
#[test]
fn kernel_itself_const() {
eval("
def test(x) = x.itself
test(0) # profile
test(1)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
PatchPoint MethodRedefined(Integer@0x1000, itself@0x1008)
v7:Fixnum = GuardType v1, Fixnum
v8:BasicObject = CCall itself@0x1010, v7
Return v8
"#]]);
}
#[test]
fn kernel_itself_known_type() {
eval("
def test = [].itself
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:ArrayExact = NewArray
PatchPoint MethodRedefined(Array@0x1000, itself@0x1008)
v8:BasicObject = CCall itself@0x1010, v3
Return v8
"#]]);
}
#[test]
fn eliminate_kernel_itself() {
eval("
def test
x = [].itself
1
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint MethodRedefined(Array@0x1000, itself@0x1008)
v7:Fixnum[1] = Const Value(1)
Return v7
"#]]);
}
#[test]
fn eliminate_module_name() {
eval("
module M; end
def test
x = M.name
1
end
test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint SingleRactorMode
PatchPoint StableConstantNames(0x1000, M)
PatchPoint MethodRedefined(Module@0x1008, name@0x1010)
v7:Fixnum[1] = Const Value(1)
Return v7
"#]]);
}
#[test]
fn eliminate_array_length() {
eval("
def test
x = [].length
5
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint MethodRedefined(Array@0x1000, length@0x1008)
v7:Fixnum[5] = Const Value(5)
Return v7
"#]]);
}
#[test]
fn eliminate_array_size() {
eval("
def test
x = [].size
5
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint MethodRedefined(Array@0x1000, size@0x1008)
v7:Fixnum[5] = Const Value(5)
Return v7
"#]]);
}
#[test]
fn kernel_itself_argc_mismatch() {
eval("
def test = 1.itself(0)
test rescue 0
test rescue 0
");
// Not specialized
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Fixnum[1] = Const Value(1)
v3:Fixnum[0] = Const Value(0)
v5:BasicObject = SendWithoutBlock v2, :itself, v3
Return v5
"#]]);
}
#[test]
fn const_send_direct_integer() {
eval("
def test(x) = 1.zero?
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v3:Fixnum[1] = Const Value(1)
PatchPoint MethodRedefined(Integer@0x1000, zero?@0x1008)
v8:BasicObject = SendWithoutBlockDirect v3, :zero? (0x1010)
Return v8
"#]]);
}
#[test]
fn class_known_send_direct_array() {
eval("
def test(x)
a = [1,2,3]
a.first
end
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject):
v2:NilClassExact = Const Value(nil)
v4:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v6:ArrayExact = ArrayDup v4
PatchPoint MethodRedefined(Array@0x1008, first@0x1010)
v11:BasicObject = SendWithoutBlockDirect v6, :first (0x1018)
Return v11
"#]]);
}
#[test]
fn string_bytesize_simple() {
eval("
def test = 'abc'.bytesize
test
test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v3:StringExact = StringCopy v2
PatchPoint MethodRedefined(String@0x1008, bytesize@0x1010)
v8:Fixnum = CCall bytesize@0x1018, v3
Return v8
"#]]);
}
#[test]
fn dont_replace_get_constant_path_with_empty_ic() {
eval("
def test = Kernel
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:BasicObject = GetConstantPath 0x1000
Return v3
"#]]);
}
#[test]
fn dont_replace_get_constant_path_with_invalidated_ic() {
eval("
def test = Kernel
test
Kernel = 5
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:BasicObject = GetConstantPath 0x1000
Return v3
"#]]);
}
#[test]
fn replace_get_constant_path_with_const() {
eval("
def test = Kernel
test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint SingleRactorMode
PatchPoint StableConstantNames(0x1000, Kernel)
v7:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008))
Return v7
"#]]);
}
#[test]
fn replace_nested_get_constant_path_with_const() {
eval("
module Foo
module Bar
class C
end
end
end
def test = Foo::Bar::C
test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint SingleRactorMode
PatchPoint StableConstantNames(0x1000, Foo::Bar::C)
v7:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008))
Return v7
"#]]);
}
#[test]
fn test_opt_new_no_initialize() {
eval("
class C; end
def test = C.new
test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint SingleRactorMode
PatchPoint StableConstantNames(0x1000, C)
v20:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008))
v4:NilClassExact = Const Value(nil)
v11:BasicObject = SendWithoutBlock v20, :new
Return v11
"#]]);
}
#[test]
fn test_opt_new_initialize() {
eval("
class C
def initialize x
@x = x
end
end
def test = C.new 1
test
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint SingleRactorMode
PatchPoint StableConstantNames(0x1000, C)
v22:BasicObject[VALUE(0x1008)] = Const Value(VALUE(0x1008))
v4:NilClassExact = Const Value(nil)
v5:Fixnum[1] = Const Value(1)
v13:BasicObject = SendWithoutBlock v22, :new, v5
Return v13
"#]]);
}
#[test]
fn test_opt_length() {
eval("
def test(a,b) = [a,b].length
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:ArrayExact = NewArray v1, v2
PatchPoint MethodRedefined(Array@0x1000, length@0x1008)
v10:Fixnum = CCall length@0x1010, v5
Return v10
"#]]);
}
#[test]
fn test_opt_size() {
eval("
def test(a,b) = [a,b].size
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject):
v5:ArrayExact = NewArray v1, v2
PatchPoint MethodRedefined(Array@0x1000, size@0x1008)
v10:Fixnum = CCall size@0x1010, v5
Return v10
"#]]);
}
#[test]
fn test_getinstancevariable() {
eval("
def test = @foo
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:BasicObject = GetIvar v0, :@foo
Return v3
"#]]);
}
#[test]
fn test_setinstancevariable() {
eval("
def test = @foo = 1
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:Fixnum[1] = Const Value(1)
SetIvar v0, :@foo, v2
Return v2
"#]]);
}
#[test]
fn test_elide_freeze_with_frozen_hash() {
eval("
def test = {}.freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
PatchPoint BOPRedefined(HASH_REDEFINED_OP_FLAG, BOP_FREEZE)
Return v3
"#]]);
}
#[test]
fn test_elide_freeze_with_refrozen_hash() {
eval("
def test = {}.freeze.freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
PatchPoint BOPRedefined(HASH_REDEFINED_OP_FLAG, BOP_FREEZE)
PatchPoint BOPRedefined(HASH_REDEFINED_OP_FLAG, BOP_FREEZE)
Return v3
"#]]);
}
#[test]
fn test_no_elide_freeze_with_unfrozen_hash() {
eval("
def test = {}.dup.freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:HashExact = NewHash
v5:BasicObject = SendWithoutBlock v3, :dup
v7:BasicObject = SendWithoutBlock v5, :freeze
Return v7
"#]]);
}
#[test]
fn test_no_elide_freeze_hash_with_args() {
eval("
def test = {}.freeze(nil)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:HashExact = NewHash
v4:NilClassExact = Const Value(nil)
v6:BasicObject = SendWithoutBlock v3, :freeze, v4
Return v6
"#]]);
}
#[test]
fn test_elide_freeze_with_frozen_ary() {
eval("
def test = [].freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE)
Return v3
"#]]);
}
#[test]
fn test_elide_freeze_with_refrozen_ary() {
eval("
def test = [].freeze.freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE)
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE)
Return v3
"#]]);
}
#[test]
fn test_no_elide_freeze_with_unfrozen_ary() {
eval("
def test = [].dup.freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:ArrayExact = NewArray
v5:BasicObject = SendWithoutBlock v3, :dup
v7:BasicObject = SendWithoutBlock v5, :freeze
Return v7
"#]]);
}
#[test]
fn test_no_elide_freeze_ary_with_args() {
eval("
def test = [].freeze(nil)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:ArrayExact = NewArray
v4:NilClassExact = Const Value(nil)
v6:BasicObject = SendWithoutBlock v3, :freeze, v4
Return v6
"#]]);
}
#[test]
fn test_elide_freeze_with_frozen_str() {
eval("
def test = ''.freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_FREEZE)
Return v3
"#]]);
}
#[test]
fn test_elide_freeze_with_refrozen_str() {
eval("
def test = ''.freeze.freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_FREEZE)
PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_FREEZE)
Return v3
"#]]);
}
#[test]
fn test_no_elide_freeze_with_unfrozen_str() {
eval("
def test = ''.dup.freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v3:StringExact = StringCopy v2
v5:BasicObject = SendWithoutBlock v3, :dup
v7:BasicObject = SendWithoutBlock v5, :freeze
Return v7
"#]]);
}
#[test]
fn test_no_elide_freeze_str_with_args() {
eval("
def test = ''.freeze(nil)
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v3:StringExact = StringCopy v2
v4:NilClassExact = Const Value(nil)
v6:BasicObject = SendWithoutBlock v3, :freeze, v4
Return v6
"#]]);
}
#[test]
fn test_elide_uminus_with_frozen_str() {
eval("
def test = -''
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_UMINUS)
Return v3
"#]]);
}
#[test]
fn test_elide_uminus_with_refrozen_str() {
eval("
def test = -''.freeze
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_FREEZE)
PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_UMINUS)
Return v3
"#]]);
}
#[test]
fn test_no_elide_uminus_with_unfrozen_str() {
eval("
def test = -''.dup
");
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v3:StringExact = StringCopy v2
v5:BasicObject = SendWithoutBlock v3, :dup
v7:BasicObject = SendWithoutBlock v5, :-@
Return v7
"#]]);
}
#[test]
fn test_objtostring_anytostring_string() {
eval(r##"
def test = "#{('foo')}"
"##);
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v3:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008))
v4:StringExact = StringCopy v3
SideExit
"#]]);
}
#[test]
fn test_objtostring_anytostring_with_non_string() {
eval(r##"
def test = "#{1}"
"##);
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
v2:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
v3:Fixnum[1] = Const Value(1)
v10:BasicObject = SendWithoutBlock v3, :to_s
v7:String = AnyToString v3, str: v10
SideExit
"#]]);
}
#[test]
fn test_eliminate_load_from_frozen_array_in_bounds() {
eval(r##"
def test = [4,5,6].freeze[1]
"##);
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE)
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_AREF)
v11:Fixnum[5] = Const Value(5)
Return v11
"#]]);
}
#[test]
fn test_eliminate_load_from_frozen_array_negative() {
eval(r##"
def test = [4,5,6].freeze[-3]
"##);
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE)
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_AREF)
v11:Fixnum[4] = Const Value(4)
Return v11
"#]]);
}
#[test]
fn test_eliminate_load_from_frozen_array_negative_out_of_bounds() {
eval(r##"
def test = [4,5,6].freeze[-10]
"##);
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE)
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_AREF)
v11:NilClassExact = Const Value(nil)
Return v11
"#]]);
}
#[test]
fn test_eliminate_load_from_frozen_array_out_of_bounds() {
eval(r##"
def test = [4,5,6].freeze[10]
"##);
assert_optimized_method_hir("test", expect![[r#"
fn test:
bb0(v0:BasicObject):
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_FREEZE)
PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_AREF)
v11:NilClassExact = Const Value(nil)
Return v11
"#]]);
}
}