calyx_opt/passes/
well_formed.rs

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