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, DiGraph, Program};
24use leo_ast::{Library, NetworkName, NodeBuilder, Stub};
25use leo_errors::{CompilerError, Handler, Result};
26use leo_passes::*;
27use leo_span::{
28    Span,
29    Symbol,
30    file_source::{DiskFileSource, FileSource},
31    source_map::FileName,
32    with_session_globals,
33};
34
35use std::{
36    fs,
37    path::{Path, PathBuf},
38    rc::Rc,
39};
40
41use indexmap::IndexMap;
42
43/// A single compiled program with its bytecode and ABI.
44pub struct CompiledProgram {
45    /// The program name (without `.aleo` suffix).
46    pub name: String,
47    /// The generated Aleo bytecode.
48    pub bytecode: String,
49    /// The ABI describing the program's public interface.
50    pub abi: leo_abi::Program,
51}
52
53/// The result of compiling a Leo program.
54pub struct Compiled {
55    /// The primary program that was compiled.
56    pub primary: CompiledProgram,
57    /// Compiled programs for imports.
58    pub imports: Vec<CompiledProgram>,
59}
60
61/// The primary entry point of the Leo compiler.
62pub struct Compiler {
63    /// The path to where the compiler outputs all generated files.
64    output_directory: PathBuf,
65    /// The program name,
66    pub program_name: Option<String>,
67    /// Options configuring compilation.
68    compiler_options: CompilerOptions,
69    /// State.
70    state: CompilerState,
71    /// The stubs for imported programs.
72    import_stubs: IndexMap<Symbol, Stub>,
73    /// How many statements were in the AST before DCE?
74    pub statements_before_dce: u32,
75    /// How many statements were in the AST after DCE?
76    pub statements_after_dce: u32,
77}
78
79impl Compiler {
80    pub fn network(&self) -> NetworkName {
81        self.state.network
82    }
83
84    /// Parses the given source into a program AST and stores it in the compiler state.
85    ///
86    /// The source file and any provided module sources are first registered in the
87    /// session source map so spans can be resolved correctly. The parser then
88    /// constructs the program AST from the main source and its modules.
89    ///
90    /// After parsing, this verifies that the program scope name matches the expected
91    /// program name (from `program.json` or the test filename). The resulting AST is
92    /// stored in `self.state.ast`, and optionally written to disk if configured.
93    pub fn parse_program(&mut self, source: &str, filename: FileName, modules: &[(&str, FileName)]) -> Result<()> {
94        // Register the source in the source map.
95        let source_file = with_session_globals(|s| s.source_map.new_source(source, filename.clone()));
96
97        // Register the sources of all the modules in the source map.
98        let modules = modules
99            .iter()
100            .map(|(source, filename)| with_session_globals(|s| s.source_map.new_source(source, filename.clone())))
101            .collect::<Vec<_>>();
102
103        // Use the parser to construct the abstract syntax tree (ast).
104        let program = leo_parser::parse_program(
105            self.state.handler.clone(),
106            &self.state.node_builder,
107            &source_file,
108            &modules,
109            self.state.network,
110        )?;
111
112        // Check that the name of its program scope matches the expected name.
113        // Note that parsing enforces that there is exactly one program scope in a file.
114        let program_scope = program.program_scopes.values().next().unwrap();
115        if let Some(program_name) = &self.program_name {
116            if program_name != &program_scope.program_id.as_symbol().to_string() {
117                return Err(CompilerError::program_name_should_match_file_name(
118                    program_scope.program_id.as_symbol(),
119                    // If this is a test, use the filename as the expected name.
120                    if self.state.is_test {
121                        format!(
122                            "`{}` (the test file name)",
123                            filename.to_string().split("/").last().expect("Could not get file name")
124                        )
125                    } else {
126                        format!("`{program_name}` (specified in `program.json`)")
127                    },
128                    program_scope.program_id.span(),
129                )
130                .into());
131            }
132        } else {
133            self.program_name = Some(program_scope.program_id.as_symbol().to_string());
134        }
135
136        self.state.ast = Ast::Program(program);
137
138        if self.compiler_options.initial_ast {
139            self.write_ast_to_json("initial.json")?;
140            self.write_ast("initial.ast")?;
141        }
142
143        Ok(())
144    }
145
146    /// Simple wrapper around `parse_program` that also returns a program AST.
147    pub fn parse_and_return_program(
148        &mut self,
149        source: &str,
150        filename: FileName,
151        modules: &[(&str, FileName)],
152    ) -> Result<Program> {
153        // Parse the program.
154        self.parse_program(source, filename, modules)?;
155
156        match &self.state.ast {
157            Ast::Program(program) => Ok(program.clone()),
158            Ast::Library(_) => unreachable!("expected Program AST"),
159        }
160    }
161
162    /// Simple wrapper around `parse_library` that also returns a library AST.
163    pub fn parse_and_return_library(
164        &mut self,
165        library_name: &str,
166        source: &str,
167        filename: FileName,
168        modules: &[(&str, FileName)],
169    ) -> Result<Library> {
170        self.parse_library(Symbol::intern(library_name), source, filename, modules)?;
171
172        match &self.state.ast {
173            Ast::Program(_) => unreachable!("expected Library AST"),
174            Ast::Library(library) => Ok(library.clone()),
175        }
176    }
177
178    /// Parses a library source (and its submodules) into a library AST.
179    ///
180    /// All source strings are registered in the session source map so span information
181    /// can be resolved correctly. The resulting AST is stored in `self.state.ast`.
182    pub fn parse_library(
183        &mut self,
184        library_name: Symbol,
185        source: &str,
186        filename: FileName,
187        modules: &[(&str, FileName)],
188    ) -> Result<()> {
189        let source_file = with_session_globals(|s| s.source_map.new_source(source, filename.clone()));
190
191        // Register each module source in the source map.
192        let module_files = modules
193            .iter()
194            .map(|(src, name)| with_session_globals(|s| s.source_map.new_source(src, name.clone())))
195            .collect::<Vec<_>>();
196
197        self.state.ast = Ast::Library(leo_parser::parse_library(
198            self.state.handler.clone(),
199            &self.state.node_builder,
200            library_name,
201            &source_file,
202            &module_files,
203            self.state.network,
204        )?);
205
206        Ok(())
207    }
208
209    /// Returns a new Leo compiler.
210    #[allow(clippy::too_many_arguments)]
211    pub fn new(
212        expected_program_name: Option<String>,
213        is_test: bool,
214        handler: Handler,
215        node_builder: Rc<NodeBuilder>,
216        output_directory: PathBuf,
217        compiler_options: Option<CompilerOptions>,
218        import_stubs: IndexMap<Symbol, Stub>,
219        network: NetworkName,
220    ) -> Self {
221        Self {
222            state: CompilerState {
223                handler,
224                node_builder: Rc::clone(&node_builder),
225                is_test,
226                network,
227                ..Default::default()
228            },
229            output_directory,
230            program_name: expected_program_name,
231            compiler_options: compiler_options.unwrap_or_default(),
232            import_stubs,
233            statements_before_dce: 0,
234            statements_after_dce: 0,
235        }
236    }
237
238    pub fn do_pass<P: Pass>(&mut self, input: P::Input) -> Result<P::Output> {
239        let output = P::do_pass(input, &mut self.state)?;
240
241        let write = match &self.compiler_options.ast_snapshots {
242            AstSnapshots::All => true,
243            AstSnapshots::Some(passes) => passes.contains(P::NAME),
244        };
245
246        if write {
247            self.write_ast_to_json(&format!("{}.json", P::NAME))?;
248            self.write_ast(&format!("{}.ast", P::NAME))?;
249        }
250
251        Ok(output)
252    }
253
254    /// Runs all frontend passes: NameValidation through StaticAnalyzing.
255    pub fn frontend_passes(&mut self) -> Result<()> {
256        self.do_pass::<NameValidation>(())?;
257        self.do_pass::<GlobalVarsCollection>(())?;
258        self.do_pass::<PathResolution>(())?;
259        self.do_pass::<GlobalItemsCollection>(())?;
260        self.do_pass::<CheckInterfaces>(())?;
261        self.do_pass::<TypeChecking>(TypeCheckingInput::new(self.state.network))?;
262        self.do_pass::<Disambiguate>(())?;
263        self.do_pass::<ProcessingAsync>(TypeCheckingInput::new(self.state.network))?;
264        self.do_pass::<StaticAnalyzing>(())
265    }
266
267    /// Runs the compiler stages.
268    ///
269    /// Returns the generated ABIs (primary and imports), which are captured
270    /// immediately after monomorphisation to ensure all types are resolved,
271    /// but not yet lowered.
272    pub fn intermediate_passes(&mut self) -> Result<(leo_abi::Program, IndexMap<String, leo_abi::Program>)> {
273        let type_checking_config = TypeCheckingInput::new(self.state.network);
274
275        self.frontend_passes()?;
276
277        self.do_pass::<ConstPropUnrollAndMorphing>(type_checking_config.clone())?;
278
279        // Generate ABIs after monomorphization to capture concrete types.
280        // Const generic structs are resolved to their monomorphized versions.
281        let abis = self.generate_abi();
282
283        self.do_pass::<StorageLowering>(type_checking_config.clone())?;
284
285        self.do_pass::<OptionLowering>(type_checking_config)?;
286
287        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: true })?;
288
289        self.do_pass::<Destructuring>(())?;
290
291        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
292
293        self.do_pass::<WriteTransforming>(())?;
294
295        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
296
297        self.do_pass::<Flattening>(())?;
298
299        self.do_pass::<FunctionInlining>(())?;
300
301        // Flattening may produce ternary expressions not in SSA form.
302        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
303
304        self.do_pass::<SsaConstPropagation>(())?;
305
306        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
307
308        self.do_pass::<CommonSubexpressionEliminating>(())?;
309
310        let output = self.do_pass::<DeadCodeEliminating>(())?;
311        self.statements_before_dce = output.statements_before;
312        self.statements_after_dce = output.statements_after;
313
314        Ok(abis)
315    }
316
317    /// Generates ABIs for the primary program and all imports.
318    ///
319    /// Returns `(primary_abi, import_abis)` where `import_abis` maps program
320    /// names to their ABIs.
321    ///
322    /// This method only expects program ASTs. Library ASTs cause this method to panic.
323    fn generate_abi(&self) -> (leo_abi::Program, IndexMap<String, leo_abi::Program>) {
324        let program = match &self.state.ast {
325            Ast::Program(program) => program,
326            Ast::Library(_) => panic!("expected Program AST"),
327        };
328
329        // Generate primary ABI (pruning happens inside generate).
330        let primary_abi = leo_abi::generate(program);
331
332        // Generate import ABIs from stubs, ignoring libraries.
333        let import_abis: IndexMap<String, leo_abi::Program> = program
334            .stubs
335            .iter()
336            .filter(|(_, stub)| !matches!(stub, Stub::FromLibrary { .. }))
337            .map(|(name, stub)| {
338                let abi = match stub {
339                    Stub::FromLeo { program, .. } => leo_abi::generate(program),
340                    Stub::FromAleo { program, .. } => leo_abi::aleo::generate(program),
341                    Stub::FromLibrary { .. } => unreachable!("filtered out"),
342                };
343                (name.to_string(), abi)
344            })
345            .collect();
346
347        (primary_abi, import_abis)
348    }
349
350    /// Compiles a program from a given source string and a list of module sources.
351    ///
352    /// # Arguments
353    ///
354    /// * `source` - The main source code as a string slice.
355    /// * `filename` - The name of the main source file.
356    /// * `modules` - A vector of tuples where each tuple contains:
357    ///     - A module source as a string slice.
358    ///     - Its associated `FileName`.
359    ///
360    /// # Returns
361    ///
362    /// * `Ok(CompiledPrograms)` containing the generated bytecode and ABI if compilation succeeds.
363    /// * `Err(CompilerError)` if any stage of the pipeline fails.
364    pub fn compile(&mut self, source: &str, filename: FileName, modules: &Vec<(&str, FileName)>) -> Result<Compiled> {
365        // Parse the program.
366        self.parse_program(source, filename, modules)?;
367        // Merge the stubs into the AST.
368        self.add_import_stubs()?;
369        // Run the intermediate compiler stages, which also generates ABIs.
370        let (primary_abi, import_abis) = self.intermediate_passes()?;
371        // Run code generation.
372        let bytecodes = CodeGenerating::do_pass((), &mut self.state)?;
373
374        // Build the primary compiled program.
375        let primary = CompiledProgram {
376            name: self.program_name.clone().unwrap(),
377            bytecode: bytecodes.primary_bytecode,
378            abi: primary_abi,
379        };
380
381        // Build compiled programs for imports, looking up ABIs by name.
382        let imports: Vec<CompiledProgram> = bytecodes
383            .import_bytecodes
384            .into_iter()
385            .map(|bc| {
386                let abi = import_abis.get(&bc.program_name).expect("ABI should exist for all imports").clone();
387                CompiledProgram { name: bc.program_name, bytecode: bc.bytecode, abi }
388            })
389            .collect();
390
391        Ok(Compiled { primary, imports })
392    }
393
394    /// Reads the main source file and all module files in the same directory tree.
395    ///
396    /// This helper walks all `.leo` files under `source_directory` (excluding the main file itself),
397    /// reads their contents, and returns:
398    /// - The main file’s source as a `String`.
399    /// - A vector of module tuples `(String, FileName)` suitable for compilation or parsing.
400    ///
401    /// # Arguments
402    ///
403    /// * `entry_file_path` - The main source file.
404    /// * `source_directory` - The directory root for discovering `.leo` module files.
405    ///
406    /// # Errors
407    ///
408    /// Returns `Err(CompilerError)` if reading any file fails.
409    fn read_sources_and_modules(
410        file_source: &impl FileSource,
411        entry_file_path: impl AsRef<Path>,
412        source_directory: impl AsRef<Path>,
413    ) -> Result<(String, Vec<(String, FileName)>)> {
414        let entry_file_path = entry_file_path.as_ref();
415        let source_directory = source_directory.as_ref();
416
417        // Read the contents of the main source file.
418        let source = file_source
419            .read_file(entry_file_path)
420            .map_err(|e| CompilerError::file_read_error(entry_file_path.display().to_string(), e))?;
421
422        let files = file_source
423            .list_leo_files(source_directory, entry_file_path)
424            .map_err(|e| CompilerError::file_read_error(source_directory.display().to_string(), e))?;
425
426        let mut modules = Vec::with_capacity(files.len());
427        for path in files {
428            let module_source = file_source
429                .read_file(&path)
430                .map_err(|e| CompilerError::file_read_error(path.display().to_string(), e))?;
431            modules.push((module_source, FileName::Real(path)));
432        }
433
434        Ok((source, modules))
435    }
436
437    /// Compiles a program from a source file and its associated module files in the same directory tree.
438    pub fn compile_from_directory(
439        &mut self,
440        entry_file_path: impl AsRef<Path>,
441        source_directory: impl AsRef<Path>,
442    ) -> Result<Compiled> {
443        self.compile_from_directory_with_file_source(entry_file_path, source_directory, &DiskFileSource)
444    }
445
446    /// Compiles a program from a source file using the given file source.
447    pub fn compile_from_directory_with_file_source(
448        &mut self,
449        entry_file_path: impl AsRef<Path>,
450        source_directory: impl AsRef<Path>,
451        file_source: &impl FileSource,
452    ) -> Result<Compiled> {
453        let (source, modules_owned) = Self::read_sources_and_modules(file_source, &entry_file_path, &source_directory)?;
454
455        // Convert owned module sources into temporary (&str, FileName) tuples.
456        let module_refs: Vec<(&str, FileName)> =
457            modules_owned.iter().map(|(src, fname)| (src.as_str(), fname.clone())).collect();
458
459        // Compile the main source along with all collected modules.
460        self.compile(&source, FileName::Real(entry_file_path.as_ref().into()), &module_refs)
461    }
462
463    /// Parses a program from a source file and its associated module files in the same directory tree.
464    pub fn parse_program_from_directory(
465        &mut self,
466        entry_file_path: impl AsRef<Path>,
467        source_directory: impl AsRef<Path>,
468    ) -> Result<Program> {
469        self.parse_program_from_directory_with_file_source(entry_file_path, source_directory, &DiskFileSource)
470    }
471
472    /// Parses a program from a source file using the given file source.
473    pub fn parse_program_from_directory_with_file_source(
474        &mut self,
475        entry_file_path: impl AsRef<Path>,
476        source_directory: impl AsRef<Path>,
477        file_source: &impl FileSource,
478    ) -> Result<Program> {
479        let (source, modules_owned) = Self::read_sources_and_modules(file_source, &entry_file_path, &source_directory)?;
480
481        // Convert owned module sources into temporary (&str, FileName) tuples.
482        let module_refs: Vec<(&str, FileName)> =
483            modules_owned.iter().map(|(src, fname)| (src.as_str(), fname.clone())).collect();
484
485        // Parse the main source along with all collected modules.
486        self.parse_program(&source, FileName::Real(entry_file_path.as_ref().into()), &module_refs)?;
487
488        match &self.state.ast {
489            Ast::Program(program) => Ok(program.clone()),
490            Ast::Library(_) => unreachable!("expected Program AST"),
491        }
492    }
493
494    /// Parses a program from a source file and its associated module files in the same directory tree.
495    pub fn parse_library_from_directory(
496        &mut self,
497        library_name: Symbol,
498        entry_file_path: impl AsRef<Path>,
499        source_directory: impl AsRef<Path>,
500    ) -> Result<Library> {
501        self.parse_library_from_directory_with_file_source(
502            library_name,
503            entry_file_path,
504            source_directory,
505            &DiskFileSource,
506        )
507    }
508
509    /// Parses a library from a source file.
510    pub fn parse_library_from_directory_with_file_source(
511        &mut self,
512        library_name: Symbol,
513        entry_file_path: impl AsRef<Path>,
514        source_directory: impl AsRef<Path>,
515        file_source: &impl FileSource,
516    ) -> Result<Library> {
517        let (source, modules_owned) = Self::read_sources_and_modules(file_source, &entry_file_path, &source_directory)?;
518
519        let module_refs: Vec<(&str, FileName)> =
520            modules_owned.iter().map(|(src, fname)| (src.as_str(), fname.clone())).collect();
521
522        self.parse_library(library_name, &source, FileName::Real(entry_file_path.as_ref().into()), &module_refs)?;
523
524        match &self.state.ast {
525            Ast::Library(library) => Ok(library.clone()),
526            Ast::Program(_) => unreachable!("expected Library AST"),
527        }
528    }
529
530    /// Writes the AST to a JSON file.
531    fn write_ast_to_json(&self, file_suffix: &str) -> Result<()> {
532        match &self.state.ast {
533            Ast::Program(program) => {
534                // Remove `Span`s if they are not enabled.
535                if self.compiler_options.ast_spans_enabled {
536                    program.to_json_file(
537                        self.output_directory.clone(),
538                        &format!("{}.{file_suffix}", self.program_name.as_ref().unwrap()),
539                    )?;
540                } else {
541                    program.to_json_file_without_keys(
542                        self.output_directory.clone(),
543                        &format!("{}.{file_suffix}", self.program_name.as_ref().unwrap()),
544                        &["_span", "span"],
545                    )?;
546                }
547            }
548            Ast::Library(_) => {
549                // no-op for libraries
550            }
551        }
552        Ok(())
553    }
554
555    /// Writes the AST to a file (Leo syntax, not JSON).
556    fn write_ast(&self, file_suffix: &str) -> Result<()> {
557        let filename = format!("{}.{file_suffix}", self.program_name.as_ref().unwrap());
558        let full_filename = self.output_directory.join(&filename);
559
560        let contents = match &self.state.ast {
561            Ast::Program(program) => program.to_string(),
562            Ast::Library(_) => String::new(), // empty for libraries
563        };
564
565        fs::write(&full_filename, contents).map_err(|e| CompilerError::failed_ast_file(full_filename.display(), e))?;
566
567        Ok(())
568    }
569
570    /// Resolves and registers all import stubs for the current program.
571    ///
572    /// This method performs a graph traversal over the program’s import relationships to:
573    /// 1. Establish parent–child relationships between stubs based on imports.
574    /// 2. Collect all reachable stubs in traversal order.
575    /// 3. Store the explored stubs back into `self.state.ast.ast.stubs`.
576    ///
577    /// The traversal starts from the imports of the main program and recursively follows
578    /// their transitive dependencies. Any missing stub during traversal results in an error.
579    ///
580    /// # Returns
581    ///
582    /// * `Ok(())` if all imports are successfully resolved and stubs are collected.
583    /// * `Err(CompilerError)` if any imported program cannot be found.
584    pub fn add_import_stubs(&mut self) -> Result<()> {
585        use indexmap::IndexSet;
586
587        // Track which programs we've already processed.
588        let mut explored = IndexSet::<Symbol>::new();
589
590        // Compute initial imports: explicit program imports + library dependencies
591        let initial_imports: IndexMap<Symbol, Span> = match &self.state.ast {
592            Ast::Program(program) => {
593                let mut map: IndexMap<Symbol, Span> =
594                    program.imports.iter().map(|(name, id)| (*name, id.span())).collect();
595                // Add any libraries that have this program as a parent
596                for (stub_name, stub) in &self.import_stubs {
597                    if matches!(stub, Stub::FromLibrary { .. })
598                        && stub.parents().contains(&Symbol::intern(self.program_name.as_ref().unwrap()))
599                    {
600                        map.insert(
601                            *stub_name,
602                            Span::default(), // library dependencies are implicit
603                        );
604                    }
605                }
606                map
607            }
608            Ast::Library(_) => IndexMap::new(),
609        };
610
611        // Initialize the exploration queue with the root’s direct imports.
612        let mut to_explore: Vec<(Symbol, Span)> = initial_imports.iter().map(|(sym, span)| (*sym, *span)).collect();
613
614        // If this is a named program, set the main program as the parent of its direct imports.
615        if let Some(main_program_name) = self.program_name.clone() {
616            let main_symbol = Symbol::intern(&main_program_name);
617            for import in initial_imports.keys() {
618                if let Some(child_stub) = self.import_stubs.get_mut(import) {
619                    child_stub.add_parent(main_symbol);
620                }
621            }
622        }
623
624        // Traverse the dependency graph breadth-first, populating parents
625        while let Some((import_symbol, span)) = to_explore.pop() {
626            // Mark this import as explored.
627            explored.insert(import_symbol);
628
629            // Look up the corresponding stub.
630            let Some(stub) = self.import_stubs.get(&import_symbol) else {
631                return Err(CompilerError::imported_program_not_found(
632                    self.program_name.as_ref().unwrap(),
633                    import_symbol,
634                    span,
635                )
636                .into());
637            };
638
639            // Combine imports: explicit stub.explicit_imports() + libraries that list this stub as parent
640            let mut combined_imports: IndexMap<Symbol, Span> = stub.explicit_imports().collect();
641            for (lib_name, lib_stub) in &self.import_stubs {
642                if matches!(lib_stub, Stub::FromLibrary { .. }) && lib_stub.parents().contains(&import_symbol) {
643                    combined_imports.insert(
644                        *lib_name,
645                        Span::default(), // library dependencies are implicit
646                    );
647                }
648            }
649
650            for (child_symbol, child_span) in combined_imports {
651                // Record parent relationship
652                if let Some(child_stub) = self.import_stubs.get_mut(&child_symbol) {
653                    child_stub.add_parent(import_symbol);
654                }
655
656                // Schedule child for exploration if not yet visited.
657                if explored.insert(child_symbol) {
658                    to_explore.push((child_symbol, child_span));
659                }
660            }
661        }
662
663        // Only Programs store stubs on the AST (at least for now).
664        if let Ast::Program(program) = &mut self.state.ast {
665            program.stubs = self
666                .import_stubs
667                .iter()
668                .filter(|(symbol, _)| explored.contains(*symbol))
669                .map(|(symbol, stub)| (*symbol, stub.clone()))
670                .collect();
671        }
672
673        Ok(())
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use super::Compiler;
680
681    use leo_ast::{NetworkName, NodeBuilder};
682    use leo_errors::Handler;
683    use leo_span::{Symbol, create_session_if_not_set_then, file_source::InMemoryFileSource};
684
685    use std::{path::PathBuf, rc::Rc};
686
687    use indexmap::IndexMap;
688
689    #[test]
690    fn parse_library_from_directory_in_memory() {
691        create_session_if_not_set_then(|_| {
692            let mut source = InMemoryFileSource::new();
693            source.set(
694                PathBuf::from("/mylib/src/lib.leo"),
695                concat!("const SCALE: u32 = 10u32;\n", "const OFFSET: u32 = SCALE + 1u32;\n",).into(),
696            );
697
698            let handler = Handler::default();
699            let node_builder = Rc::new(NodeBuilder::default());
700            let mut compiler = Compiler::new(
701                None,
702                false,
703                handler,
704                node_builder,
705                PathBuf::from("/unused"),
706                None,
707                IndexMap::new(),
708                NetworkName::TestnetV0,
709            );
710
711            let library = compiler
712                .parse_library_from_directory_with_file_source(
713                    Symbol::intern("mylib"),
714                    "/mylib/src/lib.leo",
715                    "/mylib/src",
716                    &source,
717                )
718                .unwrap_or_else(|err| panic!("parsing library from in-memory file source failed: {err}"));
719
720            assert_eq!(library.name, Symbol::intern("mylib"));
721            assert_eq!(library.consts.len(), 2, "expected 2 consts, got {}", library.consts.len());
722            assert!(
723                library.consts.iter().any(|(name, _)| *name == Symbol::intern("SCALE")),
724                "expected const `SCALE` in library"
725            );
726            assert!(
727                library.consts.iter().any(|(name, _)| *name == Symbol::intern("OFFSET")),
728                "expected const `OFFSET` in library"
729            );
730        });
731    }
732
733    #[test]
734    fn parse_program_from_directory_in_memory_with_module() {
735        create_session_if_not_set_then(|_| {
736            let mut source = InMemoryFileSource::new();
737            source.set(
738                PathBuf::from("/project/src/main.leo"),
739                concat!(
740                    "program test.aleo {\n",
741                    "  fn main() -> u32 {\n",
742                    "    return utils::helper();\n",
743                    "  }\n",
744                    "}\n",
745                )
746                .into(),
747            );
748            source.set(PathBuf::from("/project/src/utils.leo"), "fn helper() -> u32 {\n  return 42u32;\n}\n".into());
749
750            let handler = Handler::default();
751            let node_builder = Rc::new(NodeBuilder::default());
752            let mut compiler = Compiler::new(
753                Some("test.aleo".into()),
754                false,
755                handler,
756                node_builder,
757                PathBuf::from("/unused"),
758                None,
759                IndexMap::new(),
760                NetworkName::TestnetV0,
761            );
762
763            let ast = compiler
764                .parse_program_from_directory_with_file_source("/project/src/main.leo", "/project/src", &source)
765                .unwrap_or_else(|err| panic!("parsing from in-memory file source failed: {err}"));
766            let utils_key = vec![Symbol::intern("utils")];
767
768            assert!(
769                ast.modules.contains_key(&utils_key),
770                "module `utils` should be loaded from the in-memory file source; found keys: {:?}",
771                ast.modules.keys().collect::<Vec<_>>()
772            );
773        });
774    }
775}