calyx_opt/passes/
papercut.rs

1use crate::analysis::{self, AssignmentAnalysis};
2use crate::traversal::{
3    Action, ConstructVisitor, DiagnosticContext, DiagnosticPass, Named,
4    ParseVal, PassOpt, 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    /// Treat warnings as errors.
38    ///
39    /// By default, this is `false`. When enabled, warnings will cause
40    /// the command to fail.
41    warning_as_error: bool,
42}
43
44impl Papercut {
45    #[allow(unused)]
46    /// String representation of the write together and read together specifications.
47    /// Used for debugging. Should not be relied upon by external users.
48    fn fmt_write_together_spec(&self) -> String {
49        self.write_together
50            .iter()
51            .map(|(prim, writes)| {
52                let writes = writes
53                    .iter()
54                    .map(|write| {
55                        write
56                            .iter()
57                            .sorted()
58                            .map(|port| format!("{port}"))
59                            .join(", ")
60                    })
61                    .join("; ");
62                format!("{prim}: [{writes}]")
63            })
64            .join("\n")
65    }
66    /// Reports an error or warning to the given diagnostic context.
67    ///
68    /// If `as_error` is `true`, the diagnostic is emitted as an error.
69    /// Otherwise, it is emitted as a warning.
70    ///
71    /// This is implemented as an associated function rather than a
72    /// `Papercut` method because reporting requires mutable access to
73    /// `DiagnosticContext`, while the relevant `Papercut` APIs operate on
74    /// `&self`. Exposing this as a method would therefore require changing
75    /// the mutability of those APIs.
76    fn report(diag: &mut DiagnosticContext, as_error: bool, err: Error) {
77        if as_error {
78            diag.err(err);
79        } else {
80            diag.warning(err);
81        }
82    }
83}
84
85impl ConstructVisitor for Papercut {
86    fn from(ctx: &ir::Context) -> CalyxResult<Self> {
87        let write_together =
88            analysis::PortInterface::write_together_specs(ctx.lib.signatures());
89        let read_together =
90            analysis::PortInterface::comb_path_specs(ctx.lib.signatures())?;
91        let opts = Self::get_opts(ctx);
92        Ok(Papercut {
93            write_together,
94            read_together,
95            cont_cells: HashSet::new(),
96            diag: DiagnosticContext::default(),
97            warning_as_error: opts["warnings-as-error"].bool(),
98        })
99    }
100
101    fn clear_data(&mut self) {
102        // Library specifications are shared
103        self.cont_cells = HashSet::new();
104    }
105}
106
107impl Named for Papercut {
108    fn name() -> &'static str {
109        "papercut"
110    }
111
112    fn description() -> &'static str {
113        "Detect various common made mistakes"
114    }
115
116    fn opts() -> Vec<PassOpt> {
117        vec![PassOpt::new(
118            "warnings-as-error",
119            "Treat warnings generated by the papercut pass as errors.",
120            ParseVal::Bool(false),
121            PassOpt::parse_bool,
122        )]
123    }
124}
125
126/// Extract information about a port.
127fn port_information(
128    port_ref: ir::RRC<ir::Port>,
129) -> Option<((ir::Id, ir::Id), ir::Id)> {
130    let port = port_ref.borrow();
131    if let ir::PortParent::Cell(cell_wref) = &port.parent {
132        let cell_ref = cell_wref.upgrade();
133        let cell = cell_ref.borrow();
134        if let ir::CellType::Primitive { name, .. } = &cell.prototype {
135            return Some(((cell.name(), *name), port.name));
136        }
137    }
138    None
139}
140
141impl DiagnosticPass for Papercut {
142    fn diagnostics(&self) -> &DiagnosticContext {
143        &self.diag
144    }
145}
146
147impl Visitor for Papercut {
148    fn start(
149        &mut self,
150        comp: &mut ir::Component,
151        _ctx: &LibrarySignatures,
152        _comps: &[ir::Component],
153    ) -> VisResult {
154        // If the component isn't marked "nointerface", it should have an invokable
155        // interface.
156        if !comp.attributes.has(ir::BoolAttr::NoInterface) && !comp.is_comb {
157            // If the control program is empty, check that the `done` signal has been assigned to.
158            if let ir::Control::Empty(..) = *comp.control.borrow() {
159                for p in comp
160                    .signature
161                    .borrow()
162                    .find_all_with_attr(ir::NumAttr::Done)
163                {
164                    let done_use =
165                        comp.continuous_assignments.iter().find(|assign_ref| {
166                            let assign = assign_ref.dst.borrow();
167                            // If at least one assignment used the `done` port, then
168                            // we're good.
169                            assign.name == p.borrow().name && !assign.is_hole()
170                        });
171                    if done_use.is_none() {
172                        let 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);
173                        Self::report(
174                            &mut self.diag,
175                            self.warning_as_error,
176                            err,
177                        );
178                    }
179                }
180            }
181        }
182
183        // For each component that's being driven in a group and comb group, make sure all signals defined for
184        // that component's `write_together' and `read_together' are also driven.
185        // For example, for a register, both the `.in' port and the `.write_en' port need to be
186        // driven.
187        for group_ref in comp.get_groups().iter() {
188            let group = group_ref.borrow();
189            self.check_specs(&group.assignments, &group.attributes);
190        }
191        for group_ref in comp.get_static_groups().iter() {
192            let group = group_ref.borrow();
193            self.check_specs(&group.assignments, &group.attributes);
194        }
195        for cgr in comp.comb_groups.iter() {
196            let cg = cgr.borrow();
197            self.check_specs(&cg.assignments, &cg.attributes);
198        }
199
200        // Compute all cells that are driven in by the continuous assignments
201        self.cont_cells = comp
202            .continuous_assignments
203            .iter()
204            .analysis()
205            .cell_writes()
206            .map(|cr| cr.borrow().name())
207            .collect();
208
209        Ok(Action::Continue)
210    }
211
212    fn start_while(
213        &mut self,
214        s: &mut ir::While,
215        _comp: &mut ir::Component,
216        _ctx: &LibrarySignatures,
217        _comps: &[ir::Component],
218    ) -> VisResult {
219        if s.cond.is_none() {
220            let port = s.port.borrow();
221            if let ir::PortParent::Cell(cell_wref) = &port.parent {
222                let cell_ref = cell_wref.upgrade();
223                let cell = cell_ref.borrow();
224                if let ir::CellType::Primitive {
225                    is_comb,
226                    name: prim_name,
227                    ..
228                } = &cell.prototype
229                {
230                    // If the cell is combinational and not driven by continuous assignments
231                    if *is_comb && !self.cont_cells.contains(&cell.name()) {
232                        let msg = format!(
233                            "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.",
234                            cell.name(),
235                            port.name,
236                            prim_name
237                        );
238                        // Use dummy Id to get correct source location for error
239                        Self::report(
240                            &mut self.diag,
241                            self.warning_as_error,
242                            Error::papercut(msg).with_pos(&s.attributes),
243                        );
244                    }
245                }
246            }
247        }
248        Ok(Action::Continue)
249    }
250
251    fn start_if(
252        &mut self,
253        s: &mut ir::If,
254        _comp: &mut ir::Component,
255        _ctx: &LibrarySignatures,
256        _comps: &[ir::Component],
257    ) -> VisResult {
258        if s.cond.is_none() {
259            let port = s.port.borrow();
260            if let ir::PortParent::Cell(cell_wref) = &port.parent {
261                let cell_ref = cell_wref.upgrade();
262                let cell = cell_ref.borrow();
263                if let ir::CellType::Primitive {
264                    is_comb,
265                    name: prim_name,
266                    ..
267                } = &cell.prototype
268                {
269                    // If the cell is combinational and not driven by continuous assignments
270                    if *is_comb && !self.cont_cells.contains(&cell.name()) {
271                        let msg = format!(
272                            "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.",
273                            cell.name(),
274                            port.name,
275                            prim_name
276                        );
277                        // Use dummy Id to get correct source location for error
278                        Self::report(
279                            &mut self.diag,
280                            self.warning_as_error,
281                            Error::papercut(msg).with_pos(&s.attributes),
282                        );
283                    }
284                }
285            }
286        }
287        Ok(Action::Continue)
288    }
289}
290
291impl Papercut {
292    fn check_specs<T, P>(&mut self, assigns: &[ir::Assignment<T>], pos: &P)
293    where
294        P: WithPos,
295    {
296        let all_writes = assigns
297            .iter()
298            .analysis()
299            .writes()
300            .filter_map(port_information)
301            .into_grouping_map()
302            .collect::<HashSet<_>>();
303        let all_reads = assigns
304            .iter()
305            .analysis()
306            .reads()
307            .filter_map(port_information)
308            .into_grouping_map()
309            .collect::<HashSet<_>>();
310
311        for ((inst, comp_type), reads) in all_reads {
312            if let Some(spec) = self.read_together.get(&comp_type) {
313                let empty = HashSet::new();
314                let writes =
315                    all_writes.get(&(inst, comp_type)).unwrap_or(&empty);
316                for (read, required) in spec {
317                    if reads.contains(read)
318                        && required.difference(writes).next().is_some()
319                    {
320                        let missing = required
321                            .difference(writes)
322                            .sorted()
323                            .map(|port| format!("{}.{}", inst.clone(), port))
324                            .join(", ");
325                        let msg = format!(
326                            "Required signal not driven inside the group.\
327                                        \nWhen reading the port `{inst}.{read}', the ports [{missing}] must be written to.\
328                                        \nThe primitive type `{comp_type}' requires this invariant."
329                        );
330                        let err = Error::papercut(msg).with_pos(pos);
331                        Self::report(
332                            &mut self.diag,
333                            self.warning_as_error,
334                            err,
335                        );
336                    }
337                }
338            }
339        }
340
341        for ((inst, comp_type), writes) in all_writes {
342            if let Some(spec) = self.write_together.get(&comp_type) {
343                // For each write together spec.
344                for required in spec {
345                    // It should either be the case that:
346                    // 1. `writes` contains no writes that overlap with `required`
347                    //     In which case `required - writes` == `required`.
348                    // 2. `writes` contains writes that overlap with `required`
349                    //     In which case `required - writes == {}`
350                    let mut diff: HashSet<_> =
351                        required.difference(&writes).copied().collect();
352                    if diff.is_empty() || diff == *required {
353                        continue;
354                    }
355
356                    let first =
357                        writes.intersection(required).sorted().next().unwrap();
358                    let missing = diff
359                        .drain()
360                        .sorted()
361                        .map(|port| format!("{inst}.{port}"))
362                        .join(", ");
363                    let msg = format!(
364                        "Required signal not driven inside the group. \
365                                 When writing to the port `{inst}.{first}', the ports [{missing}] must also be written to. \
366                                 The primitive type `{comp_type}' specifies this using a @write_together spec."
367                    );
368                    let err = Error::papercut(msg).with_pos(pos);
369                    Self::report(&mut self.diag, self.warning_as_error, err);
370                }
371            }
372        }
373    }
374}