calyx_opt/passes/
papercut.rs1use 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
8type ReadTogether = (ir::Id, HashSet<ir::Id>);
11
12pub struct Papercut {
15 write_together: HashMap<ir::Id, Vec<HashSet<ir::Id>>>,
21
22 read_together: HashMap<ir::Id, Vec<ReadTogether>>,
26
27 cont_cells: HashSet<ir::Id>,
29}
30
31impl Papercut {
32 #[allow(unused)]
33 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 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
84fn 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 !comp.attributes.has(ir::BoolAttr::NoInterface) && !comp.is_comb {
109 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 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 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 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 *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 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 *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 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 required in spec {
274 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 Ok(Action::Continue)
306 }
307}