circomspect_program_analysis/
analysis_runner.rs1use 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#[derive(Default)]
31pub struct AnalysisRunner {
32 curve: Curve,
33 libraries: Vec<PathBuf>,
34 file_library: FileLibrary,
36 template_asts: TemplateInfo,
38 function_asts: FunctionInfo,
40 template_cfgs: CfgCache,
42 function_cfgs: CfgCache,
44 template_reports: ReportCache,
46 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 #[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 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 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 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 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 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 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 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 fn take_template_reports(&mut self, name: &str) -> ReportCollection {
202 self.template_reports.remove(name).unwrap_or_default()
203 }
204
205 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 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 if self.template_reports.contains_key(name) {
221 return Err(AnalysisError::FailedToLiftTemplate { name: name.to_string() });
223 }
224 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 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 if self.function_reports.contains_key(name) {
247 return Err(AnalysisError::FailedToLiftFunction { name: name.to_string() });
249 }
250 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 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 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 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 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 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 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 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 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 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 Declaration { .. } => assert_eq!(string, "signal input a[2]"),
422 ConstraintEquality { .. } => assert_eq!(string, "a[0] === a[1];"),
423 _ => unreachable!(),
424 }
425 }
426 }
427}