calyx_opt/passes/
papercut.rs1use 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
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
38impl Papercut {
39 #[allow(unused)]
40 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 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
92fn 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 !comp.attributes.has(ir::BoolAttr::NoInterface) && !comp.is_comb {
123 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 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 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 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 *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 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 *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 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 required in spec {
293 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}