Skip to main content

leo_compiler/
compiler.rs

1// Copyright (C) 2019-2026 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::{AstSnapshots, CompilerOptions};
22
23pub use leo_ast::{Ast, Program};
24use leo_ast::{NetworkName, NodeBuilder, Stub};
25use leo_errors::{CompilerError, Handler, Result};
26use leo_passes::*;
27use leo_span::{Symbol, source_map::FileName, with_session_globals};
28
29use std::{
30    ffi::OsStr,
31    fs,
32    path::{Path, PathBuf},
33    rc::Rc,
34};
35
36use indexmap::{IndexMap, IndexSet};
37use walkdir::WalkDir;
38
39/// A single compiled program with its bytecode and ABI.
40pub struct CompiledProgram {
41    /// The program name (without `.aleo` suffix).
42    pub name: String,
43    /// The generated Aleo bytecode.
44    pub bytecode: String,
45    /// The ABI describing the program's public interface.
46    pub abi: leo_abi::Program,
47}
48
49/// The result of compiling a Leo program.
50pub struct Compiled {
51    /// The primary program that was compiled.
52    pub primary: CompiledProgram,
53    /// Compiled programs for imports.
54    pub imports: Vec<CompiledProgram>,
55}
56
57/// The primary entry point of the Leo compiler.
58pub struct Compiler {
59    /// The path to where the compiler outputs all generated files.
60    output_directory: PathBuf,
61    /// The program name,
62    pub program_name: Option<String>,
63    /// Options configuring compilation.
64    compiler_options: CompilerOptions,
65    /// State.
66    state: CompilerState,
67    /// The stubs for imported programs.
68    import_stubs: IndexMap<Symbol, Stub>,
69    /// How many statements were in the AST before DCE?
70    pub statements_before_dce: u32,
71    /// How many statements were in the AST after DCE?
72    pub statements_after_dce: u32,
73}
74
75impl Compiler {
76    pub fn parse(&mut self, source: &str, filename: FileName, modules: &[(&str, FileName)]) -> Result<()> {
77        // Register the source in the source map.
78        let source_file = with_session_globals(|s| s.source_map.new_source(source, filename.clone()));
79
80        // Register the sources of all the modules in the source map.
81        let modules = modules
82            .iter()
83            .map(|(source, filename)| with_session_globals(|s| s.source_map.new_source(source, filename.clone())))
84            .collect::<Vec<_>>();
85
86        // Use the parser to construct the abstract syntax tree (ast).
87        self.state.ast = leo_parser::parse_ast(
88            self.state.handler.clone(),
89            &self.state.node_builder,
90            &source_file,
91            &modules,
92            self.state.network,
93        )?;
94
95        // Check that the name of its program scope matches the expected name.
96        // Note that parsing enforces that there is exactly one program scope in a file.
97        let program_scope = self.state.ast.ast.program_scopes.values().next().unwrap();
98        if self.program_name.is_none() {
99            self.program_name = Some(program_scope.program_id.name.to_string());
100        } else if self.program_name != Some(program_scope.program_id.name.to_string()) {
101            return Err(CompilerError::program_name_should_match_file_name(
102                program_scope.program_id.name,
103                // If this is a test, use the filename as the expected name.
104                if self.state.is_test {
105                    format!(
106                        "`{}` (the test file name)",
107                        filename.to_string().split("/").last().expect("Could not get file name")
108                    )
109                } else {
110                    format!("`{}` (specified in `program.json`)", self.program_name.as_ref().unwrap())
111                },
112                program_scope.program_id.name.span,
113            )
114            .into());
115        }
116
117        if self.compiler_options.initial_ast {
118            self.write_ast_to_json("initial.json")?;
119            self.write_ast("initial.ast")?;
120        }
121
122        Ok(())
123    }
124
125    /// Simple wrapper around `parse` that also returns the AST.
126    pub fn parse_and_return_ast(
127        &mut self,
128        source: &str,
129        filename: FileName,
130        modules: &[(&str, FileName)],
131    ) -> Result<Program> {
132        // Parse the program.
133        self.parse(source, filename, modules)?;
134
135        Ok(self.state.ast.ast.clone())
136    }
137
138    /// Returns a new Leo compiler.
139    #[allow(clippy::too_many_arguments)]
140    pub fn new(
141        expected_program_name: Option<String>,
142        is_test: bool,
143        handler: Handler,
144        node_builder: Rc<NodeBuilder>,
145        output_directory: PathBuf,
146        compiler_options: Option<CompilerOptions>,
147        import_stubs: IndexMap<Symbol, Stub>,
148        network: NetworkName,
149    ) -> Self {
150        Self {
151            state: CompilerState {
152                handler,
153                node_builder: Rc::clone(&node_builder),
154                is_test,
155                network,
156                ..Default::default()
157            },
158            output_directory,
159            program_name: expected_program_name,
160            compiler_options: compiler_options.unwrap_or_default(),
161            import_stubs,
162            statements_before_dce: 0,
163            statements_after_dce: 0,
164        }
165    }
166
167    fn do_pass<P: Pass>(&mut self, input: P::Input) -> Result<P::Output> {
168        let output = P::do_pass(input, &mut self.state)?;
169
170        let write = match &self.compiler_options.ast_snapshots {
171            AstSnapshots::All => true,
172            AstSnapshots::Some(passes) => passes.contains(P::NAME),
173        };
174
175        if write {
176            self.write_ast_to_json(&format!("{}.json", P::NAME))?;
177            self.write_ast(&format!("{}.ast", P::NAME))?;
178        }
179
180        Ok(output)
181    }
182
183    /// Runs the compiler stages.
184    ///
185    /// Returns the generated ABIs (primary and imports), which are captured
186    /// immediately after monomorphisation to ensure all types are resolved,
187    /// but not yet lowered.
188    pub fn intermediate_passes(&mut self) -> Result<(leo_abi::Program, IndexMap<String, leo_abi::Program>)> {
189        let type_checking_config = TypeCheckingInput::new(self.state.network);
190
191        self.do_pass::<NameValidation>(())?;
192
193        self.do_pass::<GlobalVarsCollection>(())?;
194
195        self.do_pass::<PathResolution>(())?;
196
197        self.do_pass::<GlobalItemsCollection>(())?;
198
199        self.do_pass::<TypeChecking>(type_checking_config.clone())?;
200
201        self.do_pass::<Disambiguate>(())?;
202
203        self.do_pass::<ProcessingAsync>(type_checking_config.clone())?;
204
205        self.do_pass::<StaticAnalyzing>(())?;
206
207        self.do_pass::<ConstPropUnrollAndMorphing>(type_checking_config.clone())?;
208
209        // Generate ABIs after monomorphization to capture concrete types.
210        // Const generic structs are resolved to their monomorphized versions.
211        let abis = self.generate_abi();
212
213        self.do_pass::<StorageLowering>(type_checking_config.clone())?;
214
215        self.do_pass::<OptionLowering>(type_checking_config)?;
216
217        self.do_pass::<ProcessingScript>(())?;
218
219        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: true })?;
220
221        self.do_pass::<Destructuring>(())?;
222
223        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
224
225        self.do_pass::<WriteTransforming>(())?;
226
227        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
228
229        self.do_pass::<Flattening>(())?;
230
231        self.do_pass::<FunctionInlining>(())?;
232
233        // Flattening may produce ternary expressions not in SSA form.
234        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
235
236        self.do_pass::<SsaConstPropagation>(())?;
237
238        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
239
240        self.do_pass::<CommonSubexpressionEliminating>(())?;
241
242        let output = self.do_pass::<DeadCodeEliminating>(())?;
243        self.statements_before_dce = output.statements_before;
244        self.statements_after_dce = output.statements_after;
245
246        Ok(abis)
247    }
248
249    /// Generates ABIs for the primary program and all imports.
250    ///
251    /// Returns `(primary_abi, import_abis)` where `import_abis` maps program
252    /// names to their ABIs.
253    fn generate_abi(&self) -> (leo_abi::Program, IndexMap<String, leo_abi::Program>) {
254        // Generate primary ABI (pruning happens inside generate).
255        let primary_abi = leo_abi::generate(&self.state.ast.ast);
256
257        // Generate import ABIs from stubs.
258        let import_abis: IndexMap<String, leo_abi::Program> = self
259            .state
260            .ast
261            .ast
262            .stubs
263            .iter()
264            .map(|(name, stub)| {
265                let abi = match stub {
266                    Stub::FromLeo { program, .. } => leo_abi::generate(program),
267                    Stub::FromAleo { program, .. } => leo_abi::aleo::generate(program),
268                };
269                (name.to_string(), abi)
270            })
271            .collect();
272
273        (primary_abi, import_abis)
274    }
275
276    /// Compiles a program from a given source string and a list of module sources.
277    ///
278    /// # Arguments
279    ///
280    /// * `source` - The main source code as a string slice.
281    /// * `filename` - The name of the main source file.
282    /// * `modules` - A vector of tuples where each tuple contains:
283    ///     - A module source as a string slice.
284    ///     - Its associated `FileName`.
285    ///
286    /// # Returns
287    ///
288    /// * `Ok(CompiledPrograms)` containing the generated bytecode and ABI if compilation succeeds.
289    /// * `Err(CompilerError)` if any stage of the pipeline fails.
290    pub fn compile(&mut self, source: &str, filename: FileName, modules: &Vec<(&str, FileName)>) -> Result<Compiled> {
291        // Parse the program.
292        self.parse(source, filename, modules)?;
293        // Merge the stubs into the AST.
294        self.add_import_stubs()?;
295        // Run the intermediate compiler stages, which also generates ABIs.
296        let (primary_abi, import_abis) = self.intermediate_passes()?;
297        // Run code generation.
298        let bytecodes = CodeGenerating::do_pass((), &mut self.state)?;
299
300        // Build the primary compiled program.
301        let primary = CompiledProgram {
302            name: self.program_name.clone().unwrap(),
303            bytecode: bytecodes.primary_bytecode,
304            abi: primary_abi,
305        };
306
307        // Build compiled programs for imports, looking up ABIs by name.
308        let imports: Vec<CompiledProgram> = bytecodes
309            .import_bytecodes
310            .into_iter()
311            .map(|bc| {
312                let abi = import_abis.get(&bc.program_name).expect("ABI should exist for all imports").clone();
313                CompiledProgram { name: bc.program_name, bytecode: bc.bytecode, abi }
314            })
315            .collect();
316
317        Ok(Compiled { primary, imports })
318    }
319
320    /// Reads the main source file and all module files in the same directory tree.
321    ///
322    /// This helper walks all `.leo` files under `source_directory` (excluding the main file itself),
323    /// reads their contents, and returns:
324    /// - The main file’s source as a `String`.
325    /// - A vector of module tuples `(String, FileName)` suitable for compilation or parsing.
326    ///
327    /// # Arguments
328    ///
329    /// * `entry_file_path` - The main source file.
330    /// * `source_directory` - The directory root for discovering `.leo` module files.
331    ///
332    /// # Errors
333    ///
334    /// Returns `Err(CompilerError)` if reading any file fails.
335    fn read_sources_and_modules(
336        entry_file_path: impl AsRef<Path>,
337        source_directory: impl AsRef<Path>,
338    ) -> Result<(String, Vec<(String, FileName)>)> {
339        // Read the contents of the main source file.
340        let source = fs::read_to_string(&entry_file_path)
341            .map_err(|e| CompilerError::file_read_error(entry_file_path.as_ref().display().to_string(), e))?;
342
343        // Walk all files under source_directory recursively, excluding the main source file itself.
344        let files = WalkDir::new(source_directory)
345            .into_iter()
346            .filter_map(Result::ok)
347            .filter(|e| {
348                e.file_type().is_file()
349                    && e.path() != entry_file_path.as_ref()
350                    && e.path().extension() == Some(OsStr::new("leo"))
351            })
352            .collect::<Vec<_>>();
353
354        // Read all module files and pair with FileName immediately
355        let mut modules = Vec::new();
356        for file in &files {
357            let module_source = fs::read_to_string(file.path())
358                .map_err(|e| CompilerError::file_read_error(file.path().display().to_string(), e))?;
359            modules.push((module_source, FileName::Real(file.path().into())));
360        }
361
362        Ok((source, modules))
363    }
364
365    /// Compiles a program from a source file and its associated module files in the same directory tree.
366    pub fn compile_from_directory(
367        &mut self,
368        entry_file_path: impl AsRef<Path>,
369        source_directory: impl AsRef<Path>,
370    ) -> Result<Compiled> {
371        let (source, modules_owned) = Self::read_sources_and_modules(&entry_file_path, &source_directory)?;
372
373        // Convert owned module sources into temporary (&str, FileName) tuples.
374        let module_refs: Vec<(&str, FileName)> =
375            modules_owned.iter().map(|(src, fname)| (src.as_str(), fname.clone())).collect();
376
377        // Compile the main source along with all collected modules.
378        self.compile(&source, FileName::Real(entry_file_path.as_ref().into()), &module_refs)
379    }
380
381    /// Parses a program from a source file and its associated module files in the same directory tree.
382    pub fn parse_from_directory(
383        &mut self,
384        entry_file_path: impl AsRef<Path>,
385        source_directory: impl AsRef<Path>,
386    ) -> Result<Program> {
387        let (source, modules_owned) = Self::read_sources_and_modules(&entry_file_path, &source_directory)?;
388
389        // Convert owned module sources into temporary (&str, FileName) tuples.
390        let module_refs: Vec<(&str, FileName)> =
391            modules_owned.iter().map(|(src, fname)| (src.as_str(), fname.clone())).collect();
392
393        // Parse the main source along with all collected modules.
394        self.parse(&source, FileName::Real(entry_file_path.as_ref().into()), &module_refs)?;
395        Ok(self.state.ast.ast.clone())
396    }
397
398    /// Writes the AST to a JSON file.
399    fn write_ast_to_json(&self, file_suffix: &str) -> Result<()> {
400        // Remove `Span`s if they are not enabled.
401        if self.compiler_options.ast_spans_enabled {
402            self.state.ast.to_json_file(
403                self.output_directory.clone(),
404                &format!("{}.{file_suffix}", self.program_name.as_ref().unwrap()),
405            )?;
406        } else {
407            self.state.ast.to_json_file_without_keys(
408                self.output_directory.clone(),
409                &format!("{}.{file_suffix}", self.program_name.as_ref().unwrap()),
410                &["_span", "span"],
411            )?;
412        }
413        Ok(())
414    }
415
416    /// Writes the AST to a file (Leo syntax, not JSON).
417    fn write_ast(&self, file_suffix: &str) -> Result<()> {
418        let filename = format!("{}.{file_suffix}", self.program_name.as_ref().unwrap());
419        let full_filename = self.output_directory.join(&filename);
420        let contents = self.state.ast.ast.to_string();
421        fs::write(&full_filename, contents).map_err(|e| CompilerError::failed_ast_file(full_filename.display(), e))?;
422        Ok(())
423    }
424
425    /// Resolves and registers all import stubs for the current program.
426    ///
427    /// This method performs a graph traversal over the program’s import relationships to:
428    /// 1. Establish parent–child relationships between stubs based on imports.
429    /// 2. Collect all reachable stubs in traversal order.
430    /// 3. Store the explored stubs back into `self.state.ast.ast.stubs`.
431    ///
432    /// The traversal starts from the imports of the main program and recursively follows
433    /// their transitive dependencies. Any missing stub during traversal results in an error.
434    ///
435    /// # Returns
436    ///
437    /// * `Ok(())` if all imports are successfully resolved and stubs are collected.
438    /// * `Err(CompilerError)` if any imported program cannot be found.
439    pub fn add_import_stubs(&mut self) -> Result<()> {
440        // Track which programs we've already processed.
441        let mut explored = IndexSet::<Symbol>::new();
442
443        // Initialize the exploration queue with the main program’s direct imports.
444        let mut to_explore: Vec<Symbol> = self.state.ast.ast.imports.keys().cloned().collect();
445
446        // If this is a named program, set the main program as the parent of its direct imports.
447        if let Some(main_program_name) = self.program_name.clone() {
448            let main_symbol = Symbol::intern(&main_program_name);
449            for import in self.state.ast.ast.imports.keys() {
450                if let Some(child_stub) = self.import_stubs.get_mut(import) {
451                    child_stub.add_parent(main_symbol);
452                }
453            }
454        }
455
456        // Traverse the import graph breadth-first, collecting dependencies.
457        while let Some(import_symbol) = to_explore.pop() {
458            // Mark this import as explored.
459            explored.insert(import_symbol);
460
461            // Look up the corresponding stub.
462            let Some(stub) = self.import_stubs.get(&import_symbol) else {
463                return Err(CompilerError::imported_program_not_found(
464                    self.program_name.as_ref().unwrap(),
465                    import_symbol,
466                    self.state.ast.ast.imports[&import_symbol],
467                )
468                .into());
469            };
470
471            for child_symbol in stub.imports().cloned().collect::<Vec<_>>() {
472                // Record parent relationship.
473                if let Some(child_stub) = self.import_stubs.get_mut(&child_symbol) {
474                    child_stub.add_parent(import_symbol);
475                }
476
477                // Schedule child for exploration if not yet visited.
478                if explored.insert(child_symbol) {
479                    to_explore.push(child_symbol);
480                }
481            }
482        }
483
484        // Iterate in the order of `import_stubs` to make sure they
485        // stay topologically sorted.
486        self.state.ast.ast.stubs = self
487            .import_stubs
488            .iter()
489            .filter(|(symbol, _stub)| explored.contains(*symbol))
490            .map(|(symbol, stub)| (*symbol, stub.clone()))
491            .collect();
492        Ok(())
493    }
494}