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::{AstSnapshots, CompilerOptions};
22
23pub use leo_ast::Ast;
24use leo_ast::{NetworkName, 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};
34
35use indexmap::{IndexMap, IndexSet};
36use walkdir::WalkDir;
37
38/// The primary entry point of the Leo compiler.
39pub struct Compiler {
40    /// The path to where the compiler outputs all generated files.
41    output_directory: PathBuf,
42    /// The program name,
43    pub program_name: Option<String>,
44    /// Options configuring compilation.
45    compiler_options: CompilerOptions,
46    /// State.
47    state: CompilerState,
48    /// The stubs for imported programs.
49    import_stubs: IndexMap<Symbol, Stub>,
50    /// How many statements were in the AST before DCE?
51    pub statements_before_dce: u32,
52    /// How many statements were in the AST after DCE?
53    pub statements_after_dce: u32,
54}
55
56impl Compiler {
57    pub fn parse(&mut self, source: &str, filename: FileName, modules: &[(&str, FileName)]) -> Result<()> {
58        // Register the source in the source map.
59        let source_file = with_session_globals(|s| s.source_map.new_source(source, filename.clone()));
60
61        // Register the sources of all the modules in the source map.
62        let modules = modules
63            .iter()
64            .map(|(source, filename)| with_session_globals(|s| s.source_map.new_source(source, filename.clone())))
65            .collect::<Vec<_>>();
66
67        // Use the parser to construct the abstract syntax tree (ast).
68        self.state.ast = leo_parser::parse_ast(
69            self.state.handler.clone(),
70            &self.state.node_builder,
71            &source_file,
72            &modules,
73            self.state.network,
74        )?;
75
76        // Check that the name of its program scope matches the expected name.
77        // Note that parsing enforces that there is exactly one program scope in a file.
78        let program_scope = self.state.ast.ast.program_scopes.values().next().unwrap();
79        if self.program_name.is_none() {
80            self.program_name = Some(program_scope.program_id.name.to_string());
81        } else if self.program_name != Some(program_scope.program_id.name.to_string()) {
82            return Err(CompilerError::program_name_should_match_file_name(
83                program_scope.program_id.name,
84                // If this is a test, use the filename as the expected name.
85                if self.state.is_test {
86                    format!(
87                        "`{}` (the test file name)",
88                        filename.to_string().split("/").last().expect("Could not get file name")
89                    )
90                } else {
91                    format!("`{}` (specified in `program.json`)", self.program_name.as_ref().unwrap())
92                },
93                program_scope.program_id.name.span,
94            )
95            .into());
96        }
97
98        if self.compiler_options.initial_ast {
99            self.write_ast_to_json("initial.json")?;
100            self.write_ast("initial.ast")?;
101        }
102
103        Ok(())
104    }
105
106    /// Returns a new Leo compiler.
107    #[allow(clippy::too_many_arguments)]
108    pub fn new(
109        expected_program_name: Option<String>,
110        is_test: bool,
111        handler: Handler,
112        output_directory: PathBuf,
113        compiler_options: Option<CompilerOptions>,
114        import_stubs: IndexMap<Symbol, Stub>,
115        network: NetworkName,
116    ) -> Self {
117        Self {
118            state: CompilerState { handler, is_test, network, ..Default::default() },
119            output_directory,
120            program_name: expected_program_name,
121            compiler_options: compiler_options.unwrap_or_default(),
122            import_stubs,
123            statements_before_dce: 0,
124            statements_after_dce: 0,
125        }
126    }
127
128    fn do_pass<P: Pass>(&mut self, input: P::Input) -> Result<P::Output> {
129        let output = P::do_pass(input, &mut self.state)?;
130
131        let write = match &self.compiler_options.ast_snapshots {
132            AstSnapshots::All => true,
133            AstSnapshots::Some(passes) => passes.contains(P::NAME),
134        };
135
136        if write {
137            self.write_ast_to_json(&format!("{}.json", P::NAME))?;
138            self.write_ast(&format!("{}.ast", P::NAME))?;
139        }
140
141        Ok(output)
142    }
143
144    /// Runs the compiler stages.
145    pub fn intermediate_passes(&mut self) -> Result<()> {
146        let type_checking_config = TypeCheckingInput::new(self.state.network);
147
148        self.do_pass::<NameValidation>(())?;
149
150        self.do_pass::<PathResolution>(())?;
151
152        self.do_pass::<SymbolTableCreation>(())?;
153
154        self.do_pass::<TypeChecking>(type_checking_config.clone())?;
155
156        self.do_pass::<ProcessingAsync>(type_checking_config.clone())?;
157
158        self.do_pass::<StaticAnalyzing>(())?;
159
160        self.do_pass::<ConstPropUnrollAndMorphing>(type_checking_config.clone())?;
161
162        self.do_pass::<StorageLowering>(type_checking_config.clone())?;
163
164        self.do_pass::<OptionLowering>(type_checking_config)?;
165
166        self.do_pass::<ProcessingScript>(())?;
167
168        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: true })?;
169
170        self.do_pass::<Destructuring>(())?;
171
172        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
173
174        self.do_pass::<WriteTransforming>(())?;
175
176        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
177
178        self.do_pass::<Flattening>(())?;
179
180        self.do_pass::<FunctionInlining>(())?;
181
182        // Flattening may produce ternary expressions not in SSA form.
183        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
184
185        self.do_pass::<SsaConstPropagation>(())?;
186
187        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
188
189        self.do_pass::<CommonSubexpressionEliminating>(())?;
190
191        let output = self.do_pass::<DeadCodeEliminating>(())?;
192        self.statements_before_dce = output.statements_before;
193        self.statements_after_dce = output.statements_after;
194
195        Ok(())
196    }
197
198    /// Compiles a program from a given source string and a list of module sources.
199    ///
200    /// # Arguments
201    ///
202    /// * `source` - The main source code as a string slice.
203    /// * `filename` - The name of the main source file.
204    /// * `modules` - A vector of tuples where each tuple contains:
205    ///     - A module source as a string slice.
206    ///     - Its associated `FileName`.
207    ///
208    /// # Returns
209    ///
210    /// * `Ok(String)` containing the generated bytecode if compilation succeeds.
211    /// * `Err(CompilerError)` if any stage of the pipeline fails.
212    pub fn compile(&mut self, source: &str, filename: FileName, modules: &Vec<(&str, FileName)>) -> Result<String> {
213        // Parse the program.
214        self.parse(source, filename, modules)?;
215        // Merge the stubs into the AST.
216        self.add_import_stubs()?;
217        // Run the intermediate compiler stages.
218        self.intermediate_passes()?;
219        // Run code generation.
220        let bytecode = CodeGenerating::do_pass((), &mut self.state)?;
221        Ok(bytecode.to_string())
222    }
223
224    /// Compiles a program from a source file and its associated module files in the same directory tree.
225    ///
226    /// This method reads the main source file and collects all other source files under the same
227    /// root directory (excluding the main file itself). It assumes a modular structure where additional
228    /// source files are compiled as modules, with deeper files (submodules) compiled first.
229    ///
230    /// # Arguments
231    ///
232    /// * `source_file_path` - A path to the main source file to compile. It must have a parent directory,
233    ///   which is used as the root for discovering additional module files.
234    ///
235    /// # Returns
236    ///
237    /// * `Ok(String)` containing the compiled output if successful.
238    /// * `Err(CompilerError)` if reading the main file fails or a compilation error occurs.
239    ///
240    /// # Panics
241    ///
242    /// * If the provided source file has no parent directory.
243    /// * If any discovered module file cannot be read (marked as a TODO).
244    pub fn compile_from_directory(
245        &mut self,
246        entry_file_path: impl AsRef<Path>,
247        source_directory: impl AsRef<Path>,
248    ) -> Result<String> {
249        // Read the contents of the main source file.
250        let source = fs::read_to_string(&entry_file_path)
251            .map_err(|e| CompilerError::file_read_error(entry_file_path.as_ref().display().to_string(), e))?;
252
253        // Walk all files under source_directory recursively, excluding the main source file itself.
254        let files = WalkDir::new(source_directory)
255            .into_iter()
256            .filter_map(Result::ok)
257            .filter(|e| {
258                e.file_type().is_file()
259                    && e.path() != entry_file_path.as_ref()
260                    && e.path().extension() == Some(OsStr::new("leo"))
261            })
262            .collect::<Vec<_>>();
263
264        let mut module_sources = Vec::new(); // Keep Strings alive for valid borrowing
265        let mut modules = Vec::new(); // Parsed (source, filename) tuples for compilation
266
267        // Read all module files and store their contents
268        for file in &files {
269            let source = fs::read_to_string(file.path())
270                .map_err(|e| CompilerError::file_read_error(file.path().display().to_string(), e))?;
271            module_sources.push(source); // Keep the String alive
272        }
273
274        // Create tuples of (&str, FileName) for the compiler
275        for (i, file) in files.iter().enumerate() {
276            let source = &module_sources[i]; // Borrow from the alive String
277            modules.push((&source[..], FileName::Real(file.path().into())));
278        }
279
280        // Compile the main source along with all collected modules
281        self.compile(&source, FileName::Real(entry_file_path.as_ref().into()), &modules)
282    }
283
284    /// Writes the AST to a JSON file.
285    fn write_ast_to_json(&self, file_suffix: &str) -> Result<()> {
286        // Remove `Span`s if they are not enabled.
287        if self.compiler_options.ast_spans_enabled {
288            self.state.ast.to_json_file(
289                self.output_directory.clone(),
290                &format!("{}.{file_suffix}", self.program_name.as_ref().unwrap()),
291            )?;
292        } else {
293            self.state.ast.to_json_file_without_keys(
294                self.output_directory.clone(),
295                &format!("{}.{file_suffix}", self.program_name.as_ref().unwrap()),
296                &["_span", "span"],
297            )?;
298        }
299        Ok(())
300    }
301
302    /// Writes the AST to a file (Leo syntax, not JSON).
303    fn write_ast(&self, file_suffix: &str) -> Result<()> {
304        let filename = format!("{}.{file_suffix}", self.program_name.as_ref().unwrap());
305        let full_filename = self.output_directory.join(&filename);
306        let contents = self.state.ast.ast.to_string();
307        fs::write(&full_filename, contents).map_err(|e| CompilerError::failed_ast_file(full_filename.display(), e))?;
308        Ok(())
309    }
310
311    /// Merge the imported stubs which are dependencies of the current program into the AST
312    /// in topological order.
313    pub fn add_import_stubs(&mut self) -> Result<()> {
314        let mut explored = IndexSet::<Symbol>::new();
315        let mut to_explore: Vec<Symbol> = self.state.ast.ast.imports.keys().cloned().collect();
316
317        while let Some(import) = to_explore.pop() {
318            explored.insert(import);
319            if let Some(stub) = self.import_stubs.get(&import) {
320                for new_import_id in stub.imports.iter() {
321                    if !explored.contains(&new_import_id.name.name) {
322                        to_explore.push(new_import_id.name.name);
323                    }
324                }
325            } else {
326                return Err(CompilerError::imported_program_not_found(
327                    self.program_name.as_ref().unwrap(),
328                    import,
329                    self.state.ast.ast.imports[&import].1,
330                )
331                .into());
332            }
333        }
334
335        // Iterate in the order of `import_stubs` to make sure they
336        // stay topologically sorted.
337        self.state.ast.ast.stubs = self
338            .import_stubs
339            .iter()
340            .filter(|(symbol, _stub)| explored.contains(*symbol))
341            .map(|(symbol, stub)| (*symbol, stub.clone()))
342            .collect();
343        Ok(())
344    }
345}