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