Skip to content

Commit c36aaa7

Browse files
authored
feat: add file source abstraction for in-memory compilation (#29215)
* feat: add file source abstraction for in-memory compilation * fix(cli): compile test files from their directory
1 parent 4c4824f commit c36aaa7

9 files changed

Lines changed: 356 additions & 43 deletions

File tree

crates/compiler/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ rand_chacha = { workspace = true }
3838
rayon = { workspace = true }
3939
snarkvm = { workspace = true }
4040
serde_json = { workspace = true }
41-
walkdir = { workspace = true }
4241

4342
[dev-dependencies]
4443
# leo dependencies

crates/compiler/src/compiler.rs

Lines changed: 102 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,20 @@ pub use leo_ast::{Ast, Program};
2424
use leo_ast::{NetworkName, NodeBuilder, Stub};
2525
use leo_errors::{CompilerError, Handler, Result};
2626
use leo_passes::*;
27-
use leo_span::{Symbol, source_map::FileName, with_session_globals};
27+
use leo_span::{
28+
Symbol,
29+
file_source::{DiskFileSource, FileSource},
30+
source_map::FileName,
31+
with_session_globals,
32+
};
2833

2934
use std::{
30-
ffi::OsStr,
3135
fs,
3236
path::{Path, PathBuf},
3337
rc::Rc,
3438
};
3539

3640
use indexmap::{IndexMap, IndexSet};
37-
use walkdir::WalkDir;
3841

3942
/// A single compiled program with its bytecode and ABI.
4043
pub struct CompiledProgram {
@@ -336,30 +339,28 @@ impl Compiler {
336339
///
337340
/// Returns `Err(CompilerError)` if reading any file fails.
338341
fn read_sources_and_modules(
342+
file_source: &impl FileSource,
339343
entry_file_path: impl AsRef<Path>,
340344
source_directory: impl AsRef<Path>,
341345
) -> Result<(String, Vec<(String, FileName)>)> {
342-
// Read the contents of the main source file.
343-
let source = fs::read_to_string(&entry_file_path)
344-
.map_err(|e| CompilerError::file_read_error(entry_file_path.as_ref().display().to_string(), e))?;
345-
346-
// Walk all files under source_directory recursively, excluding the main source file itself.
347-
let files = WalkDir::new(source_directory)
348-
.into_iter()
349-
.filter_map(Result::ok)
350-
.filter(|e| {
351-
e.file_type().is_file()
352-
&& e.path() != entry_file_path.as_ref()
353-
&& e.path().extension() == Some(OsStr::new("leo"))
354-
})
355-
.collect::<Vec<_>>();
346+
let entry_file_path = entry_file_path.as_ref();
347+
let source_directory = source_directory.as_ref();
356348

357-
// Read all module files and pair with FileName immediately
358-
let mut modules = Vec::new();
359-
for file in &files {
360-
let module_source = fs::read_to_string(file.path())
361-
.map_err(|e| CompilerError::file_read_error(file.path().display().to_string(), e))?;
362-
modules.push((module_source, FileName::Real(file.path().into())));
349+
// Read the contents of the main source file.
350+
let source = file_source
351+
.read_file(entry_file_path)
352+
.map_err(|e| CompilerError::file_read_error(entry_file_path.display().to_string(), e))?;
353+
354+
let files = file_source
355+
.list_leo_files(source_directory, entry_file_path)
356+
.map_err(|e| CompilerError::file_read_error(source_directory.display().to_string(), e))?;
357+
358+
let mut modules = Vec::with_capacity(files.len());
359+
for path in files {
360+
let module_source = file_source
361+
.read_file(&path)
362+
.map_err(|e| CompilerError::file_read_error(path.display().to_string(), e))?;
363+
modules.push((module_source, FileName::Real(path)));
363364
}
364365

365366
Ok((source, modules))
@@ -371,7 +372,17 @@ impl Compiler {
371372
entry_file_path: impl AsRef<Path>,
372373
source_directory: impl AsRef<Path>,
373374
) -> Result<Compiled> {
374-
let (source, modules_owned) = Self::read_sources_and_modules(&entry_file_path, &source_directory)?;
375+
self.compile_from_directory_with_file_source(entry_file_path, source_directory, &DiskFileSource)
376+
}
377+
378+
/// Compiles a program from a source file using the given file source.
379+
pub fn compile_from_directory_with_file_source(
380+
&mut self,
381+
entry_file_path: impl AsRef<Path>,
382+
source_directory: impl AsRef<Path>,
383+
file_source: &impl FileSource,
384+
) -> Result<Compiled> {
385+
let (source, modules_owned) = Self::read_sources_and_modules(file_source, &entry_file_path, &source_directory)?;
375386

376387
// Convert owned module sources into temporary (&str, FileName) tuples.
377388
let module_refs: Vec<(&str, FileName)> =
@@ -387,7 +398,17 @@ impl Compiler {
387398
entry_file_path: impl AsRef<Path>,
388399
source_directory: impl AsRef<Path>,
389400
) -> Result<Program> {
390-
let (source, modules_owned) = Self::read_sources_and_modules(&entry_file_path, &source_directory)?;
401+
self.parse_from_directory_with_file_source(entry_file_path, source_directory, &DiskFileSource)
402+
}
403+
404+
/// Parses a program from a source file using the given file source.
405+
pub fn parse_from_directory_with_file_source(
406+
&mut self,
407+
entry_file_path: impl AsRef<Path>,
408+
source_directory: impl AsRef<Path>,
409+
file_source: &impl FileSource,
410+
) -> Result<Program> {
411+
let (source, modules_owned) = Self::read_sources_and_modules(file_source, &entry_file_path, &source_directory)?;
391412

392413
// Convert owned module sources into temporary (&str, FileName) tuples.
393414
let module_refs: Vec<(&str, FileName)> =
@@ -495,3 +516,59 @@ impl Compiler {
495516
Ok(())
496517
}
497518
}
519+
520+
#[cfg(test)]
521+
mod tests {
522+
use super::Compiler;
523+
524+
use leo_ast::{NetworkName, NodeBuilder};
525+
use leo_errors::Handler;
526+
use leo_span::{Symbol, create_session_if_not_set_then, file_source::InMemoryFileSource};
527+
528+
use std::{path::PathBuf, rc::Rc};
529+
530+
use indexmap::IndexMap;
531+
532+
#[test]
533+
fn parse_from_directory_in_memory_with_module() {
534+
create_session_if_not_set_then(|_| {
535+
let mut source = InMemoryFileSource::new();
536+
source.set(
537+
PathBuf::from("/project/src/main.leo"),
538+
concat!(
539+
"program test.aleo {\n",
540+
" fn main() -> u32 {\n",
541+
" return utils::helper();\n",
542+
" }\n",
543+
"}\n",
544+
)
545+
.into(),
546+
);
547+
source.set(PathBuf::from("/project/src/utils.leo"), "fn helper() -> u32 {\n return 42u32;\n}\n".into());
548+
549+
let handler = Handler::default();
550+
let node_builder = Rc::new(NodeBuilder::default());
551+
let mut compiler = Compiler::new(
552+
Some("test".into()),
553+
false,
554+
handler,
555+
node_builder,
556+
PathBuf::from("/unused"),
557+
None,
558+
IndexMap::new(),
559+
NetworkName::TestnetV0,
560+
);
561+
562+
let ast = compiler
563+
.parse_from_directory_with_file_source("/project/src/main.leo", "/project/src", &source)
564+
.unwrap_or_else(|err| panic!("parsing from in-memory file source failed: {err}"));
565+
let utils_key = vec![Symbol::intern("utils")];
566+
567+
assert!(
568+
ast.modules.contains_key(&utils_key),
569+
"module `utils` should be loaded from the in-memory file source; found keys: {:?}",
570+
ast.modules.keys().collect::<Vec<_>>()
571+
);
572+
});
573+
}
574+
}

crates/compiler/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub use options::*;
2727

2828
// Re-export types from leo_passes for convenience
2929
pub use leo_passes::{Bytecode, CompiledPrograms};
30+
pub use leo_span::file_source::{DiskFileSource, FileSource, InMemoryFileSource};
3031

3132
pub mod run;
3233

crates/fmt/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ validate = [
2626
"dep:leo-compiler",
2727
"dep:leo-ast",
2828
"dep:leo-errors",
29-
"dep:leo-span",
3029
"dep:serde_json",
3130
]
3231

@@ -37,7 +36,7 @@ leo-ast = { workspace = true, optional = true }
3736
leo-compiler = { workspace = true, optional = true }
3837
leo-errors = { workspace = true, optional = true }
3938
leo-parser-rowan = { workspace = true }
40-
leo-span = { workspace = true, optional = true }
39+
leo-span = { workspace = true }
4140
serde_json = { workspace = true, optional = true }
4241
similar = { workspace = true }
4342
walkdir = { workspace = true }

crates/fmt/src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mod output;
3535
use clap::{Args, Parser};
3636
use colored::Colorize;
3737
use leo_parser_rowan::parse_main;
38+
use leo_span::file_source::{DiskFileSource, FileSource};
3839
use similar::{ChangeTag, TextDiff};
3940
use std::{
4041
fs,
@@ -81,12 +82,25 @@ pub struct LeoFmtCli {
8182

8283
/// Run filesystem formatting/checking for CLI callers.
8384
pub fn run_format_cli(args: &FormatCliArgs, base_dir: &Path) -> io::Result<bool> {
85+
run_format_with_file_source(args, base_dir, &DiskFileSource)
86+
}
87+
88+
/// Run formatting/checking using a custom file source for reading.
89+
///
90+
/// File discovery still uses the filesystem via [`collect_leo_files`]. The
91+
/// provided [`FileSource`] controls how file contents are read once the CLI
92+
/// paths have been resolved.
93+
pub fn run_format_with_file_source(
94+
args: &FormatCliArgs,
95+
base_dir: &Path,
96+
file_source: &impl FileSource,
97+
) -> io::Result<bool> {
8498
let paths = resolve_paths(&args.paths, base_dir);
8599
let leo_files = collect_leo_files(&paths)?;
86100
let mut has_unformatted = false;
87101

88102
for file_path in leo_files {
89-
let source = fs::read_to_string(&file_path).map_err(|error| {
103+
let source = file_source.read_file(&file_path).map_err(|error| {
90104
io::Error::new(error.kind(), format!("failed to read {}: {error}", file_path.display()))
91105
})?;
92106
let formatted = format_source(&source);

crates/leo/src/cli/commands/build.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,19 @@ fn handle_build(command: &LeoBuild, context: Context) -> Result<<LeoBuild as Com
189189

190190
leo_package::ProgramData::SourcePath { directory, source } => {
191191
// This is a local dependency, so we must compile or parse it.
192-
let source_dir = directory.join("src");
192+
let source_dir = if program.is_test {
193+
source
194+
.parent()
195+
.ok_or_else(|| {
196+
UtilError::failed_to_open_file(format_args!(
197+
"Failed to find directory for test {}",
198+
source.display()
199+
))
200+
})?
201+
.to_path_buf()
202+
} else {
203+
directory.join("src")
204+
};
193205

194206
if source == &main_source_path || program.is_test {
195207
// Compile the program (main or test).

0 commit comments

Comments
 (0)