diff --git a/crates/mun/Cargo.toml b/crates/mun/Cargo.toml index 5ad212396..9d5e308b0 100644 --- a/crates/mun/Cargo.toml +++ b/crates/mun/Cargo.toml @@ -23,6 +23,7 @@ mun_compiler = { version = "=0.2.0", path = "../mun_compiler" } mun_compiler_daemon = { version = "=0.2.0", path = "../mun_compiler_daemon" } mun_runtime = { version = "=0.2.0", path = "../mun_runtime" } mun_language_server = { version = "=0.1.0", path = "../mun_language_server" } +mun_project = { version = "=0.1.0", path = "../mun_project" } [dev-dependencies.cargo-husky] version = "1" diff --git a/crates/mun/src/main.rs b/crates/mun/src/main.rs index 142cf5ff0..596de971c 100644 --- a/crates/mun/src/main.rs +++ b/crates/mun/src/main.rs @@ -5,8 +5,10 @@ use std::time::Duration; use anyhow::anyhow; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; -use mun_compiler::{Config, DisplayColor, PathOrInline, Target}; +use mun_compiler::{Config, DisplayColor, Target}; +use mun_project::MANIFEST_FILENAME; use mun_runtime::{invoke_fn, ReturnTypeReflection, Runtime, RuntimeBuilder}; +use std::path::{Path, PathBuf}; fn setup_logging() -> Result<(), anyhow::Error> { pretty_env_logger::try_init()?; @@ -23,15 +25,17 @@ fn main() -> Result<(), anyhow::Error> { .subcommand( SubCommand::with_name("build") .arg( - Arg::with_name("INPUT") - .help("Sets the input file to use") - .required(true) - .index(1), + Arg::with_name("manifest-path") + .long("manifest-path") + .takes_value(true) + .help(&format!("Path to {}", MANIFEST_FILENAME)) + ) + .arg( + Arg::with_name("watch") + .long("watch") + .help("Run the compiler in watch mode.\ + Watch input files and trigger recompilation on changes.",) ) - .arg(Arg::with_name("watch").long("watch").help( - "Run the compiler in watch mode. - Watch input files and trigger recompilation on changes.", - )) .arg( Arg::with_name("opt-level") .short("O") @@ -80,23 +84,61 @@ fn main() -> Result<(), anyhow::Error> { ) .get_matches(); - match matches.subcommand() { + let success = match matches.subcommand() { ("build", Some(matches)) => build(matches)?, - ("start", Some(matches)) => start(matches)?, ("language-server", Some(matches)) => language_server(matches)?, + ("start", Some(matches)) => { + start(matches)?; + true + } _ => unreachable!(), - } + }; - Ok(()) + std::process::exit(if success { 0 } else { 1 }); } -/// Build the source file specified -fn build(matches: &ArgMatches) -> Result<(), anyhow::Error> { +/// Find a Mun manifest file in the specified directory or one of its parents. +pub fn find_manifest(directory: &Path) -> Option { + let mut current_dir = Some(directory); + while let Some(dir) = current_dir { + let manifest_path = dir.join(MANIFEST_FILENAME); + if manifest_path.exists() { + return Some(manifest_path); + } + current_dir = dir.parent(); + } + None +} + +/// This method is invoked when the executable is run with the `build` argument indicating that a +/// user requested us to build a project in the current directory or one of its parent directories. +/// +/// The `bool` return type for this function indicates whether the process should exit with a +/// success or failure error code. +fn build(matches: &ArgMatches) -> Result { let options = compiler_options(matches)?; + + // Locate the manifest + let manifest_path = match matches.value_of("manifest-path") { + None => { + let current_dir = + std::env::current_dir().expect("could not determine currrent working directory"); + find_manifest(¤t_dir).ok_or_else(|| { + anyhow::anyhow!( + "could not find {} in '{}' or a parent directory", + MANIFEST_FILENAME, + current_dir.display() + ) + })? + } + Some(path) => std::fs::canonicalize(Path::new(path)) + .map_err(|_| anyhow::anyhow!("'{}' does not refer to a valid manifest path", path))?, + }; + if matches.is_present("watch") { - mun_compiler_daemon::main(options) + mun_compiler_daemon::compile_and_watch_manifest(&manifest_path, options) } else { - mun_compiler::main(options).map(|_| {}) + mun_compiler::compile_manifest(&manifest_path, options) } } @@ -142,7 +184,7 @@ fn start(matches: &ArgMatches) -> Result<(), anyhow::Error> { } } -fn compiler_options(matches: &ArgMatches) -> Result { +fn compiler_options(matches: &ArgMatches) -> Result { let optimization_lvl = match matches.value_of("opt-level") { Some("0") => mun_compiler::OptimizationLevel::None, Some("1") => mun_compiler::OptimizationLevel::Less, @@ -162,16 +204,13 @@ fn compiler_options(matches: &ArgMatches) -> Result Result>, anyhow::Error> builder.spawn() } -fn language_server(_matches: &ArgMatches) -> Result<(), anyhow::Error> { - mun_language_server::run_server().map_err(|e| anyhow!("{}", e)) +/// This function is invoked when the executable is invoked with the `language-server` argument. A +/// Mun language server is started ready to serve language information about one or more projects. +/// +/// The `bool` return type for this function indicates whether the process should exit with a +/// success or failure error code. +fn language_server(_matches: &ArgMatches) -> Result { + mun_language_server::run_server().map_err(|e| anyhow::anyhow!("{}", e))?; + Ok(true) } diff --git a/crates/mun_codegen/src/assembly.rs b/crates/mun_codegen/src/assembly.rs new file mode 100644 index 000000000..ef22a8318 --- /dev/null +++ b/crates/mun_codegen/src/assembly.rs @@ -0,0 +1,46 @@ +use crate::{IrDatabase, ModuleBuilder}; +use std::path::Path; +use std::sync::Arc; +use tempfile::NamedTempFile; + +#[derive(Debug)] +pub struct Assembly { + file: NamedTempFile, +} + +impl PartialEq for Assembly { + fn eq(&self, other: &Self) -> bool { + self.path().eq(other.path()) + } +} + +impl Eq for Assembly {} + +impl Assembly { + pub const EXTENSION: &'static str = "munlib"; + + /// Returns the current location of the assembly + pub fn path(&self) -> &Path { + self.file.path() + } + + /// Copies the assembly to the specified location + pub fn copy_to>(&self, destination: P) -> Result<(), std::io::Error> { + std::fs::copy(self.path(), destination).map(|_| ()) + } +} + +/// Create a new temporary file that contains the linked object +pub fn assembly_query(db: &impl IrDatabase, file_id: hir::FileId) -> Arc { + let file = NamedTempFile::new().expect("could not create temp file for shared object"); + + let module_builder = ModuleBuilder::new(db, file_id).expect("could not create ModuleBuilder"); + let obj_file = module_builder + .build() + .expect("unable to create object file"); + obj_file + .into_shared_object(file.path()) + .expect("could not link object file"); + + Arc::new(Assembly { file }) +} diff --git a/crates/mun_codegen/src/code_gen.rs b/crates/mun_codegen/src/code_gen.rs index 4c959c52a..0425dcba1 100644 --- a/crates/mun_codegen/src/code_gen.rs +++ b/crates/mun_codegen/src/code_gen.rs @@ -1,7 +1,7 @@ use crate::code_gen::linker::LinkerError; use crate::value::{IrTypeContext, IrValueContext}; use crate::IrDatabase; -use hir::{FileId, RelativePathBuf}; +use hir::FileId; use inkwell::targets::TargetData; use inkwell::{ module::Module, @@ -11,10 +11,7 @@ use inkwell::{ }; use mun_target::spec; use std::io::{self, Write}; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{path::Path, sync::Arc}; use tempfile::NamedTempFile; use thiserror::Error; @@ -45,15 +42,14 @@ impl From for CodeGenerationError { pub struct ObjectFile { target: spec::Target, - src_path: RelativePathBuf, obj_file: NamedTempFile, } impl ObjectFile { + /// Constructs a new object file from the specified `module` for `target` pub fn new( target: &spec::Target, target_machine: &TargetMachine, - src_path: RelativePathBuf, module: Arc, ) -> Result { let obj = target_machine @@ -68,23 +64,21 @@ impl ObjectFile { Ok(Self { target: target.clone(), - src_path, obj_file, }) } - pub fn into_shared_object(self, out_dir: Option<&Path>) -> Result { + /// Links the object file into a shared object. + pub fn into_shared_object(self, output_path: &Path) -> Result<(), anyhow::Error> { // Construct a linker for the target let mut linker = linker::create_with_target(&self.target); linker.add_object(self.obj_file.path())?; - let output_path = assembly_output_path(&self.src_path, out_dir); - // Link the object linker.build_shared_object(&output_path)?; linker.finalize()?; - Ok(output_path) + Ok(()) } } @@ -182,27 +176,11 @@ impl<'a, D: IrDatabase> ModuleBuilder<'a, D> { ObjectFile::new( &self.db.target(), &self.target_machine, - self.db.file_relative_path(self.file_id), self.assembly_module, ) } } -/// Computes the output path for the assembly of the specified file. -fn assembly_output_path(src_path: &RelativePathBuf, out_dir: Option<&Path>) -> PathBuf { - let original_filename = Path::new(src_path.file_name().unwrap()); - - // Add the `munlib` suffix to the original filename - let output_file_name = original_filename.with_extension("munlib"); - - // If there is an out dir specified, prepend the output directory - if let Some(out_dir) = out_dir { - out_dir.join(output_file_name) - } else { - output_file_name - } -} - /// Optimizes the specified LLVM `Module` using the default passes for the given /// `OptimizationLevel`. fn optimize_module(module: &Module, optimization_lvl: OptimizationLevel) { diff --git a/crates/mun_codegen/src/db.rs b/crates/mun_codegen/src/db.rs index 93a2934e2..e0b4ec1c3 100644 --- a/crates/mun_codegen/src/db.rs +++ b/crates/mun_codegen/src/db.rs @@ -1,6 +1,7 @@ #![allow(clippy::type_repetition_in_bounds)] use crate::{ + assembly::Assembly, ir::{file::FileIR, file_group::FileGroupIR}, type_info::TypeInfo, CodeGenParams, Context, @@ -42,6 +43,11 @@ pub trait IrDatabase: hir::HirDatabase { #[salsa::invoke(crate::ir::file_group::ir_query)] fn group_ir(&self, file: hir::FileId) -> Arc; + /// Returns a fully linked shared object for the specified group of files. + /// TODO: Current, a group always consists of a single file. Need to add support for multiple. + #[salsa::invoke(crate::assembly::assembly_query)] + fn assembly(&self, file: hir::FileId) -> Arc; + /// Given a `hir::FileId` generate code for the module. #[salsa::invoke(crate::ir::file::ir_query)] fn file_ir(&self, file: hir::FileId) -> Arc; diff --git a/crates/mun_codegen/src/lib.rs b/crates/mun_codegen/src/lib.rs index fd03ee7fe..f9e1c8720 100644 --- a/crates/mun_codegen/src/lib.rs +++ b/crates/mun_codegen/src/lib.rs @@ -3,6 +3,7 @@ mod code_gen; mod db; #[macro_use] mod ir; +mod assembly; #[cfg(test)] mod mock; @@ -17,6 +18,7 @@ pub(crate) mod type_info; pub use inkwell::{builder::Builder, context::Context, module::Module, OptimizationLevel}; pub use crate::{ + assembly::Assembly, code_gen::ModuleBuilder, db::{IrDatabase, IrDatabaseStorage}, }; diff --git a/crates/mun_codegen/src/mock.rs b/crates/mun_codegen/src/mock.rs index d8562610e..09fa555cf 100644 --- a/crates/mun_codegen/src/mock.rs +++ b/crates/mun_codegen/src/mock.rs @@ -43,7 +43,7 @@ impl MockDatabase { db.set_file_relative_path(file_id, rel_path.clone()); db.set_file_text(file_id, Arc::new(text.to_string())); db.set_file_source_root(file_id, source_root_id); - source_root.insert_file(rel_path, file_id); + source_root.insert_file(file_id); db.set_source_root(source_root_id, Arc::new(source_root)); db.set_optimization_lvl(OptimizationLevel::None); diff --git a/crates/mun_compiler/Cargo.toml b/crates/mun_compiler/Cargo.toml index d2fcefcc5..11c0e90d3 100644 --- a/crates/mun_compiler/Cargo.toml +++ b/crates/mun_compiler/Cargo.toml @@ -18,9 +18,11 @@ mun_codegen = { version = "=0.2.0", path="../mun_codegen" } mun_syntax = { version = "=0.2.0", path="../mun_syntax" } mun_hir = { version = "=0.2.0", path="../mun_hir" } mun_target = { version = "=0.2.0", path="../mun_target" } +mun_project = { version = "=0.1.0", path = "../mun_project" } annotate-snippets = { version = "0.6.1", features = ["color"] } unicode-segmentation = "1.6.0" ansi_term = "0.12.1" +walkdir = "2.3" [dev-dependencies] insta = "0.16" diff --git a/crates/mun_compiler/src/db.rs b/crates/mun_compiler/src/db.rs index f5ba06abe..52350a9c6 100644 --- a/crates/mun_compiler/src/db.rs +++ b/crates/mun_compiler/src/db.rs @@ -1,5 +1,9 @@ -use mun_hir::salsa; +use crate::Config; +use mun_codegen::IrDatabase; +use mun_hir::{salsa, HirDatabase}; +use std::sync::Arc; +/// A compiler database is a salsa database that enables increment compilation. #[salsa::database( mun_hir::SourceDatabaseStorage, mun_hir::DefDatabaseStorage, @@ -7,15 +11,28 @@ use mun_hir::salsa; mun_codegen::IrDatabaseStorage )] #[derive(Debug)] -pub(crate) struct CompilerDatabase { +pub struct CompilerDatabase { runtime: salsa::Runtime, } impl CompilerDatabase { - pub fn new() -> Self { - CompilerDatabase { + /// Constructs a new database + pub fn new(config: &Config) -> Self { + let mut db = CompilerDatabase { runtime: salsa::Runtime::default(), - } + }; + + // Set the initial configuration + db.set_context(Arc::new(mun_codegen::Context::create())); + db.set_config(config); + + db + } + + /// Applies the given configuration to the database + pub fn set_config(&mut self, config: &Config) { + self.set_target(config.target.clone()); + self.set_optimization_lvl(config.optimization_lvl); } } diff --git a/crates/mun_compiler/src/diagnostics.rs b/crates/mun_compiler/src/diagnostics.rs index ce91cf06e..2069a4588 100644 --- a/crates/mun_compiler/src/diagnostics.rs +++ b/crates/mun_compiler/src/diagnostics.rs @@ -3,9 +3,24 @@ use mun_hir::{FileId, HirDatabase, Module}; use std::cell::RefCell; -use annotate_snippets::snippet::Snippet; - use crate::diagnostics_snippets; +use annotate_snippets::{ + display_list::DisplayList, formatter::DisplayListFormatter, snippet::Snippet, +}; + +/// Emits all specified diagnostic messages to the given stream +pub fn emit_diagnostics<'a>( + writer: &mut dyn std::io::Write, + diagnostics: impl IntoIterator, + colors: bool, +) -> Result<(), anyhow::Error> { + let dlf = DisplayListFormatter::new(colors, false); + for diagnostic in diagnostics.into_iter() { + let dl = DisplayList::from(diagnostic.clone()); + writeln!(writer, "{}", dlf.format(&dl))?; + } + Ok(()) +} /// Constructs diagnostic messages for the given file. pub fn diagnostics(db: &impl HirDatabase, file_id: FileId) -> Vec { @@ -141,6 +156,7 @@ pub fn diagnostics(db: &impl HirDatabase, file_id: FileId) -> Vec { #[cfg(test)] mod tests { use crate::{Config, DisplayColor, Driver, PathOrInline, RelativePathBuf}; + use std::io::Cursor; /// Compile passed source code and return all compilation errors fn compilation_errors(source_code: &str) -> String { @@ -158,7 +174,9 @@ mod tests { let mut compilation_errors = Vec::::new(); - let _ = driver.emit_diagnostics(&mut compilation_errors).unwrap(); + let _ = driver + .emit_diagnostics(&mut Cursor::new(&mut compilation_errors)) + .unwrap(); String::from_utf8(compilation_errors).unwrap() } diff --git a/crates/mun_compiler/src/driver.rs b/crates/mun_compiler/src/driver.rs index c16b8beca..ef3ad343c 100644 --- a/crates/mun_compiler/src/driver.rs +++ b/crates/mun_compiler/src/driver.rs @@ -1,9 +1,14 @@ //! `Driver` is a stateful compiler frontend that enables incremental compilation by retaining state //! from previous compilation. -use crate::{db::CompilerDatabase, diagnostics::diagnostics, PathOrInline}; -use mun_codegen::{IrDatabase, ModuleBuilder}; -use mun_hir::{FileId, HirDatabase, RelativePathBuf, SourceDatabase, SourceRoot, SourceRootId}; +use crate::{ + compute_source_relative_path, + db::CompilerDatabase, + diagnostics::{diagnostics, emit_diagnostics}, + ensure_package_output_dir, is_source_file, PathOrInline, RelativePath, +}; +use mun_codegen::{Assembly, IrDatabase}; +use mun_hir::{FileId, RelativePathBuf, SourceDatabase, SourceRoot, SourceRootId}; use std::{path::PathBuf, sync::Arc}; @@ -13,40 +18,43 @@ mod display_color; pub use self::config::Config; pub use self::display_color::DisplayColor; -use annotate_snippets::{ - display_list::DisplayList, - formatter::DisplayListFormatter, - snippet::{AnnotationType, Snippet}, -}; +use annotate_snippets::snippet::{AnnotationType, Snippet}; +use mun_project::Package; +use std::collections::HashMap; +use std::convert::TryInto; +use std::path::Path; +use walkdir::WalkDir; pub const WORKSPACE: SourceRootId = SourceRootId(0); #[derive(Debug)] pub struct Driver { db: CompilerDatabase, - out_dir: Option, + out_dir: PathBuf, + + source_root: SourceRoot, + path_to_file_id: HashMap, + file_id_to_path: HashMap, + next_file_id: usize, + + file_id_to_temp_assembly_path: HashMap, + display_color: DisplayColor, } impl Driver { /// Constructs a driver with a specific configuration. - pub fn with_config(config: Config) -> Self { - let mut driver = Driver { - db: CompilerDatabase::new(), - out_dir: None, + pub fn with_config(config: Config, out_dir: PathBuf) -> Result { + Ok(Self { + db: CompilerDatabase::new(&config), + out_dir, + source_root: Default::default(), + path_to_file_id: Default::default(), + file_id_to_path: Default::default(), + next_file_id: 0, + file_id_to_temp_assembly_path: Default::default(), display_color: config.display_color, - }; - - // Move relevant configuration into the database - driver.db.set_target(config.target); - driver - .db - .set_context(Arc::new(mun_codegen::Context::create())); - driver.db.set_optimization_lvl(config.optimization_lvl); - - driver.out_dir = config.out_dir; - - driver + }) } /// Constructs a driver with a configuration and a single file. @@ -54,10 +62,11 @@ impl Driver { config: Config, path: PathOrInline, ) -> Result<(Driver, FileId), anyhow::Error> { - let mut driver = Driver::with_config(config); + let out_dir = config.out_dir.clone().unwrap_or_else(|| { + std::env::current_dir().expect("could not determine current working directory") + }); - // Construct a SourceRoot - let mut source_root = SourceRoot::default(); + let mut driver = Driver::with_config(config, out_dir)?; // Get the path and contents of the path let (rel_path, text) = match path { @@ -77,15 +86,101 @@ impl Driver { }; // Store the file information in the database together with the source root - let file_id = FileId(0); - driver.db.set_file_relative_path(file_id, rel_path.clone()); + let file_id = FileId(driver.next_file_id as u32); + driver.next_file_id += 1; + driver.db.set_file_relative_path(file_id, rel_path); driver.db.set_file_text(file_id, Arc::new(text)); driver.db.set_file_source_root(file_id, WORKSPACE); - source_root.insert_file(rel_path, file_id); - driver.db.set_source_root(WORKSPACE, Arc::new(source_root)); + driver.source_root.insert_file(file_id); + driver + .db + .set_source_root(WORKSPACE, Arc::new(driver.source_root.clone())); Ok((driver, file_id)) } + + /// Constructs a driver with a package manifest directory + pub fn with_package_path>( + package_path: P, + config: Config, + ) -> Result<(Package, Driver), anyhow::Error> { + // Load the manifest file as a package + let package = Package::from_file(package_path)?; + + // Determine output directory + let output_dir = ensure_package_output_dir(&package, &config)?; + + // Construct the driver + let mut driver = Driver::with_config(config, output_dir)?; + + // Iterate over all files in the source directory of the package and store their information in + // the database + let source_directory = package + .source_directory() + .ok_or_else(|| anyhow::anyhow!("the source directory does not exist"))?; + + for source_file_path in iter_source_files(&source_directory) { + let relative_path = compute_source_relative_path(&source_directory, &source_file_path)?; + + // Load the contents of the file + let file_contents = std::fs::read_to_string(&source_file_path).map_err(|e| { + anyhow::anyhow!( + "could not read contents of '{}': {}", + source_file_path.display(), + e + ) + })?; + + let file_id = driver.alloc_file_id(&relative_path)?; + driver + .db + .set_file_relative_path(file_id, relative_path.clone()); + driver.db.set_file_text(file_id, Arc::new(file_contents)); + driver.db.set_file_source_root(file_id, WORKSPACE); + driver.source_root.insert_file(file_id); + } + + // Store the source root in the database + driver + .db + .set_source_root(WORKSPACE, Arc::new(driver.source_root.clone())); + + Ok((package, driver)) + } +} + +impl Driver { + /// Returns a file id for the file with the given `relative_path`. This function reuses FileId's + /// for paths to keep the cache as valid as possible. + /// + /// The allocation of an id might fail if more file IDs exist than can be allocated. + pub fn alloc_file_id>( + &mut self, + relative_path: P, + ) -> Result { + // Re-use existing id to get better caching performance + if let Some(id) = self.path_to_file_id.get(relative_path.as_ref()) { + return Ok(*id); + } + + // Allocate a new id + // TODO: See if we can figure out if the compiler cleared the cache of a certain file, at + // which point we can sort of reset the `next_file_id` + let id = FileId( + self.next_file_id + .try_into() + .map_err(|_e| anyhow::anyhow!("too many active source files"))?, + ); + self.next_file_id += 1; + + // Update bookkeeping + self.path_to_file_id + .insert(relative_path.as_ref().to_relative_path_buf(), id); + self.file_id_to_path + .insert(id, relative_path.as_ref().to_relative_path_buf()); + + Ok(id) + } } impl Driver { @@ -110,29 +205,156 @@ impl Driver { /// Emits all diagnostic messages currently in the database; returns true if errors were /// emitted. pub fn emit_diagnostics(&self, writer: &mut dyn std::io::Write) -> Result { - let mut has_errors = false; - let dlf = DisplayListFormatter::new(self.display_color.should_enable(), false); - for file_id in self.db.source_root(WORKSPACE).files() { - let diags = diagnostics(&self.db, file_id); - for diagnostic in diags { - let dl = DisplayList::from(diagnostic.clone()); - writeln!(writer, "{}", dlf.format(&dl)).unwrap(); - if let Some(annotation) = diagnostic.title { - if let AnnotationType::Error = annotation.annotation_type { - has_errors = true; - } - } - } + let diagnostics = self.diagnostics(); + + // Emit all diagnostics to the stream + emit_diagnostics(writer, &diagnostics, self.display_color.should_enable())?; + + // Determine if one of the snippets is actually an error + Ok(diagnostics.iter().any(|d| { + d.title + .as_ref() + .map(|a| match a.annotation_type { + AnnotationType::Error => true, + _ => false, + }) + .unwrap_or(false) + })) + } +} + +impl Driver { + /// Get the path where the driver will write the assembly for the specified file. + pub fn assembly_output_path(&self, file_id: FileId) -> PathBuf { + self.db + .file_relative_path(file_id) + .with_extension(Assembly::EXTENSION) + .to_path(&self.out_dir) + } + + /// Writes all assemblies + pub fn write_all_assemblies(&mut self) -> Result<(), anyhow::Error> { + // Create a copy of all current files + let files = self.source_root.files().collect::>(); + for file_id in files { + self.write_assembly(file_id, false)?; + } + Ok(()) + } + + /// Generates an assembly for the given file and stores it in the output location. If `force` is + /// false, the binary will not be written if there are no changes since last time it was + /// written. Returns `true` if the assembly was written, `false` if it was up to date. + pub fn write_assembly(&mut self, file_id: FileId, force: bool) -> Result { + // Determine the location of the output file + let assembly_path = self.assembly_output_path(file_id); + + // Get the compiled assembly + let assembly = self.db.assembly(file_id); + + // Did the assembly change since last time? + if !force + && assembly_path.is_file() + && self + .file_id_to_temp_assembly_path + .get(&file_id) + .map(AsRef::as_ref) + == Some(assembly.path()) + { + return Ok(false); } - Ok(has_errors) + + // It did change or we are forced, so write it to disk + assembly.copy_to(&assembly_path)?; + + // Store the information so we maybe don't have to write it next time + self.file_id_to_temp_assembly_path + .insert(file_id, assembly.path().to_path_buf()); + + Ok(true) } } impl Driver { - /// Generate an assembly for the given file - pub fn write_assembly(&mut self, file_id: FileId) -> Result { - let module_builder = ModuleBuilder::new(&self.db, file_id)?; - let obj_file = module_builder.build()?; - obj_file.into_shared_object(self.out_dir.as_deref()) + /// Returns the `FileId` of the file with the given relative path + pub fn get_file_id_for_path>(&self, path: P) -> Option { + self.path_to_file_id.get(path.as_ref()).copied() + } + + /// Tells the driver that the file at the specified `path` has changed its contents. Returns the + /// `FileId` of the modified file. + pub fn update_file>(&mut self, path: P, contents: String) -> FileId { + let file_id = *self + .path_to_file_id + .get(path.as_ref()) + .expect("writing to a file that is not part of the source root should never happen"); + self.db.set_file_text(file_id, Arc::new(contents)); + file_id } + + /// Adds a new file to the driver. Returns the `FileId` of the new file. + pub fn add_file>(&mut self, path: P, contents: String) -> FileId { + let file_id = self.alloc_file_id(path.as_ref()).unwrap(); + + // Insert the new file + self.db + .set_file_relative_path(file_id, path.as_ref().to_relative_path_buf()); + self.db.set_file_text(file_id, Arc::new(contents)); + self.db.set_file_source_root(file_id, WORKSPACE); + + // Update the source root + self.source_root.insert_file(file_id); + self.db + .set_source_root(WORKSPACE, Arc::new(self.source_root.clone())); + + file_id + } + + /// Removes the specified file from the driver. + pub fn remove_file>(&mut self, path: P) -> FileId { + let file_id = *self + .path_to_file_id + .get(path.as_ref()) + .expect("removing to a file that is not part of the source root should never happen"); + + // Update the source root + self.source_root.remove_file(file_id); + self.db + .set_source_root(WORKSPACE, Arc::new(self.source_root.clone())); + + file_id + } + + /// Renames the specified file to the specified path + pub fn rename, P2: AsRef>( + &mut self, + from: P1, + to: P2, + ) -> FileId { + let file_id = *self + .path_to_file_id + .get(from.as_ref()) + .expect("renaming from a file that is not part of the source root should never happen"); + if let Some(previous) = self.path_to_file_id.get(to.as_ref()) { + // If there was some other file with this path in the database, forget about it. + self.file_id_to_path.remove(previous); + } + + self.file_id_to_path + .insert(file_id, to.as_ref().to_relative_path_buf()); + self.path_to_file_id.remove(from.as_ref()); // FileId now belongs to to + + self.db + .set_file_relative_path(file_id, to.as_ref().to_relative_path_buf()); + + file_id + } +} + +pub fn iter_source_files(source_dir: &Path) -> impl Iterator { + WalkDir::new(source_dir) + .into_iter() + .filter_map(Result::ok) + .filter(|e| is_source_file(e.path())) + .map(|e| e.path().to_path_buf()) } diff --git a/crates/mun_compiler/src/driver/display_color.rs b/crates/mun_compiler/src/driver/display_color.rs index d9c43d4dd..212dd26c6 100644 --- a/crates/mun_compiler/src/driver/display_color.rs +++ b/crates/mun_compiler/src/driver/display_color.rs @@ -10,7 +10,7 @@ pub enum DisplayColor { } impl DisplayColor { - pub(crate) fn should_enable(self) -> bool { + pub fn should_enable(self) -> bool { match self { DisplayColor::Disable => false, DisplayColor::Auto => terminal_support_ansi(), diff --git a/crates/mun_compiler/src/lib.rs b/crates/mun_compiler/src/lib.rs index 876e8033d..140a96333 100644 --- a/crates/mun_compiler/src/lib.rs +++ b/crates/mun_compiler/src/lib.rs @@ -2,7 +2,7 @@ mod annotate; mod db; ///! This library contains the code required to go from source code to binaries. -mod diagnostics; +pub mod diagnostics; mod diagnostics_snippets; mod driver; @@ -15,6 +15,10 @@ pub use crate::driver::{Config, Driver}; pub use annotate::{AnnotationBuilder, SliceBuilder, SnippetBuilder}; pub use mun_codegen::OptimizationLevel; +pub use crate::db::CompilerDatabase; +pub use annotate_snippets::snippet::AnnotationType; +use mun_project::Package; +use std::ffi::OsStr; use std::io::stderr; #[derive(Debug, Clone)] @@ -57,12 +61,55 @@ impl CompilerOptions { } } -pub fn main(options: CompilerOptions) -> Result, anyhow::Error> { - let (mut driver, file_id) = Driver::with_file(options.config, options.input)?; +/// Returns true if the given path is considered to be a Mun source file +pub fn is_source_file>(p: P) -> bool { + p.as_ref().extension() == Some(&OsStr::new("mun")) +} + +/// Returns and creates the output dir for the specified package +pub fn ensure_package_output_dir( + package: &Package, + config: &Config, +) -> Result { + let out_dir = config + .out_dir + .clone() + .unwrap_or_else(|| package.root().join("target")); + std::fs::create_dir_all(&out_dir)?; + Ok(out_dir) +} +pub fn compile_manifest(manifest_path: &Path, config: Config) -> Result { + let (_package, mut driver) = Driver::with_package_path(manifest_path, config)?; + + // Emit diagnostics. If one of the snippets is an error, abort gracefully. if driver.emit_diagnostics(&mut stderr())? { - Ok(None) - } else { - driver.write_assembly(file_id).map(Some) - } + return Ok(false); + }; + + // Write out all assemblies + driver.write_all_assemblies()?; + + Ok(true) +} + +/// Determines the relative path of a file to the source directory. +pub fn compute_source_relative_path( + source_dir: &Path, + source_path: &Path, +) -> Result { + RelativePathBuf::from_path(source_path.strip_prefix(source_dir).map_err(|e| { + anyhow::anyhow!( + "could not determine relative source path for '{}': {}", + source_path.display(), + e + ) + })?) + .map_err(|e| { + anyhow::anyhow!( + "could not determine source relative path for '{}': {}", + source_path.display(), + e + ) + }) } diff --git a/crates/mun_compiler_daemon/Cargo.toml b/crates/mun_compiler_daemon/Cargo.toml index c60160b24..607a881fe 100644 --- a/crates/mun_compiler_daemon/Cargo.toml +++ b/crates/mun_compiler_daemon/Cargo.toml @@ -14,5 +14,10 @@ categories = ["game-development", "mun"] [dependencies] anyhow = "1.0.31" +mun_codegen = { version = "=0.2.0", path = "../mun_codegen" } mun_compiler = { version = "=0.2.0", path = "../mun_compiler" } -notify = "4.0.12" +mun_project = { version = "=0.1.0", path = "../mun_project" } +mun_hir = { version = "=0.2.0", path = "../mun_hir" } +notify = "4.0" +ctrlc = "3.1" +log = "0.4" diff --git a/crates/mun_compiler_daemon/src/lib.rs b/crates/mun_compiler_daemon/src/lib.rs index d84a7b1ac..ec0254a9d 100644 --- a/crates/mun_compiler_daemon/src/lib.rs +++ b/crates/mun_compiler_daemon/src/lib.rs @@ -1,45 +1,98 @@ use std::sync::mpsc::channel; use std::time::Duration; -use anyhow::Error; -use mun_compiler::{CompilerOptions, Driver, PathOrInline}; +use mun_compiler::{compute_source_relative_path, is_source_file, Config, Driver}; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use std::io::stderr; +use std::path::Path; +use std::sync::Arc; -pub fn main(options: CompilerOptions) -> Result<(), Error> { - // Need to canonicalize path to do comparisons - let input_path = match &options.input { - PathOrInline::Path(path) => path.canonicalize()?, - PathOrInline::Inline { .. } => panic!("cannot run compiler with inline path"), - }; +/// Compiles and watches the package at the specified path. Recompiles changes that occur. +pub fn compile_and_watch_manifest( + manifest_path: &Path, + config: Config, +) -> Result { + // Create the compiler driver + let (package, mut driver) = Driver::with_package_path(manifest_path, config)?; - let (tx, rx) = channel(); + // Start watching the source directory + let (watcher_tx, watcher_rx) = channel(); + let mut watcher: RecommendedWatcher = Watcher::new(watcher_tx, Duration::from_millis(10))?; + let source_directory = package + .source_directory() + .expect("missing source directory"); + watcher.watch(&source_directory, RecursiveMode::Recursive)?; + println!("Watching: {}", source_directory.display()); - let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_millis(10))?; - watcher.watch(&input_path, RecursiveMode::NonRecursive)?; - println!("Watching: {}", input_path.display()); - - let (mut driver, file_id) = Driver::with_file(options.config, options.input)?; - - // Compile at least once + // Emit all current errors, and write the assemblies if no errors occured if !driver.emit_diagnostics(&mut stderr())? { - driver.write_assembly(file_id)?; + driver.write_all_assemblies()? } - loop { - use notify::DebouncedEvent::*; - match rx.recv() { - Ok(Write(ref path)) | Ok(Create(ref path)) if path == &input_path => { - let contents = std::fs::read_to_string(path)?; - driver.set_file_text(file_id, &contents); - if !driver.emit_diagnostics(&mut stderr())? { - driver.write_assembly(file_id)?; - println!("Successfully compiled: {}", path.display()) + // Insert Ctrl+C handler so we can gracefully quit + let should_quit = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let r = should_quit.clone(); + ctrlc::set_handler(move || { + r.store(true, std::sync::atomic::Ordering::SeqCst); + }) + .expect("error setting ctrl-c handler"); + + // Start watching filesystem events. + while !should_quit.load(std::sync::atomic::Ordering::SeqCst) { + if let Ok(event) = watcher_rx.recv_timeout(Duration::from_millis(1)) { + use notify::DebouncedEvent::*; + match event { + Write(ref path) if is_source_file(path) => { + let relative_path = compute_source_relative_path(&source_directory, path)?; + let file_contents = std::fs::read_to_string(path)?; + log::info!("Modifying {}", relative_path.display()); + driver.update_file(relative_path, file_contents); + if !driver.emit_diagnostics(&mut stderr())? { + driver.write_all_assemblies()?; + } + } + Create(ref path) if is_source_file(path) => { + let relative_path = compute_source_relative_path(&source_directory, path)?; + let file_contents = std::fs::read_to_string(path)?; + log::info!("Creating {}", relative_path.display()); + driver.add_file(relative_path, file_contents); + if !driver.emit_diagnostics(&mut stderr())? { + driver.write_all_assemblies()?; + } + } + Remove(ref path) if is_source_file(path) => { + // Simply remove the source file from the source root + let relative_path = compute_source_relative_path(&source_directory, path)?; + log::info!("Removing {}", relative_path.display()); + let assembly_path = driver.assembly_output_path(driver.get_file_id_for_path(&relative_path).expect("cannot remove a file that was not part of the compilation in the first place")); + if assembly_path.is_file() { + std::fs::remove_file(assembly_path)?; + } + driver.remove_file(relative_path); + driver.emit_diagnostics(&mut stderr())?; + } + Rename(ref from, ref to) => { + // Renaming is done by changing the relative path of the original source file but + // not modifying any text. This ensures that most of the cache for the renamed file + // stays alive. This is effectively a rename of the file_id in the database. + let from_relative_path = compute_source_relative_path(&source_directory, from)?; + let to_relative_path = compute_source_relative_path(&source_directory, to)?; + + log::info!( + "Renaming {} to {}", + from_relative_path.display(), + to_relative_path.display(), + ); + driver.rename(from_relative_path, to_relative_path); + if !driver.emit_diagnostics(&mut stderr())? { + driver.write_all_assemblies()?; + } } + _ => {} } - Ok(_) => {} - Err(e) => eprintln!("Watcher error: {:?}", e), } } + + Ok(true) } diff --git a/crates/mun_hir/src/input.rs b/crates/mun_hir/src/input.rs index df06681f1..2e422d9f9 100644 --- a/crates/mun_hir/src/input.rs +++ b/crates/mun_hir/src/input.rs @@ -1,5 +1,4 @@ -use relative_path::{RelativePath, RelativePathBuf}; -use rustc_hash::FxHashMap; +use std::collections::HashSet; /// `FileId` is an integer which uniquely identifies a file. File paths are messy and /// system-dependent, so most of the code should work directly with `FileId`, without inspecting the @@ -21,23 +20,20 @@ pub struct SourceRootId(pub u32); #[derive(Default, Clone, Debug, PartialEq, Eq)] pub struct SourceRoot { - files: FxHashMap, + files: HashSet, } impl SourceRoot { pub fn new() -> SourceRoot { Default::default() } - pub fn insert_file(&mut self, path: RelativePathBuf, file_id: FileId) { - self.files.insert(path, file_id); + pub fn insert_file(&mut self, file_id: FileId) { + self.files.insert(file_id); } - pub fn remove_file(&mut self, path: &RelativePath) { - self.files.remove(path); + pub fn remove_file(&mut self, file_id: FileId) { + self.files.remove(&file_id); } pub fn files(&self) -> impl Iterator + '_ { - self.files.values().copied() - } - pub fn file_by_relative_path(&self, path: &RelativePath) -> Option { - self.files.get(path).copied() + self.files.iter().copied() } } diff --git a/crates/mun_hir/src/mock.rs b/crates/mun_hir/src/mock.rs index b151be167..2958a3d81 100644 --- a/crates/mun_hir/src/mock.rs +++ b/crates/mun_hir/src/mock.rs @@ -46,7 +46,7 @@ impl MockDatabase { db.set_file_relative_path(file_id, rel_path.clone()); db.set_file_text(file_id, Arc::new(text.to_string())); db.set_file_source_root(file_id, source_root_id); - source_root.insert_file(rel_path, file_id); + source_root.insert_file(file_id); db.set_source_root(source_root_id, Arc::new(source_root)); (db, file_id) diff --git a/crates/mun_project/Cargo.toml b/crates/mun_project/Cargo.toml new file mode 100644 index 000000000..ea030f650 --- /dev/null +++ b/crates/mun_project/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "mun_project" +version = "0.1.0" +authors = ["The Mun Team "] +edition = "2018" + +[dependencies] +serde = "1.0" +serde_derive = "1.0" +toml = "0.5" +semver = { version = "0.10", features = ["serde"] } +anyhow = "1.0" diff --git a/crates/mun_project/src/lib.rs b/crates/mun_project/src/lib.rs new file mode 100644 index 000000000..a32333474 --- /dev/null +++ b/crates/mun_project/src/lib.rs @@ -0,0 +1,7 @@ +mod manifest; +mod package; + +pub use manifest::{Manifest, ManifestMetadata, PackageId}; +pub use package::Package; + +pub const MANIFEST_FILENAME: &str = "mun.toml"; diff --git a/crates/mun_project/src/manifest.rs b/crates/mun_project/src/manifest.rs new file mode 100644 index 000000000..76a23d3d4 --- /dev/null +++ b/crates/mun_project/src/manifest.rs @@ -0,0 +1,109 @@ +use std::fmt; +use std::path::Path; +use std::str::FromStr; + +mod toml; + +/// Contains all information of a package. Usually this information is read from a mun.toml file. +#[derive(PartialEq, Clone, Debug)] +pub struct Manifest { + package_id: PackageId, + metadata: ManifestMetadata, +} + +/// General metadata for a package. +#[derive(PartialEq, Clone, Debug)] +pub struct ManifestMetadata { + pub authors: Vec, +} + +/// Unique identifier of a package and version +#[derive(PartialEq, Clone, Debug)] +pub struct PackageId { + name: String, + version: semver::Version, +} + +impl Manifest { + /// Try to read a manifest from a file + pub fn from_file>(path: P) -> Result { + // Load the contents of the file + let file_contents = std::fs::read_to_string(path)?; + Self::from_str(&file_contents) + } + + /// Returns the unique identifier of this manifest based on the name and version + pub fn package_id(&self) -> &PackageId { + &self.package_id + } + + /// Returns the name of the package + pub fn name(&self) -> &str { + &self.package_id.name + } + + /// Returns the version of the package + pub fn version(&self) -> &semver::Version { + &self.package_id.version + } + + /// Returns the metadata information of the package + pub fn metadata(&self) -> &ManifestMetadata { + &self.metadata + } +} + +impl PackageId { + /// Returns the name of the package + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the version of the package + pub fn version(&self) -> &semver::Version { + &self.version + } +} + +impl fmt::Display for PackageId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} v{}", self.name, self.version) + } +} + +impl std::str::FromStr for Manifest { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + // Parse the contents of the file to toml manifest + let manifest = ::toml::from_str::(s)?; + manifest.into_real_manifest() + } +} + +#[cfg(test)] +mod tests { + use crate::Manifest; + use std::str::FromStr; + + #[test] + fn parse() { + let manifest = Manifest::from_str( + r#" + [package] + name="test" + version="0.2.0" + authors = ["Mun Team"] + "#, + ) + .unwrap(); + + assert_eq!(manifest.name(), "test"); + assert_eq!( + manifest.version(), + &semver::Version::from_str("0.2.0").unwrap() + ); + assert_eq!(manifest.metadata.authors, vec!["Mun Team"]); + assert_eq!(format!("{}", manifest.package_id()), "test v0.2.0"); + } +} diff --git a/crates/mun_project/src/manifest/toml.rs b/crates/mun_project/src/manifest/toml.rs new file mode 100644 index 000000000..d3e3ae75f --- /dev/null +++ b/crates/mun_project/src/manifest/toml.rs @@ -0,0 +1,37 @@ +use super::{Manifest, ManifestMetadata, PackageId}; +use serde_derive::{Deserialize, Serialize}; + +/// A manifest as specified in a mun.toml file. +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct TomlManifest { + package: TomlProject, +} + +/// Represents the `package` section of a mun.toml file. +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct TomlProject { + name: String, + version: semver::Version, + authors: Option>, +} + +impl TomlManifest { + /// Convert this toml manifest into a "real" manifest. + pub fn into_real_manifest(self) -> Result { + let name = self.package.name.trim(); + if name.is_empty() { + anyhow::bail!("package name cannot be an empty string"); + } + + Ok(Manifest { + package_id: PackageId { + name: name.to_owned(), + version: self.package.version, + }, + metadata: ManifestMetadata { + authors: self.package.authors.unwrap_or_default(), + }, + }) + } +} diff --git a/crates/mun_project/src/package.rs b/crates/mun_project/src/package.rs new file mode 100644 index 000000000..44cb31492 --- /dev/null +++ b/crates/mun_project/src/package.rs @@ -0,0 +1,78 @@ +use crate::{Manifest, PackageId}; +use semver::Version; +use std::fmt; +use std::path::{Path, PathBuf}; + +#[derive(Clone, PartialEq, Debug)] +pub struct Package { + // The manifest of the package + manifest: Manifest, + // The location of the manifest which marks the root of the package + manifest_path: PathBuf, +} + +impl Package { + /// Creates a package from a manifest and its location + pub fn new(manifest: Manifest, manifest_path: &Path) -> Self { + Self { + manifest, + manifest_path: manifest_path.to_path_buf(), + } + } + + /// Creates a package by loading the information from a file + pub fn from_file>(path: P) -> anyhow::Result { + let path = path.as_ref(); + let manifest = Manifest::from_file(path)?; + Ok(Self { + manifest, + manifest_path: path.to_path_buf(), + }) + } + + /// Returns the manifest + pub fn manifest(&self) -> &Manifest { + &self.manifest + } + + /// Returns the path of the manifest + pub fn manifest_path(&self) -> &Path { + &self.manifest_path + } + + /// Returns the name of the package + pub fn name(&self) -> &str { + self.manifest().name() + } + + /// Returns the `PackageId` object for the package + pub fn package_id(&self) -> &PackageId { + self.manifest().package_id() + } + + /// Returns the root folder of the package + pub fn root(&self) -> &Path { + self.manifest_path().parent().unwrap() + } + + /// Returns the version of the package + pub fn version(&self) -> &Version { + self.manifest().version() + } + + /// Returns the source directory of the package, or None if no such directory exists. + pub fn source_directory(&self) -> Option { + let source_dir = self.root().join("src"); + if source_dir.is_dir() { + Some(source_dir) + } else { + None + } + } +} + +impl fmt::Display for Package { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.package_id()) + } +} diff --git a/crates/mun_runtime/benches/util/mod.rs b/crates/mun_runtime/benches/util/mod.rs index 945e627e5..12a0b67ef 100644 --- a/crates/mun_runtime/benches/util/mod.rs +++ b/crates/mun_runtime/benches/util/mod.rs @@ -31,7 +31,8 @@ pub fn runtime_from_file>(p: P) -> Rc