use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU32, Ordering};
use crate::compiler::maglev::ir::{BasicBlock, ControlNode, MaglevGraph, NodeId, ValueNode};
pub static LICM_LOOPS_DETECTED: AtomicU32 = AtomicU32::new(0);
pub static LICM_NODES_HOISTED: AtomicU32 = AtomicU32::new(0);
pub static LICM_NAMED_GENERIC_HOISTED: AtomicU32 = AtomicU32::new(0);
pub static LICM_BLOCKED_BY_SIDE_EFFECTS: AtomicU32 = AtomicU32::new(0);
pub fn hoist_loop_invariants(graph: &mut MaglevGraph) -> usize {
let num_blocks = graph.blocks().len();
let loops = detect_loops(graph);
let _ = num_blocks;
LICM_LOOPS_DETECTED.fetch_add(loops.len() as u32, Ordering::Relaxed);
let mut total = 0;
for lp in &loops {
if let Some(header_block) = graph.block_mut(lp.header) {
header_block.is_loop_header = true;
}
total += hoist_one_loop(graph, lp);
}
total
}
pub fn fold_invariant_addition_chains(graph: &mut MaglevGraph) -> usize {
let loops = detect_loops(graph);
let mut total = 0;
for lp in &loops {
total += fold_chains_in_loop(graph, lp);
}
total
}
#[derive(Clone, Copy, PartialEq)]
enum AddKind {
Int32,
CheckedSmi,
Generic,
}
fn make_add(kind: AddKind, left: NodeId, right: NodeId) -> ValueNode {
match kind {
AddKind::Int32 => ValueNode::Int32Add { left, right },
AddKind::CheckedSmi => ValueNode::CheckedSmiAdd { left, right },
AddKind::Generic => ValueNode::GenericAdd {
left,
right,
feedback_slot: 0,
},
}
}
fn fold_chains_in_loop(graph: &mut MaglevGraph, lp: &NaturalLoop) -> usize {
let mut outside_defs: HashSet<NodeId> = HashSet::new();
for block in graph.blocks() {
if !lp.body.contains(&block.id) {
for (id, _) in &block.nodes {
outside_defs.insert(*id);
}
}
}
let mut use_counts: HashMap<NodeId, usize> = HashMap::new();
for block in graph.blocks() {
for (_, node) in &block.nodes {
visit_inputs(node, &mut |id| {
*use_counts.entry(id).or_insert(0) += 1;
});
}
}
let mut loop_adds: HashMap<NodeId, (NodeId, NodeId, AddKind)> = HashMap::new();
for block in graph.blocks() {
if !lp.body.contains(&block.id) {
continue;
}
for (id, node) in &block.nodes {
match node {
ValueNode::Int32Add { left, right } => {
loop_adds.insert(*id, (*left, *right, AddKind::Int32));
}
ValueNode::CheckedSmiAdd { left, right } => {
loop_adds.insert(*id, (*left, *right, AddKind::CheckedSmi));
}
ValueNode::GenericAdd { left, right, .. } => {
loop_adds.insert(*id, (*left, *right, AddKind::Generic));
}
_ => {}
}
}
}
if loop_adds.is_empty() {
return 0;
}
let mut best_endpoint: Option<NodeId> = None;
let mut best_inv_ops: Vec<NodeId> = Vec::new();
let mut best_root: Option<NodeId> = None;
let mut best_kind = AddKind::Int32;
for &start_id in loop_adds.keys() {
let mut inv_ops: Vec<NodeId> = Vec::new();
let mut current = start_id;
let mut root_variant = None;
let mut chain_kind: Option<AddKind> = None;
while let Some(&(left, right, kind)) = loop_adds.get(¤t) {
if let Some(prev_kind) = chain_kind
&& prev_kind != kind
{
break;
}
chain_kind = Some(kind);
let l_out = outside_defs.contains(&left);
let r_out = outside_defs.contains(&right);
if l_out == r_out {
break;
}
let (invariant, variant) = if r_out { (right, left) } else { (left, right) };
inv_ops.push(invariant);
if loop_adds.contains_key(&variant) {
let uses = use_counts.get(&variant).copied().unwrap_or(0);
if uses == 1 {
current = variant;
continue;
}
}
root_variant = Some(variant);
break;
}
if inv_ops.len() >= 2 && root_variant.is_some() && inv_ops.len() > best_inv_ops.len() {
best_endpoint = Some(start_id);
best_inv_ops = inv_ops;
best_root = root_variant;
best_kind = chain_kind.unwrap_or(AddKind::Int32);
}
}
let endpoint_id = match best_endpoint {
Some(id) => id,
None => return 0,
};
let root_variant = best_root.unwrap();
best_inv_ops.reverse();
let mut prev = best_inv_ops[0];
for &inv in &best_inv_ops[1..] {
let nid = graph.alloc_node_id();
let node = make_add(best_kind, prev, inv);
if let Some(pre) = graph.block_mut(lp.preheader) {
pre.push_with_id(nid, node);
}
prev = nid;
}
let inv_sum = prev;
let replacement = make_add(best_kind, root_variant, inv_sum);
for block in graph.blocks_mut() {
if !lp.body.contains(&block.id) {
continue;
}
for (id, node) in &mut block.nodes {
if *id == endpoint_id {
*node = replacement;
return best_inv_ops.len();
}
}
}
0
}
pub struct NaturalLoop {
pub header: u32,
pub preheader: u32,
pub body: HashSet<u32>,
}
pub fn detect_loops(graph: &MaglevGraph) -> Vec<NaturalLoop> {
let mut loops = Vec::new();
let mut seen_headers: HashSet<u32> = HashSet::new();
for block in graph.blocks() {
let targets = control_targets(block);
for &target in &targets {
if seen_headers.contains(&target) {
continue; }
if let Some(lp) = build_loop(graph, target, block.id) {
if lp.body.contains(&block.id) && lp.body.contains(&target) {
seen_headers.insert(target);
loops.push(lp);
}
}
}
}
loops
}
pub fn control_targets(block: &BasicBlock) -> Vec<u32> {
match &block.control {
Some(ControlNode::Jump { target }) => vec![*target],
Some(ControlNode::Branch {
if_true, if_false, ..
}) => vec![*if_true, *if_false],
_ => Vec::new(),
}
}
fn build_loop(graph: &MaglevGraph, header: u32, back_src: u32) -> Option<NaturalLoop> {
if header == back_src {
let block = graph.block(header)?;
let preheaders: Vec<u32> = block
.predecessors
.iter()
.copied()
.filter(|&p| p != header)
.collect();
if preheaders.len() != 1 {
return None;
}
let mut body = HashSet::new();
body.insert(header);
return Some(NaturalLoop {
header,
preheader: preheaders[0],
body,
});
}
let mut reachable = HashSet::new();
let mut work = vec![header];
while let Some(b) = work.pop() {
if !reachable.insert(b) {
continue;
}
if let Some(block) = graph.block(b) {
for &t in &control_targets(block) {
if t != header {
work.push(t);
}
}
}
}
if !reachable.contains(&back_src) {
return None; }
let mut body = HashSet::new();
body.insert(header);
body.insert(back_src);
let mut worklist = vec![back_src];
while let Some(b) = worklist.pop() {
if b == header {
continue;
}
if let Some(block) = graph.block(b) {
for &pred in &block.predecessors {
if body.insert(pred) {
worklist.push(pred);
}
}
}
}
let header_block = graph.block(header)?;
let preheaders: Vec<u32> = header_block
.predecessors
.iter()
.copied()
.filter(|p| !body.contains(p))
.collect();
if preheaders.len() != 1 {
return None;
}
Some(NaturalLoop {
header,
preheader: preheaders[0],
body,
})
}
fn hoist_one_loop(graph: &mut MaglevGraph, lp: &NaturalLoop) -> usize {
let mutated_objects = collect_mutated_objects(graph, &lp.body);
let mutated_globals = collect_mutated_globals(graph, &lp.body);
let has_property_side_effects = loop_has_property_side_effects(graph, &lp.body);
let mut load_named_in_loop = 0u32;
for block in graph.blocks() {
if !lp.body.contains(&block.id) {
continue;
}
for (_, node) in &block.nodes {
if matches!(node, ValueNode::LoadNamedGeneric { .. }) {
load_named_in_loop += 1;
}
}
}
if has_property_side_effects && load_named_in_loop > 0 {
LICM_BLOCKED_BY_SIDE_EFFECTS.fetch_add(1, Ordering::Relaxed);
}
let mut total_hoisted = 0;
let mut named_generic_hoisted = 0u32;
loop {
let mut outside_defs: HashSet<NodeId> = HashSet::new();
for block in graph.blocks() {
if !lp.body.contains(&block.id) {
for (id, _) in &block.nodes {
outside_defs.insert(*id);
}
}
}
let mut to_hoist: Vec<(u32, usize, NodeId, ValueNode)> = Vec::new();
const MAX_GENERIC_LOADS_HOISTED: usize = 5;
let mut generic_loads_in_hoist = 0usize;
for block in graph.blocks() {
if !lp.body.contains(&block.id) {
continue;
}
for (pos, (id, node)) in block.nodes.iter().enumerate() {
if matches!(node, ValueNode::Phi { .. }) {
continue;
}
let pure = is_pure(node);
let inputs_out = all_inputs_outside(node, &outside_defs);
let alias_store = load_aliases_store(node, &mutated_objects);
let alias_glob = global_load_aliases_store(node, &mutated_globals);
let blocked_by_side_effects_flag =
is_generic_property_load(node) && has_property_side_effects;
let over_generic_load_cap = is_generic_property_load(node)
&& generic_loads_in_hoist >= MAX_GENERIC_LOADS_HOISTED;
if pure
&& inputs_out
&& !alias_store
&& !alias_glob
&& !blocked_by_side_effects_flag
&& !over_generic_load_cap
{
if is_generic_property_load(node) {
generic_loads_in_hoist += 1;
}
to_hoist.push((block.id, pos, *id, node.clone()));
outside_defs.insert(*id);
} else if matches!(node, ValueNode::LoadNamedGeneric { .. }) {
}
}
}
if to_hoist.is_empty() {
break;
}
for (_, _, _, node) in &to_hoist {
if matches!(node, ValueNode::LoadNamedGeneric { .. }) {
named_generic_hoisted += 1;
}
}
let mut removals_by_block: HashMap<u32, Vec<usize>> = HashMap::new();
for &(blk, pos, _, _) in &to_hoist {
removals_by_block.entry(blk).or_default().push(pos);
}
for positions in removals_by_block.values_mut() {
positions.sort_unstable();
positions.dedup();
}
for block in graph.blocks_mut() {
if let Some(positions) = removals_by_block.get(&block.id) {
for &pos in positions.iter().rev() {
if pos < block.nodes.len() {
block.nodes.remove(pos);
}
}
}
}
let pass_count = to_hoist.len();
if let Some(preheader) = graph.block_mut(lp.preheader) {
for (_, _, id, node) in to_hoist {
preheader.push_with_id(id, node);
}
}
total_hoisted += pass_count;
}
LICM_NODES_HOISTED.fetch_add(total_hoisted as u32, Ordering::Relaxed);
LICM_NAMED_GENERIC_HOISTED.fetch_add(named_generic_hoisted, Ordering::Relaxed);
if load_named_in_loop > 0 {
}
total_hoisted
}
fn collect_mutated_objects(graph: &MaglevGraph, loop_body: &HashSet<u32>) -> HashSet<NodeId> {
let mut mutated = HashSet::new();
for block in graph.blocks() {
if !loop_body.contains(&block.id) {
continue;
}
for (_, node) in &block.nodes {
match node {
ValueNode::StoreField { object, .. }
| ValueNode::StoreNamedGeneric { object, .. }
| ValueNode::StoreKeyedGeneric { object, .. }
| ValueNode::DeleteProperty { object, .. } => {
mutated.insert(*object);
}
ValueNode::StoreFixedArrayElement { elements, .. }
| ValueNode::StoreFixedDoubleArrayElement { elements, .. } => {
mutated.insert(*elements);
}
_ => {}
}
}
}
mutated
}
fn collect_mutated_globals(graph: &MaglevGraph, loop_body: &HashSet<u32>) -> HashSet<u32> {
let mut mutated = HashSet::new();
for block in graph.blocks() {
if !loop_body.contains(&block.id) {
continue;
}
for (_, node) in &block.nodes {
if let ValueNode::StoreGlobal { name, .. } = node {
mutated.insert(*name);
}
}
}
mutated
}
fn load_aliases_store(node: &ValueNode, mutated: &HashSet<NodeId>) -> bool {
match node {
ValueNode::LoadField { object, .. }
| ValueNode::LoadTaggedField { object, .. }
| ValueNode::LoadDoubleField { object, .. }
| ValueNode::LoadNamedGeneric { object, .. }
| ValueNode::LoadKeyedGeneric { object, .. } => mutated.contains(object),
ValueNode::LoadFixedArrayElement { elements, .. }
| ValueNode::LoadFixedDoubleArrayElement { elements, .. }
| ValueNode::LoadHoleyFixedDoubleArrayElement { elements, .. } => {
mutated.contains(elements)
}
_ => false,
}
}
fn global_load_aliases_store(node: &ValueNode, mutated_globals: &HashSet<u32>) -> bool {
if let ValueNode::LoadGlobal { name, .. } = node {
mutated_globals.contains(name)
} else {
false
}
}
fn is_generic_property_load(node: &ValueNode) -> bool {
matches!(
node,
ValueNode::LoadNamedGeneric { .. } | ValueNode::LoadKeyedGeneric { .. }
)
}
fn loop_has_property_side_effects(graph: &MaglevGraph, loop_body: &HashSet<u32>) -> bool {
for block in graph.blocks() {
if !loop_body.contains(&block.id) {
continue;
}
for (_, node) in &block.nodes {
if matches!(
node,
ValueNode::StoreNamedGeneric { .. }
| ValueNode::StoreKeyedGeneric { .. }
| ValueNode::DeleteProperty { .. }
| ValueNode::Call { .. }
| ValueNode::CallArrayPush { .. }
| ValueNode::CallKnownFunction { .. }
| ValueNode::CallWithSpread { .. }
| ValueNode::Construct { .. }
| ValueNode::ConstructWithSpread { .. }
| ValueNode::SpeculativeCallFusion { .. }
| ValueNode::SpeculativeSumFusion { .. }
| ValueNode::SpeculativePushFusion { .. }
| ValueNode::SpeculativeFillTrueFusion { .. }
| ValueNode::SpeculativeCountTruthyFusion { .. }
) {
return true;
}
}
}
false
}
fn is_pure(node: &ValueNode) -> bool {
!matches!(
node,
ValueNode::StoreField { .. }
| ValueNode::StoreFixedArrayElement { .. }
| ValueNode::StoreFixedDoubleArrayElement { .. }
| ValueNode::StoreNamedGeneric { .. }
| ValueNode::StoreKeyedGeneric { .. }
| ValueNode::StoreGlobal { .. }
| ValueNode::StoreContextSlot { .. }
| ValueNode::StoreCurrentContextSlot { .. }
| ValueNode::Call { .. }
| ValueNode::CallArrayPush { .. }
| ValueNode::CallKnownFunction { .. }
| ValueNode::CallBuiltin { .. }
| ValueNode::CallRuntime { .. }
| ValueNode::CallWithSpread { .. }
| ValueNode::Construct { .. }
| ValueNode::ConstructWithSpread { .. }
| ValueNode::CreateObjectLiteral { .. }
| ValueNode::CreateObjectLiteralWithProperties { .. }
| ValueNode::CreateArrayLiteral { .. }
| ValueNode::CreateShallowObjectLiteral { .. }
| ValueNode::CreateShallowArrayLiteral { .. }
| ValueNode::CreateFunctionContext { .. }
| ValueNode::CreateBlockContext { .. }
| ValueNode::CreateCatchContext { .. }
| ValueNode::CreateWithContext { .. }
| ValueNode::PushContext { .. }
| ValueNode::PopContext { .. }
| ValueNode::CreateClosure { .. }
| ValueNode::FastCreateClosure { .. }
| ValueNode::CreateEmptyObjectLiteral
| ValueNode::CreateEmptyArrayLiteral { .. }
| ValueNode::CreateMappedArguments
| ValueNode::CreateUnmappedArguments
| ValueNode::CreateRestParameter
| ValueNode::CreateRegExpLiteral { .. }
| ValueNode::Debugger
| ValueNode::Abort { .. }
| ValueNode::DeleteProperty { .. }
| ValueNode::ForInPrepare { .. }
| ValueNode::ForInNext { .. }
)
}
fn all_inputs_outside(node: &ValueNode, outside: &HashSet<NodeId>) -> bool {
let mut ok = true;
visit_inputs(node, &mut |id| {
if !outside.contains(&id) {
ok = false;
}
});
ok
}
fn visit_inputs(node: &ValueNode, f: &mut impl FnMut(NodeId)) {
match node {
ValueNode::SmiConstant { .. }
| ValueNode::Float64Constant { .. }
| ValueNode::Int32Constant { .. }
| ValueNode::Uint32Constant { .. }
| ValueNode::BigIntConstant { .. }
| ValueNode::TrueConstant
| ValueNode::FalseConstant
| ValueNode::NullConstant
| ValueNode::UndefinedConstant
| ValueNode::RootConstant { .. }
| ValueNode::ExternalConstant { .. }
| ValueNode::StringConstant { .. }
| ValueNode::ConstantPoolEntry { .. }
| ValueNode::Parameter { .. }
| ValueNode::RegisterInput { .. }
| ValueNode::ArgumentsLength
| ValueNode::RestLength
| ValueNode::LoadGlobal { .. }
| ValueNode::LoadCurrentContextSlot { .. }
| ValueNode::ArgumentsElements { .. }
| ValueNode::RestElements { .. }
| ValueNode::VirtualObject { .. }
| ValueNode::CreateFunctionContext { .. }
| ValueNode::CreateBlockContext { .. }
| ValueNode::CreateShallowObjectLiteral { .. }
| ValueNode::CreateShallowArrayLiteral { .. }
| ValueNode::CreateObjectLiteral { .. }
| ValueNode::CreateArrayLiteral { .. }
| ValueNode::CreateEmptyObjectLiteral
| ValueNode::CreateEmptyArrayLiteral { .. }
| ValueNode::CreateMappedArguments
| ValueNode::CreateUnmappedArguments
| ValueNode::CreateRestParameter
| ValueNode::CreateRegExpLiteral { .. }
| ValueNode::CreateClosure { .. }
| ValueNode::FastCreateClosure { .. }
| ValueNode::GetTemplateObject { .. }
| ValueNode::Debugger
| ValueNode::Abort { .. } => {}
ValueNode::GetArgument { index } => f(*index),
ValueNode::CheckedSmiIncrement { value }
| ValueNode::CheckedSmiDecrement { value }
| ValueNode::Int32Negate { value }
| ValueNode::Int32Increment { value }
| ValueNode::Int32Decrement { value }
| ValueNode::Float64Negate { value }
| ValueNode::Float64Ieee754Unary { value, .. }
| ValueNode::GenericBitwiseNot { value, .. }
| ValueNode::GenericNegate { value, .. }
| ValueNode::GenericIncrement { value, .. }
| ValueNode::GenericDecrement { value, .. }
| ValueNode::ChangeInt32ToFloat64 { input: value }
| ValueNode::ChangeUint32ToFloat64 { input: value }
| ValueNode::ChangeFloat64ToInt32 { input: value }
| ValueNode::CheckedFloat64ToInt32 { input: value }
| ValueNode::ChangeInt32ToTagged { input: value }
| ValueNode::ChangeUint32ToTagged { input: value }
| ValueNode::ChangeFloat64ToTagged { input: value }
| ValueNode::ChangeTaggedToInt32 { input: value }
| ValueNode::ChangeTaggedToUint32 { input: value }
| ValueNode::ChangeTaggedToFloat64 { input: value }
| ValueNode::CheckedTaggedToInt32 { input: value }
| ValueNode::CheckedTaggedToFloat64 { input: value }
| ValueNode::ToBoolean { value }
| ValueNode::TestNullOrUndefined { value }
| ValueNode::ToString { value, .. }
| ValueNode::ToObject { value, .. }
| ValueNode::ToName { value, .. }
| ValueNode::ToNumber { value, .. }
| ValueNode::ToNumberOrNumeric { value, .. }
| ValueNode::TypeOf { value }
| ValueNode::NumberToString { value, .. }
| ValueNode::TestUndetectable { value }
| ValueNode::TestTypeOf { value, .. } => f(*value),
ValueNode::CheckSmi { receiver }
| ValueNode::CheckNumber { receiver }
| ValueNode::CheckHeapObject { receiver }
| ValueNode::CheckSymbol { receiver }
| ValueNode::CheckString { receiver }
| ValueNode::CheckStringOrStringWrapper { receiver }
| ValueNode::CheckSeqOneByteString { receiver }
| ValueNode::CheckMaps { receiver, .. }
| ValueNode::CheckMapsWithMigration { receiver, .. }
| ValueNode::CheckValue { receiver, .. } => f(*receiver),
ValueNode::CheckDynamicValue { receiver, expected } => {
f(*receiver);
f(*expected);
}
ValueNode::CheckInt32IsSmi { input }
| ValueNode::CheckUint32IsSmi { input }
| ValueNode::CheckHoleyFloat64IsSmi { input }
| ValueNode::CheckFloat64IsNan { input } => f(*input),
ValueNode::LoadField { object, .. }
| ValueNode::LoadTaggedField { object, .. }
| ValueNode::LoadDoubleField { object, .. }
| ValueNode::LoadNamedGeneric { object, .. }
| ValueNode::ForInPrepare {
enumerator: object, ..
}
| ValueNode::StringLength { string: object }
| ValueNode::LoadEnumCacheLength { map: object } => f(*object),
ValueNode::LoadKeyedGeneric { object, key, .. } => {
f(*object);
f(*key);
}
ValueNode::HasInPrototypeChain { object, prototype } => {
f(*object);
f(*prototype);
}
ValueNode::StoreField { object, value, .. } => {
f(*object);
f(*value);
}
ValueNode::StoreCurrentContextSlot { value, .. } | ValueNode::StoreGlobal { value, .. } => {
f(*value)
}
ValueNode::LoadContextSlot { context, .. } => f(*context),
ValueNode::StoreContextSlot { context, value, .. } => {
f(*context);
f(*value);
}
ValueNode::PushContext { context } | ValueNode::PopContext { context } => f(*context),
ValueNode::LoadFixedArrayElement { elements, index }
| ValueNode::LoadFixedDoubleArrayElement { elements, index }
| ValueNode::LoadHoleyFixedDoubleArrayElement { elements, index } => {
f(*elements);
f(*index);
}
ValueNode::StoreFixedArrayElement {
elements,
index,
value,
}
| ValueNode::StoreFixedDoubleArrayElement {
elements,
index,
value,
} => {
f(*elements);
f(*index);
f(*value);
}
ValueNode::StoreNamedGeneric { object, value, .. } => {
f(*object);
f(*value);
}
ValueNode::StoreKeyedGeneric {
object, key, value, ..
} => {
f(*object);
f(*key);
f(*value);
}
ValueNode::CheckedSmiAdd { left, right }
| ValueNode::CheckedSmiSubtract { left, right }
| ValueNode::CheckedSmiMultiply { left, right }
| ValueNode::CheckedSmiDivide { left, right }
| ValueNode::CheckedSmiModulus { left, right }
| ValueNode::Int32Add { left, right }
| ValueNode::Int32Subtract { left, right }
| ValueNode::Int32Multiply { left, right }
| ValueNode::Int32Divide { left, right }
| ValueNode::Int32Modulus { left, right }
| ValueNode::Int32BitwiseAnd { left, right }
| ValueNode::Int32BitwiseOr { left, right }
| ValueNode::Int32BitwiseXor { left, right }
| ValueNode::Int32ShiftLeft { left, right }
| ValueNode::Int32ShiftRight { left, right }
| ValueNode::Int32ShiftRightLogical { left, right }
| ValueNode::Uint32Add { left, right }
| ValueNode::Uint32Subtract { left, right }
| ValueNode::Uint32Multiply { left, right }
| ValueNode::Uint32Divide { left, right }
| ValueNode::Uint32Modulus { left, right }
| ValueNode::Float64Add { left, right }
| ValueNode::Float64Subtract { left, right }
| ValueNode::Float64Multiply { left, right }
| ValueNode::Float64Divide { left, right }
| ValueNode::Float64Modulus { left, right }
| ValueNode::Float64Exponentiate { left, right }
| ValueNode::Int32Equal { left, right }
| ValueNode::Int32StrictEqual { left, right }
| ValueNode::Int32LessThan { left, right }
| ValueNode::Int32LessThanOrEqual { left, right }
| ValueNode::Int32GreaterThan { left, right }
| ValueNode::Int32GreaterThanOrEqual { left, right }
| ValueNode::Float64Equal { left, right }
| ValueNode::Float64LessThan { left, right }
| ValueNode::Float64LessThanOrEqual { left, right }
| ValueNode::Float64GreaterThan { left, right }
| ValueNode::Float64GreaterThanOrEqual { left, right }
| ValueNode::StringConcat { left, right }
| ValueNode::StringEqual { left, right }
| ValueNode::GenericAdd { left, right, .. }
| ValueNode::GenericSubtract { left, right, .. }
| ValueNode::GenericMultiply { left, right, .. }
| ValueNode::GenericDivide { left, right, .. }
| ValueNode::GenericModulus { left, right, .. }
| ValueNode::GenericExponentiate { left, right, .. }
| ValueNode::GenericBitwiseAnd { left, right, .. }
| ValueNode::GenericBitwiseOr { left, right, .. }
| ValueNode::GenericBitwiseXor { left, right, .. }
| ValueNode::GenericShiftLeft { left, right, .. }
| ValueNode::GenericShiftRight { left, right, .. }
| ValueNode::GenericShiftRightLogical { left, right, .. }
| ValueNode::TaggedEqual { left, right, .. }
| ValueNode::TaggedNotEqual { left, right, .. } => {
f(*left);
f(*right);
}
ValueNode::CheckInt32Condition { left, right, .. } => {
f(*left);
f(*right);
}
ValueNode::CheckCacheIndicesNotCleared { receiver, indices } => {
f(*receiver);
f(*indices);
}
ValueNode::TestInstanceOf {
object, callable, ..
} => {
f(*object);
f(*callable);
}
ValueNode::TestIn { key, object, .. } => {
f(*key);
f(*object);
}
ValueNode::StringAt { string, index } => {
f(*string);
f(*index);
}
ValueNode::ForInNext {
receiver,
cache_index,
cache_array,
..
} => {
f(*receiver);
f(*cache_index);
f(*cache_array);
}
ValueNode::DeleteProperty { object, key, .. } => {
f(*object);
f(*key);
}
ValueNode::CreateCatchContext { exception, .. } => f(*exception),
ValueNode::CreateWithContext { object, .. } => f(*object),
ValueNode::Call {
callee,
receiver,
args,
..
}
| ValueNode::CallWithSpread {
callee,
receiver,
args,
..
}
| ValueNode::CallKnownFunction {
callee,
receiver,
args,
} => {
f(*callee);
f(*receiver);
for &a in args.iter() {
f(a);
}
}
ValueNode::CallArrayPush { receiver, args, .. } => {
f(*receiver);
for &a in args.iter() {
f(a);
}
}
ValueNode::SpeculativeCallFusion { callee, .. } => {
f(*callee);
}
ValueNode::SpeculativeSumFusion { array } => {
f(*array);
}
ValueNode::SpeculativePushFusion { array, .. } => {
f(*array);
}
ValueNode::SpeculativeFillTrueFusion { array, count } => {
f(*array);
f(*count);
}
ValueNode::SpeculativeCountTruthyFusion { array, count } => {
f(*array);
f(*count);
}
ValueNode::CallBuiltin { args, .. } | ValueNode::CallRuntime { args, .. } => {
for &a in args.iter() {
f(a);
}
}
ValueNode::Construct {
constructor, args, ..
}
| ValueNode::ConstructWithSpread {
constructor, args, ..
} => {
f(*constructor);
for &a in args.iter() {
f(a);
}
}
ValueNode::Phi { inputs } => {
for &inp in inputs.iter() {
f(inp);
}
}
ValueNode::CreateObjectLiteralWithProperties { values, .. } => {
for &v in values.iter() {
f(v);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compiler::maglev::ir::{BasicBlock, ControlNode, MaglevGraph, ValueNode};
fn loop_graph() -> MaglevGraph {
let mut graph = MaglevGraph::new(1);
let mut b0 = BasicBlock::new(0);
let _p = b0.push_value(ValueNode::Parameter { index: 0 });
b0.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(0);
b1.add_predecessor(2);
let cond = b1.push_value(ValueNode::TrueConstant);
b1.set_control(ControlNode::Branch {
condition: cond,
if_true: 2,
if_false: 3,
});
graph.add_block(b1);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(1);
b2.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(1);
let undef = b3.push_value(ValueNode::UndefinedConstant);
b3.set_control(ControlNode::Return { value: undef });
graph.add_block(b3);
graph
}
#[test]
fn test_hoist_constant_from_loop_body() {
let mut graph = loop_graph();
graph.blocks_mut()[2]
.nodes
.insert(0, (NodeId(100), ValueNode::Int32Constant { value: 42 }));
assert_eq!(graph.blocks()[2].nodes.len(), 1);
hoist_loop_invariants(&mut graph);
assert_eq!(graph.blocks()[2].nodes.len(), 0);
assert!(
graph.blocks()[0]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::Int32Constant { value: 42 }))
);
}
#[test]
fn test_side_effecting_node_not_hoisted() {
let mut graph = loop_graph();
graph.blocks_mut()[2].nodes.insert(
0,
(
NodeId(200),
ValueNode::CallRuntime {
function_id: 0,
args: vec![],
},
),
);
let before = graph.blocks()[2].nodes.len();
hoist_loop_invariants(&mut graph);
assert_eq!(graph.blocks()[2].nodes.len(), before);
}
#[test]
fn test_node_with_loop_local_input_not_hoisted() {
let mut graph = loop_graph();
let inner_id = NodeId(300);
let user_id = NodeId(301);
graph.blocks_mut()[2]
.nodes
.push((inner_id, ValueNode::Int32Constant { value: 1 }));
graph.blocks_mut()[2]
.nodes
.push((user_id, ValueNode::Int32Negate { value: inner_id }));
let body_before = graph.blocks()[2].nodes.len();
hoist_loop_invariants(&mut graph);
assert_eq!(
graph.blocks()[2].nodes.len(),
body_before - 2,
"both the constant and the negate should have been hoisted"
);
}
#[test]
fn test_no_loop_no_change() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let c = block.push_value(ValueNode::Int32Constant { value: 7 });
block.set_control(ControlNode::Return { value: c });
graph.add_block(block);
let before = graph.blocks()[0].nodes.len();
hoist_loop_invariants(&mut graph);
assert_eq!(graph.blocks()[0].nodes.len(), before);
}
#[test]
fn test_hoist_arithmetic_using_preheader_values() {
let mut graph = loop_graph();
let p0_id = graph.blocks()[0].nodes[0].0;
graph.blocks_mut()[2].nodes.insert(
0,
(
NodeId(400),
ValueNode::Int32Add {
left: p0_id,
right: p0_id,
},
),
);
assert_eq!(graph.blocks()[2].nodes.len(), 1);
hoist_loop_invariants(&mut graph);
assert_eq!(graph.blocks()[2].nodes.len(), 0);
assert!(
graph.blocks()[0]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::Int32Add { .. }))
);
}
#[test]
fn test_hoist_load_field_from_loop_body() {
let mut graph = loop_graph();
let param_id = graph.blocks()[0].nodes[0].0;
graph.blocks_mut()[2].nodes.insert(
0,
(
NodeId(100),
ValueNode::LoadField {
object: param_id,
offset: 8,
},
),
);
assert_eq!(graph.blocks()[2].nodes.len(), 1);
hoist_loop_invariants(&mut graph);
assert_eq!(graph.blocks()[2].nodes.len(), 0);
assert!(
graph.blocks()[0]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::LoadField { offset: 8, .. }))
);
}
#[test]
fn test_load_field_not_hoisted_when_store_aliases() {
let mut graph = loop_graph();
let param_id = graph.blocks()[0].nodes[0].0;
let val_id = NodeId(102);
graph.blocks_mut()[2]
.nodes
.push((val_id, ValueNode::Int32Constant { value: 0 }));
let store_id = NodeId(103);
graph.blocks_mut()[2].nodes.push((
store_id,
ValueNode::StoreField {
object: param_id,
offset: 8,
value: val_id,
},
));
graph.blocks_mut()[2].nodes.insert(
0,
(
NodeId(100),
ValueNode::LoadField {
object: param_id,
offset: 16,
},
),
);
let body_len_before = graph.blocks()[2].nodes.len();
hoist_loop_invariants(&mut graph);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::LoadField { .. })),
"LoadField should NOT be hoisted when its object is mutated in the loop"
);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::StoreField { .. })),
);
assert!(graph.blocks()[2].nodes.len() >= body_len_before - 1);
}
#[test]
fn test_hoist_property_chain_from_immutable_global() {
let mut graph = loop_graph();
let root_id = NodeId(500);
let a_id = NodeId(501);
let b_id = NodeId(502);
graph.blocks_mut()[2].nodes.push((
root_id,
ValueNode::LoadGlobal {
name: 1, feedback_slot: 0,
},
));
graph.blocks_mut()[2].nodes.push((
a_id,
ValueNode::LoadNamedGeneric {
object: root_id,
name: 2, feedback_slot: 1,
},
));
graph.blocks_mut()[2].nodes.push((
b_id,
ValueNode::LoadNamedGeneric {
object: a_id,
name: 3, feedback_slot: 2,
},
));
assert_eq!(graph.blocks()[2].nodes.len(), 3);
hoist_loop_invariants(&mut graph);
assert_eq!(
graph.blocks()[2].nodes.len(),
0,
"all 3 invariant nodes should be hoisted from the loop body"
);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == root_id),
"LoadGlobal should be in preheader"
);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == b_id),
"final LoadNamedGeneric should be in preheader"
);
}
#[test]
fn test_load_global_not_hoisted_when_stored() {
let mut graph = loop_graph();
let load_i = NodeId(600);
let inc = NodeId(601);
let store_i = NodeId(602);
graph.blocks_mut()[2].nodes.push((
load_i,
ValueNode::LoadGlobal {
name: 10, feedback_slot: 0,
},
));
graph.blocks_mut()[2].nodes.push((
inc,
ValueNode::Int32Add {
left: load_i,
right: load_i,
},
));
graph.blocks_mut()[2].nodes.push((
store_i,
ValueNode::StoreGlobal {
name: 10, value: inc,
feedback_slot: 1,
},
));
let body_len = graph.blocks()[2].nodes.len();
hoist_loop_invariants(&mut graph);
assert!(
graph.blocks()[2].nodes.iter().any(|(id, _)| *id == load_i),
"LoadGlobal for stored name must stay in loop"
);
assert!(graph.blocks()[2].nodes.iter().any(|(id, _)| *id == store_i),);
assert_eq!(
graph.blocks()[2].nodes.len(),
body_len,
"nothing should be hoisted"
);
}
#[test]
fn test_detect_loop_with_reversed_block_ids() {
let mut graph = MaglevGraph::new(1);
let mut b0 = BasicBlock::new(0);
b0.push_value(ValueNode::Parameter { index: 0 });
b0.set_control(ControlNode::Jump { target: 3 }); graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(3);
let undef = b1.push_value(ValueNode::UndefinedConstant);
b1.set_control(ControlNode::Return { value: undef });
graph.add_block(b1);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(3);
b2.set_control(ControlNode::Jump { target: 3 }); graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(0); b3.add_predecessor(2); let cond = b3.push_value(ValueNode::TrueConstant);
b3.set_control(ControlNode::Branch {
condition: cond,
if_true: 2,
if_false: 1,
});
graph.add_block(b3);
let loops = detect_loops(&graph);
assert_eq!(loops.len(), 1, "must detect the loop even with non-RPO IDs");
assert_eq!(loops[0].header, 3, "header should be block 3");
assert_eq!(loops[0].preheader, 0, "preheader should be block 0");
assert!(loops[0].body.contains(&3), "body must contain header");
assert!(loops[0].body.contains(&2), "body must contain body block");
}
#[test]
fn test_hoist_from_loop_with_reversed_block_ids() {
let mut graph = MaglevGraph::new(1);
let mut b0 = BasicBlock::new(0);
let _param = b0.push_value(ValueNode::Parameter { index: 0 });
b0.set_control(ControlNode::Jump { target: 3 });
graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(3);
let undef = b1.push_value(ValueNode::UndefinedConstant);
b1.set_control(ControlNode::Return { value: undef });
graph.add_block(b1);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(3);
b2.nodes
.push((NodeId(100), ValueNode::Int32Constant { value: 42 }));
b2.set_control(ControlNode::Jump { target: 3 });
graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(0);
b3.add_predecessor(2);
let cond = b3.push_value(ValueNode::TrueConstant);
b3.set_control(ControlNode::Branch {
condition: cond,
if_true: 2,
if_false: 1,
});
graph.add_block(b3);
assert_eq!(graph.blocks()[2].nodes.len(), 1);
hoist_loop_invariants(&mut graph);
assert_eq!(
graph.blocks()[2].nodes.len(),
0,
"invariant should be hoisted from body"
);
assert!(
graph.blocks()[0]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::Int32Constant { value: 42 })),
"invariant should appear in preheader"
);
}
#[test]
fn test_load_named_generic_not_hoisted_with_call_in_loop() {
let mut graph = loop_graph();
let param_id = graph.blocks()[0].nodes[0].0;
graph.blocks_mut()[2].nodes.push((
NodeId(700),
ValueNode::LoadNamedGeneric {
object: param_id,
name: 1,
feedback_slot: 0,
},
));
graph.blocks_mut()[2].nodes.push((
NodeId(701),
ValueNode::Call {
callee: param_id,
receiver: param_id,
args: vec![],
feedback_slot: 0,
},
));
let body_before = graph.blocks()[2].nodes.len();
hoist_loop_invariants(&mut graph);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::LoadNamedGeneric { .. })),
"LoadNamedGeneric should NOT be hoisted when a Call exists in the loop"
);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::Call { .. })),
);
assert_eq!(graph.blocks()[2].nodes.len(), body_before);
}
#[test]
fn test_hoist_load_keyed_generic_no_side_effects() {
let mut graph = loop_graph();
let param_id = graph.blocks()[0].nodes[0].0;
let key_id = NodeId(50);
graph.blocks_mut()[0]
.nodes
.push((key_id, ValueNode::SmiConstant { value: 0 }));
graph.blocks_mut()[2].nodes.push((
NodeId(800),
ValueNode::LoadKeyedGeneric {
object: param_id,
key: key_id,
feedback_slot: 0,
},
));
assert_eq!(graph.blocks()[2].nodes.len(), 1);
hoist_loop_invariants(&mut graph);
assert_eq!(
graph.blocks()[2].nodes.len(),
0,
"LoadKeyedGeneric should be hoisted when no side effects in loop"
);
assert!(
graph.blocks()[0]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::LoadKeyedGeneric { .. })),
"LoadKeyedGeneric should appear in preheader"
);
}
#[test]
fn test_load_named_generic_not_hoisted_with_generic_store() {
let mut graph = loop_graph();
let param_id = graph.blocks()[0].nodes[0].0;
let other_obj = NodeId(51);
graph.blocks_mut()[0]
.nodes
.push((other_obj, ValueNode::Parameter { index: 1 }));
graph.blocks_mut()[2].nodes.push((
NodeId(900),
ValueNode::LoadNamedGeneric {
object: param_id,
name: 1,
feedback_slot: 0,
},
));
let val = NodeId(901);
graph.blocks_mut()[2]
.nodes
.push((val, ValueNode::Int32Constant { value: 0 }));
graph.blocks_mut()[2].nodes.push((
NodeId(902),
ValueNode::StoreNamedGeneric {
object: other_obj,
name: 2,
value: val,
feedback_slot: 0,
},
));
hoist_loop_invariants(&mut graph);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::LoadNamedGeneric { .. })),
"LoadNamedGeneric should NOT be hoisted when a StoreNamedGeneric exists in the loop"
);
}
#[test]
fn test_hoist_cross_block_property_chain_non_rpo() {
let mut graph = MaglevGraph::new(1);
let mut b0 = BasicBlock::new(0);
b0.push_value(ValueNode::Parameter { index: 0 });
b0.set_control(ControlNode::Jump { target: 3 }); graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(3);
let undef = b1.push_value(ValueNode::UndefinedConstant);
b1.set_control(ControlNode::Return { value: undef });
graph.add_block(b1);
let obj_id = NodeId(500);
let x_id = NodeId(501);
let y_id = NodeId(502);
let z_id = NodeId(503);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(3);
b2.nodes.push((
x_id,
ValueNode::LoadNamedGeneric {
object: obj_id,
name: 10,
feedback_slot: 1,
},
));
b2.nodes.push((
y_id,
ValueNode::LoadNamedGeneric {
object: obj_id,
name: 11,
feedback_slot: 2,
},
));
b2.nodes.push((
z_id,
ValueNode::LoadNamedGeneric {
object: obj_id,
name: 12,
feedback_slot: 3,
},
));
b2.set_control(ControlNode::Jump { target: 3 }); graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(0);
b3.add_predecessor(2);
b3.nodes.push((
obj_id,
ValueNode::LoadGlobal {
name: 1,
feedback_slot: 0,
},
));
let cond = b3.push_value(ValueNode::TrueConstant);
b3.set_control(ControlNode::Branch {
condition: cond,
if_true: 2,
if_false: 1,
});
graph.add_block(b3);
assert_eq!(graph.blocks()[2].nodes.len(), 3);
assert_eq!(
graph.blocks()[3]
.nodes
.iter()
.filter(|(_, n)| matches!(n, ValueNode::LoadGlobal { .. }))
.count(),
1
);
hoist_loop_invariants(&mut graph);
assert_eq!(
graph.blocks()[2].nodes.len(),
0,
"all LoadNamedGeneric nodes should be hoisted from loop body"
);
assert!(
!graph.blocks()[3]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::LoadGlobal { .. })),
"LoadGlobal should be hoisted from loop header"
);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == obj_id),
"LoadGlobal should be in preheader"
);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == x_id),
"LoadNamedGeneric(x) should be in preheader"
);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == y_id),
"LoadNamedGeneric(y) should be in preheader"
);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == z_id),
"LoadNamedGeneric(z) should be in preheader"
);
}
#[test]
fn test_load_named_generic_not_hoisted_with_delete_property() {
let mut graph = loop_graph();
let param_id = graph.blocks()[0].nodes[0].0;
let key_id = NodeId(52);
graph.blocks_mut()[0]
.nodes
.push((key_id, ValueNode::SmiConstant { value: 0 }));
graph.blocks_mut()[2].nodes.push((
NodeId(950),
ValueNode::LoadNamedGeneric {
object: param_id,
name: 1,
feedback_slot: 0,
},
));
let other_obj = NodeId(51);
graph.blocks_mut()[0]
.nodes
.push((other_obj, ValueNode::Parameter { index: 1 }));
graph.blocks_mut()[2].nodes.push((
NodeId(951),
ValueNode::DeleteProperty {
object: other_obj,
key: key_id,
feedback_slot: 0,
},
));
hoist_loop_invariants(&mut graph);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::LoadNamedGeneric { .. })),
"LoadNamedGeneric should NOT be hoisted when DeleteProperty exists in the loop"
);
}
#[test]
fn test_hoist_load_named_generic_with_store_global_and_call_builtin() {
let mut graph = loop_graph();
let param_id = graph.blocks()[0].nodes[0].0;
let obj_id = NodeId(500);
let a_id = NodeId(501);
let b_id = NodeId(502);
let c_id = NodeId(503);
let add_id = NodeId(504);
let store_id = NodeId(505);
let cmp_id = NodeId(506);
graph.blocks_mut()[2].nodes.push((
obj_id,
ValueNode::LoadGlobal {
name: 1,
feedback_slot: 0,
},
));
graph.blocks_mut()[2].nodes.push((
a_id,
ValueNode::LoadNamedGeneric {
object: obj_id,
name: 2,
feedback_slot: 1,
},
));
graph.blocks_mut()[2].nodes.push((
b_id,
ValueNode::LoadNamedGeneric {
object: a_id,
name: 3,
feedback_slot: 2,
},
));
graph.blocks_mut()[2].nodes.push((
c_id,
ValueNode::LoadNamedGeneric {
object: b_id,
name: 4,
feedback_slot: 3,
},
));
graph.blocks_mut()[2].nodes.push((
add_id,
ValueNode::GenericAdd {
left: param_id,
right: c_id,
feedback_slot: 4,
},
));
graph.blocks_mut()[2].nodes.push((
store_id,
ValueNode::StoreGlobal {
name: 10,
value: add_id,
feedback_slot: 5,
},
));
graph.blocks_mut()[2].nodes.push((
cmp_id,
ValueNode::CallBuiltin {
builtin_id: 0,
args: vec![param_id],
},
));
assert_eq!(graph.blocks()[2].nodes.len(), 7);
hoist_loop_invariants(&mut graph);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == obj_id),
"LoadGlobal should be hoisted to preheader"
);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == a_id),
"LoadNamedGeneric(a) should be hoisted to preheader"
);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == b_id),
"LoadNamedGeneric(b) should be hoisted to preheader"
);
assert!(
graph.blocks()[0].nodes.iter().any(|(id, _)| *id == c_id),
"LoadNamedGeneric(c) should be hoisted to preheader"
);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::StoreGlobal { .. })),
"StoreGlobal must stay in the loop"
);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::CallBuiltin { .. })),
"CallBuiltin must stay in the loop"
);
}
#[test]
fn test_call_builtin_does_not_block_load_named_generic() {
let mut graph = loop_graph();
let param_id = graph.blocks()[0].nodes[0].0;
graph.blocks_mut()[2].nodes.push((
NodeId(700),
ValueNode::LoadNamedGeneric {
object: param_id,
name: 1,
feedback_slot: 0,
},
));
graph.blocks_mut()[2].nodes.push((
NodeId(701),
ValueNode::CallBuiltin {
builtin_id: 0,
args: vec![],
},
));
hoist_loop_invariants(&mut graph);
assert!(
graph.blocks()[0]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::LoadNamedGeneric { .. })),
"LoadNamedGeneric should be hoisted when only CallBuiltin exists in the loop"
);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::CallBuiltin { .. })),
);
}
#[test]
fn test_call_runtime_does_not_block_load_named_generic() {
let mut graph = loop_graph();
let param_id = graph.blocks()[0].nodes[0].0;
graph.blocks_mut()[2].nodes.push((
NodeId(700),
ValueNode::LoadNamedGeneric {
object: param_id,
name: 1,
feedback_slot: 0,
},
));
graph.blocks_mut()[2].nodes.push((
NodeId(701),
ValueNode::CallRuntime {
function_id: 0,
args: vec![],
},
));
hoist_loop_invariants(&mut graph);
assert!(
graph.blocks()[0]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::LoadNamedGeneric { .. })),
"LoadNamedGeneric should be hoisted when only CallRuntime exists in the loop"
);
assert!(
graph.blocks()[2]
.nodes
.iter()
.any(|(_, n)| matches!(n, ValueNode::CallRuntime { .. })),
);
}
#[test]
fn test_fold_invariant_addition_chain() {
let mut graph = MaglevGraph::new(1);
let mut b0 = BasicBlock::new(0);
b0.push_with_id(NodeId(100), ValueNode::Int32Constant { value: 1 });
b0.push_with_id(NodeId(101), ValueNode::Int32Constant { value: 2 });
b0.push_with_id(NodeId(102), ValueNode::Int32Constant { value: 3 });
b0.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(0);
b1.add_predecessor(2);
b1.push_with_id(
NodeId(200),
ValueNode::Phi {
inputs: vec![NodeId(100), NodeId(203)],
},
);
let cond = b1.push_value(ValueNode::TrueConstant);
b1.set_control(ControlNode::Branch {
condition: cond,
if_true: 2,
if_false: 3,
});
graph.add_block(b1);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(1);
b2.push_with_id(
NodeId(201),
ValueNode::Int32Add {
left: NodeId(200),
right: NodeId(100),
},
);
b2.push_with_id(
NodeId(202),
ValueNode::Int32Add {
left: NodeId(201),
right: NodeId(101),
},
);
b2.push_with_id(
NodeId(203),
ValueNode::Int32Add {
left: NodeId(202),
right: NodeId(102),
},
);
b2.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(1);
let undef = b3.push_value(ValueNode::UndefinedConstant);
b3.set_control(ControlNode::Return { value: undef });
graph.add_block(b3);
let body_adds_before = graph.blocks()[2]
.nodes
.iter()
.filter(|(_, n)| matches!(n, ValueNode::Int32Add { .. }))
.count();
assert_eq!(body_adds_before, 3, "should start with 3 additions in body");
let folded = super::fold_invariant_addition_chains(&mut graph);
assert_eq!(folded, 3, "should fold 3 invariant operands");
let endpoint = graph.blocks()[2]
.nodes
.iter()
.find(|(id, _)| *id == NodeId(203))
.map(|(_, n)| n);
match endpoint {
Some(ValueNode::Int32Add { left, right }) => {
assert_eq!(*left, NodeId(200), "endpoint left should be the Phi");
assert!(
right.0 != 100 && right.0 != 101 && right.0 != 102,
"endpoint right should be the new invariant sum node"
);
}
other => panic!("expected Int32Add for endpoint, got {other:?}"),
}
let preheader_adds = graph.blocks()[0]
.nodes
.iter()
.filter(|(_, n)| matches!(n, ValueNode::Int32Add { .. }))
.count();
assert_eq!(
preheader_adds, 2,
"preheader should have 2 addition nodes for invariant sum (a+b, (a+b)+c)"
);
}
}