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