Loop analysis of the IL
* Implemented in two passes * First pass discovers the loops headers (they dominate one of their predecessors) * Second pass traverses the blocks of each loop * Discovers the loop tree structure * Offers a new LoopAnalysis data structure queried from outside the module
This commit is contained in:
committed by
Jakob Stoklund Olesen
parent
4f26764e71
commit
b02ccea8dc
@@ -12,6 +12,7 @@
|
|||||||
use dominator_tree::DominatorTree;
|
use dominator_tree::DominatorTree;
|
||||||
use flowgraph::ControlFlowGraph;
|
use flowgraph::ControlFlowGraph;
|
||||||
use ir::Function;
|
use ir::Function;
|
||||||
|
use loop_analysis::LoopAnalysis;
|
||||||
use isa::TargetIsa;
|
use isa::TargetIsa;
|
||||||
use legalize_function;
|
use legalize_function;
|
||||||
use regalloc;
|
use regalloc;
|
||||||
@@ -32,6 +33,9 @@ pub struct Context {
|
|||||||
|
|
||||||
/// Register allocation context.
|
/// Register allocation context.
|
||||||
pub regalloc: regalloc::Context,
|
pub regalloc: regalloc::Context,
|
||||||
|
|
||||||
|
/// Loop analysis of `func`.
|
||||||
|
pub loop_analysis: LoopAnalysis,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
@@ -45,6 +49,7 @@ impl Context {
|
|||||||
cfg: ControlFlowGraph::new(),
|
cfg: ControlFlowGraph::new(),
|
||||||
domtree: DominatorTree::new(),
|
domtree: DominatorTree::new(),
|
||||||
regalloc: regalloc::Context::new(),
|
regalloc: regalloc::Context::new(),
|
||||||
|
loop_analysis: LoopAnalysis::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ impl<T: EntityRef> PartialEq for EntityList<T> {
|
|||||||
impl<T: EntityRef> Eq for EntityList<T> {}
|
impl<T: EntityRef> Eq for EntityList<T> {}
|
||||||
|
|
||||||
/// A memory pool for storing lists of `T`.
|
/// A memory pool for storing lists of `T`.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ListPool<T: EntityRef> {
|
pub struct ListPool<T: EntityRef> {
|
||||||
// The main array containing the lists.
|
// The main array containing the lists.
|
||||||
data: Vec<T>,
|
data: Vec<T>,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ impl<K, V> EntityMap<K, V>
|
|||||||
pub fn keys(&self) -> Keys<K> {
|
pub fn keys(&self) -> Keys<K> {
|
||||||
Keys {
|
Keys {
|
||||||
pos: 0,
|
pos: 0,
|
||||||
len: self.elems.len(),
|
rev_pos: self.elems.len(),
|
||||||
unused: PhantomData,
|
unused: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ pub struct Keys<K>
|
|||||||
where K: EntityRef
|
where K: EntityRef
|
||||||
{
|
{
|
||||||
pos: usize,
|
pos: usize,
|
||||||
len: usize,
|
rev_pos: usize,
|
||||||
unused: PhantomData<K>,
|
unused: PhantomData<K>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ impl<K> Iterator for Keys<K>
|
|||||||
type Item = K;
|
type Item = K;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if self.pos < self.len {
|
if self.pos < self.rev_pos {
|
||||||
let k = K::new(self.pos);
|
let k = K::new(self.pos);
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
Some(k)
|
Some(k)
|
||||||
@@ -203,6 +203,20 @@ impl<K> Iterator for Keys<K>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<K> DoubleEndedIterator for Keys<K>
|
||||||
|
where K: EntityRef
|
||||||
|
{
|
||||||
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.rev_pos > self.pos {
|
||||||
|
let k = K::new(self.rev_pos - 1);
|
||||||
|
self.rev_pos -= 1;
|
||||||
|
Some(k)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use ir::layout::Cursor;
|
|||||||
use ir::types;
|
use ir::types;
|
||||||
use ir::{Ebb, Inst, Value, Type, SigRef, Signature, FuncRef, ValueList, ValueListPool};
|
use ir::{Ebb, Inst, Value, Type, SigRef, Signature, FuncRef, ValueList, ValueListPool};
|
||||||
use write::write_operands;
|
use write::write_operands;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::ops::{Index, IndexMut};
|
use std::ops::{Index, IndexMut};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub mod entity_map;
|
|||||||
pub mod flowgraph;
|
pub mod flowgraph;
|
||||||
pub mod ir;
|
pub mod ir;
|
||||||
pub mod isa;
|
pub mod isa;
|
||||||
|
pub mod loop_analysis;
|
||||||
pub mod regalloc;
|
pub mod regalloc;
|
||||||
pub mod result;
|
pub mod result;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|||||||
336
lib/cretonne/src/loop_analysis.rs
Normal file
336
lib/cretonne/src/loop_analysis.rs
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
//! A loop analysis represented as mappings of loops to their header Ebb
|
||||||
|
//! and parent in the loop tree.
|
||||||
|
|
||||||
|
use ir::{Function, Ebb, Layout};
|
||||||
|
use flowgraph::ControlFlowGraph;
|
||||||
|
use dominator_tree::DominatorTree;
|
||||||
|
use entity_map::{EntityMap, PrimaryEntityData};
|
||||||
|
use packed_option::{PackedOption, ReservedValue};
|
||||||
|
use entity_map::{EntityRef, Keys};
|
||||||
|
use std::u32;
|
||||||
|
|
||||||
|
/// A opaque reference to a code loop.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct Loop(u32);
|
||||||
|
impl EntityRef for Loop {
|
||||||
|
fn new(index: usize) -> Self {
|
||||||
|
assert!(index < (u32::MAX as usize));
|
||||||
|
Loop(index as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index(self) -> usize {
|
||||||
|
self.0 as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReservedValue for Loop {
|
||||||
|
fn reserved_value() -> Loop {
|
||||||
|
Loop(u32::MAX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Loop tree information for a single function.
|
||||||
|
///
|
||||||
|
/// Loops are referenced by the Loop object, and for each loop you can access its header EBB,
|
||||||
|
/// its eventual parent in the loop tree and all the EBB belonging to the loop.
|
||||||
|
pub struct LoopAnalysis {
|
||||||
|
loops: EntityMap<Loop, LoopData>,
|
||||||
|
ebb_loop_map: EntityMap<Ebb, PackedOption<Loop>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoopData {
|
||||||
|
header: Ebb,
|
||||||
|
parent: PackedOption<Loop>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimaryEntityData for LoopData {}
|
||||||
|
|
||||||
|
impl LoopData {
|
||||||
|
/// Creates a `LoopData` object with the loop header and its eventual parent in the loop tree.
|
||||||
|
pub fn new(header: Ebb, parent: Option<Loop>) -> LoopData {
|
||||||
|
LoopData {
|
||||||
|
header: header,
|
||||||
|
parent: parent.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Methods for querying the loop analysis.
|
||||||
|
impl LoopAnalysis {
|
||||||
|
/// Allocate a new blank loop analysis struct. Use `compute` to compute the loop analysis for
|
||||||
|
/// a function.
|
||||||
|
pub fn new() -> LoopAnalysis {
|
||||||
|
LoopAnalysis {
|
||||||
|
loops: EntityMap::new(),
|
||||||
|
ebb_loop_map: EntityMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all the loops contained in a function.
|
||||||
|
pub fn loops(&self) -> Keys<Loop> {
|
||||||
|
self.loops.keys()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the header EBB of a particular loop.
|
||||||
|
///
|
||||||
|
/// The characteristic property of a loop header block is that it dominates some of its
|
||||||
|
/// predecessors.
|
||||||
|
pub fn loop_header(&self, lp: Loop) -> Ebb {
|
||||||
|
self.loops[lp].header
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the eventual parent of a loop in the loop tree.
|
||||||
|
pub fn loop_parent(&self, lp: Loop) -> Option<Loop> {
|
||||||
|
self.loops[lp].parent.expand()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine if an Ebb belongs to a loop by running a finger along the loop tree.
|
||||||
|
///
|
||||||
|
/// Returns `true` if `ebb` is in loop `lp`.
|
||||||
|
pub fn is_in_loop(&self, ebb: Ebb, lp: Loop) -> bool {
|
||||||
|
let ebb_loop = self.ebb_loop_map[ebb];
|
||||||
|
match ebb_loop.expand() {
|
||||||
|
None => false,
|
||||||
|
Some(ebb_loop) => self.is_child_loop(ebb_loop, lp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines if a loop is contained in another loop.
|
||||||
|
///
|
||||||
|
/// `is_child_loop(child,parent)` returns `true` if and only if `child` is a child loop of
|
||||||
|
/// `parent` (or `child == parent`).
|
||||||
|
pub fn is_child_loop(&self, child: Loop, parent: Loop) -> bool {
|
||||||
|
let mut finger = Some(child);
|
||||||
|
while let Some(finger_loop) = finger {
|
||||||
|
if finger_loop == parent {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finger = self.loop_parent(finger_loop);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoopAnalysis {
|
||||||
|
/// Detects the loops in a function. Needs the control flow graph and the dominator tree.
|
||||||
|
pub fn compute(&mut self, func: &Function, cfg: &ControlFlowGraph, domtree: &DominatorTree) {
|
||||||
|
self.loops.clear();
|
||||||
|
self.ebb_loop_map.clear();
|
||||||
|
self.ebb_loop_map.resize(func.dfg.num_ebbs());
|
||||||
|
self.find_loop_headers(cfg, domtree, &func.layout);
|
||||||
|
self.discover_loop_blocks(cfg, domtree, &func.layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traverses the CFG in reverse postorder and create a loop object for every EBB having a
|
||||||
|
// back edge.
|
||||||
|
fn find_loop_headers(&mut self,
|
||||||
|
cfg: &ControlFlowGraph,
|
||||||
|
domtree: &DominatorTree,
|
||||||
|
layout: &Layout) {
|
||||||
|
// We traverse the CFg in reverse postorder
|
||||||
|
for ebb in cfg.postorder_ebbs().iter().rev() {
|
||||||
|
for &(_, pred_inst) in cfg.get_predecessors(*ebb) {
|
||||||
|
// If the ebb dominates one of its predecessors it is a back edge
|
||||||
|
if domtree.ebb_dominates(ebb.clone(), pred_inst, layout) {
|
||||||
|
// This ebb is a loop header, so we create its associated loop
|
||||||
|
let lp = self.loops.push(LoopData::new(*ebb, None));
|
||||||
|
self.ebb_loop_map[*ebb] = lp.into();
|
||||||
|
break;
|
||||||
|
// We break because we only need one back edge to identify a loop header.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intended to be called after `find_loop_headers`. For each detected loop header,
|
||||||
|
// discovers all the ebb belonging to the loop and its inner loops. After a call to this
|
||||||
|
// function, the loop tree is fully constructed.
|
||||||
|
fn discover_loop_blocks(&mut self,
|
||||||
|
cfg: &ControlFlowGraph,
|
||||||
|
domtree: &DominatorTree,
|
||||||
|
layout: &Layout) {
|
||||||
|
let mut stack: Vec<Ebb> = Vec::new();
|
||||||
|
// We handle each loop header in reverse order, corresponding to a pesudo postorder
|
||||||
|
// traversal of the graph.
|
||||||
|
for lp in self.loops().rev() {
|
||||||
|
for &(pred, pred_inst) in cfg.get_predecessors(self.loops[lp].header) {
|
||||||
|
// We follow the back edges
|
||||||
|
if domtree.ebb_dominates(self.loops[lp].header, pred_inst, layout) {
|
||||||
|
stack.push(pred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while let Some(node) = stack.pop() {
|
||||||
|
let continue_dfs: Option<Ebb>;
|
||||||
|
match self.ebb_loop_map[node].expand() {
|
||||||
|
None => {
|
||||||
|
// The node hasn't been visited yet, we tag it as part of the loop
|
||||||
|
self.ebb_loop_map[node] = PackedOption::from(lp);
|
||||||
|
continue_dfs = Some(node);
|
||||||
|
}
|
||||||
|
Some(node_loop) => {
|
||||||
|
// We copy the node_loop into a mutable reference passed along the while
|
||||||
|
let mut node_loop = node_loop;
|
||||||
|
// The node is part of a loop, which can be lp or an inner loop
|
||||||
|
let mut node_loop_parent_option = self.loops[node_loop].parent;
|
||||||
|
while let Some(node_loop_parent) = node_loop_parent_option.expand() {
|
||||||
|
if node_loop_parent == lp {
|
||||||
|
// We have encounterd lp so we stop (already visited)
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
node_loop = node_loop_parent;
|
||||||
|
// We lookup the parent loop
|
||||||
|
node_loop_parent_option = self.loops[node_loop].parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now node_loop_parent is either:
|
||||||
|
// - None and node_loop is an new inner loop of lp
|
||||||
|
// - Some(...) and the initial node_loop was a known inner loop of lp
|
||||||
|
match node_loop_parent_option.expand() {
|
||||||
|
Some(_) => continue_dfs = None,
|
||||||
|
None => {
|
||||||
|
if node_loop != lp {
|
||||||
|
self.loops[node_loop].parent = lp.into();
|
||||||
|
continue_dfs = Some(self.loops[node_loop].header)
|
||||||
|
} else {
|
||||||
|
// If lp is a one-block loop then we make sure we stop
|
||||||
|
continue_dfs = None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now we have handled the popped node and need to continue the DFS by adding the
|
||||||
|
// predecessors of that node
|
||||||
|
if let Some(continue_dfs) = continue_dfs {
|
||||||
|
for &(pred, _) in cfg.get_predecessors(continue_dfs) {
|
||||||
|
stack.push(pred)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use ir::{Function, InstBuilder, Cursor, types};
|
||||||
|
use loop_analysis::{Loop, LoopAnalysis};
|
||||||
|
use flowgraph::ControlFlowGraph;
|
||||||
|
use dominator_tree::DominatorTree;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_loops_detection() {
|
||||||
|
let mut func = Function::new();
|
||||||
|
let ebb0 = func.dfg.make_ebb();
|
||||||
|
let ebb1 = func.dfg.make_ebb();
|
||||||
|
let ebb2 = func.dfg.make_ebb();
|
||||||
|
let ebb3 = func.dfg.make_ebb();
|
||||||
|
let cond = func.dfg.append_ebb_arg(ebb0, types::I32);
|
||||||
|
|
||||||
|
{
|
||||||
|
let dfg = &mut func.dfg;
|
||||||
|
let cur = &mut Cursor::new(&mut func.layout);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb0);
|
||||||
|
dfg.ins(cur).jump(ebb1, &[]);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb1);
|
||||||
|
dfg.ins(cur).jump(ebb2, &[]);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb2);
|
||||||
|
dfg.ins(cur).brnz(cond, ebb1, &[]);
|
||||||
|
dfg.ins(cur).jump(ebb3, &[]);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb3);
|
||||||
|
dfg.ins(cur).brnz(cond, ebb0, &[]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut loop_analysis = LoopAnalysis::new();
|
||||||
|
let mut cfg = ControlFlowGraph::new();
|
||||||
|
let mut domtree = DominatorTree::new();
|
||||||
|
cfg.compute(&func);
|
||||||
|
domtree.compute(&func, &cfg);
|
||||||
|
loop_analysis.compute(&func, &cfg, &domtree);
|
||||||
|
|
||||||
|
let loops = loop_analysis.loops().collect::<Vec<Loop>>();
|
||||||
|
assert_eq!(loops.len(), 2);
|
||||||
|
assert_eq!(loop_analysis.loop_header(loops[0]), ebb0);
|
||||||
|
assert_eq!(loop_analysis.loop_header(loops[1]), ebb1);
|
||||||
|
assert_eq!(loop_analysis.loop_parent(loops[1]), Some(loops[0]));
|
||||||
|
assert_eq!(loop_analysis.loop_parent(loops[0]), None);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb0, loops[0]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb0, loops[1]), false);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb1, loops[1]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb1, loops[0]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb2, loops[1]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb2, loops[0]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb3, loops[0]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb0, loops[1]), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_loop_detection() {
|
||||||
|
let mut func = Function::new();
|
||||||
|
let ebb0 = func.dfg.make_ebb();
|
||||||
|
let ebb1 = func.dfg.make_ebb();
|
||||||
|
let ebb2 = func.dfg.make_ebb();
|
||||||
|
let ebb3 = func.dfg.make_ebb();
|
||||||
|
let ebb4 = func.dfg.make_ebb();
|
||||||
|
let ebb5 = func.dfg.make_ebb();
|
||||||
|
let cond = func.dfg.append_ebb_arg(ebb0, types::I32);
|
||||||
|
|
||||||
|
{
|
||||||
|
let dfg = &mut func.dfg;
|
||||||
|
let cur = &mut Cursor::new(&mut func.layout);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb0);
|
||||||
|
dfg.ins(cur).brnz(cond, ebb1, &[]);
|
||||||
|
dfg.ins(cur).jump(ebb3, &[]);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb1);
|
||||||
|
dfg.ins(cur).jump(ebb2, &[]);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb2);
|
||||||
|
dfg.ins(cur).brnz(cond, ebb1, &[]);
|
||||||
|
dfg.ins(cur).jump(ebb5, &[]);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb3);
|
||||||
|
dfg.ins(cur).jump(ebb4, &[]);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb4);
|
||||||
|
dfg.ins(cur).brnz(cond, ebb3, &[]);
|
||||||
|
dfg.ins(cur).jump(ebb5, &[]);
|
||||||
|
|
||||||
|
cur.insert_ebb(ebb5);
|
||||||
|
dfg.ins(cur).brnz(cond, ebb0, &[]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut loop_analysis = LoopAnalysis::new();
|
||||||
|
let mut cfg = ControlFlowGraph::new();
|
||||||
|
let mut domtree = DominatorTree::new();
|
||||||
|
cfg.compute(&func);
|
||||||
|
domtree.compute(&func, &cfg);
|
||||||
|
loop_analysis.compute(&func, &cfg, &domtree);
|
||||||
|
|
||||||
|
let loops = loop_analysis.loops().collect::<Vec<Loop>>();
|
||||||
|
assert_eq!(loops.len(), 3);
|
||||||
|
assert_eq!(loop_analysis.loop_header(loops[0]), ebb0);
|
||||||
|
assert_eq!(loop_analysis.loop_header(loops[1]), ebb1);
|
||||||
|
assert_eq!(loop_analysis.loop_header(loops[2]), ebb3);
|
||||||
|
assert_eq!(loop_analysis.loop_parent(loops[1]), Some(loops[0]));
|
||||||
|
assert_eq!(loop_analysis.loop_parent(loops[2]), Some(loops[0]));
|
||||||
|
assert_eq!(loop_analysis.loop_parent(loops[0]), None);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb0, loops[0]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb1, loops[1]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb2, loops[1]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb3, loops[2]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb4, loops[2]), true);
|
||||||
|
assert_eq!(loop_analysis.is_in_loop(ebb5, loops[0]), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user