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
11type ReadTogether = (ir::Id, HashSet<ir::Id>);
14
15#[derive(Debug)]
18pub struct Papercut {
19 write_together: HashMap<ir::Id, Vec<HashSet<ir::Id>>>,
25
26 read_together: HashMap<ir::Id, Vec<ReadTogether>>,
30
31 cont_cells: HashSet<ir::Id>,
33
34 diag: DiagnosticContext,
36
37 warning_as_error: bool,
42}
43
44impl Papercut {
45 #[allow(unused)]
46 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 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 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
126fn 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 !comp.attributes.has(ir::BoolAttr::NoInterface) && !comp.is_comb {
157 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 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 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 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 *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 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 *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 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 required in spec {
345 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}