circomspect_program_analysis/
signal_assignments.rs

1use log::{debug, trace};
2use program_structure::intermediate_representation::degree_meta::{DegreeRange, DegreeMeta};
3use std::collections::HashSet;
4
5use program_structure::cfg::{Cfg, DefinitionType};
6use program_structure::report_code::ReportCode;
7use program_structure::report::{Report, ReportCollection};
8use program_structure::ir::*;
9use program_structure::ir::AccessType;
10use program_structure::ir::variable_meta::VariableMeta;
11
12pub struct SignalAssignmentWarning {
13    signal: VariableName,
14    access: Vec<AccessType>,
15    assignment_meta: Meta,
16    constraint_metas: Vec<Meta>,
17}
18
19impl SignalAssignmentWarning {
20    pub fn into_report(self) -> Report {
21        let mut report = Report::warning(
22            "Using the signal assignment operator `<--` does not constrain the assigned signal."
23                .to_string(),
24            ReportCode::SignalAssignmentStatement,
25        );
26        // Add signal assignment warning.
27        if let Some(file_id) = self.assignment_meta.file_id {
28            report.add_primary(
29                self.assignment_meta.location,
30                file_id,
31                format!(
32                    "The assigned signal `{}{}` is not constrained here.",
33                    self.signal,
34                    access_to_string(&self.access)
35                ),
36            );
37        }
38        // Add any constraints as secondary labels.
39        for meta in self.constraint_metas {
40            if let Some(file_id) = meta.file_id {
41                report.add_secondary(
42                    meta.location,
43                    file_id,
44                    Some(format!(
45                        "The signal `{}{}` is constrained here.",
46                        self.signal,
47                        access_to_string(&self.access),
48                    )),
49                );
50            }
51        }
52        // If no constraints are identified, suggest using `<==` instead.
53        if report.secondary().is_empty() {
54            report.add_note(
55                "Consider if it is possible to rewrite the statement using `<==` instead."
56                    .to_string(),
57            );
58        }
59        report
60    }
61}
62
63pub struct UnecessarySignalAssignmentWarning {
64    signal: VariableName,
65    access: Vec<AccessType>,
66    assignment_meta: Meta,
67}
68
69impl UnecessarySignalAssignmentWarning {
70    pub fn into_report(self) -> Report {
71        let mut report = Report::warning(
72            "Using the signal assignment operator `<--` is not necessary here.".to_string(),
73            ReportCode::UnnecessarySignalAssignment,
74        );
75        // Add signal assignment warning.
76        if let Some(file_id) = self.assignment_meta.file_id {
77            report.add_primary(
78                self.assignment_meta.location,
79                file_id,
80                format!(
81                    "The expression assigned to `{}{}` is quadratic.",
82                    self.signal,
83                    access_to_string(&self.access)
84                ),
85            );
86        }
87        // We always suggest using `<==` instead.
88        report.add_note(
89            "Consider rewriting the statement using the constraint assignment operator `<==`."
90                .to_string(),
91        );
92        report
93    }
94}
95
96type AssignmentSet = HashSet<Assignment>;
97/// A signal assignment (implemented using either `<--` or `<==`).
98#[derive(Clone, Hash, PartialEq, Eq)]
99struct Assignment {
100    pub meta: Meta,
101    pub signal: VariableName,
102    pub access: Vec<AccessType>,
103    pub degree: Option<DegreeRange>,
104}
105
106impl Assignment {
107    fn new(
108        meta: &Meta,
109        signal: &VariableName,
110        access: &[AccessType],
111        degree: Option<&DegreeRange>,
112    ) -> Assignment {
113        Assignment {
114            meta: meta.clone(),
115            signal: signal.clone(),
116            access: access.to_owned(),
117            degree: degree.cloned(),
118        }
119    }
120
121    fn is_quadratic(&self) -> bool {
122        if let Some(range) = &self.degree {
123            range.is_quadratic()
124        } else {
125            false
126        }
127    }
128}
129
130type ConstraintSet = HashSet<Constraint>;
131
132#[derive(Clone, Hash, PartialEq, Eq)]
133struct Constraint {
134    pub meta: Meta,
135    pub lhe: Expression,
136    pub rhe: Expression,
137}
138
139impl Constraint {
140    fn new(meta: &Meta, lhe: &Expression, rhe: &Expression) -> Constraint {
141        Constraint { meta: meta.clone(), lhe: lhe.clone(), rhe: rhe.clone() }
142    }
143}
144
145/// This structure tracks signal assignments and constraints in a single
146/// template.
147#[derive(Clone, Default)]
148struct SignalUse {
149    assignments: AssignmentSet,
150    constraints: ConstraintSet,
151}
152
153impl SignalUse {
154    /// Create a new `ConstraintInfo` instance.
155    fn new() -> SignalUse {
156        SignalUse::default()
157    }
158
159    /// Add a signal assignment `var[access] <-- expr`.
160    fn add_assignment(
161        &mut self,
162        var: &VariableName,
163        access: &[AccessType],
164        meta: &Meta,
165        degree: Option<&DegreeRange>,
166    ) {
167        trace!("adding signal assignment for `{var:?}` access");
168        self.assignments.insert(Assignment::new(meta, var, access, degree));
169    }
170
171    /// Add a constraint `lhe === rhe`.
172    fn add_constraint(&mut self, lhe: &Expression, rhe: &Expression, meta: &Meta) {
173        trace!("adding constraint `{lhe:?} === {rhe:?}`");
174        self.constraints.insert(Constraint::new(meta, lhe, rhe));
175    }
176
177    /// Get all assignments.
178    fn get_assignments(&self) -> &AssignmentSet {
179        &self.assignments
180    }
181
182    /// Get the set of constraints that contain the given variable.
183    fn get_constraints(&self, signal: &VariableName, access: &Vec<AccessType>) -> Vec<&Constraint> {
184        self.constraints
185            .iter()
186            .filter(|constraint| {
187                let lhe = constraint.lhe.signals_read().iter();
188                let rhe = constraint.rhe.signals_read().iter();
189                lhe.chain(rhe)
190                    .any(|signal_use| signal_use.name() == signal && signal_use.access() == access)
191            })
192            .collect()
193    }
194
195    /// Returns the corresponding `Meta` of a constraint containing the given
196    /// signal, or `None` if no such constraint exists.
197    fn get_constraint_metas(&self, signal: &VariableName, access: &Vec<AccessType>) -> Vec<Meta> {
198        self.get_constraints(signal, access)
199            .iter()
200            .map(|constraint| constraint.meta.clone())
201            .collect()
202    }
203}
204
205/// The signal assignment operator `y <-- x` does not constrain the signal `y`.
206/// If the developer meant to use the constraint assignment operator `<==` this
207/// could lead to unexpected results.
208pub fn find_signal_assignments(cfg: &Cfg) -> ReportCollection {
209    use DefinitionType::*;
210    if matches!(cfg.definition_type(), Function | CustomTemplate) {
211        // Exit early if this is a function or custom template.
212        return ReportCollection::new();
213    }
214    debug!("running signal assignment analysis pass");
215    let mut signal_use = SignalUse::new();
216    for basic_block in cfg.iter() {
217        for stmt in basic_block.iter() {
218            visit_statement(stmt, &mut signal_use);
219        }
220    }
221    let mut reports = ReportCollection::new();
222    for assignment in signal_use.get_assignments() {
223        if assignment.is_quadratic() {
224            reports.push(build_unecessary_assignment_report(
225                &assignment.signal,
226                &assignment.access,
227                &assignment.meta,
228            ))
229        } else {
230            let constraint_metas =
231                signal_use.get_constraint_metas(&assignment.signal, &assignment.access);
232            reports.push(build_assignment_report(
233                &assignment.signal,
234                &assignment.access,
235                &assignment.meta,
236                &constraint_metas,
237            ));
238        }
239    }
240
241    debug!("{} new reports generated", reports.len());
242    reports
243}
244
245fn visit_statement(stmt: &Statement, signal_use: &mut SignalUse) {
246    use Expression::*;
247    use Statement::*;
248    match stmt {
249        Substitution { meta, var, op, rhe } => {
250            let access = if let Update { access, .. } = rhe { access.clone() } else { Vec::new() };
251            match op {
252                AssignOp::AssignSignal => {
253                    signal_use.add_assignment(var, &access, meta, rhe.degree());
254                }
255                // A signal cannot occur as the LHS of both a signal assignment
256                // and a signal constraint assignment. However, we still need to
257                // record the constraint added for each constraint assignment
258                // found.
259                AssignOp::AssignConstraintSignal => {
260                    let lhe = Expression::Variable { meta: meta.clone(), name: var.clone() };
261                    signal_use.add_constraint(&lhe, rhe, meta)
262                }
263                AssignOp::AssignLocalOrComponent => {}
264            }
265        }
266        ConstraintEquality { meta, lhe, rhe } => {
267            signal_use.add_constraint(lhe, rhe, meta);
268        }
269        _ => {}
270    }
271}
272
273fn build_unecessary_assignment_report(
274    signal: &VariableName,
275    access: &[AccessType],
276    assignment_meta: &Meta,
277) -> Report {
278    UnecessarySignalAssignmentWarning {
279        signal: signal.clone(),
280        access: access.to_owned(),
281        assignment_meta: assignment_meta.clone(),
282    }
283    .into_report()
284}
285
286fn build_assignment_report(
287    signal: &VariableName,
288    access: &[AccessType],
289    assignment_meta: &Meta,
290    constraint_metas: &[Meta],
291) -> Report {
292    SignalAssignmentWarning {
293        signal: signal.clone(),
294        access: access.to_owned(),
295        assignment_meta: assignment_meta.clone(),
296        constraint_metas: constraint_metas.to_owned(),
297    }
298    .into_report()
299}
300
301#[must_use]
302fn access_to_string(access: &[AccessType]) -> String {
303    access.iter().map(|access| access.to_string()).collect::<Vec<String>>().join("")
304}
305
306#[cfg(test)]
307mod tests {
308    use parser::parse_definition;
309    use program_structure::{cfg::IntoCfg, constants::Curve};
310
311    use super::*;
312
313    #[test]
314    fn test_signal_assignments() {
315        let src = r#"
316            template T(a) {
317                signal input in;
318                signal output out;
319
320                out <-- in + a;
321            }
322        "#;
323        validate_reports(src, 1);
324
325        let src = r#"
326            template T(a) {
327                signal input in;
328                signal output out;
329
330                in + a === out;
331                out <-- in + a;
332            }
333        "#;
334        validate_reports(src, 1);
335
336        let src = r#"
337            template T(n) {
338                signal input in;
339                signal output out[n];
340
341                in + 1 === out[0];
342                out[0] <-- in + 1;
343            }
344        "#;
345        validate_reports(src, 1);
346
347        let src = r#"
348            template T(n) {
349                signal output out[n];
350
351                in + 1 === out[0];
352                out[0] <-- in * in;
353            }
354        "#;
355        validate_reports(src, 1);
356    }
357
358    fn validate_reports(src: &str, expected_len: usize) {
359        // Build CFG.
360        println!("{}", src);
361        let mut reports = ReportCollection::new();
362        let cfg = parse_definition(src)
363            .unwrap()
364            .into_cfg(&Curve::default(), &mut reports)
365            .unwrap()
366            .into_ssa()
367            .unwrap();
368        assert!(reports.is_empty());
369
370        // Generate report collection.
371        let reports = find_signal_assignments(&cfg);
372        for report in &reports {
373            println!("{}", report.message())
374        }
375
376        assert_eq!(reports.len(), expected_len);
377    }
378}