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}