calyx_opt/passes/
well_formed.rs

1use crate::traversal::{Action, ConstructVisitor, Named, VisResult, Visitor};
2use crate::traversal::{DiagnosticContext, DiagnosticPass, DiagnosticResult};
3use calyx_ir::{
4    self as ir, Cell, CellType, Component, GetAttributes, LibrarySignatures,
5    RESERVED_NAMES,
6};
7use calyx_ir::{BoolAttr, Seq};
8use calyx_utils::{CalyxResult, Error, WithPos};
9use ir::Nothing;
10use ir::StaticTiming;
11use itertools::Itertools;
12use linked_hash_map::LinkedHashMap;
13use std::collections::HashMap;
14use std::collections::HashSet;
15
16// given a port and a vec of components `comps`,
17// returns true if the port's parent is a static primitive
18// otherwise returns false
19fn port_is_static_prim(port: &ir::Port) -> bool {
20    // if port parent is hole then obviously not static
21    let parent_cell = match &port.parent {
22        ir::PortParent::Cell(cell_wref) => cell_wref.upgrade(),
23        ir::PortParent::Group(_)
24        | ir::PortParent::StaticGroup(_)
25        | ir::PortParent::FSM(_) => {
26            return false;
27        }
28    };
29    // if celltype is this component/constant, then obviously not static
30    // if primitive, then we can quickly check whether it is static
31    // if component, then we have to go throuch `comps` to see whether its static
32    // for some reason, need to store result in variable, otherwise it gives a
33    // lifetime error
34
35    match parent_cell.borrow().prototype {
36        ir::CellType::Primitive { latency, .. } => latency.is_some(),
37        ir::CellType::Component { .. }
38        | ir::CellType::ThisComponent
39        | ir::CellType::Constant { .. } => false,
40    }
41}
42
43#[derive(Default)]
44struct ActiveAssignments {
45    // Set of currently active assignments
46    assigns: Vec<ir::Assignment<Nothing>>,
47    // Stack representing the number of assignments added at each level
48    num_assigns: Vec<usize>,
49}
50impl ActiveAssignments {
51    /// Push a set of assignments to the stack.
52    pub fn push(&mut self, assign: &[ir::Assignment<Nothing>]) {
53        let prev_size = self.assigns.len();
54        self.assigns.extend(assign.iter().cloned());
55        // Number of assignments added at this level
56        self.num_assigns.push(self.assigns.len() - prev_size);
57    }
58
59    /// Pop the last set of assignments from the stack.
60    pub fn pop(&mut self) {
61        let num_assigns = self.num_assigns.pop().unwrap();
62        self.assigns.truncate(self.assigns.len() - num_assigns);
63    }
64
65    pub fn iter(&self) -> impl Iterator<Item = &ir::Assignment<Nothing>> {
66        self.assigns.iter()
67    }
68}
69
70/// Pass to check if the program is well-formed.
71///
72/// Catches the following errors:
73/// 1. Programs that don't use a defined group or combinational group.
74/// 2. Groups that don't write to their done signal.
75/// 3. Groups that write to another group's done signal.
76/// 4. Ref cells that have unallowed types.
77/// 5. Invoking components with unmentioned ref cells.
78/// 6. Invoking components with wrong ref cell name.
79/// 7. Invoking components with impatible fed-in cell type for ref cells.
80pub struct WellFormed {
81    /// Reserved names
82    reserved_names: HashSet<ir::Id>,
83    /// Names of the groups that have been used in the control.
84    used_groups: HashSet<ir::Id>,
85    /// Names of combinational groups used in the control.
86    used_comb_groups: HashSet<ir::Id>,
87    /// ref cells of components used in the control. Used for type checking.
88    ref_cells: HashMap<ir::Id, LinkedHashMap<ir::Id, Cell>>,
89    /// Stack of currently active combinational groups
90    active_comb: ActiveAssignments,
91    /// groups that have done holes
92    has_done_hole: HashSet<ir::Id>,
93    /// Diagnostic context to accumulate multiple errors.
94    diag: DiagnosticContext,
95}
96
97impl std::fmt::Debug for WellFormed {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("WellFormed")
100            .field("has_done_hole", &self.has_done_hole)
101            .field("diag", &self.diag)
102            .finish_non_exhaustive()
103    }
104}
105
106enum Invoke<'a> {
107    StaticInvoke(&'a ir::StaticInvoke),
108    Invoke(&'a ir::Invoke),
109}
110
111impl Invoke<'_> {
112    fn get_ref_cells(&self) -> &Vec<(ir::Id, ir::RRC<Cell>)> {
113        match self {
114            Invoke::StaticInvoke(s) => &s.ref_cells,
115            Invoke::Invoke(s) => &s.ref_cells,
116        }
117    }
118
119    fn get_attributes(&self) -> &ir::Attributes {
120        match self {
121            Invoke::StaticInvoke(s) => s.get_attributes(),
122            Invoke::Invoke(s) => s.get_attributes(),
123        }
124    }
125}
126
127fn require_subtype(
128    invoke: Invoke,
129    self_ref_cells: &HashMap<ir::Id, LinkedHashMap<ir::Id, Cell>>,
130    id: &ir::Id,
131) -> CalyxResult<()> {
132    let cell_map = &self_ref_cells[id];
133    let mut mentioned_cells = HashSet::new();
134    for (outcell, incell) in invoke.get_ref_cells().iter() {
135        if let Some(oc) = cell_map.get(outcell) {
136            if !subtype(oc, &incell.borrow()) {
137                return Err(Error::malformed_control(format!(
138                    "The type passed in `{}` is not a subtype of the expected type `{}`.",
139                    incell.borrow().prototype.surface_name().unwrap(),
140                    oc.prototype.surface_name().unwrap()
141                ))
142                .with_pos(invoke.get_attributes()));
143            } else {
144                mentioned_cells.insert(outcell);
145            }
146        } else {
147            return Err(Error::malformed_control(format!(
148                "{id} does not have ref cell named {outcell}"
149            )));
150        }
151    }
152    for id in cell_map.keys() {
153        if !mentioned_cells.contains(id) {
154            return Err(Error::malformed_control(format!(
155                "unmentioned ref cell: {id}"
156            ))
157            .with_pos(invoke.get_attributes()));
158        }
159    }
160    Ok(())
161}
162
163impl ConstructVisitor for WellFormed {
164    fn from(ctx: &ir::Context) -> CalyxResult<Self>
165    where
166        Self: Sized,
167    {
168        let reserved_names =
169            RESERVED_NAMES.iter().map(|s| ir::Id::from(*s)).collect();
170
171        let mut ref_cells = HashMap::new();
172        for comp in ctx.components.iter() {
173            // Non-main components cannot use @external attribute
174            let cellmap: LinkedHashMap<ir::Id, Cell> = comp
175                .cells
176                .iter()
177                .filter_map(|cr| {
178                    let cell = cr.borrow();
179                    // Make sure @external cells are not defined in non-entrypoint components
180                    if cell.attributes.has(ir::BoolAttr::External)
181                        && comp.name != ctx.entrypoint
182                    {
183                        Some(Err(Error::malformed_structure("Cell cannot be marked `@external` in non-entrypoint component").with_pos(&cell.attributes)))
184                    } else if cell.is_reference() {
185                        Some(Ok((cell.name(), cell.clone())))
186                    } else {
187                        None
188                    }
189                })
190                .collect::<CalyxResult<_>>()?;
191            ref_cells.insert(comp.name, cellmap);
192        }
193
194        let w_f = WellFormed {
195            reserved_names,
196            used_groups: HashSet::new(),
197            used_comb_groups: HashSet::new(),
198            ref_cells,
199            active_comb: ActiveAssignments::default(),
200            has_done_hole: HashSet::new(),
201            diag: DiagnosticContext::default(),
202        };
203
204        Ok(w_f)
205    }
206
207    fn clear_data(&mut self) {
208        self.used_groups = HashSet::default();
209        self.used_comb_groups = HashSet::default();
210    }
211}
212
213impl Named for WellFormed {
214    fn name() -> &'static str {
215        "well-formed"
216    }
217
218    fn description() -> &'static str {
219        "Check if the structure and control are well formed."
220    }
221}
222
223impl DiagnosticPass for WellFormed {
224    fn diagnostics(&self) -> &DiagnosticContext {
225        &self.diag
226    }
227}
228
229/// Returns an error if the assignments are obviously conflicting. This happens when two
230/// assignments assign to the same port unconditionally.
231/// Because there are two types of assignments, we take in `assigns1` and `assigns2`.
232/// Regardless, we check for conflicts across (assigns1.chained(assigns2)).
233fn obvious_conflicts<'a, I1, I2>(assigns1: I1, assigns2: I2) -> CalyxResult<()>
234where
235    I1: Iterator<Item = &'a ir::Assignment<Nothing>>,
236    I2: Iterator<Item = &'a ir::Assignment<StaticTiming>>,
237{
238    let dsts1 = assigns1
239        .filter(|a| a.guard.is_true())
240        .map(|a| (a.dst.borrow().canonical(), a.attributes.copy_span()));
241    let dsts2 = assigns2
242        .filter(|a| a.guard.is_true())
243        .map(|a| (a.dst.borrow().canonical(), a.attributes.copy_span()));
244    let dsts = dsts1.chain(dsts2);
245    let dst_grps = dsts
246        .sorted_by(|(dst1, _), (dst2, _)| ir::Canonical::cmp(dst1, dst2))
247        .group_by(|(dst, _)| dst.clone());
248
249    for (_, group) in &dst_grps {
250        let assigns = group.collect_vec();
251        if assigns.len() > 1 {
252            let mut asgn_iter = assigns.into_iter().rev();
253            return Err(Error::malformed_structure(
254                "Obviously conflicting assignments found",
255            )
256            .with_pos(&asgn_iter.next().unwrap().1)
257            .with_annotations(asgn_iter.map(|(cannon, pos)| {
258                (pos, format!("`{cannon}` is also written to here"))
259            })));
260        }
261    }
262    Ok(())
263}
264
265/// Returns true if `cell_in` is a subtype of `cell_out`.
266/// Currenly this only checks for [`type_equivalence`](#method.calyx_ir::structure::Port::type_equivalent)
267/// between ports. It does not fully examine the cells
268/// for subtype compatability for things like nested ref cells.
269// XXX(nate): Cells don't contain information about their own `ref` cells so we'd need to extract it from `ir:Component` I think?
270fn subtype(cell_out: &Cell, cell_in: &Cell) -> bool {
271    for port in cell_out.ports() {
272        match cell_in.find(port.borrow().name) {
273            Some(port_in) => {
274                if !port.borrow().type_equivalent(&port_in.borrow()) {
275                    return false;
276                }
277            }
278            None => {
279                return false;
280            }
281        }
282    }
283    true
284}
285
286/// Confirms (in agreement with [this discussion](https://github.com/calyxir/calyx/issues/1828))
287/// that the `@fast` sequence `seq` is composed of alternating static-dynamic controls.
288fn check_fast_seq_invariant(seq: &Seq) -> CalyxResult<()> {
289    if seq.stmts.is_empty() {
290        return Ok(());
291    }
292    let mut last_is_static = seq
293        .stmts
294        .first()
295        .expect("non-empty already asserted")
296        .is_static();
297    for stmt in seq.stmts.iter().skip(1) {
298        if stmt.is_static() == last_is_static {
299            return Err(Error::malformed_control(
300                "`seq` marked `@fast` does not contain alternating static-dynamic control children (see #1828)",
301            ));
302        }
303        last_is_static = stmt.is_static();
304    }
305    Ok(())
306}
307
308impl Visitor for WellFormed {
309    fn start(
310        &mut self,
311        comp: &mut Component,
312        _ctx: &LibrarySignatures,
313        comps: &[ir::Component],
314    ) -> VisResult {
315        for cell_ref in comp.cells.iter() {
316            let cell = cell_ref.borrow();
317            // Check if any of the cells use a reserved name.
318            if self.reserved_names.contains(&cell.name()) {
319                self.diag.err(
320                    Error::reserved_name(cell.name())
321                        .with_pos(cell.get_attributes()),
322                );
323            }
324            // Check if a `ref` cell is invalid
325            if cell.is_reference() {
326                if cell.is_primitive(Some("std_const")) {
327                    self.diag.err(
328                        Error::malformed_structure(
329                            "constant not allowed for ref cells".to_string(),
330                        )
331                        .with_pos(cell.get_attributes()),
332                    );
333                }
334                if matches!(cell.prototype, CellType::ThisComponent) {
335                    unreachable!(
336                        "the current component not allowed for ref cells"
337                    );
338                }
339            }
340        }
341
342        // If the component is combinational, make sure all cells are also combinational,
343        // there are no group or comb group definitions, and the control program is empty
344        if comp.is_comb {
345            if !matches!(&*comp.control.borrow(), ir::Control::Empty(..)) {
346                self.diag.err(Error::malformed_structure(format!("Component `{}` is marked combinational but has a non-empty control program", comp.name)));
347            }
348
349            if !comp.get_groups().is_empty() {
350                let group = comp.get_groups().iter().next().unwrap().borrow();
351                self.diag.err(Error::malformed_structure(format!("Component `{}` is marked combinational but contains a group `{}`", comp.name, group.name())).with_pos(&group.attributes));
352            }
353
354            if !comp.get_static_groups().is_empty() {
355                let group =
356                    comp.get_static_groups().iter().next().unwrap().borrow();
357                self.diag.err(Error::malformed_structure(format!("Component `{}` is marked combinational but contains a group `{}`", comp.name, group.name())).with_pos(&group.attributes));
358            }
359
360            if !comp.comb_groups.is_empty() {
361                let group = comp.comb_groups.iter().next().unwrap().borrow();
362                self.diag.err(Error::malformed_structure(format!("Component `{}` is marked combinational but contains a group `{}`", comp.name, group.name())).with_pos(&group.attributes));
363            }
364
365            for cell_ref in comp.cells.iter() {
366                let cell = cell_ref.borrow();
367                let is_comb = match &cell.prototype {
368                    CellType::Primitive { is_comb, .. } => is_comb.to_owned(),
369                    CellType::Constant { .. } => true,
370                    CellType::Component { name } => {
371                        let comp_idx =
372                            comps.iter().position(|x| x.name == name).unwrap();
373                        let comp = comps
374                            .get(comp_idx)
375                            .expect("Found cell that does not exist");
376                        comp.is_comb
377                    }
378                    _ => false,
379                };
380                if !is_comb {
381                    self.diag.err(Error::malformed_structure(format!("Component `{}` is marked combinational but contains non-combinational cell `{}`", comp.name, cell.name())).with_pos(&cell.attributes));
382                }
383            }
384        }
385        // in ast_to_ir, we should have already checked that static components have static_control_body
386        if comp.latency.is_some() {
387            assert!(
388                matches!(&*comp.control.borrow(), &ir::Control::Static(_)),
389                "static component {} does not have static control. This should have been checked in ast_to_ir",
390                comp.name
391            );
392        }
393
394        // Checking that @interval annotations are placed correctly.
395        // There are two options for @interval annotations:
396        // 1. You have written only continuous assignments (this is similar
397        // to primitives written in Verilog).
398        // 2. You are using static<n> control.
399        let comp_sig = &comp.signature.borrow();
400        let go_ports =
401            comp_sig.find_all_with_attr(ir::NumAttr::Go).collect_vec();
402        if go_ports.iter().any(|go_port| {
403            go_port.borrow().attributes.has(ir::NumAttr::Interval)
404        }) {
405            match &*comp.control.borrow() {
406                ir::Control::Static(_) | ir::Control::Empty(_) => (),
407                _ => return self.diag.early_return_err(Error::malformed_structure(
408                    format!("component {} has dynamic control but has @interval annotations", comp.name),
409                    )
410                    .with_pos(&comp.attributes)),
411            };
412            if !comp.control.borrow().is_empty() {
413                // Getting "reference value" should be the same for all go ports and
414                // the control.
415                let reference_val = match go_ports[0]
416                    .borrow()
417                    .attributes
418                    .get(ir::NumAttr::Interval)
419                {
420                    Some(val) => val,
421                    None => {
422                        return self.diag.early_return_err(Error::malformed_structure(
423                        "@interval(n) attribute on all @go ports since there is static<n> control",
424                        )
425                                      .with_pos(&comp.attributes));
426                    }
427                };
428                // Checking go ports.
429                for go_port in &go_ports {
430                    let go_port_val = match go_port
431                        .borrow()
432                        .attributes
433                        .get(ir::NumAttr::Interval)
434                    {
435                        Some(val) => val,
436                        None => {
437                            self.diag.err(Error::malformed_structure(format!(
438                                "@go port expected @interval({reference_val}) attribute on all ports \
439                                since the component has static<n> control",
440                            ))
441                            .with_pos(&comp.attributes));
442                            continue;
443                        }
444                    };
445                    if go_port_val != reference_val {
446                        self.diag.err(Error::malformed_structure(format!(
447                            "@go port expected @interval {reference_val}, got @interval {go_port_val}",
448                        ))
449                        .with_pos(&go_port.borrow().attributes));
450                        continue;
451                    }
452                    // Checking control latency
453                    match comp.control.borrow().get_latency() {
454                        None => {
455                            unreachable!("already checked control is static")
456                        }
457                        Some(control_latency) => {
458                            if control_latency != reference_val {
459                                self.diag.err(Error::malformed_structure(format!(
460                                    "component {} expected @interval {reference_val}, got @interval {control_latency}", comp.name,
461                                ))
462                                .with_pos(&comp.attributes));
463                            }
464                        }
465                    }
466                }
467            }
468        }
469
470        // For each non-combinational group, check if there is at least one write to the done
471        // signal of that group and that the write is to the group's done signal.
472        for gr in comp.get_groups().iter() {
473            let group = gr.borrow();
474            let gname = group.name();
475            let mut has_done = false;
476            // Find an assignment writing to this group's done condition.
477            for assign in &group.assignments {
478                let dst = assign.dst.borrow();
479                if port_is_static_prim(&dst) {
480                    self.diag.err(
481                        Error::malformed_structure(format!(
482                            "Static cell `{}` written to in non-static group",
483                            dst.get_parent_name()
484                        ))
485                        .with_pos(&assign.attributes),
486                    );
487                }
488                if dst.is_hole() && dst.name == "done" {
489                    // Group has multiple done conditions
490                    if has_done {
491                        self.diag.err(
492                            Error::malformed_structure(format!(
493                                "Group `{gname}` has multiple done conditions"
494                            ))
495                            .with_pos(&assign.attributes),
496                        );
497                    } else {
498                        has_done = true;
499                    }
500                    // Group uses another group's done condition
501                    if gname != dst.get_parent_name() {
502                        self.diag.err(Error::malformed_structure(
503                            format!(
504                                "Group `{}` refers to the done condition of another group (`{}`)",
505                                gname,
506                                dst.get_parent_name(),
507                            )).with_pos(&assign.attributes));
508                    }
509                }
510            }
511
512            // If group has done condition, record this fact,
513            // otherwise record an error
514            if has_done {
515                self.has_done_hole.insert(gname);
516            } else {
517                self.diag.err(
518                    Error::malformed_structure(format!(
519                        "No writes to the `done' hole for group `{gname}'",
520                    ))
521                    .with_pos(&group.attributes),
522                );
523            }
524        }
525
526        // Don't need to check done condition for static groups. Instead, just
527        // checking that the static timing intervals are well formed, and
528        // that don't write to static components
529        for gr in comp.get_static_groups().iter() {
530            let group = gr.borrow();
531            let group_latency = group.get_latency();
532            // Check that for each interval %[beg, end], end > beg.
533            for assign in &group.assignments {
534                assign.guard.check_for_each_info(
535                    &mut |static_timing: &StaticTiming| {
536                        if static_timing.get_interval().0
537                            >= static_timing.get_interval().1
538                        {
539                            Err(Error::malformed_structure(format!(
540                                "Static Timing Guard has improper interval: `{static_timing}`"
541                            ))
542                            .with_pos(&assign.attributes))
543                        } else if static_timing.get_interval().1 > group_latency {
544                            Err(Error::malformed_structure(format!(
545                                "Static Timing Guard has interval `{static_timing}`, which is out of bounds since its static group has latency {group_latency}"
546                            ))
547                            .with_pos(&assign.attributes))
548                        } else {
549                            Ok(())
550                        }
551                    },
552                )?;
553            }
554        }
555
556        // Check for obvious conflicting assignments in the continuous assignments
557        obvious_conflicts(
558            comp.continuous_assignments.iter(),
559            std::iter::empty::<&ir::Assignment<StaticTiming>>(),
560        )
561        .accumulate_err(&mut self.diag)?;
562        // Check for obvious conflicting assignments between the continuous assignments and the groups
563        for cgr in comp.comb_groups.iter() {
564            for assign in &cgr.borrow().assignments {
565                let dst = assign.dst.borrow();
566                if port_is_static_prim(&dst) {
567                    self.diag.err(
568                        Error::malformed_structure(format!(
569                            "Static cell `{}` written to in non-static group",
570                            dst.get_parent_name()
571                        ))
572                        .with_pos(&assign.attributes),
573                    );
574                }
575            }
576            obvious_conflicts(
577                cgr.borrow()
578                    .assignments
579                    .iter()
580                    .chain(comp.continuous_assignments.iter()),
581                std::iter::empty::<&ir::Assignment<StaticTiming>>(),
582            )
583            .accumulate_err(&mut self.diag)?;
584        }
585
586        Ok(Action::Continue)
587    }
588
589    fn static_enable(
590        &mut self,
591        s: &mut ir::StaticEnable,
592        comp: &mut Component,
593        _ctx: &LibrarySignatures,
594        _comps: &[ir::Component],
595    ) -> VisResult {
596        self.used_groups.insert(s.group.borrow().name());
597
598        let group = s.group.borrow();
599
600        // check for obvious conflicts within static groups and continuous/comb group assigns
601        obvious_conflicts(
602            comp.continuous_assignments
603                .iter()
604                .chain(self.active_comb.iter()),
605            group.assignments.iter(),
606        )
607        .map_err(|err| {
608            err.with_annotation(
609                &s.attributes,
610                "Assignments activated by group static enable, causing the conflict",
611            )
612        })
613        .accumulate_err(&mut self.diag)?;
614
615        Ok(Action::Continue)
616    }
617
618    fn enable(
619        &mut self,
620        s: &mut ir::Enable,
621        comp: &mut Component,
622        _ctx: &LibrarySignatures,
623        _comps: &[ir::Component],
624    ) -> VisResult {
625        let group = s.group.borrow();
626        let gname = group.name();
627        self.used_groups.insert(gname);
628
629        // if this group doesn't have a done hole, we can't run this analysis
630        // so we abort early
631        if !self.has_done_hole.contains(&gname) {
632            return Ok(Action::Continue);
633        }
634
635        let asgn = group.done_cond();
636        let const_done_assign =
637            asgn.guard.is_true() && asgn.src.borrow().is_constant(1, 1);
638
639        if const_done_assign {
640            self.diag.err(Error::malformed_structure("Group with constant done condition is invalid. Use `comb group` instead to define a combinational group.").with_pos(&group.attributes));
641        }
642
643        // A group with "static"=0 annotation
644        if group
645            .attributes
646            .get(ir::NumAttr::Promotable)
647            .map(|v| v == 0)
648            .unwrap_or(false)
649        {
650            self.diag.err(Error::malformed_structure("Group with annotation \"promotable\"=0 is invalid. Use `comb group` instead to define a combinational group or if the group's done condition is not constant, provide the correct \"static\" annotation.").with_pos(&group.attributes));
651        }
652
653        // Check if the group has obviously conflicting assignments with the continuous assignments and the active combinational groups
654        obvious_conflicts(
655            group
656                .assignments
657                .iter()
658                .chain(comp.continuous_assignments.iter())
659                .chain(self.active_comb.iter()),
660            std::iter::empty::<&ir::Assignment<StaticTiming>>(),
661        )
662        .map_err(|err| {
663            err.with_annotation(
664                &s.attributes,
665                "Assignments activated by group enable, causing the conflict",
666            )
667        })
668        .accumulate_err(&mut self.diag)?;
669
670        Ok(Action::Continue)
671    }
672
673    fn invoke(
674        &mut self,
675        s: &mut ir::Invoke,
676        _comp: &mut Component,
677        _ctx: &LibrarySignatures,
678        _comps: &[ir::Component],
679    ) -> VisResult {
680        if let Some(c) = &s.comb_group {
681            self.used_comb_groups.insert(c.borrow().name());
682        }
683        // Only refers to ports defined in the invoked instance.
684        let cell = s.comp.borrow();
685
686        if let CellType::Component { name: id } = &cell.prototype {
687            require_subtype(Invoke::Invoke(s), &self.ref_cells, id)?;
688        }
689        Ok(Action::Continue)
690    }
691
692    fn static_invoke(
693        &mut self,
694        s: &mut ir::StaticInvoke,
695        _comp: &mut Component,
696        _ctx: &LibrarySignatures,
697        _comps: &[ir::Component],
698    ) -> VisResult {
699        // Only refers to ports defined in the invoked instance.
700        let cell = s.comp.borrow();
701
702        if let CellType::Component { name: id } = &cell.prototype {
703            require_subtype(Invoke::StaticInvoke(s), &self.ref_cells, id)?;
704        }
705        Ok(Action::Continue)
706    }
707
708    fn start_if(
709        &mut self,
710        s: &mut ir::If,
711        _comp: &mut Component,
712        _sigs: &LibrarySignatures,
713        _comps: &[ir::Component],
714    ) -> VisResult {
715        if let Some(cgr) = &s.cond {
716            let cg = cgr.borrow();
717            let assigns = &cg.assignments;
718            // Check if the combinational group conflicts with the active combinational groups
719            obvious_conflicts(
720                assigns.iter().chain(self.active_comb.iter()),
721                std::iter::empty::<&ir::Assignment<StaticTiming>>(),
722            )
723            .map_err(|err| {
724                err.with_annotation(
725                    &s.attributes,
726                    format!(
727                        "Assignments from `{}' are activated here, causing the conflict",
728                        cg.name()
729                    ),
730                )
731            })
732            .accumulate_err(&mut self.diag)?;
733            // Push the combinational group to the stack of active groups
734            self.active_comb.push(assigns);
735        } else if !s.port.borrow().has_attribute(ir::BoolAttr::Stable) {
736            let err = Error::misc(format!(
737                "If statement has no comb group and its condition port {} is unstable",
738                s.port.borrow().canonical()
739            )).with_pos(&s.attributes);
740            self.diag.warning(err);
741        }
742        Ok(Action::Continue)
743    }
744
745    fn start_static_if(
746        &mut self,
747        s: &mut ir::StaticIf,
748        _comp: &mut Component,
749        _sigs: &LibrarySignatures,
750        _comps: &[ir::Component],
751    ) -> VisResult {
752        if !s.port.borrow().has_attribute(ir::BoolAttr::Stable) {
753            let err = Error::misc(format!(
754                "static if statement's condition port {} is unstable",
755                s.port.borrow().canonical()
756            ))
757            .with_pos(&s.attributes);
758            self.diag.warning(err);
759        }
760        Ok(Action::Continue)
761    }
762
763    fn start_seq(
764        &mut self,
765        s: &mut calyx_ir::Seq,
766        _comp: &mut Component,
767        _sigs: &LibrarySignatures,
768        _comps: &[calyx_ir::Component],
769    ) -> VisResult {
770        if s.attributes.has(BoolAttr::Fast) {
771            check_fast_seq_invariant(s)?;
772        }
773
774        Ok(Action::Continue)
775    }
776
777    fn finish_if(
778        &mut self,
779        s: &mut ir::If,
780        _comp: &mut Component,
781        _ctx: &LibrarySignatures,
782        _comps: &[ir::Component],
783    ) -> VisResult {
784        // Add cond group as a used port.
785        if let Some(cond) = &s.cond {
786            self.used_comb_groups.insert(cond.borrow().name());
787            // Remove assignments from this combinational group
788            self.active_comb.pop();
789        }
790        Ok(Action::Continue)
791    }
792
793    fn start_while(
794        &mut self,
795        s: &mut ir::While,
796        _comp: &mut Component,
797        _sigs: &LibrarySignatures,
798        _comps: &[ir::Component],
799    ) -> VisResult {
800        if let Some(cgr) = &s.cond {
801            let cg = cgr.borrow();
802            let assigns = &cg.assignments;
803            // Check if the combinational group conflicts with the active combinational groups
804            obvious_conflicts(
805                assigns.iter().chain(self.active_comb.iter()),
806                std::iter::empty::<&ir::Assignment<StaticTiming>>(),
807            )
808            .map_err(|err| {
809                let msg = s.attributes.copy_span().format(format!(
810                    "Assignments from `{}' are activated here",
811                    cg.name()
812                ));
813                err.with_post_msg(Some(msg))
814            })
815            .accumulate_err(&mut self.diag)?;
816            // Push the combinational group to the stack of active groups
817            self.active_comb.push(assigns);
818        } else if !s.port.borrow().has_attribute(ir::BoolAttr::Stable) {
819            let err = Error::misc(format!(
820                "While loop has no comb group and its condition port `{}` is unstable",
821                s.port.borrow().canonical()
822            )).with_pos(&s.attributes);
823            self.diag.warning(err);
824        }
825        Ok(Action::Continue)
826    }
827
828    fn finish_while(
829        &mut self,
830        s: &mut ir::While,
831        _comp: &mut Component,
832        _ctx: &LibrarySignatures,
833        _comps: &[ir::Component],
834    ) -> VisResult {
835        // Add cond group as a used port.
836        if let Some(cond) = &s.cond {
837            self.used_comb_groups.insert(cond.borrow().name());
838            // Remove assignments from this combinational group
839            self.active_comb.pop();
840        }
841        Ok(Action::Continue)
842    }
843
844    fn finish(
845        &mut self,
846        comp: &mut Component,
847        _ctx: &LibrarySignatures,
848        _comps: &[ir::Component],
849    ) -> VisResult {
850        // Go signals of groups mentioned in other groups are considered used
851        comp.for_each_assignment(|assign| {
852            assign.for_each_port(|pr| {
853                let port = pr.borrow();
854                if port.is_hole() && port.name == "go" {
855                    self.used_groups.insert(port.get_parent_name());
856                }
857                None
858            })
859        });
860        comp.for_each_static_assignment(|assign| {
861            assign.for_each_port(|pr| {
862                let port = pr.borrow();
863                if port.is_hole() && port.name == "go" {
864                    self.used_groups.insert(port.get_parent_name());
865                }
866                None
867            })
868        });
869
870        // Find unused groups
871        let mut all_groups: HashSet<ir::Id> = comp
872            .get_groups()
873            .iter()
874            .map(|g| g.borrow().name())
875            .collect();
876        let static_groups: HashSet<ir::Id> = comp
877            .get_static_groups()
878            .iter()
879            .map(|g| g.borrow().name())
880            .collect();
881        all_groups.extend(static_groups);
882
883        if let Some(group) = all_groups.difference(&self.used_groups).next() {
884            match comp.find_group(*group) {
885                Some(gr) => {
886                    let gr = gr.borrow();
887                    self.diag.err(
888                        Error::unused(*group, "group").with_pos(&gr.attributes),
889                    );
890                }
891                None => {
892                    let gr = comp.find_static_group(*group).unwrap();
893                    let gr = gr.borrow();
894                    self.diag.err(
895                        Error::unused(*group, "group").with_pos(&gr.attributes),
896                    );
897                }
898            }
899        };
900
901        let all_comb_groups: HashSet<ir::Id> =
902            comp.comb_groups.iter().map(|g| g.borrow().name()).collect();
903        if let Some(comb_group) =
904            all_comb_groups.difference(&self.used_comb_groups).next()
905        {
906            let cgr = comp.find_comb_group(*comb_group).unwrap();
907            let cgr = cgr.borrow();
908            self.diag.err(
909                Error::unused(*comb_group, "combinational group")
910                    .with_pos(&cgr.attributes),
911            );
912        }
913        Ok(Action::Continue)
914    }
915}