Skip to main content

cubecl_ir/interfaces/
control_flow.rs

1use core::{fmt, ops::Range};
2
3use crate::{dialect::RegionPtrExt, prelude::*};
4use derive_more::From;
5use pliron::{
6    attribute::AttrObj,
7    builtin::ops::FuncOp,
8    graph::HasLabel,
9    linked_list::ContainsLinkedList,
10    printable::{self, Printable},
11    region::Region,
12    utils::table::IMap,
13    value::Use,
14};
15use smallvec::SmallVec;
16
17#[derive(Clone, Copy, PartialEq, Eq, Hash)]
18pub enum RegionPredecessor {
19    Parent,
20    Terminator(TraitOp<dyn RegionBranchTerminatorOpInterface>),
21}
22
23impl Printable for RegionPredecessor {
24    fn fmt(&self, ctx: &Context, _: &printable::State, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            RegionPredecessor::Parent => f.write_str("Parent"),
27            RegionPredecessor::Terminator(op) => {
28                let block = op.get_operation().deref(ctx).get_parent_block().unwrap();
29                write!(f, "{}", block.label(ctx))
30            }
31        }
32    }
33}
34
35#[derive(Clone, Copy, PartialEq, Eq, Hash, From)]
36pub enum RegionSuccessor {
37    Region(Ptr<Region>),
38    AfterOp,
39}
40
41#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
42pub struct InvocationBounds {
43    lower_bound: usize,
44    upper_bound: Option<usize>,
45}
46
47impl InvocationBounds {
48    pub fn unknown() -> Self {
49        Self {
50            lower_bound: 0,
51            upper_bound: None,
52        }
53    }
54
55    pub fn never() -> Self {
56        Self {
57            lower_bound: 0,
58            upper_bound: Some(0),
59        }
60    }
61
62    pub fn once() -> Self {
63        Self {
64            lower_bound: 1,
65            upper_bound: Some(1),
66        }
67    }
68
69    pub fn zero_or_one() -> Self {
70        Self {
71            lower_bound: 0,
72            upper_bound: Some(1),
73        }
74    }
75}
76
77#[op_interface]
78pub trait CallableOpInterface {
79    verify_op_succ!();
80    fn callable_region(&self, ctx: &Context) -> Option<Ptr<Region>>;
81    fn argument_types(&self, ctx: &Context) -> Vec<TypeHandle>;
82    fn result_types(&self, ctx: &Context) -> Vec<TypeHandle>;
83}
84
85#[op_interface_impl]
86impl CallableOpInterface for FuncOp {
87    fn callable_region(&self, ctx: &Context) -> Option<Ptr<Region>> {
88        Some(self.get_region(ctx))
89    }
90
91    fn argument_types(&self, ctx: &Context) -> Vec<TypeHandle> {
92        let ty = self.get_attr_builtin_func_type(ctx).unwrap().get_type(ctx);
93        type_cast::<dyn FunctionTypeInterface>(&*ty.deref(ctx))
94            .unwrap()
95            .arg_types()
96    }
97
98    fn result_types(&self, ctx: &Context) -> Vec<TypeHandle> {
99        let ty = self.get_attr_builtin_func_type(ctx).unwrap().get_type(ctx);
100        type_cast::<dyn FunctionTypeInterface>(&*ty.deref(ctx))
101            .unwrap()
102            .res_types()
103    }
104}
105
106#[format]
107#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
108pub enum SymbolVisibility {
109    Public,
110    Private,
111    Nested,
112}
113
114#[op_interface]
115pub trait SymbolOpInterface: pliron::builtin::op_interfaces::SymbolOpInterface {
116    verify_op_succ!();
117
118    fn get_visibility(&self, ctx: &Context) -> SymbolVisibility;
119    fn set_visibility(&self, ctx: &mut Context, visibility: SymbolVisibility);
120
121    fn is_public(&self, ctx: &Context) -> bool {
122        matches!(self.get_visibility(ctx), SymbolVisibility::Public)
123    }
124
125    fn is_private(&self, ctx: &Context) -> bool {
126        matches!(self.get_visibility(ctx), SymbolVisibility::Private)
127    }
128
129    fn is_nested(&self, ctx: &Context) -> bool {
130        matches!(self.get_visibility(ctx), SymbolVisibility::Nested)
131    }
132}
133
134#[op_interface]
135pub trait CallOpInterface: pliron::builtin::op_interfaces::CallOpInterface {
136    verify_op_succ!();
137    fn args_range(&self, ctx: &Context) -> Range<usize>;
138}
139
140/// This interface provides information for region-holding operations that
141/// exhibit branching behavior between held regions. It models the control flow
142/// edges between regions (and between the op and its regions), as well as the
143/// data flow (value propagation) that occurs along those control flow edges.
144///
145/// This interface is meant to model well-defined cases of control-flow and
146/// value propagation, where what occurs along control-flow edges is assumed to
147/// be side-effect free.
148///
149/// A "region branch point" indicates the point from which a branch (edge)
150/// originates. It can indicate:
151/// 1. A `RegionBranchTerminatorOpInterface` terminator in any of the
152///    immediately nested regions of this op.
153/// 2. `RegionBranchPoint::parent()`: the branch originates from outside of the
154///    op, i.e., when first executing this op.
155///
156/// When branching from a region branch point to a region successor, the
157/// "successor operands" to be forwarded from the region branch point can be
158/// specified with `getEntrySuccessorOperands` /
159/// `RegionBranchTerminatorOpInterface::getSuccessorOperands`.
160///
161/// A "region successor" indicates the target of a branch. It can indicate:
162/// 1. A region of this op.
163/// 2. A parent operation, i.e., the control flow leaves/resumes after that op.
164///
165/// The SSA values to which successor operands are forwarded are called
166/// "successor inputs".
167///
168/// By default, successor operands and successor block arguments/successor
169/// results must have the same type. `areTypesCompatible` can be implemented to
170/// allow non-equal types.
171///
172///
173/// Note: This interface works in conjunction with
174/// `RegionBranchTerminatorOpInterface`. All immediately nested block
175/// terminators that model branching between regions must implement the
176/// `RegionBranchTerminatorOpInterface`. Otherwise, analyses/transformations
177/// may miss control flow edges and produce incorrect results. Not every block
178/// terminator is necessarily a region branch terminator: e.g., in the presence
179/// of unstructured control flow, a block terminator could indicate a branch to
180/// a different block within the same region.
181///
182/// Example:
183///
184/// ```ignore
185/// %r = scf.for %iv = %lb to %ub step %step iter_args(%a = %b)
186///     -> tensor<5xf32> {
187///   ...
188///   scf.yield %c : tensor<5xf32>
189/// }
190/// ```
191///
192/// `scf.for` has one region. There are two region branch points with two
193/// identical region successors:
194/// * parent => op(%r), region0(%a)
195/// * `scf.yield` => op(%r), region0(%a)
196///
197/// `%a` and %r are successor inputs. `%b` is an entry successor operand. `%c`
198/// is a successor operand.
199#[op_interface]
200pub trait RegionBranchOpInterface {
201    verify_op_succ!();
202
203    /// Returns the operands of this operation that are forwarded to the
204    /// successor inputs when branching to `successor`. `successor` is
205    /// guaranteed to be among the successors that are returned by
206    /// `entry_successor_regions`/`successor_regions(parent())`.
207    ///
208    /// Example: In the above example, this method returns the operand %b of the
209    /// `scf.for` op, regardless of the value of `successor`. I.e., this op always
210    /// forwards the same operands, regardless of whether the loop has 0 or more
211    /// iterations.
212    fn entry_successor_operands(
213        &self,
214        ctx: &Context,
215        successor: RegionSuccessor,
216    ) -> Vec<Use<Value>> {
217        let _ = (ctx, successor);
218        vec![]
219    }
220
221    /// Returns all potential region successors when first executing the op.
222    ///
223    /// Unlike `successor_regions`, this method also receives the constant
224    /// operands of this op (one entry per operand, `None` if the operand has
225    /// no/unknown constant value). The implementation may use this information
226    /// to filter out successors. By default, it simply dispatches to
227    /// `successor_regions`.
228    ///
229    /// Note: The control flow does not necessarily have to enter any region of
230    /// this op.
231    ///
232    /// Example: In the above example, this method may return two region
233    /// region successors: the single region of the `scf.for` op and the
234    /// `scf.for` operation (that implements this interface). If %lb, %ub, %step
235    /// are constants and it can be determined the loop does not have any
236    /// iterations, this method may choose to return only this operation.
237    /// Similarly, if it can be determined that the loop has at least one
238    /// iteration, this method may choose to return only the region of the loop.
239    fn entry_successor_regions(
240        &self,
241        ctx: &Context,
242        operands: &[Option<AttrObj>],
243    ) -> Vec<RegionSuccessor> {
244        let _ = (ctx, operands);
245        self.successor_regions(ctx, RegionPredecessor::Parent)
246    }
247
248    /// Returns all potential region successors when branching from `predecessor`.
249    /// These are the regions that may be selected during the flow of control.
250    ///
251    /// When `pred = RegionPredecessor::Parent`, this method returns the
252    /// region successors when entering the operation. Otherwise, this method
253    /// returns the successor regions when branching from the region indicated
254    /// by `pred`.
255    ///
256    /// Example: In the above example, this method returns the region of the
257    /// `scf.for` and `parent` for either region branch point. An implementation
258    /// may choose to filter out region successors when it is statically known
259    /// (e.g., by examining the operands of this op) that those successors are
260    /// not branched to.
261    fn successor_regions(&self, ctx: &Context, pred: RegionPredecessor) -> Vec<RegionSuccessor>;
262
263    /// Returns all potential region successors when branching from any
264    /// terminator in `region`.
265    fn all_successor_regions_of_region(
266        &self,
267        ctx: &Context,
268        region: Ptr<Region>,
269    ) -> Vec<RegionSuccessor> {
270        let mut res = vec![];
271        for block in region.deref(ctx).iter(ctx) {
272            let Some(term) = block.deref(ctx).get_terminator(ctx) else {
273                continue;
274            };
275            if let Some(terminator) = TraitOp::try_from_op(term, ctx) {
276                res.extend(self.successor_regions(ctx, RegionPredecessor::Terminator(terminator)));
277            }
278        }
279        res
280    }
281
282    /// Return all successor inputs for the given region successor. If the
283    /// given region successor is a region, then the returned values are block
284    /// arguments. Otherwise, if the given region successor is an operation,
285    /// the returned values are op results.
286    fn successor_inputs(&self, ctx: &Context, successor: RegionSuccessor) -> Vec<Value>;
287
288    /// Returns all potential branching points (predecessors) for a given
289    /// region successor.
290    fn predecessors(&self, ctx: &Context, successor: RegionSuccessor) -> Vec<RegionPredecessor> {
291        let mut predecessors = vec![];
292        for pred in all_region_predecessors(self.get_operation(), ctx) {
293            let successors = self.successor_regions(ctx, pred);
294            let is_pred = successors.iter().any(|succ| succ == &successor);
295            if is_pred {
296                predecessors.push(pred);
297            }
298        }
299        predecessors
300    }
301
302    /// Returns all potential values across all (predecessors) for a given successor
303    /// input.
304    fn predecessor_values(
305        &self,
306        ctx: &Context,
307        successor: RegionSuccessor,
308        index: usize,
309    ) -> Vec<Use<Value>> {
310        let mut predecessor_values = vec![];
311        let predecessors = self.predecessors(ctx, successor);
312        for predecessor in predecessors {
313            match predecessor {
314                RegionPredecessor::Parent => {
315                    predecessor_values.push(self.entry_successor_operands(ctx, successor)[index]);
316                }
317                RegionPredecessor::Terminator(term) => {
318                    predecessor_values.push(term.successor_operands(ctx, successor)[index]);
319                }
320            }
321        }
322        predecessor_values
323    }
324
325    /// Populates `invocationBounds` with the minimum and maximum number of
326    /// times this operation will invoke the attached regions (assuming the
327    /// regions yield normally, i.e. do not abort or invoke an infinite loop).
328    /// The minimum number of invocations is at least 0. If the maximum number
329    /// of invocations cannot be statically determined, then it will be set to
330    /// `InvocationBounds::getUnknown()`.
331    ///
332    /// This method also passes along the constant operands of this op.
333    /// `operands` contains an entry for every operand of this op, with a null
334    /// attribute if the operand has no constant value.
335    ///
336    /// This method may be called speculatively on operations where the provided
337    /// operands are not necessarily the same as the operation's current
338    /// operands. This may occur in analyses that wish to determine "what would
339    /// be the region invocations if these were the operands?"
340    fn region_invocation_bounds(
341        &self,
342        ctx: &Context,
343        operands: &[Option<AttrObj>],
344    ) -> Vec<InvocationBounds> {
345        let _ = (ctx, operands);
346        vec![InvocationBounds::unknown()]
347    }
348
349    /// This method is called to compare types along control-flow edges. By
350    /// default, the types are checked as equal.
351    fn are_types_compatible(&self, ctx: &Context, lhs: TypeHandle, rhs: TypeHandle) -> bool {
352        let _ = ctx;
353        lhs == rhs
354    }
355}
356
357type RegionBranchSuccessorMapping = IMap<Use<Value>, SmallVec<[Value; 4]>>;
358
359impl dyn RegionBranchOpInterface {
360    /// Return the successor operands from the source branch point to the
361    /// destination region successor.
362    ///
363    /// If the branch point is the parent op, this function returns entry
364    /// successor operands of this op. Otherwise, it returns successor operands
365    /// of the respective terminator.
366    pub fn successor_operands(
367        &self,
368        ctx: &Context,
369        src: RegionPredecessor,
370        dest: RegionSuccessor,
371    ) -> Vec<Use<Value>> {
372        match src {
373            RegionPredecessor::Parent => self.entry_successor_operands(ctx, dest),
374            RegionPredecessor::Terminator(term) => term.successor_operands(ctx, dest),
375        }
376    }
377
378    /// Return all successor inputs for the given region successor.
379    ///
380    /// If the "successor" is a region, it will return non-forwarded arguments,
381    /// if it is a "parent", it will return non-forwarded results.
382    pub fn non_successor_inputs(&self, ctx: &Context, successor: RegionSuccessor) -> Vec<Value> {
383        let results = match successor {
384            RegionSuccessor::Region(region) => region.arguments(ctx),
385            RegionSuccessor::AfterOp => {
386                let parent = self.get_operation().deref(ctx).get_parent_op(ctx).unwrap();
387                parent.results(ctx)
388            }
389        };
390        let successor_inputs = self.successor_inputs(ctx, successor);
391        results
392            .into_iter()
393            .enumerate()
394            .filter(|(i, _)| !successor_inputs.iter().any(|it| it.find_index(ctx) == *i))
395            .map(|it| it.1)
396            .collect()
397    }
398    /// Return all possible region branch points: the region branch op itself
399    /// and all region branch terminators.
400    pub fn all_region_predecessors(&self, ctx: &Context) -> Vec<RegionPredecessor> {
401        all_region_predecessors(self.get_operation(), ctx)
402    }
403
404    pub fn successor_operand_input_mapping(
405        &self,
406        ctx: &Context,
407        src: RegionPredecessor,
408    ) -> RegionBranchSuccessorMapping {
409        let successors = self.successor_regions(ctx, src);
410        let mut mapping = RegionBranchSuccessorMapping::default();
411        for dst in successors {
412            let operands = self.successor_operands(ctx, src, dst);
413            let inputs = self.successor_inputs(ctx, dst);
414            assert_eq!(
415                operands.len(),
416                inputs.len(),
417                "expected the same number of operands and inputs"
418            );
419            for (operand, input) in operands.into_iter().zip(inputs) {
420                mapping.entry(operand).or_default().push(input);
421            }
422        }
423        mapping
424    }
425}
426
427pub fn all_region_predecessors(op: Ptr<Operation>, ctx: &Context) -> Vec<RegionPredecessor> {
428    let mut predecessors = vec![RegionPredecessor::Parent];
429    for region in op.deref(ctx).regions() {
430        for block in region.deref(ctx).iter(ctx) {
431            if let Some(term) = block.deref(ctx).get_terminator(ctx)
432                && let Some(term) =
433                    TraitOp::<dyn RegionBranchTerminatorOpInterface>::try_from_op(term, ctx)
434            {
435                predecessors.push(RegionPredecessor::Terminator(term));
436            }
437        }
438    }
439    predecessors
440}
441
442/// This interface provides information for branching terminator operations
443/// in the presence of a parent `RegionBranchOpInterface` implementation. It
444/// acts as a marker for valid region branch points and specifies which
445/// operands are passed to which region successor.
446///
447/// Note: If an operation does not implement the
448/// `RegionBranchTerminatorOpInterface`, then that op has no region successors.
449/// (However, there may be other block terminators in the same region that
450/// implement the `RegionBranchTerminatorOpInterface`, so the enclosing region
451/// may have region successors.)
452#[op_interface]
453pub trait RegionBranchTerminatorOpInterface {
454    verify_op_succ!();
455
456    /// Returns a range of operands that are semantically "returned" by passing
457    /// them to the region successor.
458    fn successor_operands(&self, ctx: &Context, successor: RegionSuccessor) -> Vec<Use<Value>>;
459
460    /// Returns all potential region successors that are branched to after this
461    /// terminator based on the given constant operands.
462    ///
463    /// This method also receives the constant operands of this op (one entry
464    /// per operand, `None` if the operand has no/unknown constant value). The
465    /// implementation may use this information to filter out successors.
466    /// By default, it simply dispatches to the parent
467    /// `RegionBranchOpInterface`'s `successor_regions` implementation.
468    fn successor_regions(
469        &self,
470        ctx: &Context,
471        operands: &[Option<AttrObj>],
472    ) -> Vec<RegionSuccessor> {
473        let _ = operands;
474        let op = TraitOp::try_from_op(self.get_operation(), ctx).unwrap();
475        let parent = self.get_operation().deref(ctx).get_parent_op(ctx).unwrap();
476        let parent = parent.dyn_op(ctx);
477        op_cast::<dyn RegionBranchOpInterface>(&*parent)
478            .unwrap()
479            .successor_regions(ctx, RegionPredecessor::Terminator(op))
480    }
481}