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 indexmap::{IndexMap, IndexSet};
30use std::{
31    fs,
32    path::{Path, PathBuf},
33};
34
35/// The primary entry point of the Leo compiler.
36pub struct Compiler {
37    /// The path to where the compiler outputs all generated files.
38    output_directory: PathBuf,
39    /// The program name,
40    pub program_name: Option<String>,
41    /// Options configuring compilation.
42    compiler_options: CompilerOptions,
43    /// State.
44    state: CompilerState,
45    /// The stubs for imported programs.
46    import_stubs: IndexMap<Symbol, Stub>,
47    /// How many statements were in the AST before DCE?
48    pub statements_before_dce: u32,
49    /// How many statements were in the AST after DCE?
50    pub statements_after_dce: u32,
51}
52
53impl Compiler {
54    pub fn parse(&mut self, source: &str, filename: FileName) -> Result<()> {
55        // Register the source in the source map.
56        let source_file = with_session_globals(|s| s.source_map.new_source(source, filename));
57
58        // Use the parser to construct the abstract syntax tree (ast).
59        self.state.ast = leo_parser::parse_ast(
60            self.state.handler.clone(),
61            &self.state.node_builder,
62            &source_file.src,
63            source_file.absolute_start,
64            self.state.network,
65        )?;
66
67        // Check that the name of its program scope matches the expected name.
68        // Note that parsing enforces that there is exactly one program scope in a file.
69        let program_scope = self.state.ast.ast.program_scopes.values().next().unwrap();
70        if self.program_name.is_none() {
71            self.program_name = Some(program_scope.program_id.name.to_string());
72        } else if self.program_name != Some(program_scope.program_id.name.to_string()) {
73            return Err(CompilerError::program_name_should_match_file_name(
74                program_scope.program_id.name,
75                self.program_name.as_ref().unwrap(),
76                program_scope.program_id.name.span,
77            )
78            .into());
79        }
80
81        if self.compiler_options.initial_ast {
82            self.write_ast_to_json("initial.json")?;
83            self.write_ast("initial.ast")?;
84        }
85
86        Ok(())
87    }
88
89    pub fn parse_from_file(&mut self, source_file_path: impl AsRef<Path>) -> Result<()> {
90        // Load the program file.
91        let source = fs::read_to_string(&source_file_path)
92            .map_err(|e| CompilerError::file_read_error(source_file_path.as_ref().display().to_string(), e))?;
93        self.parse(&source, FileName::Real(source_file_path.as_ref().into()))
94    }
95
96    /// Returns a new Leo compiler.
97    #[allow(clippy::too_many_arguments)]
98    pub fn new(
99        expected_program_name: Option<String>,
100        is_test: bool,
101        handler: Handler,
102        output_directory: PathBuf,
103        compiler_options: Option<CompilerOptions>,
104        import_stubs: IndexMap<Symbol, Stub>,
105        network: NetworkName,
106    ) -> Self {
107        Self {
108            state: CompilerState { handler, is_test, network, ..Default::default() },
109            output_directory,
110            program_name: expected_program_name,
111            compiler_options: compiler_options.unwrap_or_default(),
112            import_stubs,
113            statements_before_dce: 0,
114            statements_after_dce: 0,
115        }
116    }
117
118    fn do_pass<P: Pass>(&mut self, input: P::Input) -> Result<P::Output> {
119        let output = P::do_pass(input, &mut self.state)?;
120
121        let write = match &self.compiler_options.ast_snapshots {
122            AstSnapshots::All => true,
123            AstSnapshots::Some(passes) => passes.contains(P::NAME),
124        };
125
126        if write {
127            self.write_ast_to_json(&format!("{}.json", P::NAME))?;
128            self.write_ast(&format!("{}.ast", P::NAME))?;
129        }
130
131        Ok(output)
132    }
133
134    /// Runs the compiler stages.
135    pub fn intermediate_passes(&mut self) -> Result<()> {
136        let type_checking_config = TypeCheckingInput::new(self.state.network);
137
138        self.do_pass::<SymbolTableCreation>(())?;
139
140        self.do_pass::<TypeChecking>(type_checking_config.clone())?;
141
142        self.do_pass::<ProcessingAsync>(type_checking_config.clone())?;
143
144        self.do_pass::<StaticAnalyzing>(())?;
145
146        self.do_pass::<ConstPropUnrollAndMorphing>(type_checking_config)?;
147
148        self.do_pass::<ProcessingScript>(())?;
149
150        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: true })?;
151
152        self.do_pass::<Destructuring>(())?;
153
154        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
155
156        self.do_pass::<WriteTransforming>(())?;
157
158        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
159
160        self.do_pass::<Flattening>(())?;
161
162        self.do_pass::<FunctionInlining>(())?;
163
164        let output = self.do_pass::<DeadCodeEliminating>(())?;
165        self.statements_before_dce = output.statements_before;
166        self.statements_after_dce = output.statements_after;
167
168        Ok(())
169    }
170
171    /// Returns a compiled Leo program.
172    pub fn compile(&mut self, source: &str, filename: FileName) -> Result<String> {
173        // Parse the program.
174        self.parse(source, filename)?;
175        // Merge the stubs into the AST.
176        self.add_import_stubs()?;
177        // Run the intermediate compiler stages.
178        self.intermediate_passes()?;
179        // Run code generation.
180        let bytecode = CodeGenerating::do_pass((), &mut self.state)?;
181        Ok(bytecode)
182    }
183
184    pub fn compile_from_file(&mut self, source_file_path: impl AsRef<Path>) -> Result<String> {
185        let source = fs::read_to_string(&source_file_path)
186            .map_err(|e| CompilerError::file_read_error(source_file_path.as_ref().display().to_string(), e))?;
187        self.compile(&source, FileName::Real(source_file_path.as_ref().into()))
188    }
189
190    /// Writes the AST to a JSON file.
191    fn write_ast_to_json(&self, file_suffix: &str) -> Result<()> {
192        // Remove `Span`s if they are not enabled.
193        if self.compiler_options.ast_spans_enabled {
194            self.state.ast.to_json_file(
195                self.output_directory.clone(),
196                &format!("{}.{file_suffix}", self.program_name.as_ref().unwrap()),
197            )?;
198        } else {
199            self.state.ast.to_json_file_without_keys(
200                self.output_directory.clone(),
201                &format!("{}.{file_suffix}", self.program_name.as_ref().unwrap()),
202                &["_span", "span"],
203            )?;
204        }
205        Ok(())
206    }
207
208    /// Writes the AST to a file (Leo syntax, not JSON).
209    fn write_ast(&self, file_suffix: &str) -> Result<()> {
210        let filename = format!("{}.{file_suffix}", self.program_name.as_ref().unwrap());
211        let full_filename = self.output_directory.join(&filename);
212        let contents = self.state.ast.ast.to_string();
213        fs::write(&full_filename, contents).map_err(|e| CompilerError::failed_ast_file(full_filename.display(), e))?;
214        Ok(())
215    }
216
217    /// Merge the imported stubs which are dependencies of the current program into the AST
218    /// in topological order.
219    pub fn add_import_stubs(&mut self) -> Result<()> {
220        let mut explored = IndexSet::<Symbol>::new();
221        let mut to_explore: Vec<Symbol> = self.state.ast.ast.imports.keys().cloned().collect();
222
223        while let Some(import) = to_explore.pop() {
224            explored.insert(import);
225            if let Some(stub) = self.import_stubs.get(&import) {
226                for new_import_id in stub.imports.iter() {
227                    if !explored.contains(&new_import_id.name.name) {
228                        to_explore.push(new_import_id.name.name);
229                    }
230                }
231            } else {
232                return Err(CompilerError::imported_program_not_found(
233                    self.program_name.as_ref().unwrap(),
234                    import,
235                    self.state.ast.ast.imports[&import].1,
236                )
237                .into());
238            }
239        }
240
241        // Iterate in the order of `import_stubs` to make sure they
242        // stay topologically sorted.
243        self.state.ast.ast.stubs = self
244            .import_stubs
245            .iter()
246            .filter(|(symbol, _stub)| explored.contains(*symbol))
247            .map(|(symbol, stub)| (*symbol, stub.clone()))
248            .collect();
249        Ok(())
250    }
251}