calyx_opt/passes/
static_promotion.rs

1use crate::analysis::{
2    CompactionAnalysis, InferenceAnalysis, PromotionAnalysis,
3};
4use crate::traversal::{
5    Action, ConstructVisitor, Named, Order, ParseVal, PassOpt, VisResult,
6    Visitor,
7};
8use calyx_ir::{self as ir, LibrarySignatures};
9use calyx_utils::CalyxResult;
10use ir::GetAttributes;
11use itertools::Itertools;
12use std::num::NonZeroU64;
13use std::rc::Rc;
14
15const APPROX_ENABLE_SIZE: u64 = 1;
16const APPROX_IF_SIZE: u64 = 3;
17const APPROX_WHILE_REPEAT_SIZE: u64 = 3;
18
19/// Promote control to static when (conservatively) possible, using @promote_static
20/// annotations from `infer_static`.
21///
22/// Promotion occurs the following policies:
23/// 1. ``Threshold'': How large the island must be. We have three const
24/// defined as heuristics to measure approximately how big each control program
25/// is. It must be larger than that threshold.
26/// 2. ``Cycle limit": The maximum number of cycles the island can be when we
27/// promote it.
28/// 3. ``If Diff Limit": The maximum difference in latency between if statments
29/// that we can tolerate to promote it.
30///
31pub struct StaticPromotion {
32    /// An InferenceAnalysis object so that we can re-infer the latencies of
33    /// certain components.
34    inference_analysis: InferenceAnalysis,
35    /// PromotionAnalysis object so that we can easily infer control, and keep
36    /// track of which groups were promoted.
37    promotion_analysis: PromotionAnalysis,
38    /// CompactionAnalysis object so that we can easily perform compaction
39    compaction_analysis: CompactionAnalysis,
40    /// Threshold for promotion
41    threshold: u64,
42    /// Threshold for difference in latency for if statements
43    if_diff_limit: Option<u64>,
44    /// Whether we should stop promoting when we see a loop.
45    cycle_limit: Option<u64>,
46    /// Whether to perform compaction. True by default
47    compaction: bool,
48}
49
50// Override constructor to build latency_data information from the primitives
51// library.
52impl ConstructVisitor for StaticPromotion {
53    fn from(ctx: &ir::Context) -> CalyxResult<Self> {
54        let opts = Self::get_opts(ctx);
55        Ok(StaticPromotion {
56            inference_analysis: InferenceAnalysis::from_ctx(ctx),
57            promotion_analysis: PromotionAnalysis::default(),
58            compaction_analysis: CompactionAnalysis::default(),
59            threshold: opts["threshold"].pos_num().unwrap(),
60            if_diff_limit: opts["if-diff-limit"].pos_num(),
61            cycle_limit: opts["cycle-limit"].pos_num(),
62            compaction: opts["compaction"].bool(),
63        })
64    }
65
66    // This pass shared information between components
67    fn clear_data(&mut self) {
68        self.promotion_analysis = PromotionAnalysis::default();
69        self.compaction_analysis = CompactionAnalysis::default();
70    }
71}
72
73impl Named for StaticPromotion {
74    fn name() -> &'static str {
75        "static-promotion"
76    }
77
78    fn description() -> &'static str {
79        "promote dynamic control programs to static when possible"
80    }
81
82    fn opts() -> Vec<PassOpt> {
83        vec![
84            PassOpt::new(
85                "threshold",
86                "minimum number of groups needed to promote a dynamic control program to static",
87                ParseVal::Num(1),
88                PassOpt::parse_num,
89            ),
90            PassOpt::new(
91                "cycle-limit",
92                "maximum number of cycles to promote a dynamic control program to static",
93                ParseVal::Num(33554432),
94                PassOpt::parse_num,
95            ),
96            PassOpt::new(
97                "if-diff-limit",
98                "the maximum difference between if branches that we tolerate for promotion",
99                ParseVal::Num(1),
100                PassOpt::parse_num,
101            ),
102            PassOpt::new(
103                "compaction",
104                "Whether to perform compaction.  True by Default ",
105                ParseVal::Bool(true),
106                PassOpt::parse_bool,
107            )
108        ]
109    }
110}
111
112impl StaticPromotion {
113    // Remove @promotable(n) attribute if n is above the cycle limit, since
114    // we know we will never promote such a control.
115    // This can be helpful to the pass when applying the heuristics.
116    fn remove_large_promotables(&self, c: &mut ir::Control) {
117        if let Some(pr) = c.get_attribute(ir::NumAttr::Promotable) {
118            if !self.within_cycle_limit(pr) {
119                c.get_mut_attributes().remove(ir::NumAttr::Promotable)
120            }
121        }
122    }
123
124    fn within_cycle_limit(&self, latency: u64) -> bool {
125        if self.cycle_limit.is_none() {
126            return true;
127        }
128        latency < self.cycle_limit.unwrap()
129    }
130
131    fn within_if_diff_limit(&self, diff: u64) -> bool {
132        if self.if_diff_limit.is_none() {
133            return true;
134        }
135        diff <= self.if_diff_limit.unwrap()
136    }
137
138    fn fits_heuristics(&self, c: &ir::Control) -> bool {
139        let approx_size = Self::approx_size(c);
140        let latency = PromotionAnalysis::get_inferred_latency(c);
141        self.within_cycle_limit(latency) && approx_size > self.threshold
142    }
143
144    fn approx_size_static(sc: &ir::StaticControl, promoted: bool) -> u64 {
145        if !(sc.get_attributes().has(ir::BoolAttr::Promoted) || promoted) {
146            return APPROX_ENABLE_SIZE;
147        }
148        match sc {
149            ir::StaticControl::Empty(_) => 0,
150            ir::StaticControl::Enable(_) | ir::StaticControl::Invoke(_) => {
151                APPROX_ENABLE_SIZE
152            }
153            ir::StaticControl::Repeat(ir::StaticRepeat { body, .. }) => {
154                Self::approx_size_static(body, true) + APPROX_WHILE_REPEAT_SIZE
155            }
156            ir::StaticControl::If(ir::StaticIf {
157                tbranch, fbranch, ..
158            }) => {
159                Self::approx_size_static(tbranch, true)
160                    + Self::approx_size_static(fbranch, true)
161                    + APPROX_IF_SIZE
162            }
163            ir::StaticControl::Par(ir::StaticPar { stmts, .. })
164            | ir::StaticControl::Seq(ir::StaticSeq { stmts, .. }) => stmts
165                .iter()
166                .map(|stmt| Self::approx_size_static(stmt, true))
167                .sum(),
168        }
169    }
170
171    /// Calculates the approximate "size" of the control statements.
172    /// Tries to approximate the number of dynamic FSM transitions that will occur
173    fn approx_size(c: &ir::Control) -> u64 {
174        match c {
175            ir::Control::Empty(_) => 0,
176            ir::Control::Enable(_) | ir::Control::Invoke(_) => {
177                APPROX_ENABLE_SIZE
178            }
179            ir::Control::Seq(ir::Seq { stmts, .. })
180            | ir::Control::Par(ir::Par { stmts, .. }) => {
181                stmts.iter().map(Self::approx_size).sum()
182            }
183            ir::Control::Repeat(ir::Repeat { body, .. })
184            | ir::Control::While(ir::While { body, .. }) => {
185                Self::approx_size(body) + APPROX_WHILE_REPEAT_SIZE
186            }
187            ir::Control::If(ir::If {
188                tbranch, fbranch, ..
189            }) => {
190                Self::approx_size(tbranch)
191                    + Self::approx_size(fbranch)
192                    + APPROX_IF_SIZE
193            }
194            ir::Control::Static(sc) => Self::approx_size_static(sc, false),
195        }
196    }
197
198    /// Uses `approx_size` function to sum the sizes of the control statements
199    /// in the given vector
200    fn approx_control_vec_size(v: &[ir::Control]) -> u64 {
201        v.iter().map(Self::approx_size).sum()
202    }
203
204    fn promote_seq_heuristic(
205        &mut self,
206        builder: &mut ir::Builder,
207        mut control_vec: Vec<ir::Control>,
208    ) -> Vec<ir::Control> {
209        if control_vec.is_empty() {
210            // Base case len == 0
211            vec![]
212        } else if control_vec.len() == 1 {
213            // Base case len == 1.
214            // Promote if it fits the promotion heuristics.
215            let mut stmt = control_vec.pop().unwrap();
216            if self.fits_heuristics(&stmt) {
217                vec![ir::Control::Static(
218                    self.promotion_analysis
219                        .convert_to_static(&mut stmt, builder),
220                )]
221            } else {
222                vec![stmt]
223            }
224        } else {
225            let mut possibly_compacted_ctrl = if self.compaction {
226                // If compaction is turned on, then we possibly compact
227                self.compaction_analysis.compact_control_vec(
228                    control_vec,
229                    &mut self.promotion_analysis,
230                    builder,
231                )
232            } else {
233                // Otherwise it's just the og control vec
234                control_vec
235            };
236            // If length == 1 this means we have a vec[compacted_static_par],
237            // so we can return.
238            // (Note that the og control_vec must be of length >=2, since we
239            // have already checked for two base cases.)
240            if possibly_compacted_ctrl.len() == 1 {
241                return possibly_compacted_ctrl;
242            }
243            // Otherwise we cannot compact at all,
244            // so go through normal promotion heuristic analysis.
245            if Self::approx_control_vec_size(&possibly_compacted_ctrl)
246                <= self.threshold
247            {
248                // Too small to be promoted, return as is
249                return possibly_compacted_ctrl;
250            } else if !self.within_cycle_limit(
251                possibly_compacted_ctrl
252                    .iter()
253                    .map(PromotionAnalysis::get_inferred_latency)
254                    .sum(),
255            ) {
256                // Too large, try to break up
257                let right = possibly_compacted_ctrl
258                    .split_off(possibly_compacted_ctrl.len() / 2);
259                let mut left_res = self
260                    .promote_seq_heuristic(builder, possibly_compacted_ctrl);
261                let right_res = self.promote_seq_heuristic(builder, right);
262                left_res.extend(right_res);
263                return left_res;
264            }
265            // Correct size, convert the entire vec
266            let s_seq_stmts = self
267                .promotion_analysis
268                .convert_vec_to_static(builder, possibly_compacted_ctrl);
269            let latency = s_seq_stmts.iter().map(|sc| sc.get_latency()).sum();
270            let sseq = ir::Control::Static(ir::StaticControl::seq(
271                s_seq_stmts,
272                latency,
273            ));
274            vec![sseq]
275        }
276    }
277
278    /// First checks if the vec of control statements meets the self.threshold
279    /// and is within self.cycle_limit
280    /// If so, converts vec of control to a static par, and returns a vec containing
281    /// the static par.
282    /// Otherwise, just returns the vec without changing it.
283    fn promote_vec_par_heuristic(
284        &mut self,
285        builder: &mut ir::Builder,
286        mut control_vec: Vec<ir::Control>,
287    ) -> Vec<ir::Control> {
288        if control_vec.is_empty() {
289            // Base case
290            return vec![];
291        } else if control_vec.len() == 1 {
292            return vec![control_vec.pop().unwrap()];
293        } else if Self::approx_control_vec_size(&control_vec) <= self.threshold
294        {
295            // Too small to be promoted, return as is
296            return control_vec;
297        } else if !self.within_cycle_limit(
298            control_vec
299                .iter()
300                .map(PromotionAnalysis::get_inferred_latency)
301                .max()
302                .unwrap_or_else(|| unreachable!("Empty Par Block")),
303        ) {
304            // Too large to be promoted, take out largest thread and try to promote rest.
305            // Can safely unwrap bc we already checked for an empty vector.
306            let (index, _) = control_vec
307                .iter()
308                .enumerate()
309                .max_by_key(|&(_, c)| Self::approx_size(c))
310                .unwrap();
311            // Pop the largest element from the vector
312            let largest_thread = control_vec.remove(index);
313            let mut left = self.promote_vec_par_heuristic(builder, control_vec);
314            left.push(largest_thread);
315            return left;
316        }
317        // Convert vec to static par
318        let s_par_stmts = self
319            .promotion_analysis
320            .convert_vec_to_static(builder, control_vec);
321        let latency = s_par_stmts
322            .iter()
323            .map(|sc| sc.get_latency())
324            .max()
325            .unwrap_or_else(|| unreachable!("empty par block"));
326        let spar =
327            ir::Control::Static(ir::StaticControl::par(s_par_stmts, latency));
328        vec![spar]
329    }
330}
331
332impl Visitor for StaticPromotion {
333    // Require post order traversal of components to ensure `invoke` nodes
334    // get timing information for components.
335    fn iteration_order() -> Order {
336        Order::Post
337    }
338
339    fn finish(
340        &mut self,
341        comp: &mut ir::Component,
342        _lib: &LibrarySignatures,
343        _comps: &[ir::Component],
344    ) -> VisResult {
345        if comp.name != "main" {
346            let comp_sig = comp.signature.borrow();
347            if comp.control.borrow().is_static() {
348                // We ended up promoting it
349                if !comp.is_static() {
350                    // Need this attribute for a weird, in-between state.
351                    // It has a known latency but also produces a done signal.
352                    comp.attributes.insert(ir::BoolAttr::Promoted, 1);
353                }
354                // (Possibly) new latency because of compaction
355                let new_latency = NonZeroU64::new(
356                    comp.control.borrow().get_latency().unwrap(),
357                )
358                .unwrap();
359                // This makes the component appear as a static<n> component.
360                comp.latency = Some(new_latency);
361                // Adjust inference analysis to account for this new latency.
362                self.inference_analysis
363                    .adjust_component((comp.name, new_latency.into()));
364            } else if !comp.control.borrow().is_empty() {
365                // This is for the case where we didn't end up promoting, so
366                // we remove it from our inference_analysis.
367                // Note that sometimes you can have components with only continuous
368                // assignments with @interval annotations: in that case,
369                // we don't want to remove our inference analysis.
370                self.inference_analysis.remove_component(comp.name);
371            };
372
373            let go_ports =
374                comp_sig.find_all_with_attr(ir::NumAttr::Go).collect_vec();
375            // Either we have upgraded component to static<n> or we have decided
376            // not to promote component at all. Either way, we can remove the
377            // @promotable attribute.
378            for go_port in go_ports {
379                go_port
380                    .borrow_mut()
381                    .attributes
382                    .remove(ir::NumAttr::Promotable);
383            }
384        }
385        // Remove @promotable attribute from control.
386        // Probably not necessary, since we'll ignore it anyways, but makes for
387        // cleaner code.
388        InferenceAnalysis::remove_promotable_attribute(
389            &mut comp.control.borrow_mut(),
390        );
391        Ok(Action::Continue)
392    }
393
394    fn start(
395        &mut self,
396        comp: &mut ir::Component,
397        _sigs: &LibrarySignatures,
398        _comps: &[ir::Component],
399    ) -> VisResult {
400        // Re-infer static timing based on the components we have updated in
401        // this pass.
402        self.inference_analysis.fixup_timing(comp);
403        // Update the continuous reads and writes
404        self.compaction_analysis.update_cont_read_writes(comp);
405        Ok(Action::Continue)
406    }
407
408    fn enable(
409        &mut self,
410        s: &mut ir::Enable,
411        comp: &mut ir::Component,
412        sigs: &LibrarySignatures,
413        _comps: &[ir::Component],
414    ) -> VisResult {
415        let mut builder = ir::Builder::new(comp, sigs);
416        if let Some(latency) = s.attributes.get(ir::NumAttr::Promotable) {
417            // Convert to static if enable is
418            // within cycle limit and size is above threshold.
419            if self.within_cycle_limit(latency)
420                && (APPROX_ENABLE_SIZE > self.threshold)
421            {
422                return Ok(Action::change(ir::Control::Static(
423                    self.promotion_analysis
424                        .convert_enable_to_static(s, &mut builder),
425                )));
426            }
427        }
428        Ok(Action::Continue)
429    }
430
431    fn invoke(
432        &mut self,
433        s: &mut ir::Invoke,
434        _comp: &mut ir::Component,
435        _sigs: &LibrarySignatures,
436        _comps: &[ir::Component],
437    ) -> VisResult {
438        if let Some(latency) = s.attributes.get(ir::NumAttr::Promotable) {
439            // Convert to static if within cycle limit and size is above threshold.
440            if self.within_cycle_limit(latency)
441                && (APPROX_ENABLE_SIZE > self.threshold)
442            {
443                return Ok(Action::change(ir::Control::Static(
444                    self.promotion_analysis.convert_invoke_to_static(s),
445                )));
446            }
447        }
448        Ok(Action::Continue)
449    }
450
451    fn finish_seq(
452        &mut self,
453        s: &mut ir::Seq,
454        comp: &mut ir::Component,
455        sigs: &LibrarySignatures,
456        _comps: &[ir::Component],
457    ) -> VisResult {
458        self.inference_analysis.fixup_seq(s);
459        // Remove @promotable attributes that are too large to be promoted.
460        // This helps the promotion heuristic make smarter decisions
461        s.stmts
462            .iter_mut()
463            .for_each(|c| self.remove_large_promotables(c));
464
465        let mut builder = ir::Builder::new(comp, sigs);
466        let old_stmts = std::mem::take(&mut s.stmts);
467        let mut new_stmts: Vec<ir::Control> = Vec::new();
468        let mut cur_vec: Vec<ir::Control> = Vec::new();
469        for stmt in old_stmts {
470            if PromotionAnalysis::can_be_promoted(&stmt) {
471                cur_vec.push(stmt);
472            } else {
473                // Use heuristics to decide how to promote this cur_vec of promotable stmts.
474                let possibly_promoted_stmts =
475                    self.promote_seq_heuristic(&mut builder, cur_vec);
476                new_stmts.extend(possibly_promoted_stmts);
477                // Add the current (non-promotable) stmt
478                new_stmts.push(stmt);
479                // New cur_vec
480                cur_vec = Vec::new();
481            }
482        }
483        new_stmts.extend(self.promote_seq_heuristic(&mut builder, cur_vec));
484        let mut new_ctrl = if new_stmts.len() == 1 {
485            new_stmts.pop().unwrap()
486        } else {
487            ir::Control::Seq(ir::Seq {
488                stmts: new_stmts,
489                attributes: ir::Attributes::default(),
490            })
491        };
492        self.inference_analysis.fixup_ctrl(&mut new_ctrl);
493        Ok(Action::change(new_ctrl))
494    }
495
496    fn finish_par(
497        &mut self,
498        s: &mut ir::Par,
499        comp: &mut ir::Component,
500        sigs: &LibrarySignatures,
501        _comps: &[ir::Component],
502    ) -> VisResult {
503        self.inference_analysis.fixup_par(s);
504
505        let mut builder = ir::Builder::new(comp, sigs);
506        // Check if entire par is promotable
507        if let Some(latency) = s.attributes.get(ir::NumAttr::Promotable) {
508            let approx_size: u64 = s.stmts.iter().map(Self::approx_size).sum();
509            if approx_size <= self.threshold {
510                // Par is too small to promote, continue.
511                return Ok(Action::Continue);
512            } else if self.within_cycle_limit(latency) {
513                // Promote entire par
514                let spar = ir::Control::Static(ir::StaticControl::par(
515                    self.promotion_analysis.convert_vec_to_static(
516                        &mut builder,
517                        std::mem::take(&mut s.stmts),
518                    ),
519                    latency,
520                ));
521                return Ok(Action::change(spar));
522            }
523        }
524        let mut new_stmts: Vec<ir::Control> = Vec::new();
525        // The par either a) takes too many cylces to promote entirely or
526        // b) has dynamic stmts in it. Either way, the solution is to
527        // break it up.
528        // Split the par into static and dynamic stmts, and use heuristics
529        // to choose whether to promote the static ones. This replacement will
530        // not have a `@promotable` attribute.
531        // This temporarily messes up  its parents' `@promotable`
532        // attribute, but this is fine since we know its parent will never try
533        // to promote it.
534        let (s_stmts, d_stmts): (Vec<ir::Control>, Vec<ir::Control>) = s
535            .stmts
536            .drain(..)
537            .partition(PromotionAnalysis::can_be_promoted);
538        new_stmts.extend(self.promote_vec_par_heuristic(&mut builder, s_stmts));
539        new_stmts.extend(d_stmts);
540        let new_par = ir::Control::Par(ir::Par {
541            stmts: new_stmts,
542            attributes: ir::Attributes::default(),
543        });
544        Ok(Action::change(new_par))
545    }
546
547    fn finish_if(
548        &mut self,
549        s: &mut ir::If,
550        comp: &mut ir::Component,
551        sigs: &LibrarySignatures,
552        _comps: &[ir::Component],
553    ) -> VisResult {
554        self.inference_analysis.fixup_if(s);
555        let mut builder = ir::Builder::new(comp, sigs);
556        if let Some(latency) = s.attributes.get(ir::NumAttr::Promotable) {
557            let approx_size_if = Self::approx_size(&s.tbranch)
558                + Self::approx_size(&s.fbranch)
559                + APPROX_IF_SIZE;
560            let branch_diff = PromotionAnalysis::get_inferred_latency(
561                &s.tbranch,
562            )
563            .abs_diff(PromotionAnalysis::get_inferred_latency(&s.fbranch));
564            if approx_size_if > self.threshold
565                && self.within_cycle_limit(latency)
566                && self.within_if_diff_limit(branch_diff)
567            {
568                // Meets size threshold so promote to static
569                let static_tbranch = self
570                    .promotion_analysis
571                    .convert_to_static(&mut s.tbranch, &mut builder);
572                let static_fbranch = self
573                    .promotion_analysis
574                    .convert_to_static(&mut s.fbranch, &mut builder);
575                return Ok(Action::change(ir::Control::Static(
576                    ir::StaticControl::static_if(
577                        Rc::clone(&s.port),
578                        Box::new(static_tbranch),
579                        Box::new(static_fbranch),
580                        latency,
581                    ),
582                )));
583            }
584            // If this takes too many cycles, then we will
585            // never promote this if statement, meaning we will never promote any
586            // of its parents. We can therefore safely remove the `@promotable` attribute.
587            // This isn't strictly necessary, but it is helpful for parent control
588            // programs applying heuristics.
589            if !(self.within_cycle_limit(latency)) {
590                s.attributes.remove(ir::NumAttr::Promotable);
591            }
592        }
593        Ok(Action::Continue)
594    }
595
596    // upgrades @bound while loops to static repeats
597    fn finish_while(
598        &mut self,
599        s: &mut ir::While,
600        comp: &mut ir::Component,
601        sigs: &LibrarySignatures,
602        _comps: &[ir::Component],
603    ) -> VisResult {
604        self.inference_analysis.fixup_while(s);
605
606        let mut builder = ir::Builder::new(comp, sigs);
607        // First check that while loop is promotable
608        if let Some(latency) = s.attributes.get(ir::NumAttr::Promotable) {
609            let approx_size =
610                Self::approx_size(&s.body) + APPROX_WHILE_REPEAT_SIZE;
611            // Then check that it fits the heuristics
612            if approx_size > self.threshold && self.within_cycle_limit(latency)
613            {
614                // Turn repeat into static repeat
615                let sc = self
616                    .promotion_analysis
617                    .convert_to_static(&mut s.body, &mut builder);
618                let static_repeat = ir::StaticControl::repeat(
619                    s.attributes.get(ir::NumAttr::Bound).unwrap_or_else(|| {
620                        unreachable!(
621                            "Unbounded loop has has @promotable attribute"
622                        )
623                    }),
624                    latency,
625                    Box::new(sc),
626                );
627                return Ok(Action::Change(Box::new(ir::Control::Static(
628                    static_repeat,
629                ))));
630            }
631            // If this takes too many cycles, then we will
632            // never promote this if statement, meaning we will never promote any
633            // of its parents. We can therefore safely remove the `@promotable` attribute.
634            // This isn't strictly necessary, but it is helpful for parent control
635            // programs applying heuristics.
636            if !(self.within_cycle_limit(latency)) {
637                s.attributes.remove(ir::NumAttr::Promotable);
638            }
639        }
640        Ok(Action::Continue)
641    }
642
643    // upgrades repeats with static bodies to static repeats
644    fn finish_repeat(
645        &mut self,
646        s: &mut ir::Repeat,
647        comp: &mut ir::Component,
648        sigs: &LibrarySignatures,
649        _comps: &[ir::Component],
650    ) -> VisResult {
651        self.inference_analysis.fixup_repeat(s);
652
653        let mut builder = ir::Builder::new(comp, sigs);
654        if let Some(latency) = s.attributes.get(ir::NumAttr::Promotable) {
655            let approx_size =
656                Self::approx_size(&s.body) + APPROX_WHILE_REPEAT_SIZE;
657            if approx_size > self.threshold && self.within_cycle_limit(latency)
658            {
659                // Meets size threshold, so turn repeat into static repeat
660                let sc = self
661                    .promotion_analysis
662                    .convert_to_static(&mut s.body, &mut builder);
663                let static_repeat = ir::StaticControl::repeat(
664                    s.num_repeats,
665                    latency,
666                    Box::new(sc),
667                );
668                return Ok(Action::Change(Box::new(ir::Control::Static(
669                    static_repeat,
670                ))));
671            }
672            // If this takes too many cycles, then we will
673            // never promote this if statement, meaning we will never promote any
674            // of its parents. We can therefore safely remove the `@promotable` attribute.
675            // This isn't strictly necessary, but it is helpful for parent control
676            // programs applying heuristics.
677            if !(self.within_cycle_limit(latency)) {
678                s.attributes.remove(ir::NumAttr::Promotable);
679            }
680        }
681        Ok(Action::Continue)
682    }
683}