leo_compiler/
compiler.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17//! The compiler for Leo programs.
18//!
19//! The [`Compiler`] type compiles Leo programs into R1CS circuits.
20
21use crate::CompilerOptions;
22
23pub use leo_ast::Ast;
24use leo_ast::{NodeBuilder, Program, Stub};
25use leo_errors::{CompilerError, Result, emitter::Handler};
26use leo_passes::*;
27use leo_span::{Symbol, source_map::FileName, symbol::with_session_globals};
28
29use snarkvm::prelude::Network;
30
31use indexmap::{IndexMap, IndexSet};
32use std::{
33    fs,
34    path::{Path, PathBuf},
35};
36
37/// The primary entry point of the Leo compiler.
38#[derive(Clone)]
39pub struct Compiler<'a, N: Network> {
40    /// The handler is used for error and warning emissions.
41    handler: &'a Handler,
42    /// The path to where the compiler outputs all generated files.
43    output_directory: PathBuf,
44    /// The program name,
45    pub program_name: String,
46    /// The AST for the program.
47    pub ast: Ast,
48    /// Options configuring compilation.
49    compiler_options: CompilerOptions,
50    /// The `NodeCounter` used to generate sequentially increasing `NodeID`s.
51    node_builder: NodeBuilder,
52    /// The `Assigner` is used to construct (unique) assignment statements.
53    assigner: Assigner,
54    /// The type table.
55    type_table: TypeTable,
56    /// The stubs for imported programs. Produced by `Retriever` module.
57    import_stubs: IndexMap<Symbol, Stub>,
58    /// How many statements were in the AST before DCE?
59    pub statements_before_dce: u32,
60    /// How many statements were in the AST after DCE?
61    pub statements_after_dce: u32,
62    // Allows the compiler to be generic over the network.
63    phantom: std::marker::PhantomData<N>,
64}
65
66impl<'a, N: Network> Compiler<'a, N> {
67    pub fn parse(&mut self, source: &str, filename: FileName) -> Result<()> {
68        // Register the source in the source map.
69        let source_file = with_session_globals(|s| s.source_map.new_source(source, filename));
70
71        // Use the parser to construct the abstract syntax tree (ast).
72        self.ast =
73            leo_parser::parse_ast::<N>(self.handler, &self.node_builder, &source_file.src, source_file.start_pos)?;
74
75        // If the program is imported, then check that the name of its program scope matches the file name.
76        // Note that parsing enforces that there is exactly one program scope in a file.
77        // TODO: Clean up check.
78        let program_scope = self.ast.ast.program_scopes.values().next().unwrap();
79        self.program_name = program_scope.program_id.name.to_string();
80
81        if self.compiler_options.output.initial_ast {
82            self.write_ast_to_json("initial_ast.json")?;
83        }
84
85        Ok(())
86    }
87
88    pub fn parse_from_file(&mut self, source_file_path: impl AsRef<Path>) -> Result<()> {
89        // Load the program file.
90        let source = fs::read_to_string(&source_file_path)
91            .map_err(|e| CompilerError::file_read_error(source_file_path.as_ref().display().to_string(), e))?;
92        self.parse(&source, FileName::Real(source_file_path.as_ref().into()))
93    }
94
95    /// Returns a new Leo compiler.
96    pub fn new(
97        handler: &'a Handler,
98        output_directory: PathBuf,
99        compiler_options: Option<CompilerOptions>,
100        import_stubs: IndexMap<Symbol, Stub>,
101    ) -> Self {
102        let node_builder = NodeBuilder::default();
103        let assigner = Assigner::default();
104        let type_table = TypeTable::default();
105        Self {
106            handler,
107            output_directory,
108            program_name: Default::default(),
109            ast: Ast::new(Program::default()),
110            compiler_options: compiler_options.unwrap_or_default(),
111            node_builder,
112            assigner,
113            import_stubs,
114            statements_before_dce: 0,
115            statements_after_dce: 0,
116            type_table,
117            phantom: Default::default(),
118        }
119    }
120
121    /// Runs the symbol table pass.
122    pub fn symbol_table_pass(&self) -> Result<SymbolTable> {
123        let symbol_table = SymbolTableCreator::do_pass((&self.ast, self.handler))?;
124        Ok(symbol_table)
125    }
126
127    /// Runs the type checker pass.
128    pub fn type_checker_pass(&'a self, symbol_table: &mut SymbolTable) -> Result<(StructGraph, CallGraph)> {
129        let (struct_graph, call_graph) =
130            TypeChecker::do_pass((&self.ast, self.handler, symbol_table, &self.type_table, NetworkLimits {
131                max_array_elements: N::MAX_ARRAY_ELEMENTS,
132                max_mappings: N::MAX_MAPPINGS,
133                max_functions: N::MAX_FUNCTIONS,
134            }))?;
135        Ok((struct_graph, call_graph))
136    }
137
138    /// Runs the static analysis pass.
139    pub fn static_analysis_pass(&mut self, symbol_table: &SymbolTable) -> Result<()> {
140        StaticAnalyzer::<N>::do_pass((
141            &self.ast,
142            self.handler,
143            symbol_table,
144            &self.type_table,
145            self.compiler_options.build.conditional_block_max_depth,
146            self.compiler_options.build.disable_conditional_branch_type_checking,
147        ))
148    }
149
150    /// Run const propagation and loop unrolling until we hit a fixed point or find an error.
151    pub fn const_propagation_and_unroll_loop(&mut self, symbol_table: &mut SymbolTable) -> Result<()> {
152        const LARGE_LOOP_BOUND: usize = 1024usize;
153
154        for _ in 0..LARGE_LOOP_BOUND {
155            let loop_unroll_output = self.loop_unrolling_pass(symbol_table)?;
156
157            let const_prop_output = self.const_propagation_pass(symbol_table)?;
158
159            if !const_prop_output.changed && !loop_unroll_output.loop_unrolled {
160                // We've got a fixed point, so see if we have any errors.
161                if let Some(not_evaluated_span) = const_prop_output.const_not_evaluated {
162                    return Err(CompilerError::const_not_evaluated(not_evaluated_span).into());
163                }
164
165                if let Some(not_evaluated_span) = const_prop_output.array_index_not_evaluated {
166                    return Err(CompilerError::array_index_not_evaluated(not_evaluated_span).into());
167                }
168
169                if let Some(not_unrolled_span) = loop_unroll_output.loop_not_unrolled {
170                    return Err(CompilerError::loop_bounds_not_evaluated(not_unrolled_span).into());
171                }
172
173                if self.compiler_options.output.unrolled_ast {
174                    self.write_ast_to_json("unrolled_ast.json")?;
175                }
176
177                return Ok(());
178            }
179        }
180
181        // Note that it's challenging to write code in practice that demonstrates this error, because Leo code
182        // with many nested loops or operations will blow the stack in the compiler before this bound is hit.
183        Err(CompilerError::const_prop_unroll_many_loops(LARGE_LOOP_BOUND, Default::default()).into())
184    }
185
186    /// Runs the const propagation pass.
187    pub fn const_propagation_pass(&mut self, symbol_table: &mut SymbolTable) -> Result<ConstPropagatorOutput> {
188        let (ast, output) = ConstPropagator::do_pass((
189            std::mem::take(&mut self.ast),
190            self.handler,
191            symbol_table,
192            &self.type_table,
193            &self.node_builder,
194        ))?;
195
196        self.ast = ast;
197
198        Ok(output)
199    }
200
201    /// Runs the loop unrolling pass.
202    pub fn loop_unrolling_pass(&mut self, symbol_table: &mut SymbolTable) -> Result<UnrollerOutput> {
203        let (ast, output) = Unroller::do_pass((
204            std::mem::take(&mut self.ast),
205            self.handler,
206            &self.node_builder,
207            symbol_table,
208            &self.type_table,
209        ))?;
210
211        self.ast = ast;
212
213        Ok(output)
214    }
215
216    /// Runs the static single assignment pass.
217    pub fn static_single_assignment_pass(&mut self, symbol_table: &SymbolTable) -> Result<()> {
218        self.ast = StaticSingleAssigner::do_pass((
219            std::mem::take(&mut self.ast),
220            &self.node_builder,
221            &self.assigner,
222            symbol_table,
223            &self.type_table,
224        ))?;
225
226        if self.compiler_options.output.ssa_ast {
227            self.write_ast_to_json("ssa_ast.json")?;
228        }
229
230        Ok(())
231    }
232
233    /// Runs the flattening pass.
234    pub fn flattening_pass(&mut self, symbol_table: &SymbolTable) -> Result<()> {
235        self.ast = Flattener::do_pass((
236            std::mem::take(&mut self.ast),
237            symbol_table,
238            &self.type_table,
239            &self.node_builder,
240            &self.assigner,
241        ))?;
242
243        if self.compiler_options.output.flattened_ast {
244            self.write_ast_to_json("flattened_ast.json")?;
245        }
246
247        Ok(())
248    }
249
250    /// Runs the destructuring pass.
251    pub fn destructuring_pass(&mut self) -> Result<()> {
252        self.ast = Destructurer::do_pass((
253            std::mem::take(&mut self.ast),
254            &self.type_table,
255            &self.node_builder,
256            &self.assigner,
257        ))?;
258
259        if self.compiler_options.output.destructured_ast {
260            self.write_ast_to_json("destructured_ast.json")?;
261        }
262
263        Ok(())
264    }
265
266    /// Runs the function inlining pass.
267    pub fn function_inlining_pass(&mut self, call_graph: &CallGraph) -> Result<()> {
268        let ast = FunctionInliner::do_pass((
269            std::mem::take(&mut self.ast),
270            &self.node_builder,
271            call_graph,
272            &self.type_table,
273        ))?;
274        self.ast = ast;
275
276        if self.compiler_options.output.inlined_ast {
277            self.write_ast_to_json("inlined_ast.json")?;
278        }
279
280        Ok(())
281    }
282
283    /// Runs the dead code elimination pass.
284    pub fn dead_code_elimination_pass(&mut self) -> Result<()> {
285        if self.compiler_options.build.dce_enabled {
286            let output = DeadCodeEliminator::do_pass((std::mem::take(&mut self.ast), &self.type_table))?;
287            self.ast = output.ast;
288            self.statements_before_dce = output.statements_before;
289            self.statements_after_dce = output.statements_after;
290        }
291
292        if self.compiler_options.output.dce_ast {
293            self.write_ast_to_json("dce_ast.json")?;
294        }
295
296        Ok(())
297    }
298
299    /// Runs the code generation pass.
300    pub fn code_generation_pass(
301        &mut self,
302        symbol_table: &SymbolTable,
303        struct_graph: &StructGraph,
304        call_graph: &CallGraph,
305    ) -> Result<String> {
306        CodeGenerator::do_pass((&self.ast, symbol_table, &self.type_table, struct_graph, call_graph, &self.ast.ast))
307    }
308
309    /// Runs the compiler stages.
310    pub fn intermediate_passes(&mut self) -> Result<(SymbolTable, StructGraph, CallGraph)> {
311        let mut st = self.symbol_table_pass()?;
312
313        let (struct_graph, call_graph) = self.type_checker_pass(&mut st)?;
314
315        self.static_analysis_pass(&st)?;
316
317        self.const_propagation_and_unroll_loop(&mut st)?;
318
319        self.static_single_assignment_pass(&st)?;
320
321        self.flattening_pass(&st)?;
322
323        self.destructuring_pass()?;
324
325        self.function_inlining_pass(&call_graph)?;
326
327        self.dead_code_elimination_pass()?;
328
329        Ok((st, struct_graph, call_graph))
330    }
331
332    /// Returns a compiled Leo program.
333    pub fn compile(&mut self, source: &str, filename: FileName) -> Result<String> {
334        // Parse the program.
335        self.parse(source, filename)?;
336        // Copy the dependencies specified in `program.json` into the AST.
337        self.add_import_stubs()?;
338        // Run the intermediate compiler stages.
339        let (symbol_table, struct_graph, call_graph) = self.intermediate_passes()?;
340        // Run code generation.
341        let bytecode = self.code_generation_pass(&symbol_table, &struct_graph, &call_graph)?;
342        Ok(bytecode)
343    }
344
345    pub fn compile_from_file(&mut self, source_file_path: impl AsRef<Path>) -> Result<String> {
346        let source = fs::read_to_string(&source_file_path)
347            .map_err(|e| CompilerError::file_read_error(source_file_path.as_ref().display().to_string(), e))?;
348        self.compile(&source, FileName::Real(source_file_path.as_ref().into()))
349    }
350
351    /// Writes the AST to a JSON file.
352    fn write_ast_to_json(&self, file_suffix: &str) -> Result<()> {
353        // Remove `Span`s if they are not enabled.
354        if self.compiler_options.output.ast_spans_enabled {
355            self.ast.to_json_file(self.output_directory.clone(), &format!("{}.{file_suffix}", self.program_name))?;
356        } else {
357            self.ast.to_json_file_without_keys(
358                self.output_directory.clone(),
359                &format!("{}.{file_suffix}", self.program_name),
360                &["_span", "span"],
361            )?;
362        }
363        Ok(())
364    }
365
366    /// Merges the dependencies defined in `program.json` with the dependencies imported in `.leo` file
367    pub fn add_import_stubs(&mut self) -> Result<()> {
368        // Create a list of both the explicit dependencies specified in the `.leo` file, as well as the implicit ones derived from those dependencies.
369        let (mut unexplored, mut explored): (IndexSet<Symbol>, IndexSet<Symbol>) =
370            (self.ast.ast.imports.keys().cloned().collect(), IndexSet::new());
371        while !unexplored.is_empty() {
372            let mut current_dependencies: IndexSet<Symbol> = IndexSet::new();
373            for program_name in unexplored.iter() {
374                if let Some(stub) = self.import_stubs.get(program_name) {
375                    // Add the program to the explored set
376                    explored.insert(*program_name);
377                    for dependency in stub.imports.iter() {
378                        // If dependency is already explored then don't need to re-explore it
379                        if explored.insert(dependency.name.name) {
380                            current_dependencies.insert(dependency.name.name);
381                        }
382                    }
383                } else {
384                    return Err(CompilerError::imported_program_not_found(
385                        self.program_name.clone(),
386                        *program_name,
387                        self.ast.ast.imports[program_name].1,
388                    )
389                    .into());
390                }
391            }
392
393            // Create next batch to explore
394            unexplored = current_dependencies;
395        }
396
397        // Combine the dependencies from `program.json` and `.leo` file while preserving the post-order
398        self.ast.ast.stubs =
399            self.import_stubs.clone().into_iter().filter(|(program_name, _)| explored.contains(program_name)).collect();
400        Ok(())
401    }
402}