calyx_opt/passes/
papercut.rs

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