calyx_opt/passes/
papercut.rs

1use crate::analysis::{self, AssignmentAnalysis};
2use crate::traversal::{Action, ConstructVisitor, Named, VisResult, Visitor};
3use calyx_ir::{self as ir, LibrarySignatures};
4use calyx_utils::{CalyxResult, Error};
5use itertools::Itertools;
6use std::collections::{HashMap, HashSet};
7
8/// Tuple containing (port, set of ports).
9/// When the first port is read from, all of the ports in the set must be written to.
10type ReadTogether = (ir::Id, HashSet<ir::Id>);
11
12/// Pass to check for common errors such as missing assignments to `done` holes
13/// of groups.
14pub struct Papercut {
15    /// Map from (primitive name) -> Vec<(set of ports)>
16    /// When any of the ports in a set is driven, all ports in that set must
17    /// be driven.
18    /// For example, when driving the `in` port of a register, the `write_en`
19    /// signal must also be driven.
20    write_together: HashMap<ir::Id, Vec<HashSet<ir::Id>>>,
21
22    /// Map from (primitive name) -> Vec<(port, set of ports)>
23    /// When the `port` in the tuple is being read from, all the ports in the
24    /// set must be driven.
25    read_together: HashMap<ir::Id, Vec<ReadTogether>>,
26
27    /// The cells that are driven through continuous assignments
28    cont_cells: HashSet<ir::Id>,
29}
30
31impl Papercut {
32    #[allow(unused)]
33    /// String representation of the write together and read together specifications.
34    /// Used for debugging. Should not be relied upon by external users.
35    fn fmt_write_together_spec(&self) -> String {
36        self.write_together
37            .iter()
38            .map(|(prim, writes)| {
39                let writes = writes
40                    .iter()
41                    .map(|write| {
42                        write
43                            .iter()
44                            .sorted()
45                            .map(|port| format!("{port}"))
46                            .join(", ")
47                    })
48                    .join("; ");
49                format!("{}: [{}]", prim, writes)
50            })
51            .join("\n")
52    }
53}
54
55impl ConstructVisitor for Papercut {
56    fn from(ctx: &ir::Context) -> CalyxResult<Self> {
57        let write_together =
58            analysis::PortInterface::write_together_specs(ctx.lib.signatures());
59        let read_together =
60            analysis::PortInterface::comb_path_specs(ctx.lib.signatures())?;
61        Ok(Papercut {
62            write_together,
63            read_together,
64            cont_cells: HashSet::new(),
65        })
66    }
67
68    fn clear_data(&mut self) {
69        // Library specifications are shared
70        self.cont_cells = HashSet::new();
71    }
72}
73
74impl Named for Papercut {
75    fn name() -> &'static str {
76        "papercut"
77    }
78
79    fn description() -> &'static str {
80        "Detect various common made mistakes"
81    }
82}
83
84/// Extract information about a port.
85fn port_information(
86    port_ref: ir::RRC<ir::Port>,
87) -> Option<((ir::Id, ir::Id), ir::Id)> {
88    let port = port_ref.borrow();
89    if let ir::PortParent::Cell(cell_wref) = &port.parent {
90        let cell_ref = cell_wref.upgrade();
91        let cell = cell_ref.borrow();
92        if let ir::CellType::Primitive { name, .. } = &cell.prototype {
93            return Some(((cell.name(), *name), port.name));
94        }
95    }
96    None
97}
98
99impl Visitor for Papercut {
100    fn start(
101        &mut self,
102        comp: &mut ir::Component,
103        _ctx: &LibrarySignatures,
104        _comps: &[ir::Component],
105    ) -> VisResult {
106        // If the component isn't marked "nointerface", it should have an invokable
107        // interface.
108        if !comp.attributes.has(ir::BoolAttr::NoInterface) && !comp.is_comb {
109            // If the control program is empty, check that the `done` signal has been assigned to.
110            if let ir::Control::Empty(..) = *comp.control.borrow() {
111                for p in comp
112                    .signature
113                    .borrow()
114                    .find_all_with_attr(ir::NumAttr::Done)
115                {
116                    let done_use =
117                        comp.continuous_assignments.iter().find(|assign_ref| {
118                            let assign = assign_ref.dst.borrow();
119                            // If at least one assignment used the `done` port, then
120                            // we're good.
121                            assign.name == p.borrow().name && !assign.is_hole()
122                        });
123                    if done_use.is_none() {
124                        return Err(Error::papercut(format!("Component `{}` has an empty control program and does not assign to the done port `{}`. Without an assignment to the done port, the component cannot return control flow.", comp.name, p.borrow().name)));
125                    }
126                }
127            }
128        }
129
130        // For each component that's being driven in a group and comb group, make sure all signals defined for
131        // that component's `write_together' and `read_together' are also driven.
132        // For example, for a register, both the `.in' port and the `.write_en' port need to be
133        // driven.
134        for group_ref in comp.get_groups().iter() {
135            let group = group_ref.borrow();
136            self.check_specs(&group.assignments)
137                .map_err(|err| err.with_pos(&group.attributes))?;
138        }
139        for group_ref in comp.get_static_groups().iter() {
140            let group = group_ref.borrow();
141            self.check_specs(&group.assignments)
142                .map_err(|err| err.with_pos(&group.attributes))?;
143        }
144        for cgr in comp.comb_groups.iter() {
145            let cg = cgr.borrow();
146            self.check_specs(&cg.assignments)
147                .map_err(|err| err.with_pos(&cg.attributes))?;
148        }
149
150        // Compute all cells that are driven in by the continuous assignments0
151        self.cont_cells = comp
152            .continuous_assignments
153            .iter()
154            .analysis()
155            .cell_writes()
156            .map(|cr| cr.borrow().name())
157            .collect();
158
159        Ok(Action::Continue)
160    }
161
162    fn start_while(
163        &mut self,
164        s: &mut ir::While,
165        _comp: &mut ir::Component,
166        _ctx: &LibrarySignatures,
167        _comps: &[ir::Component],
168    ) -> VisResult {
169        if s.cond.is_none() {
170            let port = s.port.borrow();
171            if let ir::PortParent::Cell(cell_wref) = &port.parent {
172                let cell_ref = cell_wref.upgrade();
173                let cell = cell_ref.borrow();
174                if let ir::CellType::Primitive {
175                    is_comb,
176                    name: prim_name,
177                    ..
178                } = &cell.prototype
179                {
180                    // If the cell is combinational and not driven by continuous assignments
181                    if *is_comb && !self.cont_cells.contains(&cell.name()) {
182                        let msg = format!("Port `{}.{}` is an output port on combinational primitive `{}` and will always output 0. Add a `with` statement to the `while` statement to ensure it has a valid value during execution.", cell.name(), port.name, prim_name);
183                        // Use dummy Id to get correct source location for error
184                        return Err(
185                            Error::papercut(msg).with_pos(&s.attributes)
186                        );
187                    }
188                }
189            }
190        }
191        Ok(Action::Continue)
192    }
193
194    fn start_if(
195        &mut self,
196        s: &mut ir::If,
197        _comp: &mut ir::Component,
198        _ctx: &LibrarySignatures,
199        _comps: &[ir::Component],
200    ) -> VisResult {
201        if s.cond.is_none() {
202            let port = s.port.borrow();
203            if let ir::PortParent::Cell(cell_wref) = &port.parent {
204                let cell_ref = cell_wref.upgrade();
205                let cell = cell_ref.borrow();
206                if let ir::CellType::Primitive {
207                    is_comb,
208                    name: prim_name,
209                    ..
210                } = &cell.prototype
211                {
212                    // If the cell is combinational and not driven by continuous assignments
213                    if *is_comb && !self.cont_cells.contains(&cell.name()) {
214                        let msg = format!("Port `{}.{}` is an output port on combinational primitive `{}` and will always output 0. Add a `with` statement to the `if` statement to ensure it has a valid value during execution.", cell.name(), port.name, prim_name);
215                        // Use dummy Id to get correct source location for error
216                        return Err(
217                            Error::papercut(msg).with_pos(&s.attributes)
218                        );
219                    }
220                }
221            }
222        }
223        Ok(Action::Continue)
224    }
225}
226
227impl Papercut {
228    fn check_specs<T>(&mut self, assigns: &[ir::Assignment<T>]) -> VisResult {
229        let all_writes = assigns
230            .iter()
231            .analysis()
232            .writes()
233            .filter_map(port_information)
234            .into_grouping_map()
235            .collect::<HashSet<_>>();
236        let all_reads = assigns
237            .iter()
238            .analysis()
239            .reads()
240            .filter_map(port_information)
241            .into_grouping_map()
242            .collect::<HashSet<_>>();
243        for ((inst, comp_type), reads) in all_reads {
244            if let Some(spec) = self.read_together.get(&comp_type) {
245                let empty = HashSet::new();
246                let writes =
247                    all_writes.get(&(inst, comp_type)).unwrap_or(&empty);
248                for (read, required) in spec {
249                    if reads.contains(read)
250                        && required.difference(writes).next().is_some()
251                    {
252                        let missing = required
253                            .difference(writes)
254                            .sorted()
255                            .map(|port| format!("{}.{}", inst.clone(), port))
256                            .join(", ");
257                        let msg =
258                            format!("Required signal not driven inside the group.\
259                                        \nWhen reading the port `{}.{}', the ports [{}] must be written to.\
260                                        \nThe primitive type `{}' requires this invariant.",
261                                    inst,
262                                    read,
263                                    missing,
264                                    comp_type);
265                        return Err(Error::papercut(msg));
266                    }
267                }
268            }
269        }
270        for ((inst, comp_type), writes) in all_writes {
271            if let Some(spec) = self.write_together.get(&comp_type) {
272                // For each write together spec.
273                for required in spec {
274                    // It should either be the case that:
275                    // 1. `writes` contains no writes that overlap with `required`
276                    //     In which case `required - writes` == `required`.
277                    // 2. `writes` contains writes that overlap with `required`
278                    //     In which case `required - writes == {}`
279                    let mut diff: HashSet<_> =
280                        required.difference(&writes).copied().collect();
281                    if diff.is_empty() || diff == *required {
282                        continue;
283                    }
284
285                    let first =
286                        writes.intersection(required).sorted().next().unwrap();
287                    let missing = diff
288                        .drain()
289                        .sorted()
290                        .map(|port| format!("{}.{}", inst, port))
291                        .join(", ");
292                    let msg =
293                        format!("Required signal not driven inside the group. \
294                                 When writing to the port `{}.{}', the ports [{}] must also be written to. \
295                                 The primitive type `{}' specifies this using a @write_together spec.",
296                                inst,
297                                first,
298                                missing,
299                                comp_type);
300                    return Err(Error::papercut(msg));
301                }
302            }
303        }
304        // This return value is not used
305        Ok(Action::Continue)
306    }
307}