circomspect_program_analysis/
analysis_runner.rs

1use log::{debug, trace};
2use std::path::PathBuf;
3use std::collections::HashMap;
4
5use parser::ParseResult;
6
7use program_structure::{
8    writers::{LogWriter, ReportWriter},
9    template_data::TemplateInfo,
10    function_data::FunctionInfo,
11    file_definition::{FileLibrary, FileLocation, FileID},
12    cfg::{Cfg, IntoCfg},
13    constants::Curve,
14    report::{ReportCollection, Report},
15};
16
17#[cfg(test)]
18use program_structure::template_library::TemplateLibrary;
19
20use crate::{
21    analysis_context::{AnalysisContext, AnalysisError},
22    get_analysis_passes, config,
23};
24
25type CfgCache = HashMap<String, Cfg>;
26type ReportCache = HashMap<String, ReportCollection>;
27
28/// A type responsible for caching CFGs and running analysis passes over all
29/// functions and templates.
30#[derive(Default)]
31pub struct AnalysisRunner {
32    curve: Curve,
33    libraries: Vec<PathBuf>,
34    /// The corresponding file library including file includes.
35    file_library: FileLibrary,
36    /// Template ASTs generated by the parser.
37    template_asts: TemplateInfo,
38    /// Function ASTs generated by the parser.
39    function_asts: FunctionInfo,
40    /// Cached template CFGs generated on demand.
41    template_cfgs: CfgCache,
42    /// Cached function CFGs generated on demand.
43    function_cfgs: CfgCache,
44    /// Reports created during CFG generation.
45    template_reports: ReportCache,
46    /// Reports created during CFG generation.
47    function_reports: ReportCache,
48}
49
50impl AnalysisRunner {
51    pub fn new(curve: Curve) -> Self {
52        AnalysisRunner { curve, ..Default::default() }
53    }
54
55    pub fn with_libraries(mut self, libraries: &[PathBuf]) -> Self {
56        self.libraries.extend_from_slice(libraries);
57        self
58    }
59
60    pub fn with_files(mut self, input_files: &[PathBuf]) -> (Self, ReportCollection) {
61        let reports =
62            match parser::parse_files(input_files, &self.libraries, &config::COMPILER_VERSION) {
63                ParseResult::Program(program, warnings) => {
64                    self.template_asts = program.templates;
65                    self.function_asts = program.functions;
66                    self.file_library = program.file_library;
67                    warnings
68                }
69                ParseResult::Library(library, warnings) => {
70                    self.template_asts = library.templates;
71                    self.function_asts = library.functions;
72                    self.file_library = library.file_library;
73                    warnings
74                }
75            };
76        (self, reports)
77    }
78
79    /// Convenience method used to generate a runner for testing purposes.
80    #[cfg(test)]
81    pub fn with_src(mut self, file_contents: &[&str]) -> Self {
82        use parser::parse_definition;
83
84        let mut library_contents = HashMap::new();
85        let mut file_library = FileLibrary::default();
86        for (file_index, file_source) in file_contents.iter().enumerate() {
87            let file_name = format!("file-{file_index}.circom");
88            let file_id = file_library.add_file(file_name, file_source.to_string(), true);
89            library_contents.insert(file_id, vec![parse_definition(file_source).unwrap()]);
90        }
91        let template_library = TemplateLibrary::new(library_contents, file_library.clone());
92        self.template_asts = template_library.templates;
93        self.function_asts = template_library.functions;
94        self.file_library = template_library.file_library;
95
96        self
97    }
98
99    pub fn file_library(&self) -> &FileLibrary {
100        &self.file_library
101    }
102
103    pub fn template_names(&self, user_input_only: bool) -> Vec<String> {
104        // Clone template names to avoid holding multiple references to `self`.
105        self.template_asts
106            .iter()
107            .filter_map(|(name, ast)| {
108                if !user_input_only || self.file_library.is_user_input(ast.get_file_id()) {
109                    Some(name)
110                } else {
111                    None
112                }
113            })
114            .cloned()
115            .collect()
116    }
117
118    pub fn function_names(&self, user_input_only: bool) -> Vec<String> {
119        // Clone function names to avoid holding multiple references to `self`.
120        self.function_asts
121            .iter()
122            .filter_map(|(name, ast)| {
123                if !user_input_only || self.file_library.is_user_input(ast.get_file_id()) {
124                    Some(name)
125                } else {
126                    None
127                }
128            })
129            .cloned()
130            .collect()
131    }
132
133    fn analyze_template<W: LogWriter + ReportWriter>(&mut self, name: &str, writer: &mut W) {
134        writer.write_message(&format!("analyzing template '{name}'"));
135
136        // We take ownership of the CFG and any previously generated reports
137        // here to avoid holding multiple mutable and immutable references to
138        // `self`. This may lead to the CFG being regenerated during analysis if
139        // the template is invoked recursively. If it is then ¯\_(ツ)_/¯.
140        let mut reports = self.take_template_reports(name);
141        if let Ok(cfg) = self.take_template(name) {
142            for analysis_pass in get_analysis_passes() {
143                reports.append(&mut analysis_pass(self, &cfg));
144            }
145            // Re-insert the CFG into the hash map.
146            if self.replace_template(name, cfg) {
147                debug!("template `{name}` CFG was regenerated during analysis");
148            }
149        }
150        writer.write_reports(&reports, &self.file_library);
151    }
152
153    pub fn analyze_templates<W: LogWriter + ReportWriter>(
154        &mut self,
155        writer: &mut W,
156        user_input_only: bool,
157    ) {
158        for name in self.template_names(user_input_only) {
159            self.analyze_template(&name, writer);
160        }
161    }
162
163    fn analyze_function<W: LogWriter + ReportWriter>(&mut self, name: &str, writer: &mut W) {
164        writer.write_message(&format!("analyzing function '{name}'"));
165
166        // We take ownership of the CFG and any previously generated reports
167        // here to avoid holding multiple mutable and immutable references to
168        // `self`. This may lead to the CFG being regenerated during analysis if
169        // the function is invoked recursively. If it is then ¯\_(ツ)_/¯.
170        let mut reports = self.take_function_reports(name);
171        if let Ok(cfg) = self.take_function(name) {
172            for analysis_pass in get_analysis_passes() {
173                reports.append(&mut analysis_pass(self, &cfg));
174            }
175            // Re-insert the CFG into the hash map.
176            if self.replace_function(name, cfg) {
177                debug!("function `{name}` CFG was regenerated during analysis");
178            }
179        }
180        writer.write_reports(&reports, &self.file_library);
181    }
182
183    pub fn analyze_functions<W: LogWriter + ReportWriter>(
184        &mut self,
185        writer: &mut W,
186        user_input_only: bool,
187    ) {
188        for name in self.function_names(user_input_only) {
189            self.analyze_function(&name, writer);
190        }
191    }
192
193    /// Report cache from CFG generation. These will be emitted when the
194    /// template is analyzed.
195    fn append_template_reports(&mut self, name: &str, reports: &mut ReportCollection) {
196        self.template_reports.entry(name.to_string()).or_default().append(reports);
197    }
198
199    /// Report cache from CFG generation. These will be emitted when the
200    /// template is analyzed.
201    fn take_template_reports(&mut self, name: &str) -> ReportCollection {
202        self.template_reports.remove(name).unwrap_or_default()
203    }
204
205    /// Report cache from CFG generation. These will be emitted when the
206    /// function is analyzed.
207    fn append_function_reports(&mut self, name: &str, reports: &mut ReportCollection) {
208        self.function_reports.entry(name.to_string()).or_default().append(reports);
209    }
210
211    /// Report cache from CFG generation. These will be emitted when the
212    /// function is analyzed.
213    fn take_function_reports(&mut self, name: &str) -> ReportCollection {
214        self.function_reports.remove(name).unwrap_or_default()
215    }
216
217    fn cache_template(&mut self, name: &str) -> Result<&Cfg, AnalysisError> {
218        if !self.template_cfgs.contains_key(name) {
219            // The template CFG needs to be generated from the AST.
220            if self.template_reports.contains_key(name) {
221                // We have already failed to generate the CFG.
222                return Err(AnalysisError::FailedToLiftTemplate { name: name.to_string() });
223            }
224            // Get the AST corresponding to the template.
225            let Some(ast) = self.template_asts.get(name) else {
226                trace!("failed to lift unknown template `{name}`");
227                return Err(AnalysisError::UnknownTemplate { name: name.to_string() });
228            };
229            // Generate the template CFG from the AST. Cache any reports.
230            let mut reports = ReportCollection::new();
231            let cfg = generate_cfg(ast, &self.curve, &mut reports).map_err(|report| {
232                reports.push(*report);
233                trace!("failed to lift template `{name}`");
234                AnalysisError::FailedToLiftTemplate { name: name.to_string() }
235            })?;
236            self.append_template_reports(name, &mut reports);
237            self.template_cfgs.insert(name.to_string(), cfg);
238            trace!("successfully lifted template `{name}`");
239        }
240        Ok(self.template_cfgs.get(name).unwrap())
241    }
242
243    fn cache_function(&mut self, name: &str) -> Result<&Cfg, AnalysisError> {
244        if !self.function_cfgs.contains_key(name) {
245            // The function CFG needs to be generated from the AST.
246            if self.function_reports.contains_key(name) {
247                // We have already failed to generate the CFG.
248                return Err(AnalysisError::FailedToLiftFunction { name: name.to_string() });
249            }
250            // Get the AST corresponding to the function.
251            let Some(ast) = self.function_asts.get(name) else {
252                trace!("failed to lift unknown function `{name}`");
253                return Err(AnalysisError::UnknownFunction { name: name.to_string() });
254            };
255            // Generate the function CFG from the AST. Cache any reports.
256            let mut reports = ReportCollection::new();
257            let cfg = generate_cfg(ast, &self.curve, &mut reports).map_err(|report| {
258                reports.push(*report);
259                trace!("failed to lift function `{name}`");
260                AnalysisError::FailedToLiftFunction { name: name.to_string() }
261            })?;
262            self.append_function_reports(name, &mut reports);
263            self.function_cfgs.insert(name.to_string(), cfg);
264            trace!("successfully lifted function `{name}`");
265        }
266        Ok(self.function_cfgs.get(name).unwrap())
267    }
268
269    pub fn take_template(&mut self, name: &str) -> Result<Cfg, AnalysisError> {
270        self.cache_template(name)?;
271        // The CFG must be available since caching was successful.
272        Ok(self.template_cfgs.remove(name).unwrap())
273    }
274
275    pub fn take_function(&mut self, name: &str) -> Result<Cfg, AnalysisError> {
276        self.cache_function(name)?;
277        // The CFG must be available since caching was successful.
278        Ok(self.function_cfgs.remove(name).unwrap())
279    }
280
281    pub fn replace_template(&mut self, name: &str, cfg: Cfg) -> bool {
282        self.template_cfgs.insert(name.to_string(), cfg).is_some()
283    }
284
285    pub fn replace_function(&mut self, name: &str, cfg: Cfg) -> bool {
286        self.function_cfgs.insert(name.to_string(), cfg).is_some()
287    }
288}
289
290impl AnalysisContext for AnalysisRunner {
291    fn is_template(&self, name: &str) -> bool {
292        self.template_asts.contains_key(name)
293    }
294
295    fn is_function(&self, name: &str) -> bool {
296        self.function_asts.contains_key(name)
297    }
298
299    fn template(&mut self, name: &str) -> Result<&Cfg, AnalysisError> {
300        self.cache_template(name)
301    }
302
303    fn function(&mut self, name: &str) -> Result<&Cfg, AnalysisError> {
304        self.cache_function(name)
305    }
306
307    fn underlying_str(
308        &self,
309        file_id: &FileID,
310        file_location: &FileLocation,
311    ) -> Result<String, AnalysisError> {
312        let Ok(file) = self.file_library.to_storage().get(*file_id) else {
313            return Err(AnalysisError::UnknownFile { file_id: *file_id });
314        };
315        if file_location.end <= file.source().len() {
316            Ok(file.source()[file_location.start..file_location.end].to_string())
317        } else {
318            Err(AnalysisError::InvalidLocation {
319                file_id: *file_id,
320                file_location: file_location.clone(),
321            })
322        }
323    }
324}
325
326fn generate_cfg<Ast: IntoCfg>(
327    ast: Ast,
328    curve: &Curve,
329    reports: &mut ReportCollection,
330) -> Result<Cfg, Box<Report>> {
331    ast.into_cfg(curve, reports)
332        .map_err(|error| Box::new(error.into()))?
333        .into_ssa()
334        .map_err(|error| Box::new(error.into()))
335}
336
337#[cfg(test)]
338mod tests {
339    use program_structure::ir::Statement;
340
341    use super::*;
342
343    #[test]
344    fn test_function() {
345        let mut runner = AnalysisRunner::new(Curve::Goldilocks).with_src(&[r#"
346            function foo(a) {
347                return a[0] + a[1];
348            }
349        "#]);
350
351        // Check that `foo` is a known function, that we can access the CFG
352        // for `foo`, and that the CFG is properly cached.
353        assert!(runner.is_function("foo"));
354        assert!(!runner.function_cfgs.contains_key("foo"));
355        assert!(runner.function("foo").is_ok());
356        assert!(runner.function_cfgs.contains_key("foo"));
357
358        // Check that the `take_function` and `replace_function` APIs work as expected.
359        let cfg = runner.take_function("foo").unwrap();
360        assert!(!runner.function_cfgs.contains_key("foo"));
361        assert!(!runner.replace_function("foo", cfg));
362        assert!(runner.function_cfgs.contains_key("foo"));
363
364        // Check that `baz` is not a known function, that attempting to access
365        // `baz` produces an error, and that nothing is cached.
366        assert!(!runner.is_function("baz"));
367        assert!(!runner.function_cfgs.contains_key("baz"));
368        assert!(matches!(runner.function("baz"), Err(AnalysisError::UnknownFunction { .. })));
369        assert!(!runner.function_cfgs.contains_key("baz"));
370    }
371
372    #[test]
373    fn test_template() {
374        let mut runner = AnalysisRunner::new(Curve::Goldilocks).with_src(&[r#"
375            template Foo(n) {
376                signal input a[2];
377
378                a[0] === a[1];
379            }
380        "#]);
381
382        // Check that `Foo` is a known template, that we can access the CFG
383        // for `Foo`, and that the CFG is properly cached.
384        assert!(runner.is_template("Foo"));
385        assert!(!runner.template_cfgs.contains_key("Foo"));
386        assert!(runner.template("Foo").is_ok());
387        assert!(runner.template_cfgs.contains_key("Foo"));
388
389        // Check that the `take_template` and `replace_template` APIs work as expected.
390        let cfg = runner.take_template("Foo").unwrap();
391        assert!(!runner.template_cfgs.contains_key("Foo"));
392        assert!(!runner.replace_template("Foo", cfg));
393        assert!(runner.template_cfgs.contains_key("Foo"));
394
395        // Check that `Baz` is not a known template, that attempting to access
396        // `Baz` produces an error, and that nothing is cached.
397        assert!(!runner.is_template("Baz"));
398        assert!(!runner.template_cfgs.contains_key("Baz"));
399        assert!(matches!(runner.template("Baz"), Err(AnalysisError::UnknownTemplate { .. })));
400        assert!(!runner.template_cfgs.contains_key("Baz"));
401    }
402
403    #[test]
404    fn test_underlying_str() {
405        use Statement::*;
406        let mut runner = AnalysisRunner::new(Curve::Goldilocks).with_src(&[r#"
407            template Foo(n) {
408                signal input a[2];
409
410                a[0] === a[1];
411            }
412        "#]);
413
414        let cfg = runner.take_template("Foo").unwrap();
415        for stmt in cfg.entry_block().iter() {
416            let file_id = stmt.meta().file_id().unwrap();
417            let file_location = stmt.meta().file_location();
418            let string = runner.underlying_str(&file_id, &file_location).unwrap();
419            match stmt {
420                // TODO: Why do some statements include the semi-colon and others don't?
421                Declaration { .. } => assert_eq!(string, "signal input a[2]"),
422                ConstraintEquality { .. } => assert_eq!(string, "a[0] === a[1];"),
423                _ => unreachable!(),
424            }
425        }
426    }
427}