diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs index 56b0bf09cd6..ea2a70ede8c 100644 --- a/compiler-core/src/ast.rs +++ b/compiler-core/src/ast.rs @@ -12,7 +12,7 @@ pub use self::untyped::{FunctionLiteralKind, UntypedExpr}; pub use self::constant::{Constant, TypedConstant, UntypedConstant}; use crate::analyse::Inferred; -use crate::build::{Located, Target}; +use crate::build::{Located, Target, module_erlang_name}; use crate::parse::SpannedString; use crate::type_::error::VariableOrigin; use crate::type_::expression::Implementations; @@ -52,6 +52,12 @@ pub struct Module { pub names: Names, } +impl Module { + pub fn erlang_name(&self) -> EcoString { + module_erlang_name(&self.name) + } +} + impl TypedModule { pub fn find_node(&self, byte_index: u32) -> Option> { self.definitions diff --git a/compiler-core/src/build.rs b/compiler-core/src/build.rs index 7eb7d37a466..9fb32ad9b15 100644 --- a/compiler-core/src/build.rs +++ b/compiler-core/src/build.rs @@ -246,10 +246,14 @@ pub struct Module { } impl Module { + pub fn erlang_name(&self) -> EcoString { + module_erlang_name(&self.name) + } + pub fn compiled_erlang_path(&self) -> Utf8PathBuf { - let mut path = self.name.replace("/", "@"); - path.push_str(".erl"); - Utf8PathBuf::from(path.as_ref()) + let mut path = Utf8PathBuf::from(&module_erlang_name(&self.name)); + assert!(path.set_extension("erl"), "Couldn't set file extension"); + path } pub fn is_test(&self) -> bool { @@ -321,6 +325,10 @@ impl Module { } } +pub fn module_erlang_name(gleam_name: &EcoString) -> EcoString { + gleam_name.replace("/", "@") +} + #[derive(Debug, Clone, PartialEq)] pub struct UnqualifiedImport<'a> { pub name: &'a EcoString, diff --git a/compiler-core/src/build/module_loader.rs b/compiler-core/src/build/module_loader.rs index 35f4453b663..e83e9f2c2eb 100644 --- a/compiler-core/src/build/module_loader.rs +++ b/compiler-core/src/build/module_loader.rs @@ -10,8 +10,8 @@ use serde::{Deserialize, Serialize}; use super::{ Mode, Origin, SourceFingerprint, Target, - package_compiler::{CacheMetadata, CachedModule, Input, UncompiledModule, module_name}, - package_loader::CodegenRequired, + package_compiler::{CacheMetadata, CachedModule, Input, UncompiledModule}, + package_loader::{CodegenRequired, GleamFile}, }; use crate::{ Error, Result, @@ -28,7 +28,6 @@ pub(crate) struct ModuleLoader<'a, IO> { pub target: Target, pub codegen: CodegenRequired, pub package_name: &'a EcoString, - pub source_directory: &'a Utf8Path, pub artefact_directory: &'a Utf8Path, pub origin: Origin, /// The set of modules that have had partial compilation done since the last @@ -48,14 +47,13 @@ where /// Whether the module has changed or not is determined by comparing the /// modification time of the source file with the value recorded in the /// `.timestamp` file in the artefact directory. - pub fn load(&self, path: Utf8PathBuf) -> Result { - let name = module_name(self.source_directory, &path); - let artefact = name.replace("/", "@"); - let source_mtime = self.io.modification_time(&path)?; + pub fn load(&self, file: GleamFile) -> Result { + let name = file.module_name.clone(); + let source_mtime = self.io.modification_time(&file.path)?; - let read_source = |name| self.read_source(path, name, source_mtime); + let read_source = |name| self.read_source(file.path.clone(), name, source_mtime); - let meta = match self.read_cache_metadata(&artefact)? { + let meta = match self.read_cache_metadata(&file)? { Some(meta) => meta, None => return read_source(name).map(Input::New), }; @@ -84,16 +82,13 @@ where } } - Ok(Input::Cached(self.cached(name, meta))) + Ok(Input::Cached(self.cached(file, meta))) } - /// Read the timestamp file from the artefact directory for the given - /// artefact slug. If the file does not exist, return `None`. - fn read_cache_metadata(&self, artefact: &str) -> Result> { - let meta_path = self - .artefact_directory - .join(artefact) - .with_extension("cache_meta"); + /// Read the cache metadata file from the artefact directory for the given + /// source file. If the file does not exist, return `None`. + fn read_cache_metadata(&self, source_file: &GleamFile) -> Result> { + let meta_path = source_file.cache_files(&self.artefact_directory).meta_path; if !self.io.is_file(&meta_path) { return Ok(None); @@ -129,12 +124,12 @@ where ) } - fn cached(&self, name: EcoString, meta: CacheMetadata) -> CachedModule { + fn cached(&self, file: GleamFile, meta: CacheMetadata) -> CachedModule { CachedModule { dependencies: meta.dependencies, - source_path: self.source_directory.join(format!("{}.gleam", name)), + source_path: file.path, origin: self.origin, - name, + name: file.module_name, line_numbers: meta.line_numbers, } } diff --git a/compiler-core/src/build/module_loader/tests.rs b/compiler-core/src/build/module_loader/tests.rs index c18f710d4ad..eeca162f57e 100644 --- a/compiler-core/src/build/module_loader/tests.rs +++ b/compiler-core/src/build/module_loader/tests.rs @@ -9,19 +9,17 @@ use std::time::Duration; #[test] fn no_cache_present() { let name = "package".into(); - let src = Utf8Path::new("/src"); let artefact = Utf8Path::new("/artefact"); let fs = InMemoryFileSystem::new(); let warnings = WarningEmitter::null(); let incomplete_modules = HashSet::new(); - let loader = make_loader(&warnings, &name, &fs, src, artefact, &incomplete_modules); + let loader = make_loader(&warnings, &name, &fs, artefact, &incomplete_modules); fs.write(&Utf8Path::new("/src/main.gleam"), "const x = 1") .unwrap(); - let result = loader - .load(Utf8Path::new("/src/main.gleam").to_path_buf()) - .unwrap(); + let file = GleamFile::new("/src".into(), "/src/main.gleam".into()); + let result = loader.load(file).unwrap(); assert!(result.is_new()); } @@ -29,20 +27,18 @@ fn no_cache_present() { #[test] fn cache_present_and_fresh() { let name = "package".into(); - let src = Utf8Path::new("/src"); let artefact = Utf8Path::new("/artefact"); let fs = InMemoryFileSystem::new(); let warnings = WarningEmitter::null(); let incomplete_modules = HashSet::new(); - let loader = make_loader(&warnings, &name, &fs, src, artefact, &incomplete_modules); + let loader = make_loader(&warnings, &name, &fs, artefact, &incomplete_modules); // The mtime of the source is older than that of the cache write_src(&fs, TEST_SOURCE_1, "/src/main.gleam", 0); write_cache(&fs, TEST_SOURCE_1, "/artefact/main.cache_meta", 1, false); - let result = loader - .load(Utf8Path::new("/src/main.gleam").to_path_buf()) - .unwrap(); + let file = GleamFile::new("/src".into(), "/src/main.gleam".into()); + let result = loader.load(file).unwrap(); assert!(result.is_cached()); } @@ -50,20 +46,18 @@ fn cache_present_and_fresh() { #[test] fn cache_present_and_stale() { let name = "package".into(); - let src = Utf8Path::new("/src"); let artefact = Utf8Path::new("/artefact"); let fs = InMemoryFileSystem::new(); let warnings = WarningEmitter::null(); let incomplete_modules = HashSet::new(); - let loader = make_loader(&warnings, &name, &fs, src, artefact, &incomplete_modules); + let loader = make_loader(&warnings, &name, &fs, artefact, &incomplete_modules); // The mtime of the source is newer than that of the cache write_src(&fs, TEST_SOURCE_2, "/src/main.gleam", 2); write_cache(&fs, TEST_SOURCE_1, "/artefact/main.cache_meta", 1, false); - let result = loader - .load(Utf8Path::new("/src/main.gleam").to_path_buf()) - .unwrap(); + let file = GleamFile::new("/src".into(), "/src/main.gleam".into()); + let result = loader.load(file).unwrap(); assert!(result.is_new()); } @@ -71,20 +65,18 @@ fn cache_present_and_stale() { #[test] fn cache_present_and_stale_but_source_is_the_same() { let name = "package".into(); - let src = Utf8Path::new("/src"); let artefact = Utf8Path::new("/artefact"); let fs = InMemoryFileSystem::new(); let warnings = WarningEmitter::null(); let incomplete_modules = HashSet::new(); - let loader = make_loader(&warnings, &name, &fs, src, artefact, &incomplete_modules); + let loader = make_loader(&warnings, &name, &fs, artefact, &incomplete_modules); // The mtime of the source is newer than that of the cache write_src(&fs, TEST_SOURCE_1, "/src/main.gleam", 2); write_cache(&fs, TEST_SOURCE_1, "/artefact/main.cache_meta", 1, false); - let result = loader - .load(Utf8Path::new("/src/main.gleam").to_path_buf()) - .unwrap(); + let file = GleamFile::new("/src".into(), "/src/main.gleam".into()); + let result = loader.load(file).unwrap(); assert!(result.is_cached()); } @@ -92,21 +84,19 @@ fn cache_present_and_stale_but_source_is_the_same() { #[test] fn cache_present_and_stale_source_is_the_same_lsp_mode() { let name = "package".into(); - let src = Utf8Path::new("/src"); let artefact = Utf8Path::new("/artefact"); let fs = InMemoryFileSystem::new(); let warnings = WarningEmitter::null(); let incomplete_modules = HashSet::new(); - let mut loader = make_loader(&warnings, &name, &fs, src, artefact, &incomplete_modules); + let mut loader = make_loader(&warnings, &name, &fs, artefact, &incomplete_modules); loader.mode = Mode::Lsp; // The mtime of the source is newer than that of the cache write_src(&fs, TEST_SOURCE_1, "/src/main.gleam", 2); write_cache(&fs, TEST_SOURCE_1, "/artefact/main.cache_meta", 1, false); - let result = loader - .load(Utf8Path::new("/src/main.gleam").to_path_buf()) - .unwrap(); + let file = GleamFile::new("/src".into(), "/src/main.gleam".into()); + let result = loader.load(file).unwrap(); assert!(result.is_cached()); } @@ -114,22 +104,20 @@ fn cache_present_and_stale_source_is_the_same_lsp_mode() { #[test] fn cache_present_and_stale_source_is_the_same_lsp_mode_and_invalidated() { let name = "package".into(); - let src = Utf8Path::new("/src"); let artefact = Utf8Path::new("/artefact"); let fs = InMemoryFileSystem::new(); let warnings = WarningEmitter::null(); let mut incomplete_modules = HashSet::new(); let _ = incomplete_modules.insert("main".into()); - let mut loader = make_loader(&warnings, &name, &fs, src, artefact, &incomplete_modules); + let mut loader = make_loader(&warnings, &name, &fs, artefact, &incomplete_modules); loader.mode = Mode::Lsp; // The mtime of the source is newer than that of the cache write_src(&fs, TEST_SOURCE_1, "/src/main.gleam", 2); write_cache(&fs, TEST_SOURCE_1, "/artefact/main.cache_meta", 1, false); - let result = loader - .load(Utf8Path::new("/src/main.gleam").to_path_buf()) - .unwrap(); + let file = GleamFile::new("/src".into(), "/src/main.gleam".into()); + let result = loader.load(file).unwrap(); assert!(result.is_new()); } @@ -137,21 +125,19 @@ fn cache_present_and_stale_source_is_the_same_lsp_mode_and_invalidated() { #[test] fn cache_present_without_codegen_when_required() { let name = "package".into(); - let src = Utf8Path::new("/src"); let artefact = Utf8Path::new("/artefact"); let fs = InMemoryFileSystem::new(); let warnings = WarningEmitter::null(); let incomplete_modules = HashSet::new(); - let mut loader = make_loader(&warnings, &name, &fs, src, artefact, &incomplete_modules); + let mut loader = make_loader(&warnings, &name, &fs, artefact, &incomplete_modules); loader.codegen = CodegenRequired::Yes; // The mtime of the cache is newer than that of the source write_src(&fs, TEST_SOURCE_1, "/src/main.gleam", 0); write_cache(&fs, TEST_SOURCE_1, "/artefact/main.cache_meta", 1, false); - let result = loader - .load(Utf8Path::new("/src/main.gleam").to_path_buf()) - .unwrap(); + let file = GleamFile::new("/src".into(), "/src/main.gleam".into()); + let result = loader.load(file).unwrap(); assert!(result.is_new()); } @@ -159,21 +145,19 @@ fn cache_present_without_codegen_when_required() { #[test] fn cache_present_with_codegen_when_required() { let name = "package".into(); - let src = Utf8Path::new("/src"); let artefact = Utf8Path::new("/artefact"); let fs = InMemoryFileSystem::new(); let warnings = WarningEmitter::null(); let incomplete_modules = HashSet::new(); - let mut loader = make_loader(&warnings, &name, &fs, src, artefact, &incomplete_modules); + let mut loader = make_loader(&warnings, &name, &fs, artefact, &incomplete_modules); loader.codegen = CodegenRequired::Yes; // The mtime of the cache is newer than that of the source write_src(&fs, TEST_SOURCE_1, "/src/main.gleam", 0); write_cache(&fs, TEST_SOURCE_1, "/artefact/main.cache_meta", 1, true); - let result = loader - .load(Utf8Path::new("/src/main.gleam").to_path_buf()) - .unwrap(); + let file = GleamFile::new("/src".into(), "/src/main.gleam".into()); + let result = loader.load(file).unwrap(); assert!(result.is_cached()); } @@ -181,21 +165,19 @@ fn cache_present_with_codegen_when_required() { #[test] fn cache_present_without_codegen_when_not_required() { let name = "package".into(); - let src = Utf8Path::new("/src"); let artefact = Utf8Path::new("/artefact"); let fs = InMemoryFileSystem::new(); let warnings = WarningEmitter::null(); let incomplete_modules = HashSet::new(); - let mut loader = make_loader(&warnings, &name, &fs, src, artefact, &incomplete_modules); + let mut loader = make_loader(&warnings, &name, &fs, artefact, &incomplete_modules); loader.codegen = CodegenRequired::No; // The mtime of the cache is newer than that of the source write_src(&fs, TEST_SOURCE_1, "/src/main.gleam", 0); write_cache(&fs, TEST_SOURCE_1, "/artefact/main.cache_meta", 1, false); - let result = loader - .load(Utf8Path::new("/src/main.gleam").to_path_buf()) - .unwrap(); + let file = GleamFile::new("/src".into(), "/src/main.gleam".into()); + let result = loader.load(file).unwrap(); assert!(result.is_cached()); } @@ -232,7 +214,6 @@ fn make_loader<'a>( warnings: &'a WarningEmitter, package_name: &'a EcoString, fs: &InMemoryFileSystem, - src: &'a Utf8Path, artefact: &'a Utf8Path, incomplete_modules: &'a HashSet, ) -> ModuleLoader<'a, InMemoryFileSystem> { @@ -243,7 +224,6 @@ fn make_loader<'a>( target: Target::Erlang, codegen: CodegenRequired::No, package_name, - source_directory: &src, artefact_directory: &artefact, origin: Origin::Src, incomplete_modules, diff --git a/compiler-core/src/build/package_compiler.rs b/compiler-core/src/build/package_compiler.rs index 326e8786832..c2eb927a562 100644 --- a/compiler-core/src/build/package_compiler.rs +++ b/compiler-core/src/build/package_compiler.rs @@ -1,4 +1,6 @@ use crate::analyse::{ModuleAnalyzerConstructor, TargetSupport}; +use crate::build::package_loader::CacheFiles; +use crate::io::files_with_extension; use crate::line_numbers::{self, LineNumbers}; use crate::type_::PRELUDE_MODULE_NAME; use crate::{ @@ -285,17 +287,13 @@ where tracing::debug!("writing_module_caches"); for module in modules { - let module_name = module.name.replace("/", "@"); + let cache_files = CacheFiles::new(&artefact_dir, &module.name); - // Write metadata file - let name = format!("{}.cache", &module_name); - let path = artefact_dir.join(name); + // Write cache file let bytes = ModuleEncoder::new(&module.ast.type_info).encode()?; - self.io.write_bytes(&path, &bytes)?; + self.io.write_bytes(&cache_files.cache_path, &bytes)?; - // Write cache info - let name = format!("{}.cache_meta", &module_name); - let path = artefact_dir.join(name); + // Write cache metadata let info = CacheMetadata { mtime: module.mtime, codegen_performed: self.perform_codegen, @@ -303,18 +301,17 @@ where fingerprint: SourceFingerprint::new(&module.code), line_numbers: module.ast.type_info.line_numbers.clone(), }; - self.io.write_bytes(&path, &info.to_binary())?; + self.io + .write_bytes(&cache_files.meta_path, &info.to_binary())?; // Write warnings. // Dependency packages don't get warnings persisted as the // programmer doesn't want to be told every time about warnings they // cannot fix directly. if self.cached_warnings.should_use() { - let name = format!("{}.cache_warnings", &module_name); - let path = artefact_dir.join(name); let warnings = &module.ast.type_info.warnings; let data = bincode::serialize(warnings).expect("Serialise warnings"); - self.io.write_bytes(&path, &data)?; + self.io.write_bytes(&cache_files.warnings_path, &data)?; } } Ok(()) @@ -593,25 +590,6 @@ fn analyse( Outcome::Ok(modules) } -pub(crate) fn module_name(package_path: &Utf8Path, full_module_path: &Utf8Path) -> EcoString { - // /path/to/project/_build/default/lib/the_package/src/my/module.gleam - - // my/module.gleam - let mut module_path = full_module_path - .strip_prefix(package_path) - .expect("Stripping package prefix from module path") - .to_path_buf(); - - // my/module - let _ = module_path.set_extension(""); - - // Stringify - let name = module_path.to_string(); - - // normalise windows paths - name.replace("\\", "/").into() -} - #[derive(Debug)] pub(crate) enum Input { New(UncompiledModule), diff --git a/compiler-core/src/build/package_loader.rs b/compiler-core/src/build/package_loader.rs index af22b9cc9f3..b40221e4e50 100644 --- a/compiler-core/src/build/package_loader.rs +++ b/compiler-core/src/build/package_loader.rs @@ -17,13 +17,11 @@ use vec1::Vec1; use crate::{ Error, Result, ast::SrcSpan, - build::{Module, Origin, module_loader::ModuleLoader, package_compiler::module_name}, + build::{Module, Origin, module_loader::ModuleLoader}, config::PackageConfig, dep_tree, error::{FileIoAction, FileKind, ImportCycleLocationDetails}, - io::{ - CommandExecutor, FileSystemReader, FileSystemWriter, gleam_cache_files, gleam_source_files, - }, + io::{self, CommandExecutor, FileSystemReader, FileSystemWriter, files_with_extension}, metadata, type_, uid::UniqueIdGenerator, warning::WarningEmitter, @@ -112,10 +110,13 @@ where // which should be loaded. let mut inputs = self.read_sources_and_caches()?; - // Check for any removed modules, by looking at cache files that don't exist in inputs - for cache_file in gleam_cache_files(&self.io, &self.artefact_directory) { - let module = module_name(&self.artefact_directory, &cache_file); + // Check for any removed modules, by looking at cache files that don't exist in inputs. + // Delete the cache files for removed modules and mark them as stale + // to trigger refreshing dependent modules. + for module in CacheFiles::modules_with_meta_files(&self.io, &self.artefact_directory) { if (!inputs.contains_key(&module)) { + tracing::debug!(%module, "module_removed"); + CacheFiles::new(&self.artefact_directory, &module).delete(&self.io); self.stale_modules.add(module); } } @@ -179,15 +180,13 @@ where } fn load_cached_module(&self, info: CachedModule) -> Result { - let dir = self.artefact_directory; - let name = info.name.replace("/", "@"); - let path = dir.join(name.as_ref()).with_extension("cache"); - let bytes = self.io.read_bytes(&path)?; + let cache_files = CacheFiles::new(&self.artefact_directory, &info.name); + let bytes = self.io.read_bytes(&cache_files.cache_path)?; let mut module = metadata::ModuleDecoder::new(self.ids.clone()).read(bytes.as_slice())?; // Load warnings if self.cached_warnings.should_use() { - let path = dir.join(name.as_ref()).with_extension("cache_warnings"); + let path = cache_files.warnings_path; if self.io.exists(&path) { let bytes = self.io.read_bytes(&path)?; module.warnings = bincode::deserialize(&bytes).map_err(|e| Error::FileIo { @@ -202,26 +201,6 @@ where Ok(module) } - pub fn is_gleam_path(&self, path: &Utf8Path, dir: &Utf8Path) -> bool { - use regex::Regex; - use std::cell::OnceCell; - const RE: OnceCell = OnceCell::new(); - - RE.get_or_init(|| { - Regex::new(&format!( - "^({module}{slash})*{module}\\.gleam$", - module = "[a-z][_a-z0-9]*", - slash = "(/|\\\\)", - )) - .expect("is_gleam_path() RE regex") - }) - .is_match( - path.strip_prefix(dir) - .expect("is_gleam_path(): strip_prefix") - .as_str(), - ) - } - fn read_sources_and_caches(&self) -> Result> { let span = tracing::info_span!("load"); let _enter = span.enter(); @@ -237,40 +216,34 @@ where codegen: self.codegen, package_name: self.package_name, artefact_directory: self.artefact_directory, - source_directory: &src, origin: Origin::Src, incomplete_modules: self.incomplete_modules, }; // Src - for path in gleam_source_files(&self.io, &src) { - // If the there is a .gleam file with a path that would be an - // invalid module name it does not get loaded. For example, if it - // has a uppercase letter in it. - // Emit a warning so that the programmer understands why it has been - // skipped. - if !self.is_gleam_path(&path, &src) { - self.warnings.emit(crate::Warning::InvalidSource { path }); - continue; + for file in GleamFile::iterate_files_in_directory(&self.io, &src) { + match file { + Ok(file) => { + let input = loader.load(file)?; + inputs.insert(input)?; + } + Err(warning) => self.warnings.emit(warning), } - - let input = loader.load(path)?; - inputs.insert(input)?; } // Test if self.mode.includes_tests() { let test = self.root.join("test"); loader.origin = Origin::Test; - loader.source_directory = &test; - for path in gleam_source_files(&self.io, &test) { - if !self.is_gleam_path(&path, &test) { - self.warnings.emit(crate::Warning::InvalidSource { path }); - continue; + for file in GleamFile::iterate_files_in_directory(&self.io, &test) { + match file { + Ok(file) => { + let input = loader.load(file)?; + inputs.insert(input)?; + } + Err(warning) => self.warnings.emit(warning), } - let input = loader.load(path)?; - inputs.insert(input)?; } } @@ -293,18 +266,12 @@ where fn load_stale_module(&self, cached: CachedModule) -> Result { let mtime = self.io.modification_time(&cached.source_path)?; - // We need to delete any existing cache_meta files for this module. + // We need to delete any existing cache files for this module. // While we figured it out this time because the module has stale dependencies, // next time the dependencies might no longer be stale, but we still need to be able to tell - // that this module needs to be recompiled until it sucessfully compiles at least once. + // that this module needs to be recompiled until it successfully compiles at least once. // This can happen if the stale dependency includes breaking changes. - let artefact = cached.name.replace("/", "@"); - let meta_path = self - .artefact_directory - .join(artefact.as_str()) - .with_extension("cache_meta"); - - let _ = self.io.delete_file(&meta_path); + CacheFiles::new(&self.artefact_directory, &cached.name).delete(&self.io); read_source( self.io.clone(), @@ -1715,3 +1682,143 @@ impl<'a> Inputs<'a> { Ok(()) } } + +/// A Gleam source file (`.gleam`) and the module name deduced from it +pub struct GleamFile { + pub path: Utf8PathBuf, + pub module_name: EcoString, +} + +impl GleamFile { + pub fn new(dir: &Utf8Path, path: Utf8PathBuf) -> Self { + Self { + module_name: Self::module_name(&path, &dir), + path, + } + } + + /// Iterates over Gleam source files (`.gleam`) in a certain directory. + /// Symlinks are followed. + /// If the there is a .gleam file with a path that would be an + /// invalid module name it should not be loaded. For example, if it + /// has a uppercase letter in it. + pub fn iterate_files_in_directory<'b>( + io: &'b impl FileSystemReader, + dir: &'b Utf8Path, + ) -> impl Iterator> + 'b { + tracing::trace!("gleam_source_files {:?}", dir); + files_with_extension(io, dir, "gleam").map(move |path| { + if (Self::is_gleam_path(&path, &dir)) { + Ok(Self::new(dir, path)) + } else { + Err(crate::Warning::InvalidSource { path }) + } + }) + } + + pub fn cache_files(&self, artefact_directory: &Utf8Path) -> CacheFiles { + CacheFiles::new(artefact_directory, &self.module_name) + } + + fn module_name(path: &Utf8Path, dir: &Utf8Path) -> EcoString { + // /path/to/project/_build/default/lib/the_package/src/my/module.gleam + + // my/module.gleam + let mut module_path = path + .strip_prefix(dir) + .expect("Stripping package prefix from module path") + .to_path_buf(); + + // my/module + let _ = module_path.set_extension(""); + + // Stringify + let name = module_path.to_string(); + + // normalise windows paths + name.replace("\\", "/").into() + } + + fn is_gleam_path(path: &Utf8Path, dir: &Utf8Path) -> bool { + use regex::Regex; + use std::cell::OnceCell; + const RE: OnceCell = OnceCell::new(); + + RE.get_or_init(|| { + Regex::new(&format!( + "^({module}{slash})*{module}\\.gleam$", + module = "[a-z][_a-z0-9]*", + slash = "(/|\\\\)", + )) + .expect("is_gleam_path() RE regex") + }) + .is_match( + path.strip_prefix(dir) + .expect("is_gleam_path(): strip_prefix") + .as_str(), + ) + } +} + +/// The collection of cache files paths related to a module. +/// These files are not guaranteed to exist. +pub struct CacheFiles { + pub cache_path: Utf8PathBuf, + pub meta_path: Utf8PathBuf, + pub warnings_path: Utf8PathBuf, +} + +impl CacheFiles { + pub fn new(artefact_directory: &Utf8Path, module_name: &EcoString) -> Self { + let file_name = module_name.replace("/", "@"); + let cache_path = artefact_directory + .join(file_name.as_str()) + .with_extension("cache"); + let meta_path = artefact_directory + .join(file_name.as_str()) + .with_extension("cache_meta"); + let warnings_path = artefact_directory + .join(file_name.as_str()) + .with_extension("cache_warnings"); + + Self { + cache_path, + meta_path, + warnings_path, + } + } + + pub fn delete(&self, io: &dyn io::FileSystemWriter) { + // TODO: Should we check for errors here? + let _ = io.delete_file(&self.cache_path); + let _ = io.delete_file(&self.meta_path); + let _ = io.delete_file(&self.warnings_path); + } + + /// Iterates over `.cache_meta` files in the given directory, + /// and returns the respective module names. + /// Symlinks are followed. + pub fn modules_with_meta_files<'a>( + io: &'a impl FileSystemReader, + dir: &'a Utf8Path, + ) -> impl Iterator + 'a { + tracing::trace!("CacheFiles::modules_with_meta_files {:?}", dir); + files_with_extension(io, dir, "cache_meta").map(move |path| Self::module_name(&dir, &path)) + } + + fn module_name(dir: &Utf8Path, path: &Utf8Path) -> EcoString { + // /path/to/artefact/dir/my@module.cache_meta + + // my@module.cache_meta + let mut module_path = path + .strip_prefix(dir) + .expect("Stripping package prefix from module path") + .to_path_buf(); + + // my@module + let _ = module_path.set_extension(""); + + // my/module + module_path.to_string().replace("@", "/").into() + } +} diff --git a/compiler-core/src/build/package_loader/tests.rs b/compiler-core/src/build/package_loader/tests.rs index a4271826ea0..7e4ebd05af5 100644 --- a/compiler-core/src/build/package_loader/tests.rs +++ b/compiler-core/src/build/package_loader/tests.rs @@ -45,7 +45,9 @@ fn write_cache( fingerprint: SourceFingerprint::new(src), line_numbers: line_numbers.clone(), }; - let path = Utf8Path::new("/artefact").join(format!("{name}.cache_meta")); + + let artefact_name = name.replace("/", "@"); + let path = Utf8Path::new("/artefact").join(format!("{artefact_name}.cache_meta")); fs.write_bytes(&path, &cache_metadata.to_binary()).unwrap(); let cache = crate::type_::ModuleInterface { @@ -65,7 +67,7 @@ fn write_cache( documentation: Default::default(), contains_echo: false, }; - let path = Utf8Path::new("/artefact").join(format!("{name}.cache")); + let path = Utf8Path::new("/artefact").join(format!("{artefact_name}.cache")); fs.write_bytes( &path, &metadata::ModuleEncoder::new(&cache).encode().unwrap(), @@ -228,7 +230,7 @@ fn module_is_stale_if_deps_removed() { let artefact = Utf8Path::new("/artefact"); // Source is removed, cache is present - write_cache(&fs, "one", 0, vec![], TEST_SOURCE_1); + write_cache(&fs, "nested/one", 0, vec![], TEST_SOURCE_1); // Cache is fresh but dep is removed write_src(&fs, "/src/two.gleam", 1, "import one"); @@ -236,8 +238,8 @@ fn module_is_stale_if_deps_removed() { &fs, "two", 2, - vec![(EcoString::from("one"), SrcSpan { start: 0, end: 0 })], - "import one", + vec![(EcoString::from("nested/one"), SrcSpan { start: 0, end: 0 })], + "import nested/one", ); let loaded = run_loader(fs, root, artefact); @@ -360,3 +362,17 @@ fn invalid_nested_module_name_in_test() { }], ); } + +#[test] +fn cache_files_are_removed_when_source_removed() { + let fs = InMemoryFileSystem::new(); + let root = Utf8Path::new("/"); + let artefact = Utf8Path::new("/artefact"); + + // Source is removed, cache is present + write_cache(&fs, "nested/one", 0, vec![], TEST_SOURCE_1); + + _ = run_loader(fs.clone(), root, artefact); + + assert_eq!(fs.files().len(), 0); +} diff --git a/compiler-core/src/codegen.rs b/compiler-core/src/codegen.rs index 7a00897f6bb..7c3d0f3f5e1 100644 --- a/compiler-core/src/codegen.rs +++ b/compiler-core/src/codegen.rs @@ -1,7 +1,9 @@ use crate::{ Result, analyse::TargetSupport, - build::{ErlangAppCodegenConfiguration, Module, package_compiler::StdlibPackage}, + build::{ + ErlangAppCodegenConfiguration, Module, module_erlang_name, package_compiler::StdlibPackage, + }, config::PackageConfig, erlang, io::FileSystemWriter, @@ -38,7 +40,7 @@ impl<'a> Erlang<'a> { root: &Utf8Path, ) -> Result<()> { for module in modules { - let erl_name = module.name.replace("/", "@"); + let erl_name = module.erlang_name(); self.erlang_module(&writer, module, &erl_name, root)?; self.erlang_record_headers(&writer, module, &erl_name)?; } @@ -107,12 +109,12 @@ impl<'a> ErlangApp<'a> { .erlang .application_start_module .as_ref() - .map(|module| tuple("mod", &format!("{{'{}', []}}", module.replace("/", "@")))) + .map(|module| tuple("mod", &format!("{{'{}', []}}", module_erlang_name(module)))) .unwrap_or_default(); let modules = modules .iter() - .map(|m| m.name.replace("/", "@")) + .map(|m| m.erlang_name()) .chain(native_modules) .unique() .sorted() diff --git a/compiler-core/src/erlang.rs b/compiler-core/src/erlang.rs index 7e5133dcba0..2ae7f0cf928 100644 --- a/compiler-core/src/erlang.rs +++ b/compiler-core/src/erlang.rs @@ -6,7 +6,7 @@ mod pattern; #[cfg(test)] mod tests; -use crate::build::Target; +use crate::build::{Target, module_erlang_name}; use crate::strings::convert_string_escape_chars; use crate::type_::is_prelude_module; use crate::{ @@ -182,7 +182,7 @@ fn module_document<'a>( let header = "-module(" .to_doc() - .append(module.name.replace("/", "@")) + .append(module.erlang_name()) .append(").") .append(line()); @@ -1779,7 +1779,7 @@ fn docs_args_call<'a>( // This also enables an optimisation in the Erlang compiler in which // some Erlang BIFs can be replaced with literals if their arguments // are literals, such as `binary_to_atom`. - atom_string(module.replace("/", "@").to_string()) + atom_string(module_erlang_name(module).to_string()) .append(":") .append(atom_string(name.to_string())) .append(args) diff --git a/compiler-core/src/io.rs b/compiler-core/src/io.rs index 2d1c14d38b1..41b76c21f8e 100644 --- a/compiler-core/src/io.rs +++ b/compiler-core/src/io.rs @@ -274,27 +274,9 @@ pub trait FileSystemReader { fn canonicalise(&self, path: &Utf8Path) -> Result; } -/// Iterates over Gleam source files (`.gleam`) in a certain directory. +/// Iterates over files with the given extension in a certain directory. /// Symlinks are followed. -pub fn gleam_source_files<'a>( - io: &'a impl FileSystemReader, - dir: &'a Utf8Path, -) -> impl Iterator + 'a { - tracing::trace!("gleam_source_files {:?}", dir); - files_with_extension(io, dir, "gleam") -} - -/// Iterates over Gleam cache files (`.cache`) in a certain directory. -/// Symlinks are followed. -pub fn gleam_cache_files<'a>( - io: &'a impl FileSystemReader, - dir: &'a Utf8Path, -) -> impl Iterator + 'a { - tracing::trace!("gleam_cache_files {:?}", dir); - files_with_extension(io, dir, "cache") -} - -fn files_with_extension<'a>( +pub fn files_with_extension<'a>( io: &'a impl FileSystemReader, dir: &'a Utf8Path, extension: &'a str, diff --git a/compiler-core/src/language_server/tests/compilation.rs b/compiler-core/src/language_server/tests/compilation.rs index 3645a55f4f9..bfddd2d68a5 100644 --- a/compiler-core/src/language_server/tests/compilation.rs +++ b/compiler-core/src/language_server/tests/compilation.rs @@ -119,7 +119,7 @@ fn compile_recompile() { // This time it does not compile the module again, instead using the // cache from the previous run. let response = engine.compile_please(); - assert!(response.result.is_ok()); + assert_eq!(response.result, Ok(())); assert!(response.warnings.is_empty()); assert_eq!(response.compilation, Compilation::Yes(vec![]));