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, errors};
22
23use leo_ast::{AleoProgram, FunctionStub, Identifier, Library, NetworkName, NodeBuilder, ProgramId, Stub};
24pub use leo_ast::{Ast, DiGraph, Program};
25use leo_errors::{Handler, Result};
26use leo_package::{
27    CompilationUnit,
28    Dependency,
29    Location,
30    MANIFEST_FILENAME,
31    Manifest,
32    PackageKind,
33    ProgramData,
34    resolve_workspace_dependency,
35};
36use leo_passes::*;
37use leo_span::{
38    Span,
39    Symbol,
40    create_session_if_not_set_then,
41    file_source::{DiskFileSource, FileSource},
42    source_map::FileName,
43    with_session_globals,
44};
45
46use std::{
47    fs,
48    path::{Path, PathBuf},
49    rc::Rc,
50};
51
52use indexmap::{IndexMap, map::Entry};
53
54/// Borrowed frontend state after parsing and semantic frontend passes complete.
55pub struct FrontendAnalysis<'a> {
56    /// Parsed AST after import-stub registration and frontend passes.
57    pub ast: &'a Ast,
58    /// Name-resolution state produced by the frontend pipeline.
59    pub symbol_table: &'a SymbolTable,
60    /// Type information produced by semantic frontend passes.
61    pub type_table: &'a TypeTable,
62}
63
64/// Import stubs together with the filesystem inputs that invalidate them.
65pub struct LoadedImportStubs {
66    /// Import stubs available for compiler or LSP frontend analysis.
67    pub stubs: IndexMap<Symbol, Stub>,
68    /// Package inputs whose metadata changes should force a stub reload.
69    pub watch_paths: Vec<PathBuf>,
70}
71
72/// A single compiled program with its bytecode and ABI.
73pub struct CompiledProgram {
74    /// The program name (without `.aleo` suffix).
75    pub name: String,
76    /// The generated Aleo bytecode.
77    pub bytecode: String,
78    /// The ABI describing the program's public interface.
79    pub abi: leo_abi::Program,
80}
81
82/// The result of compiling a Leo program.
83pub struct Compiled {
84    /// The primary program that was compiled.
85    pub primary: CompiledProgram,
86    /// Compiled programs for imports.
87    pub imports: Vec<CompiledProgram>,
88    /// Interface ABIs from the primary program.
89    pub interfaces: Vec<leo_abi::interfaces::CompiledInterface>,
90}
91
92/// The primary entry point of the Leo compiler.
93pub struct Compiler {
94    /// The path to where the compiler outputs all generated files.
95    output_directory: PathBuf,
96    /// The name of the compilation unit (program or library).
97    pub unit_name: Option<String>,
98    /// Options configuring compilation.
99    compiler_options: CompilerOptions,
100    /// State.
101    state: CompilerState,
102    /// The stubs for imported programs.
103    import_stubs: IndexMap<Symbol, Stub>,
104    /// How many statements were in the AST before DCE?
105    pub statements_before_dce: u32,
106    /// How many statements were in the AST after DCE?
107    pub statements_after_dce: u32,
108}
109
110impl Compiler {
111    /// Return the network selected for this compiler instance.
112    pub fn network(&self) -> NetworkName {
113        self.state.network
114    }
115
116    /// Parses the given source into a program AST and stores it in the compiler state.
117    ///
118    /// The source file and any provided module sources are first registered in the
119    /// session source map so spans can be resolved correctly. The parser then
120    /// constructs the program AST from the main source and its modules.
121    ///
122    /// After parsing, this verifies that the program scope name matches the expected
123    /// program name (from `program.json` or the test filename). The resulting AST is
124    /// stored in `self.state.ast`, and optionally written to disk if configured.
125    pub fn parse_program(&mut self, source: &str, filename: FileName, modules: &[(&str, FileName)]) -> Result<()> {
126        // Register the source in the source map.
127        let source_file = with_session_globals(|s| s.source_map.new_source(source, filename.clone()));
128
129        // Register the sources of all the modules in the source map.
130        let modules = modules
131            .iter()
132            .map(|(source, filename)| with_session_globals(|s| s.source_map.new_source(source, filename.clone())))
133            .collect::<Vec<_>>();
134
135        // Use the parser to construct the abstract syntax tree (ast).
136        let program = leo_parser::parse_program(
137            self.state.handler.clone(),
138            &self.state.node_builder,
139            &source_file,
140            &modules,
141            self.state.network,
142        )?;
143
144        // Check that the name of its program scope matches the expected name.
145        // Note that parsing enforces that there is exactly one program scope in a file.
146        let program_scope = program.program_scopes.values().next().unwrap();
147        if let Some(unit_name) = &self.unit_name {
148            if unit_name != &program_scope.program_id.as_symbol().to_string() {
149                return Err(crate::errors::program_name_should_match_file_name(
150                    program_scope.program_id.as_symbol(),
151                    // If this is a test, use the filename as the expected name.
152                    if self.state.is_test {
153                        format!(
154                            "`{}` (the test file name)",
155                            filename.to_string().split("/").last().expect("Could not get file name")
156                        )
157                    } else {
158                        format!("`{unit_name}` (specified in `program.json`)")
159                    },
160                    program_scope.program_id.span(),
161                )
162                .into());
163            }
164        } else {
165            self.unit_name = Some(program_scope.program_id.as_symbol().to_string());
166        }
167
168        self.state.ast = Ast::Program(program);
169
170        if self.compiler_options.initial_ast {
171            self.write_ast_to_json("initial.json")?;
172            self.write_ast("initial.ast")?;
173        }
174
175        Ok(())
176    }
177
178    /// Simple wrapper around `parse_program` that also returns a program AST.
179    pub fn parse_and_return_program(
180        &mut self,
181        source: &str,
182        filename: FileName,
183        modules: &[(&str, FileName)],
184    ) -> Result<Program> {
185        // Parse the program.
186        self.parse_program(source, filename, modules)?;
187
188        match &self.state.ast {
189            Ast::Program(program) => Ok(program.clone()),
190            Ast::Library(_) => unreachable!("expected Program AST"),
191        }
192    }
193
194    /// Simple wrapper around `parse_library` that also returns a library AST.
195    pub fn parse_and_return_library(
196        &mut self,
197        library_name: &str,
198        source: &str,
199        filename: FileName,
200        modules: &[(&str, FileName)],
201    ) -> Result<Library> {
202        self.parse_library(Symbol::intern(library_name), source, filename, modules)?;
203
204        match &self.state.ast {
205            Ast::Program(_) => unreachable!("expected Library AST"),
206            Ast::Library(library) => Ok(library.clone()),
207        }
208    }
209
210    /// Parses a library source (and its submodules) into a library AST.
211    ///
212    /// All source strings are registered in the session source map so span information
213    /// can be resolved correctly. The resulting AST is stored in `self.state.ast`.
214    pub fn parse_library(
215        &mut self,
216        library_name: Symbol,
217        source: &str,
218        filename: FileName,
219        modules: &[(&str, FileName)],
220    ) -> Result<()> {
221        let source_file = with_session_globals(|s| s.source_map.new_source(source, filename.clone()));
222
223        // Register each module source in the source map.
224        let module_files = modules
225            .iter()
226            .map(|(src, name)| with_session_globals(|s| s.source_map.new_source(src, name.clone())))
227            .collect::<Vec<_>>();
228
229        self.state.ast = Ast::Library(leo_parser::parse_library(
230            self.state.handler.clone(),
231            &self.state.node_builder,
232            library_name,
233            &source_file,
234            &module_files,
235            self.state.network,
236        )?);
237
238        // Downstream passes (e.g. `add_import_stubs`) read `unit_name` to identify the
239        // current compilation target. Libraries don't embed their own name in the source the
240        // way programs do, so adopt the name supplied by the caller if none was pre-set.
241        if self.unit_name.is_none() {
242            self.unit_name = Some(library_name.to_string());
243        }
244
245        Ok(())
246    }
247
248    /// Parses a package entry file, merges import stubs when applicable, and runs frontend passes.
249    ///
250    /// Unlike the full compile pipeline, this stops after semantic frontend
251    /// analysis and returns borrowed access to the AST, symbol table, and type
252    /// table. The LSP uses this to build semantic indices without running code
253    /// generation or writing artifacts to disk.
254    pub fn analyze_frontend_from_directory_with_file_source(
255        &mut self,
256        entry_file_path: impl AsRef<Path>,
257        source_directory: impl AsRef<Path>,
258        file_source: &impl FileSource,
259    ) -> Result<FrontendAnalysis<'_>> {
260        self.analyze_frontend_from_directory_with_file_source_and_check(
261            entry_file_path,
262            source_directory,
263            file_source,
264            || Ok(()),
265        )
266    }
267
268    /// Equivalent to [`Self::analyze_frontend_from_directory_with_file_source`], but checks
269    /// `should_continue` at parse and pass boundaries so editor tooling can abandon
270    /// stale work before completing the entire frontend pipeline.
271    pub fn analyze_frontend_from_directory_with_file_source_and_check<C>(
272        &mut self,
273        entry_file_path: impl AsRef<Path>,
274        source_directory: impl AsRef<Path>,
275        file_source: &impl FileSource,
276        mut should_continue: C,
277    ) -> Result<FrontendAnalysis<'_>>
278    where
279        C: FnMut() -> Result<()>,
280    {
281        should_continue()?;
282        let is_library = self.unit_name.as_deref().is_some_and(|name| !name.ends_with(".aleo"));
283
284        if is_library {
285            let library_name = Symbol::intern(self.unit_name.as_deref().expect("library analysis requires a name"));
286            self.parse_library_from_directory_with_file_source(
287                library_name,
288                &entry_file_path,
289                &source_directory,
290                file_source,
291            )?;
292        } else {
293            self.parse_program_from_directory_with_file_source(&entry_file_path, &source_directory, file_source)?;
294            self.add_import_stubs()?;
295        }
296
297        // Re-check after parsing/import setup so editor callers can drop stale
298        // work before entering the semantic pass pipeline.
299        should_continue()?;
300        self.frontend_passes_with_check(&mut should_continue)?;
301
302        Ok(FrontendAnalysis {
303            ast: &self.state.ast,
304            symbol_table: &self.state.symbol_table,
305            type_table: &self.state.type_table,
306        })
307    }
308
309    /// Returns a new Leo compiler.
310    #[allow(clippy::too_many_arguments)]
311    pub fn new(
312        expected_unit_name: Option<String>,
313        is_test: bool,
314        handler: Handler,
315        node_builder: Rc<NodeBuilder>,
316        output_directory: PathBuf,
317        compiler_options: Option<CompilerOptions>,
318        import_stubs: IndexMap<Symbol, Stub>,
319        network: NetworkName,
320    ) -> Self {
321        Self {
322            state: CompilerState {
323                handler,
324                node_builder: Rc::clone(&node_builder),
325                is_test,
326                network,
327                ..Default::default()
328            },
329            output_directory,
330            unit_name: expected_unit_name,
331            compiler_options: compiler_options.unwrap_or_default(),
332            import_stubs,
333            statements_before_dce: 0,
334            statements_after_dce: 0,
335        }
336    }
337
338    /// Run a compiler pass without an external cancellation check.
339    pub fn do_pass<P: Pass>(&mut self, input: P::Input) -> Result<P::Output> {
340        self.do_pass_with_check::<P, _>(input, &mut || Ok(()))
341    }
342
343    /// Runs a compiler pass and checks whether the caller still wants the
344    /// result once the pass and any requested snapshots have completed.
345    fn do_pass_with_check<P: Pass, C>(&mut self, input: P::Input, should_continue: &mut C) -> Result<P::Output>
346    where
347        C: FnMut() -> Result<()>,
348    {
349        let output = P::do_pass(input, &mut self.state)?;
350
351        let write = match &self.compiler_options.ast_snapshots {
352            AstSnapshots::All => true,
353            AstSnapshots::Some(passes) => passes.contains(P::NAME),
354        };
355
356        if write {
357            self.write_ast_to_json(&format!("{}.json", P::NAME))?;
358            self.write_ast(&format!("{}.ast", P::NAME))?;
359        }
360
361        should_continue()?;
362        Ok(output)
363    }
364
365    /// Runs all frontend passes: NameValidation through StaticAnalyzing.
366    pub fn frontend_passes(&mut self) -> Result<()> {
367        self.frontend_passes_with_check(|| Ok(()))
368    }
369
370    /// Runs all frontend passes while checking whether the caller still wants the result.
371    pub fn frontend_passes_with_check<C>(&mut self, mut should_continue: C) -> Result<()>
372    where
373        C: FnMut() -> Result<()>,
374    {
375        // Bail out if the parser already found errors.  The error-recovering parser may have
376        // produced ErrExpression nodes in the AST, which would cause panics in later passes.
377        self.state.handler.last_err()?;
378
379        self.do_pass_with_check::<NameValidation, _>((), &mut should_continue)?;
380        self.do_pass_with_check::<GlobalVarsCollection, _>((), &mut should_continue)?;
381        self.do_pass_with_check::<PathResolution, _>((), &mut should_continue)?;
382        self.do_pass_with_check::<GlobalItemsCollection, _>((), &mut should_continue)?;
383        self.do_pass_with_check::<CheckInterfaces, _>((), &mut should_continue)?;
384        self.do_pass_with_check::<TypeChecking, _>(TypeCheckingInput::new(self.state.network), &mut should_continue)?;
385        self.do_pass_with_check::<Disambiguate, _>((), &mut should_continue)?;
386        self.do_pass_with_check::<CeiAnalyzing, _>((), &mut should_continue)?;
387        self.do_pass_with_check::<ProcessingAsync, _>(
388            TypeCheckingInput::new(self.state.network),
389            &mut should_continue,
390        )?;
391        self.do_pass_with_check::<StaticAnalyzing, _>((), &mut should_continue)?;
392        Ok(())
393    }
394
395    /// Runs the compiler stages.
396    ///
397    /// Returns the generated ABIs (primary and imports), which are captured
398    /// immediately after monomorphisation to ensure all types are resolved,
399    /// but not yet lowered.
400    pub fn intermediate_passes(
401        &mut self,
402    ) -> Result<(leo_abi::Program, IndexMap<String, leo_abi::Program>, Vec<leo_abi::interfaces::CompiledInterface>)>
403    {
404        let type_checking_config = TypeCheckingInput::new(self.state.network);
405
406        self.frontend_passes()?;
407
408        self.do_pass::<ConstPropUnrollAndMorphing>(type_checking_config.clone())?;
409
410        // Generate ABIs after monomorphization to capture concrete types.
411        // Const generic structs are resolved to their monomorphized versions.
412        let abis = self.generate_abi();
413
414        self.do_pass::<StorageLowering>(type_checking_config.clone())?;
415
416        self.do_pass::<OptionLowering>(type_checking_config)?;
417
418        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: true })?;
419
420        self.do_pass::<Destructuring>(())?;
421
422        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
423
424        self.do_pass::<WriteTransforming>(())?;
425
426        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
427
428        self.do_pass::<Flattening>(())?;
429
430        self.do_pass::<FunctionInlining>(())?;
431
432        // Flattening may produce ternary expressions not in SSA form.
433        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
434
435        self.do_pass::<SsaConstPropagation>(())?;
436
437        self.do_pass::<SsaForming>(SsaFormingInput { rename_defs: false })?;
438
439        self.do_pass::<CommonSubexpressionEliminating>(())?;
440
441        let output = self.do_pass::<DeadCodeEliminating>(())?;
442        self.statements_before_dce = output.statements_before;
443        self.statements_after_dce = output.statements_after;
444
445        Ok(abis)
446    }
447
448    /// Generates ABIs for the primary program, all imports, and interfaces.
449    ///
450    /// Returns `(primary_abi, import_abis, interface_abis)` where `import_abis`
451    /// maps program names to their ABIs.
452    ///
453    /// This method only expects program ASTs. Library ASTs cause this method to panic.
454    fn generate_abi(
455        &self,
456    ) -> (leo_abi::Program, IndexMap<String, leo_abi::Program>, Vec<leo_abi::interfaces::CompiledInterface>) {
457        let program = match &self.state.ast {
458            Ast::Program(program) => program,
459            Ast::Library(_) => panic!("expected Program AST"),
460        };
461
462        // Generate primary ABI (pruning happens inside generate).
463        let primary_abi = leo_abi::generate(program);
464
465        // Generate interface ABIs.
466        let interface_abis = leo_abi::interfaces::generate_program_interfaces(program);
467
468        // Generate import ABIs from stubs, ignoring libraries.
469        let import_abis: IndexMap<String, leo_abi::Program> = program
470            .stubs
471            .iter()
472            .filter(|(_, stub)| !matches!(stub, Stub::FromLibrary { .. }))
473            .map(|(name, stub)| {
474                let abi = match stub {
475                    Stub::FromLeo { program, .. } => leo_abi::generate(program),
476                    Stub::FromAleo { program, .. } => leo_abi::aleo::generate(program),
477                    Stub::FromLibrary { .. } => unreachable!("filtered out"),
478                };
479                (name.to_string(), abi)
480            })
481            .collect();
482
483        (primary_abi, import_abis, interface_abis)
484    }
485
486    /// Generates interface ABIs for a validated library.
487    ///
488    /// Must be called after `build_library()` since it reads the resolved AST.
489    pub fn generate_library_interface_abis(&self) -> Vec<leo_abi::interfaces::CompiledInterface> {
490        let Ast::Library(library) = &self.state.ast else {
491            panic!("expected Library AST");
492        };
493        leo_abi::interfaces::generate_library_interfaces(library)
494    }
495
496    /// Compiles a program from a given source string and a list of module sources.
497    ///
498    /// # Arguments
499    ///
500    /// * `source` - The main source code as a string slice.
501    /// * `filename` - The name of the main source file.
502    /// * `modules` - A vector of tuples where each tuple contains:
503    ///     - A module source as a string slice.
504    ///     - Its associated `FileName`.
505    ///
506    /// # Returns
507    ///
508    /// * `Ok(CompiledPrograms)` containing the generated bytecode and ABI if compilation succeeds.
509    /// * `Err(CompilerError)` if any stage of the pipeline fails.
510    pub fn compile(&mut self, source: &str, filename: FileName, modules: &Vec<(&str, FileName)>) -> Result<Compiled> {
511        // Parse the program.
512        self.parse_program(source, filename, modules)?;
513        // Merge the stubs into the AST.
514        self.add_import_stubs()?;
515        // Run the intermediate compiler stages, which also generates ABIs.
516        let (primary_abi, import_abis, interfaces) = self.intermediate_passes()?;
517        // Run code generation.
518        let generated = self.do_pass::<CodeGenerating>(())?;
519        // Run peephole optimization and serialize to bytecode.
520        let bytecodes = self.do_pass::<PeepholeOptimizing>(generated)?;
521
522        // Build the primary compiled program.
523        let primary = CompiledProgram {
524            name: self.unit_name.clone().unwrap(),
525            bytecode: bytecodes.primary_bytecode,
526            abi: primary_abi,
527        };
528
529        // Build compiled programs for imports, looking up ABIs by name.
530        let imports: Vec<CompiledProgram> = bytecodes
531            .import_bytecodes
532            .into_iter()
533            .map(|bc| {
534                let abi = import_abis.get(&bc.program_name).expect("ABI should exist for all imports").clone();
535                CompiledProgram { name: bc.program_name, bytecode: bc.bytecode, abi }
536            })
537            .collect();
538
539        Ok(Compiled { primary, imports, interfaces })
540    }
541
542    /// Reads the main source file and all module files in the same directory tree.
543    ///
544    /// This helper walks all `.leo` files under `source_directory` (excluding the main file itself),
545    /// reads their contents, and returns:
546    /// - The main file’s source as a `String`.
547    /// - A vector of module tuples `(String, FileName)` suitable for compilation or parsing.
548    ///
549    /// # Arguments
550    ///
551    /// * `entry_file_path` - The main source file.
552    /// * `source_directory` - The directory root for discovering `.leo` module files.
553    ///
554    /// # Errors
555    ///
556    /// Returns `Err(CompilerError)` if reading any file fails.
557    fn read_sources_and_modules(
558        file_source: &impl FileSource,
559        entry_file_path: impl AsRef<Path>,
560        source_directory: impl AsRef<Path>,
561    ) -> Result<(String, Vec<(String, FileName)>)> {
562        let entry_file_path = entry_file_path.as_ref();
563        let source_directory = source_directory.as_ref();
564
565        // Read the contents of the main source file.
566        let source = file_source
567            .read_file(entry_file_path)
568            .map_err(|e| crate::errors::file_read_error(entry_file_path.display().to_string(), e))?;
569
570        let files = file_source
571            .list_leo_files(source_directory, entry_file_path)
572            .map_err(|e| crate::errors::file_read_error(source_directory.display().to_string(), e))?;
573
574        let mut modules = Vec::with_capacity(files.len());
575        for path in files {
576            let module_source = file_source
577                .read_file(&path)
578                .map_err(|e| crate::errors::file_read_error(path.display().to_string(), e))?;
579            modules.push((module_source, FileName::Real(path)));
580        }
581
582        Ok((source, modules))
583    }
584
585    /// Compiles a program from a source file and its associated module files in the same directory tree.
586    pub fn compile_from_directory(
587        &mut self,
588        entry_file_path: impl AsRef<Path>,
589        source_directory: impl AsRef<Path>,
590    ) -> Result<Compiled> {
591        self.compile_from_directory_with_file_source(entry_file_path, source_directory, &DiskFileSource)
592    }
593
594    /// Compiles a program from a source file using the given file source.
595    pub fn compile_from_directory_with_file_source(
596        &mut self,
597        entry_file_path: impl AsRef<Path>,
598        source_directory: impl AsRef<Path>,
599        file_source: &impl FileSource,
600    ) -> Result<Compiled> {
601        let (source, modules_owned) = Self::read_sources_and_modules(file_source, &entry_file_path, &source_directory)?;
602
603        // Convert owned module sources into temporary (&str, FileName) tuples.
604        let module_refs: Vec<(&str, FileName)> =
605            modules_owned.iter().map(|(src, fname)| (src.as_str(), fname.clone())).collect();
606
607        // Compile the main source along with all collected modules.
608        self.compile(&source, FileName::Real(entry_file_path.as_ref().into()), &module_refs)
609    }
610
611    /// Parses a program from a source file and its associated module files in the same directory tree.
612    pub fn parse_program_from_directory(
613        &mut self,
614        entry_file_path: impl AsRef<Path>,
615        source_directory: impl AsRef<Path>,
616    ) -> Result<Program> {
617        self.parse_program_from_directory_with_file_source(entry_file_path, source_directory, &DiskFileSource)
618    }
619
620    /// Parses a program from a source file using the given file source.
621    pub fn parse_program_from_directory_with_file_source(
622        &mut self,
623        entry_file_path: impl AsRef<Path>,
624        source_directory: impl AsRef<Path>,
625        file_source: &impl FileSource,
626    ) -> Result<Program> {
627        let (source, modules_owned) = Self::read_sources_and_modules(file_source, &entry_file_path, &source_directory)?;
628
629        // Convert owned module sources into temporary (&str, FileName) tuples.
630        let module_refs: Vec<(&str, FileName)> =
631            modules_owned.iter().map(|(src, fname)| (src.as_str(), fname.clone())).collect();
632
633        // Parse the main source along with all collected modules.
634        self.parse_program(&source, FileName::Real(entry_file_path.as_ref().into()), &module_refs)?;
635
636        match &self.state.ast {
637            Ast::Program(program) => Ok(program.clone()),
638            Ast::Library(_) => unreachable!("expected Program AST"),
639        }
640    }
641
642    /// Parses a program from a source file and its associated module files in the same directory tree.
643    pub fn parse_library_from_directory(
644        &mut self,
645        library_name: Symbol,
646        entry_file_path: impl AsRef<Path>,
647        source_directory: impl AsRef<Path>,
648    ) -> Result<Library> {
649        self.parse_library_from_directory_with_file_source(
650            library_name,
651            entry_file_path,
652            source_directory,
653            &DiskFileSource,
654        )
655    }
656
657    /// Parses a library from a source file.
658    pub fn parse_library_from_directory_with_file_source(
659        &mut self,
660        library_name: Symbol,
661        entry_file_path: impl AsRef<Path>,
662        source_directory: impl AsRef<Path>,
663        file_source: &impl FileSource,
664    ) -> Result<Library> {
665        let (source, modules_owned) = Self::read_sources_and_modules(file_source, &entry_file_path, &source_directory)?;
666
667        let module_refs: Vec<(&str, FileName)> =
668            modules_owned.iter().map(|(src, fname)| (src.as_str(), fname.clone())).collect();
669
670        self.parse_library(library_name, &source, FileName::Real(entry_file_path.as_ref().into()), &module_refs)?;
671
672        match &self.state.ast {
673            Ast::Library(library) => Ok(library.clone()),
674            Ast::Program(_) => unreachable!("expected Library AST"),
675        }
676    }
677
678    /// Writes the AST to a JSON file under the unit's snapshots directory.
679    fn write_ast_to_json(&self, filename: &str) -> Result<()> {
680        match &self.state.ast {
681            Ast::Program(program) => {
682                // Snapshots are opt-in; create the directory lazily on first write.
683                fs::create_dir_all(&self.output_directory)
684                    .map_err(|e| crate::errors::failed_ast_file(self.output_directory.display(), e))?;
685                // Remove `Span`s if they are not enabled.
686                if self.compiler_options.ast_spans_enabled {
687                    program.to_json_file(self.output_directory.clone(), filename)?;
688                } else {
689                    program.to_json_file_without_keys(self.output_directory.clone(), filename, &["_span", "span"])?;
690                }
691            }
692            Ast::Library(_) => {
693                // no-op for libraries
694            }
695        }
696        Ok(())
697    }
698
699    /// Writes the AST to a file (Leo syntax, not JSON) under the unit's snapshots directory.
700    fn write_ast(&self, filename: &str) -> Result<()> {
701        // Snapshots are opt-in; create the directory lazily on first write.
702        fs::create_dir_all(&self.output_directory)
703            .map_err(|e| crate::errors::failed_ast_file(self.output_directory.display(), e))?;
704        let full_filename = self.output_directory.join(filename);
705
706        let contents = match &self.state.ast {
707            Ast::Program(program) => program.to_string(),
708            Ast::Library(_) => String::new(), // empty for libraries
709        };
710
711        fs::write(&full_filename, contents).map_err(|e| crate::errors::failed_ast_file(full_filename.display(), e))?;
712
713        Ok(())
714    }
715
716    /// Resolves and registers all import stubs for the current program.
717    ///
718    /// This method performs a graph traversal over the program’s import relationships to:
719    /// 1. Establish parent–child relationships between stubs based on imports.
720    /// 2. Collect all reachable stubs in traversal order.
721    /// 3. Store the explored stubs back into `self.state.ast.ast.stubs`.
722    ///
723    /// The traversal starts from the imports of the main program and recursively follows
724    /// their transitive dependencies. Any missing stub during traversal results in an error.
725    ///
726    /// # Returns
727    ///
728    /// * `Ok(())` if all imports are successfully resolved and stubs are collected.
729    /// * `Err(CompilerError)` if any imported program cannot be found.
730    pub fn add_import_stubs(&mut self) -> Result<()> {
731        use indexmap::IndexSet;
732
733        // Track which programs we've already processed.
734        let mut explored = IndexSet::<Symbol>::new();
735
736        // Compute initial imports: explicit program imports + library dependencies
737        let initial_imports: IndexMap<Symbol, Span> = match &self.state.ast {
738            Ast::Program(program) => {
739                let mut map: IndexMap<Symbol, Span> =
740                    program.imports.iter().map(|(name, id)| (*name, id.span())).collect();
741                // Add any libraries that have this program as a parent
742                for (stub_name, stub) in &self.import_stubs {
743                    if matches!(stub, Stub::FromLibrary { .. })
744                        && stub.parents().contains(&Symbol::intern(self.unit_name.as_ref().unwrap()))
745                    {
746                        map.insert(
747                            *stub_name,
748                            Span::default(), // library dependencies are implicit
749                        );
750                    }
751                }
752                map
753            }
754            Ast::Library(_) => {
755                // Libraries have no explicit `imports` field; their dependencies are expressed
756                // indirectly through parent relations on the stubs map. A stub is a dep of this
757                // library iff its parent set contains the library's own name.
758                let library_name = Symbol::intern(self.unit_name.as_ref().unwrap());
759                self.import_stubs
760                    .iter()
761                    .filter(|(_, stub)| stub.parents().contains(&library_name))
762                    .map(|(name, _)| (*name, Span::default()))
763                    .collect()
764            }
765        };
766
767        // Initialize the exploration queue with the root’s direct imports.
768        let mut to_explore: Vec<(Symbol, Span)> = initial_imports.iter().map(|(sym, span)| (*sym, *span)).collect();
769
770        // If this is a named program, set the main program as the parent of its direct imports.
771        if let Some(main_program_name) = self.unit_name.clone() {
772            let main_symbol = Symbol::intern(&main_program_name);
773            for import in initial_imports.keys() {
774                if let Some(child_stub) = self.import_stubs.get_mut(import) {
775                    child_stub.add_parent(main_symbol);
776                }
777            }
778        }
779
780        // Traverse the dependency graph breadth-first, populating parents
781        while let Some((import_symbol, span)) = to_explore.pop() {
782            // Mark this import as explored.
783            explored.insert(import_symbol);
784
785            // Look up the corresponding stub.
786            let Some(stub) = self.import_stubs.get(&import_symbol) else {
787                return Err(crate::errors::imported_program_not_found(
788                    self.unit_name.as_ref().unwrap(),
789                    import_symbol,
790                    span,
791                )
792                .into());
793            };
794
795            // Combine imports: explicit stub.explicit_imports() + libraries that list this stub as parent
796            let mut combined_imports: IndexMap<Symbol, Span> = stub.explicit_imports().collect();
797            for (lib_name, lib_stub) in &self.import_stubs {
798                if matches!(lib_stub, Stub::FromLibrary { .. }) && lib_stub.parents().contains(&import_symbol) {
799                    combined_imports.insert(
800                        *lib_name,
801                        Span::default(), // library dependencies are implicit
802                    );
803                }
804            }
805
806            for (child_symbol, child_span) in combined_imports {
807                // Record parent relationship
808                if let Some(child_stub) = self.import_stubs.get_mut(&child_symbol) {
809                    child_stub.add_parent(import_symbol);
810                }
811
812                // Schedule child for exploration if not yet visited.
813                if explored.insert(child_symbol) {
814                    to_explore.push((child_symbol, child_span));
815                }
816            }
817        }
818
819        // Collect all reachable stubs and store them on the AST.
820        let reachable: IndexMap<Symbol, Stub> = self
821            .import_stubs
822            .iter()
823            .filter(|(symbol, _)| explored.contains(*symbol))
824            .map(|(symbol, stub)| (*symbol, stub.clone()))
825            .collect();
826        match &mut self.state.ast {
827            Ast::Program(program) => program.stubs = reachable,
828            Ast::Library(library) => library.stubs = reachable,
829        }
830
831        Ok(())
832    }
833
834    /// Builds a library: parses the source, resolves import stubs, and runs all frontend passes.
835    ///
836    /// Unlike [`Self::compile`], this does not run monomorphisation, lowerings, or code generation.
837    /// No bytecode is produced. Returns the validated library AST, which callers can convert into
838    /// a [`Stub`] for downstream units in the same build graph.
839    pub fn build_library(
840        &mut self,
841        library_name: Symbol,
842        source: &str,
843        filename: FileName,
844        modules: &[(&str, FileName)],
845    ) -> Result<Library> {
846        self.parse_library(library_name, source, filename, modules)?;
847        self.add_import_stubs()?;
848        self.frontend_passes()?;
849
850        match &self.state.ast {
851            Ast::Library(library) => Ok(library.clone()),
852            Ast::Program(_) => unreachable!("expected Library AST"),
853        }
854    }
855
856    /// Builds a library from a source file and its associated module files in the same directory tree.
857    pub fn build_library_from_directory(
858        &mut self,
859        library_name: Symbol,
860        entry_file_path: impl AsRef<Path>,
861        source_directory: impl AsRef<Path>,
862    ) -> Result<Library> {
863        self.build_library_from_directory_with_file_source(
864            library_name,
865            entry_file_path,
866            source_directory,
867            &DiskFileSource,
868        )
869    }
870
871    /// Builds a library from a source file using the given file source.
872    pub fn build_library_from_directory_with_file_source(
873        &mut self,
874        library_name: Symbol,
875        entry_file_path: impl AsRef<Path>,
876        source_directory: impl AsRef<Path>,
877        file_source: &impl FileSource,
878    ) -> Result<Library> {
879        let (source, modules_owned) = Self::read_sources_and_modules(file_source, &entry_file_path, &source_directory)?;
880
881        let module_refs: Vec<(&str, FileName)> =
882            modules_owned.iter().map(|(src, fname)| (src.as_str(), fname.clone())).collect();
883
884        self.build_library(library_name, &source, FileName::Real(entry_file_path.as_ref().into()), &module_refs)
885    }
886}
887
888/// Loads only locally resolvable dependency stubs for a package.
889///
890/// The LSP should not fetch or install dependencies while the user is typing, so
891/// this helper walks the local manifest tree, builds stubs for local packages and
892/// checked-in `.aleo` files, and silently skips network-only dependencies.
893///
894/// The returned `watch_paths` cover the manifests and source files that can
895/// change the stub set. Editor caches can hash or stat those paths to know when
896/// dependency-backed semantic state must be rebuilt.
897pub fn load_import_stubs_for_package(package_root: &Path, network: NetworkName) -> Result<LoadedImportStubs> {
898    load_import_stubs_for_package_with_file_source(package_root, network, &DiskFileSource)
899}
900
901/// Load local dependency stubs using an explicit file source for Leo source reads.
902///
903/// This variant lets editor integrations serve unsaved overlays and record the
904/// exact disk bytes used for dependency source stubs. Manifest discovery still
905/// reads the real filesystem because dependencies are package-level metadata,
906/// but every parsed Leo source file flows through `file_source`.
907pub fn load_import_stubs_for_package_with_file_source(
908    package_root: &Path,
909    network: NetworkName,
910    file_source: &impl FileSource,
911) -> Result<LoadedImportStubs> {
912    create_session_if_not_set_then(|_| {
913        let package_root =
914            package_root.canonicalize().map_err(|error| crate::errors::failed_path(package_root.display(), error))?;
915        let declared_dependencies = collect_local_declared_dependencies(&package_root)?;
916        let mut import_stubs = IndexMap::new();
917        let mut watch_paths = vec![package_root.join(MANIFEST_FILENAME)];
918
919        for (name, dependency) in &declared_dependencies {
920            let Some(path) = dependency.path.as_ref() else {
921                continue;
922            };
923
924            let unit = if path.extension().is_some_and(|extension| extension == "aleo") && path.is_file() {
925                watch_paths.push(path.clone());
926                CompilationUnit::from_aleo_path(*name, path, &declared_dependencies)?
927            } else {
928                let unit = CompilationUnit::from_package_path(*name, path)?;
929                watch_paths.extend(unit_watch_paths(&unit, file_source)?);
930                unit
931            };
932
933            let stub = match &unit.data {
934                ProgramData::Bytecode(bytecode) => disassemble_dependency_bytecode(unit.name, bytecode, network)?,
935                ProgramData::SourcePath { directory, source } => load_source_dependency_stub(
936                    &unit,
937                    source,
938                    dependency_source_directory(directory, source),
939                    network,
940                    file_source,
941                )?,
942            };
943            import_stubs.insert(unit.name, stub);
944        }
945
946        watch_paths.sort();
947        watch_paths.dedup();
948
949        Ok(LoadedImportStubs { stubs: import_stubs, watch_paths })
950    })
951}
952
953/// Return the directory root the parser should scan for sibling Leo modules.
954fn dependency_source_directory(directory: &Path, source: &Path) -> PathBuf {
955    let source_root = directory.join("src");
956    if source.starts_with(&source_root) { source_root } else { directory.to_path_buf() }
957}
958
959/// Collect the transitive set of manifest-declared local dependencies.
960///
961/// Network dependencies are intentionally excluded here because editor semantic
962/// analysis must stay local-only.
963fn collect_local_declared_dependencies(package_root: &Path) -> Result<IndexMap<Symbol, Dependency>> {
964    let manifest = Manifest::read_from_file(package_root.join(MANIFEST_FILENAME))?;
965    let mut declared = IndexMap::new();
966    collect_local_declared_dependencies_recursive(package_root, &manifest, &mut declared)?;
967    Ok(declared)
968}
969
970/// Walk local manifests recursively and record each dependency once.
971fn collect_local_declared_dependencies_recursive(
972    base_path: &Path,
973    manifest: &Manifest,
974    declared: &mut IndexMap<Symbol, Dependency>,
975) -> Result<()> {
976    for dependency in manifest.dependencies.iter().flatten() {
977        let dependency = normalize_local_dependency(base_path, dependency.clone())?;
978        // Resolve workspace deps early - converts to Location::Local with an absolute path.
979        let dependency = if dependency.location == Location::Workspace {
980            resolve_workspace_dependency(base_path, dependency)?
981        } else {
982            dependency
983        };
984        if dependency.location != Location::Local {
985            continue;
986        }
987
988        let Some(path) = dependency.path.as_ref() else {
989            continue;
990        };
991        let symbol = Symbol::intern(&dependency.name);
992
993        match declared.entry(symbol) {
994            Entry::Occupied(_) => continue,
995            Entry::Vacant(entry) => {
996                entry.insert(dependency.clone());
997                let manifest_path = path.join(MANIFEST_FILENAME);
998                if path.is_dir() && manifest_path.is_file() {
999                    let child = Manifest::read_from_file(manifest_path)?;
1000                    collect_local_declared_dependencies_recursive(path, &child, declared)?;
1001                }
1002            }
1003        }
1004    }
1005
1006    Ok(())
1007}
1008
1009/// Canonicalize a local dependency path relative to the manifest that declared it.
1010fn normalize_local_dependency(base_path: &Path, mut dependency: Dependency) -> Result<Dependency> {
1011    if let Some(path) = dependency.path.as_mut()
1012        && !path.is_absolute()
1013    {
1014        let joined = base_path.join(&*path);
1015        *path = joined.canonicalize().map_err(|error| crate::errors::failed_path(joined.display(), error))?;
1016    }
1017
1018    Ok(dependency)
1019}
1020
1021/// Return the manifest and source files whose metadata should invalidate one stubbed unit.
1022fn unit_watch_paths(unit: &CompilationUnit, file_source: &impl FileSource) -> Result<Vec<PathBuf>> {
1023    let ProgramData::SourcePath { directory, source } = &unit.data else {
1024        return Ok(Vec::new());
1025    };
1026
1027    let source_directory = dependency_source_directory(directory, source);
1028    let mut watch_paths = vec![directory.join(MANIFEST_FILENAME), source_directory.clone(), source.clone()];
1029    if source_directory.is_dir() {
1030        collect_source_directories(&source_directory, &mut watch_paths)?;
1031        let mut modules = file_source
1032            .list_leo_files(&source_directory, source)
1033            .map_err(|error| crate::errors::file_read_error(source_directory.display().to_string(), error))?;
1034        watch_paths.append(&mut modules);
1035    }
1036
1037    Ok(watch_paths)
1038}
1039
1040/// Collect source directories whose mtimes signal nested module creation/removal.
1041fn collect_source_directories(dir: &Path, watch_paths: &mut Vec<PathBuf>) -> Result<()> {
1042    for entry in fs::read_dir(dir).map_err(|error| errors::file_read_error(dir.display().to_string(), error))? {
1043        let entry = entry.map_err(|error| errors::file_read_error(dir.display().to_string(), error))?;
1044        let path = entry.path();
1045        if path.is_dir() {
1046            // Watching only existing `.leo` files misses the first file added to
1047            // an already-existing nested module directory. Include directories
1048            // so LSP-side cache revisions notice those create/remove events.
1049            watch_paths.push(path.clone());
1050            collect_source_directories(&path, watch_paths)?;
1051        }
1052    }
1053    Ok(())
1054}
1055
1056/// Parse a local dependency just far enough to recover the public interface
1057/// stub consumed by downstream import resolution.
1058fn load_source_dependency_stub(
1059    unit: &CompilationUnit,
1060    source: &Path,
1061    source_directory: PathBuf,
1062    network: NetworkName,
1063    file_source: &impl FileSource,
1064) -> Result<Stub> {
1065    let handler = Handler::default();
1066    let node_builder = Rc::new(NodeBuilder::default());
1067    let mut compiler = Compiler::new(
1068        Some(unit.name.to_string()),
1069        false,
1070        handler,
1071        node_builder,
1072        PathBuf::default(),
1073        Some(CompilerOptions::default()),
1074        IndexMap::new(),
1075        network,
1076    );
1077
1078    match unit.kind {
1079        PackageKind::Library => {
1080            let library_name = Symbol::intern(&unit.name.to_string());
1081            let library = compiler.parse_library_from_directory_with_file_source(
1082                library_name,
1083                source,
1084                &source_directory,
1085                file_source,
1086            )?;
1087            Ok(library.into())
1088        }
1089        PackageKind::Program | PackageKind::Test => {
1090            let program =
1091                compiler.parse_program_from_directory_with_file_source(source, &source_directory, file_source)?;
1092            Ok(extract_program_interface_stub(unit.name, &program))
1093        }
1094    }
1095}
1096
1097/// Build the public interface stub for a source dependency program.
1098fn extract_program_interface_stub(_program_name: Symbol, program: &Program) -> Stub {
1099    let scope = program.program_scopes.values().next().expect("program AST should contain one program scope");
1100
1101    // Source dependencies contribute only their public interface to the import
1102    // graph. Build the same stub shape we would get from disassembled bytecode
1103    // so downstream passes and the LSP can treat source and bytecode imports
1104    // uniformly.
1105    let functions = scope
1106        .functions
1107        .iter()
1108        .map(|(sym, func)| {
1109            (*sym, FunctionStub {
1110                annotations: func.annotations.clone(),
1111                variant: func.variant,
1112                identifier: func.identifier,
1113                input: func.input.clone(),
1114                output: func.output.clone(),
1115                output_type: func.output_type.clone(),
1116                span: func.span,
1117                id: func.id,
1118            })
1119        })
1120        .collect();
1121
1122    let imports = program
1123        .imports
1124        .keys()
1125        .map(|sym| {
1126            let sym_str = sym.to_string();
1127            // Import stubs track bare program names and always use the `aleo`
1128            // network identifier, matching the normalized form produced by the
1129            // bytecode disassembler.
1130            let name_only = sym_str.strip_suffix(".aleo").unwrap_or(&sym_str);
1131            ProgramId {
1132                name: Identifier { name: Symbol::intern(name_only), span: Default::default(), id: Default::default() },
1133                network: Identifier { name: Symbol::intern("aleo"), span: Default::default(), id: Default::default() },
1134            }
1135        })
1136        .collect();
1137
1138    AleoProgram {
1139        imports,
1140        stub_id: scope.program_id,
1141        consts: scope.consts.clone(),
1142        composites: scope.composites.clone(),
1143        mappings: scope.mappings.clone(),
1144        functions,
1145        span: scope.span,
1146    }
1147    .into()
1148}
1149
1150/// Convert checked-in dependency bytecode into the same stub shape used for
1151/// source dependencies so import consumers can stay agnostic to how a
1152/// dependency was declared.
1153fn disassemble_dependency_bytecode(program_name: Symbol, bytecode: &str, network: NetworkName) -> Result<Stub> {
1154    let disassembled = match network {
1155        NetworkName::MainnetV0 => {
1156            leo_disassembler::disassemble_from_str_unchecked::<snarkvm::prelude::MainnetV0>(program_name, bytecode)
1157        }
1158        NetworkName::TestnetV0 => {
1159            leo_disassembler::disassemble_from_str_unchecked::<snarkvm::prelude::TestnetV0>(program_name, bytecode)
1160        }
1161        NetworkName::CanaryV0 => {
1162            leo_disassembler::disassemble_from_str_unchecked::<snarkvm::prelude::CanaryV0>(program_name, bytecode)
1163        }
1164    };
1165
1166    disassembled
1167        .map(Into::into)
1168        .map_err(|err| crate::errors::file_read_error(format!("dependency bytecode for `{program_name}`"), err).into())
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173    use super::Compiler;
1174
1175    use leo_ast::{NetworkName, NodeBuilder};
1176    use leo_errors::{BufferEmitter, Handler};
1177    use leo_span::{Symbol, create_session_if_not_set_then, file_source::InMemoryFileSource};
1178
1179    use std::{path::PathBuf, rc::Rc};
1180
1181    use indexmap::IndexMap;
1182
1183    /// Verifies library parsing can read every source file from an in-memory source.
1184    #[test]
1185    fn parse_library_from_directory_in_memory() {
1186        create_session_if_not_set_then(|_| {
1187            let mut source = InMemoryFileSource::new();
1188            source.set(
1189                PathBuf::from("/mylib/src/lib.leo"),
1190                concat!("const SCALE: u32 = 10u32;\n", "const OFFSET: u32 = SCALE + 1u32;\n",).into(),
1191            );
1192
1193            let handler = Handler::default();
1194            let node_builder = Rc::new(NodeBuilder::default());
1195            let mut compiler = Compiler::new(
1196                None,
1197                false,
1198                handler,
1199                node_builder,
1200                PathBuf::from("/unused"),
1201                None,
1202                IndexMap::new(),
1203                NetworkName::TestnetV0,
1204            );
1205
1206            let library = compiler
1207                .parse_library_from_directory_with_file_source(
1208                    Symbol::intern("mylib"),
1209                    "/mylib/src/lib.leo",
1210                    "/mylib/src",
1211                    &source,
1212                )
1213                .unwrap_or_else(|err| panic!("parsing library from in-memory file source failed: {err}"));
1214
1215            assert_eq!(library.name, Symbol::intern("mylib"));
1216            assert_eq!(library.consts.len(), 2, "expected 2 consts, got {}", library.consts.len());
1217            assert!(
1218                library.consts.iter().any(|(name, _)| *name == Symbol::intern("SCALE")),
1219                "expected const `SCALE` in library"
1220            );
1221            assert!(
1222                library.consts.iter().any(|(name, _)| *name == Symbol::intern("OFFSET")),
1223                "expected const `OFFSET` in library"
1224            );
1225        });
1226    }
1227
1228    /// Verifies in-memory library builds still reject type errors.
1229    #[test]
1230    fn build_library_from_directory_in_memory_rejects_type_error() {
1231        create_session_if_not_set_then(|_| {
1232            let mut source = InMemoryFileSource::new();
1233            // `true + 1u32` must be rejected by type checking.
1234            source
1235                .set(PathBuf::from("/badlib/src/lib.leo"), "fn broken() -> u32 {\n    return true + 1u32;\n}\n".into());
1236
1237            // Capture errors in a buffer so the test can inspect them without writing to stderr.
1238            let emitter = BufferEmitter::new();
1239            let handler = Handler::new(emitter.clone());
1240            let node_builder = Rc::new(NodeBuilder::default());
1241            let mut compiler = Compiler::new(
1242                Some("badlib".into()),
1243                false,
1244                handler,
1245                node_builder,
1246                PathBuf::from("/unused"),
1247                None,
1248                IndexMap::new(),
1249                NetworkName::TestnetV0,
1250            );
1251
1252            let result = compiler.build_library_from_directory_with_file_source(
1253                Symbol::intern("badlib"),
1254                "/badlib/src/lib.leo",
1255                "/badlib/src",
1256                &source,
1257            );
1258
1259            assert!(result.is_err(), "expected build_library to fail on a library with a type error");
1260
1261            let errors = emitter.extract_errs().to_string();
1262            assert!(errors.contains("ETYC"), "expected a type-checking error (prefix `ETYC`) but captured:\n{errors}");
1263        });
1264    }
1265
1266    /// Verifies in-memory program parsing can load sibling modules.
1267    #[test]
1268    fn parse_program_from_directory_in_memory_with_module() {
1269        create_session_if_not_set_then(|_| {
1270            let mut source = InMemoryFileSource::new();
1271            source.set(
1272                PathBuf::from("/project/src/main.leo"),
1273                concat!(
1274                    "program test.aleo {\n",
1275                    "  fn main() -> u32 {\n",
1276                    "    return utils::helper();\n",
1277                    "  }\n",
1278                    "}\n",
1279                )
1280                .into(),
1281            );
1282            source.set(PathBuf::from("/project/src/utils.leo"), "fn helper() -> u32 {\n  return 42u32;\n}\n".into());
1283
1284            let handler = Handler::default();
1285            let node_builder = Rc::new(NodeBuilder::default());
1286            let mut compiler = Compiler::new(
1287                Some("test.aleo".into()),
1288                false,
1289                handler,
1290                node_builder,
1291                PathBuf::from("/unused"),
1292                None,
1293                IndexMap::new(),
1294                NetworkName::TestnetV0,
1295            );
1296
1297            let ast = compiler
1298                .parse_program_from_directory_with_file_source("/project/src/main.leo", "/project/src", &source)
1299                .unwrap_or_else(|err| panic!("parsing from in-memory file source failed: {err}"));
1300            let utils_key = vec![Symbol::intern("utils")];
1301
1302            assert!(
1303                ast.modules.contains_key(&utils_key),
1304                "module `utils` should be loaded from the in-memory file source; found keys: {:?}",
1305                ast.modules.keys().collect::<Vec<_>>()
1306            );
1307        });
1308    }
1309}