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