diff --git a/compose-cli/src/file.rs b/compose-cli/src/file.rs index f0f10702..ec1c1703 100644 --- a/compose-cli/src/file.rs +++ b/compose-cli/src/file.rs @@ -21,17 +21,17 @@ pub fn file(args: FileArgs) -> Result<(), CliError> { let warnings: Vec<_> = source.warnings().into_iter().map(|w| w.into()).collect(); if !warnings.is_empty() { - crate::print_diagnostics(&world, &[], &warnings).unwrap(); + crate::print_diagnostics(&world, &[], &warnings).expect("failed to print diagnostics"); } let Warned { value, warnings } = compose_eval::eval_source(&source, &mut vm); if let Err(err) = value { - crate::print_diagnostics(&world, &err, &warnings).unwrap(); + crate::print_diagnostics(&world, &err, &warnings).expect("failed to print diagnostics"); return Err(CliError::Execution); } - crate::print_diagnostics(&world, &[], &warnings).unwrap(); + crate::print_diagnostics(&world, &[], &warnings).expect("failed to print diagnostics"); Ok(()) } diff --git a/compose-cli/src/world.rs b/compose-cli/src/world.rs index f48b4e24..6a498eb6 100644 --- a/compose-cli/src/world.rs +++ b/compose-cli/src/world.rs @@ -89,8 +89,8 @@ impl SystemWorld { } // try to read from disk - let path = file_id.path().0.clone(); - let text = std::fs::read_to_string(&path).map_err(|e| FileError::from_io(e, &path))?; + let path = file_id.path().as_path(); + let text = std::fs::read_to_string(path).map_err(|e| FileError::from_io(e, path))?; let source = Source::new(file_id, text); sources.insert(source.id(), source.clone()); Ok(source) diff --git a/compose-error-codes-doc-tests/src/lib.rs b/compose-error-codes-doc-tests/src/lib.rs index c1de548f..6de36a73 100644 --- a/compose-error-codes-doc-tests/src/lib.rs +++ b/compose-error-codes-doc-tests/src/lib.rs @@ -4,10 +4,8 @@ Documentation and doc-tests for the Compose error codes. The purpose of this crate is to provide the compose error codes as a Rust module, so that they can be included in docs.rs and the examples can be tested. -The source of the error codes is in the [`compose-error-codes`] crate. The description of the error code is transformed +The source of the error codes is in the `compose-error-codes` crate. The description of the error code is transformed via `compose_doc::transform_markdown` into documented items with doc-tests. */ include!(concat!(env!("OUT_DIR"), "/Error_Codes")); - -pub use compose_error_codes; diff --git a/compose-syntax/src/file.rs b/compose-syntax/src/file.rs index 30bd2765..b560701e 100644 --- a/compose-syntax/src/file.rs +++ b/compose-syntax/src/file.rs @@ -43,8 +43,8 @@ impl VirtualPath { self.0.display().to_string() } - pub fn to_path_buf(&self) -> PathBuf { - self.0.clone() + pub fn as_path(&self) -> &PathBuf { + &self.0 } } diff --git a/compose-syntax/src/source.rs b/compose-syntax/src/source.rs index 8cbb15d0..72b4dbed 100644 --- a/compose-syntax/src/source.rs +++ b/compose-syntax/src/source.rs @@ -5,6 +5,9 @@ use crate::{parse, Span, SyntaxError, SyntaxNode}; use std::path::PathBuf; use std::sync::Arc; +/// A Compose source file. +/// +/// Uses reference counting, so it is quite inexpensive to clone #[derive(Clone, Debug)] pub struct Source(Arc); diff --git a/compose/src/embedding/mod.rs b/compose/src/embedding/mod.rs index 0ff3ed21..3209e193 100644 --- a/compose/src/embedding/mod.rs +++ b/compose/src/embedding/mod.rs @@ -1,5 +1,238 @@ /*! # Embedding Compose -Coming soon! -*/ \ No newline at end of file +To learn about embedding Compose in your application, we'll create a CLI similar to the one +provided in compose_cli. + +It will support executing a Compose source file, loading other source files, reading from stdin, and printing to stdout. + +## Creating a World + +We want to create a [`World`](crate::World) implementation backed by the file system working from the current directory. + +```rust +use std::collections::HashMap; +use std::fs; +use std::io::{Read, Write}; +use std::path::{PathBuf, Path}; +use std::sync::Mutex; +use compose::library::diag::{FileError, FileResult}; +use compose::library::{Library, World}; +use compose::syntax::{FileId, Source}; + +struct SystemWorld { + entrypoint: FileId, + // Since the world trait works through a shared reference, we need to add a mutex + // to allow mutation through a shared reference. + sources: Mutex>, + library: Library, + // Keep track of the root directory so that we can resolve relative paths. + root: PathBuf, +} + +impl SystemWorld { + fn from_file(path: impl AsRef) -> FileResult { + let path = path.as_ref(); + let root = path.parent().unwrap().to_path_buf() + .canonicalize() + .map_err(|e| FileError::from_io(e, path))?; + + let entrypoint = FileId::new(path); + let text = fs::read_to_string(path) + .map_err(|e| FileError::from_io(e, path))?; + let source = Source::new(entrypoint, text); + + let mut sources = HashMap::new(); + sources.insert(source.id(), source); + + Ok(Self { + entrypoint, + sources: Mutex::new(sources), + library: Library::default(), + root, + }) + } + + fn add_source(&self, source: Source) { + self.sources.lock().unwrap().insert(source.id(), source); + } + + fn get_or_read_from_disk(&self, file_id: FileId) -> FileResult { + let mut sources = self.sources.lock().unwrap(); + if let Some(source) = sources.get(&file_id) { + // Source uses reference counting and is inexpensive to clone, so we can return it directly. + return Ok(source.clone()); + } + + let path = file_id.path(); + + let file_contents = fs::read_to_string(path.as_path()) + .map_err(|e| FileError::from_io(e, path.as_path()))?; + + let source = Source::new(file_id, file_contents); + + sources.insert(source.id(), source.clone()); + + Ok(source) + } +} + +impl World for SystemWorld { + fn entry_point(&self) -> FileId { + self.entrypoint + } + + fn source(&self, file_id: FileId) -> FileResult { + self.get_or_read_from_disk(file_id) + } + + fn library(&self) -> &Library { + &self.library + } + + fn write(&self, f: &mut dyn FnMut(&mut dyn Write) -> std::io::Result<()>) -> std::io::Result<()> { + f(&mut std::io::stdout()) + } + + fn read(&self, f: &mut dyn FnMut(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()> { + f(&mut std::io::stdin()) + } + + fn name(&self, id: FileId) -> String { + // return the path relative to the root, if within the root. + id.path() + .0 + .strip_prefix(&self.root) + .unwrap_or(&id.path().0) + .display() + .to_string() + } +} +``` + +That was most of the work! All that's left to do is reading the +file path from the command line arguments, creating a world, creating a vm, and evaluating the file. + +```rust +# use std::collections::HashMap; +# use std::fs; +# use std::io::{Read, Write}; +use std::path::PathBuf; +# use std::path::Path; +# use std::sync::Mutex; +# use compose::library::diag::{FileError, FileResult}; +# use compose::library::{Library, World}; +# use compose::syntax::{FileId, Source}; +# +# struct SystemWorld { +# entrypoint: FileId, +# // Since the world trait works through a shared reference, we need to add a mutex +# // to allow mutation through a shared reference. +# sources: Mutex>, +# library: Library, +# // Keep track of the root directory so that we can resolve relative paths. +# root: PathBuf, +# } +# +# impl SystemWorld { +# fn from_file(path: impl AsRef) -> FileResult { +# let path = path.as_ref(); +# let root = path.parent().unwrap().to_path_buf() +# .canonicalize() +# .map_err(|e| FileError::from_io(e, &path))?; +# +# let entrypoint = FileId::new(path); +# let text = fs::read_to_string(path) +# .map_err(|e| FileError::from_io(e, &path))?; +# let source = Source::new(entrypoint, text); +# +# let mut sources = HashMap::new(); +# sources.insert(source.id(), source); +# +# Ok(Self { +# entrypoint, +# sources: Mutex::new(sources), +# library: Library::default(), +# root, +# }) +# } +# +# fn add_source(&self, source: Source) { +# self.sources.lock().unwrap().insert(source.id(), source); +# } +# +# fn get_or_read_from_disk(&self, file_id: FileId) -> FileResult { +# let mut sources = self.sources.lock().unwrap(); +# if let Some(source) = sources.get(&file_id) { +# // Source uses reference counting and is inexpensive to clone, so we can return it directly. +# return Ok(source.clone()); +# } +# +# let path = file_id.path(); +# +# let file_contents = fs::read_to_string(path.as_path()) +# .map_err(|e| FileError::from_io(e, path.as_path()))?; +# +# let source = Source::new(file_id, file_contents); +# +# sources.insert(source.id(), source.clone()); +# +# Ok(source) +# } +# } +# +# impl World for SystemWorld { +# fn entry_point(&self) -> FileId { +# self.entrypoint +# } +# +# fn source(&self, file_id: FileId) -> FileResult { +# self.get_or_read_from_disk(file_id) +# } +# +# fn library(&self) -> &Library { +# &self.library +# } +# +# fn write(&self, f: &mut dyn FnMut(&mut dyn Write) -> std::io::Result<()>) -> std::io::Result<()> { +# f(&mut std::io::stdout()) +# } +# +# fn read(&self, f: &mut dyn FnMut(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()> { +# f(&mut std::io::stdin()) +# } +# +# fn name(&self, id: FileId) -> String { +# // return the path relative to the root, if within the root. +# id.path() +# .0 +# .strip_prefix(&self.root) +# .unwrap_or(&id.path().0) +# .display() +# .to_string() +# } +# } + + +use std::process::exit; +use compose_eval::{eval, Machine}; +use compose_library::diag::print_diagnostics; + +fn file(file_path: impl AsRef) { + let path = file_path.as_ref(); + let world = SystemWorld::from_file(&path).expect("Failed to load entrypoint file"); + let mut vm = Machine::new(&world); + + let result = eval(&mut vm); + + if let Err(err) = result.value { + print_diagnostics(&world, &err, &result.warnings, false).expect("failed to print diagnostics"); + exit(1); + } + + // print warnings + print_diagnostics(&world, &[], &result.warnings, false).expect("failed to print diagnostics"); +} +``` +*/ + diff --git a/compose/src/lib.rs b/compose/src/lib.rs index 0a823220..5e51caba 100644 --- a/compose/src/lib.rs +++ b/compose/src/lib.rs @@ -81,7 +81,7 @@ impl World for ExampleWorld { if file_id == self.main.id() { Ok(self.main.clone()) } else { - Err(FileError::NotFound(file_id.path().to_path_buf())) + Err(FileError::NotFound(file_id.path().as_path().clone())) } } @@ -166,7 +166,7 @@ use std::process::exit; # if file_id == self.main.id() { # Ok(self.main.clone()) # } else { -# Err(FileError::NotFound(file_id.path().to_path_buf())) +# Err(FileError::NotFound(file_id.path().as_path().clone())) # } # } #