diff --git a/README.md b/README.md index cb5d5359..8b71af4e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # The Compose Programming Language ```rust -# compose::test::assert_eval(r#" println("'Hello, world' from Compose!"); -# "#); ``` Compose is a functionally flavoured interpreted programming language with Rust-like syntax. @@ -64,7 +62,7 @@ This should print `Hello, from Compose!` to the console. ### Run a file ```bash -compose file examples/hello.cmps +compose file hello.cmps ``` ### Start a REPL @@ -76,7 +74,7 @@ compose repl Load a file in the REPL: ```bash -compose repl --from examples/prelude.cmps +compose repl --from prelude.cmps ``` ### Explain an error diff --git a/compose-cli/src/file.rs b/compose-cli/src/file.rs index 1b61069a..f0f10702 100644 --- a/compose-cli/src/file.rs +++ b/compose-cli/src/file.rs @@ -1,7 +1,7 @@ use crate::error::CliError; use crate::world::SystemWorld; use crate::FileArgs; -use compose_eval::{EvalConfig, Machine}; +use compose_eval::{Machine}; use compose_library::diag::Warned; use crate::repl::print_tokens; @@ -24,7 +24,7 @@ pub fn file(args: FileArgs) -> Result<(), CliError> { crate::print_diagnostics(&world, &[], &warnings).unwrap(); } - let Warned { value, warnings } = compose_eval::eval_source(&source, &mut vm, &EvalConfig::default()); + let Warned { value, warnings } = compose_eval::eval_source(&source, &mut vm); if let Err(err) = value { crate::print_diagnostics(&world, &err, &warnings).unwrap(); diff --git a/compose-cli/src/repl/mod.rs b/compose-cli/src/repl/mod.rs index fbca92b0..129d26fa 100644 --- a/compose-cli/src/repl/mod.rs +++ b/compose-cli/src/repl/mod.rs @@ -1,15 +1,15 @@ use crate::error::CliError; -use crate::repl::editor::{print_input, EditorFooter, EditorGutter, EditorHistory, EditorReader}; +use crate::repl::editor::{EditorFooter, EditorGutter, EditorHistory, EditorReader, print_input}; use crate::world::SystemWorld; -use crate::{explain, ReplArgs}; +use crate::{ReplArgs, explain}; use compose_editor::editor::Editor; use compose_editor::renderer::full::CrosstermRenderer; -use compose_eval::{EvalConfig, Machine}; -use compose_library::diag::{eco_format, Warned}; +use compose_eval::Machine; +use compose_library::diag::{Warned, eco_format}; +use compose_library::repr::Repr; use compose_library::{Value, World}; use compose_syntax::{FileId, Lexer, Source, SyntaxKind}; use std::fs; -use compose_library::repr::Repr; mod editor; @@ -172,8 +172,7 @@ fn eval_initial_pass(vm: &mut Machine, world: &SystemWorld) { // Evaluate every node in the source, printing any diagnostics along the way. // Do not return early if there are any errors, as we want to print all diagnostics. for i in 0..source.nodes().len() { - let Warned { value, warnings } = - compose_eval::eval_source_range(&source, i..i + 1, vm, &EvalConfig::default()); + let Warned { value, warnings } = compose_eval::eval_source_range(&source, i..i + 1, vm); crate::print_diagnostics(world, &[], &warnings).unwrap(); if let Err(err) = value { crate::print_diagnostics(world, &err, &warnings).unwrap(); @@ -192,7 +191,7 @@ pub fn eval_repl_input(vm: &mut Machine, world: &SystemWorld, input: &str, args: let source = entrypoint(world); let len_after_edit = source.nodes().len(); - + if args.print_tokens { print_tokens(input, source.id()); } @@ -212,12 +211,8 @@ pub fn eval_repl_input(vm: &mut Machine, world: &SystemWorld, input: &str, args: crate::print_diagnostics(world, &[], &syntax_warnings).unwrap(); } - let Warned { value, warnings } = compose_eval::eval_source_range( - &source, - len_before_edit..len_after_edit, - vm, - &EvalConfig::default(), - ); + let Warned { value, warnings } = + compose_eval::eval_source_range(&source, len_before_edit..len_after_edit, vm); if args.debug { println!("{vm:#?}\n"); diff --git a/compose-cli/src/world.rs b/compose-cli/src/world.rs index aa788775..f48b4e24 100644 --- a/compose-cli/src/world.rs +++ b/compose-cli/src/world.rs @@ -1,5 +1,5 @@ use compose_library::diag::{FileError, FileResult}; -use compose_library::{library, Library, World}; +use compose_library::{Library, World}; use compose_syntax::{FileId, Source}; use std::collections::HashMap; use std::fmt::Debug; @@ -53,7 +53,7 @@ impl SystemWorld { sources: Mutex::new(sources), entrypoint, root, - library: library(), + library: Library::default(), }) } @@ -67,7 +67,7 @@ impl SystemWorld { sources: Mutex::new(sources), entrypoint, root: PathBuf::new(), - library: library(), + library: Library::default(), } } diff --git a/compose-doc/src/realise.rs b/compose-doc/src/realise.rs index 6eb52908..0c6854ad 100644 --- a/compose-doc/src/realise.rs +++ b/compose-doc/src/realise.rs @@ -1,5 +1,5 @@ use crate::world::DocWorld; -use compose_eval::{EvalConfig, Machine}; +use compose_eval::{Machine}; use compose_library::diag::{SourceDiagnostic, Warned}; @@ -10,9 +10,6 @@ pub(crate) fn eval_code(code: &str) -> EvalResult { let Warned { value, warnings } = compose_eval::eval_source( &world.source, &mut vm, - &EvalConfig { - include_syntax_warnings: true, - }, ); let stdout = world.stdout.lock().expect("failed to lock stdout").clone(); diff --git a/compose-doc/src/world.rs b/compose-doc/src/world.rs index dfdb3e29..fc41ec9f 100644 --- a/compose-doc/src/world.rs +++ b/compose-doc/src/world.rs @@ -1,7 +1,7 @@ use std::io::{Read, Write}; use std::sync::Mutex; use compose_library::diag::FileResult; -use compose_library::{library, Library, World}; +use compose_library::{Library, World}; use compose_syntax::{FileId, Source}; #[derive(Debug)] @@ -28,7 +28,7 @@ impl DocWorld { Self { source, - library: library(), + library: Library::default(), stdout: Mutex::new(String::new()) } } diff --git a/compose-error-codes-doc-tests/src/lib.rs b/compose-error-codes-doc-tests/src/lib.rs index acde73ff..c1de548f 100644 --- a/compose-error-codes-doc-tests/src/lib.rs +++ b/compose-error-codes-doc-tests/src/lib.rs @@ -9,3 +9,5 @@ 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-eval/src/lib.rs b/compose-eval/src/lib.rs index 7d404455..b7368245 100644 --- a/compose-eval/src/lib.rs +++ b/compose-eval/src/lib.rs @@ -12,12 +12,12 @@ pub mod test; mod vm; pub use crate::vm::Machine; -pub use evaluated::Evaluated; -use compose_library::diag::{error, SourceDiagnostic, SourceResult, Warned}; -use compose_library::Value; +use compose_library::diag::{IntoSourceDiagnostic, SourceDiagnostic, SourceResult, Warned, error}; +use compose_library::{Value, Vm}; use compose_syntax::ast::Statement; -use compose_syntax::Source; -use ecow::{eco_vec, EcoVec}; +use compose_syntax::{Source, Span}; +use ecow::{EcoVec, eco_vec}; +pub use evaluated::Evaluated; use std::cmp::min; use std::ops::Range; @@ -148,23 +148,27 @@ impl Eval for ast::Statement<'_> { ``` */ - pub trait Eval { fn eval(self, vm: &mut Machine) -> SourceResult; } -#[derive(Default)] -pub struct EvalConfig { - /// Whether to include syntax warnings in the returned result. - pub include_syntax_warnings: bool, -} - pub fn eval_source( source: &Source, vm: &mut Machine, - eval_config: &EvalConfig, ) -> Warned> { - eval_source_range(source, 0..usize::MAX, vm, eval_config) + eval_source_range(source, 0..usize::MAX, vm) +} + +pub fn eval(vm: &mut Machine) -> Warned> { + let entry_point = vm.engine().world.entry_point(); + let source = match vm.engine().world.source(entry_point) { + Ok(source) => source, + Err(err) => { + return Warned::new(Err(eco_vec!(err.into_source_diagnostic(Span::detached())))); + } + }; + + eval_source(&source, vm) } /// Eval a source file. @@ -174,7 +178,6 @@ pub fn eval_source_range( source: &Source, eval_range: Range, vm: &mut Machine, - config: &EvalConfig, ) -> Warned> { let mut result = Value::unit(); @@ -188,15 +191,11 @@ pub fn eval_source_range( .map(|e| e.into()) .collect::>(); - let syntax_warnings = if config.include_syntax_warnings { - nodes - .iter() - .flat_map(|n| n.warnings()) - .map(|e| e.into()) - .collect::>() - } else { - eco_vec![] - }; + let syntax_warnings = nodes + .iter() + .flat_map(|n| n.warnings()) + .map(|e| e.into()) + .collect::>(); if !errors.is_empty() { return Warned::new(Err(errors)).with_warnings(syntax_warnings); @@ -209,19 +208,17 @@ pub fn eval_source_range( let span = node.span(); let err = error!(span, "expected a statement, found {:?}", node); - return build_err(&syntax_warnings, vm, eco_vec![err], config); + return build_err(&syntax_warnings, vm, eco_vec![err]); } }; result = match statement.eval(vm) { Ok(value) => value.value, - Err(err) => return build_err(&syntax_warnings, vm, err, config), + Err(err) => return build_err(&syntax_warnings, vm, err), } } let mut warnings = vm.sink_mut().take_warnings(); - if config.include_syntax_warnings { - warnings.extend_from_slice(&syntax_warnings); - } + warnings.extend_from_slice(&syntax_warnings); Warned::new(Ok(result)).with_warnings(warnings) } @@ -230,12 +227,9 @@ pub fn build_err( syntax_warnings: &[SourceDiagnostic], vm: &mut Machine, errs: EcoVec, - config: &EvalConfig, ) -> Warned> { let mut warnings = vm.sink_mut().take_warnings(); - if config.include_syntax_warnings { - warnings.extend_from_slice(syntax_warnings); - } + warnings.extend_from_slice(syntax_warnings); Warned::new(Err(errs)).with_warnings(warnings) } diff --git a/compose-eval/src/statement.rs b/compose-eval/src/statement.rs index 833eb1b9..3e9de768 100644 --- a/compose-eval/src/statement.rs +++ b/compose-eval/src/statement.rs @@ -1,6 +1,6 @@ use crate::evaluated::Evaluated; use crate::vm::FlowEvent; -use crate::{Eval, EvalConfig, Machine, eval_source}; +use crate::{Eval, Machine, eval_source}; use compose_library::diag::{SourceResult, Trace, TracePoint, Warned, bail, error}; use compose_syntax::ast::{AstNode, BreakStatement}; use compose_syntax::{FileId, ast}; @@ -117,7 +117,7 @@ impl Eval for ast::ModuleImport<'_> { let module = vm .with_frame(|vm| { - let Warned { value, warnings } = eval_source(&source, vm, &EvalConfig::default()); + let Warned { value, warnings } = eval_source(&source, vm); value?; vm.sink_mut().warnings.extend(warnings); diff --git a/compose-eval/src/test/mod.rs b/compose-eval/src/test/mod.rs index f280b0ea..e9050daa 100644 --- a/compose-eval/src/test/mod.rs +++ b/compose-eval/src/test/mod.rs @@ -1,10 +1,10 @@ -use crate::{EvalConfig, Machine}; +use crate::{Machine}; use compose_error_codes::ErrorCode; use compose_library::diag::compose_codespan_reporting::term::termcolor::{ ColorChoice, StandardStream, }; use compose_library::diag::{FileError, FileResult, SourceDiagnostic, SourceResult, Warned, write_diagnostics, write_diagnostics_to_string}; -use compose_library::{Library, Value, World, library}; +use compose_library::{Library, Value, World, }; use compose_syntax::{FileId, Source}; use ecow::{EcoVec, eco_format, eco_vec}; use std::collections::HashMap; @@ -64,7 +64,7 @@ impl TestWorld { Self { sources: Mutex::new(sources), entrypoint, - library: library(), + library: Library::default(), stdout: Mutex::new(String::new()), } } @@ -163,9 +163,6 @@ pub fn eval_code_with_vm(vm: &mut Machine, world: &TestWorld, input: &str) -> Te &source, len_before_edit..len_after_edit, vm, - &EvalConfig { - include_syntax_warnings: true, - }, ); TestResult { diff --git a/compose-library/src/lib.rs b/compose-library/src/lib.rs index 8a22e5d2..5fc106f9 100644 --- a/compose-library/src/lib.rs +++ b/compose-library/src/lib.rs @@ -25,10 +25,37 @@ use compose_library::{ #[derive(Clone)] pub struct Library { - /// The module containing global functions, types and values. + /// The module containing global functions, types, and values. pub global: Module, } +impl Default for Library { + fn default() -> Self { + let mut global = Scope::new_lexical(); + + global.define_func::(); + global.define_func::(); + global.define_func::(); + global.define_func::(); + global.define_type::(); + global.define_type::(); + global.define_type::(); + global.define_type::(); + global.define_type::(); + global.define_type::(); + global.define_type::(); + global.define_type::(); + global.define_type::(); + global.define_type::(); + + global.define("std", Module::new("std", global.clone())); + + Library { + global: Module::new("global", global), + } + } +} + impl Library { pub fn empty() -> Self { Self { @@ -47,30 +74,4 @@ impl Trace for Library { fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { self.global.visit_refs(f); } -} - - -pub fn library() -> Library { - let mut global = Scope::new_lexical(); - - global.define_func::(); - global.define_func::(); - global.define_func::(); - global.define_func::(); - global.define_type::(); - global.define_type::(); - global.define_type::(); - global.define_type::(); - global.define_type::(); - global.define_type::(); - global.define_type::(); - global.define_type::(); - global.define_type::(); - global.define_type::(); - - global.define("std", Module::new("std", global.clone())); - - Library { - global: Module::new("global", global), - } -} +} \ No newline at end of file diff --git a/compose/src/cli/mod.rs b/compose/src/cli/mod.rs new file mode 100644 index 00000000..6f4418b9 --- /dev/null +++ b/compose/src/cli/mod.rs @@ -0,0 +1,59 @@ +/*! +# Compose CLI + +## Installation + +Install the latest version from GitHub using [Cargo](https://doc.rust-lang.org/cargo/): + +```bash +cargo install --git https://github.com/Dutch-Raptor/compose.git +``` + +After installation, the `compose` executable will be available on your PATH: + +```bash +compose --version +``` + +## Usage + +Create a new file called `hello.cmps` with the following contents: + +```text +println("Hello, from Compose!") +``` + +and run it: + +```bash +compose file hello.cmps +``` + +This should print `Hello, from Compose!` to the console. + +### Run a file + +```bash +compose file hello.cmps +``` + +### Start a REPL + +```bash +compose repl +``` + +Load a file in the REPL: + +```bash +compose repl --from prelude.cmps +``` + +### Explain an error + +```bash +compose explain E0003 +``` + +This gives examples, explanations, and suggested fixes. +*/ \ No newline at end of file diff --git a/compose/src/embedding/mod.rs b/compose/src/embedding/mod.rs new file mode 100644 index 00000000..0ff3ed21 --- /dev/null +++ b/compose/src/embedding/mod.rs @@ -0,0 +1,5 @@ +/*! +# Embedding Compose + +Coming soon! +*/ \ No newline at end of file diff --git a/compose/src/language/basics.rs b/compose/src/language/basics.rs index e7f2556d..009071a8 100644 --- a/compose/src/language/basics.rs +++ b/compose/src/language/basics.rs @@ -181,7 +181,7 @@ compose_doc!( Closures can **capture variables** from their surrounding environment. In Compose, only **boxed values** (`box`) can be captured by reference. This helps ensure clarity and memory safety. - Variables need to be captured explicitly using a capture list: `let closure = |x| () => x + 1;` (the `|x|` part is the capture list). + Variables need to be captured explicitly using a capture list: `let closure = { |x| => x + 1; };` (the `|x|` part is the capture list). #### ✅ Capturing by Reference diff --git a/compose/src/language/mod.rs b/compose/src/language/mod.rs index 4f8bafe4..9d8316fb 100644 --- a/compose/src/language/mod.rs +++ b/compose/src/language/mod.rs @@ -1,3 +1,6 @@ +/*! +# Language overview +*/ mod introduction; mod basics; mod patterns; diff --git a/compose/src/lib.rs b/compose/src/lib.rs index 01d763d7..0a823220 100644 --- a/compose/src/lib.rs +++ b/compose/src/lib.rs @@ -7,7 +7,7 @@ println("'Hello, world' from Compose!"); # "#); ``` -Compose is a functionally flavoured interpreted programming language with Rust-like syntax. +Compose is a functional-flavoured, interpreted programming language with Rust-like syntax. Features: - Expression focused: blocks and control flow (`if`, `match`, loops) are expressions and produce values. @@ -15,7 +15,7 @@ Features: - High quality diagnostics - Variables are immutable by default - Garbage collection -- Portable: runs on any platform that supports rust +- Portable: runs on any platform that supports Rust
@@ -23,55 +23,65 @@ Compose is being developed as a hobby project and is not intended for production
-To learn about the language, see the [language] module. -To learn about how compose is implemented internally, see the [implementation] docs. +## Documentation overview -# Using this crate +- The [`language`] module documents Compose syntax and semantics. +- The [`implementation`] module documents the internals of the language implementation. +- The [`embedding`] module documents how to embed Compose in your application. +- The [`cli`] module documents the CLI. -Add compose to your dependencies: +## Using this crate + +> Below follows a minimal example of embedding Compose in your application. If you want to take a deeper dive, check out the [embedding] documentation. + +Add Compose to your dependencies: ```toml [dependencies] compose = { git = "https://github.com/Dutch-Raptor/compose.git" } ``` -To make Compose portable, all interaction with the outside world goes through the [`World`] trait. +### Creating a [`World`] + +Compose is designed to be **embedded** in your application. All interactions with the outside world (like reading files, printing to the console, etc.) go through the [`World`] trait. This allows running Compose in a CLI, in tests, or embedded in another application. + +A [`World`] should provide: +- The entrypoint source of the program. +- A way to access source files. +- Standard input and output. +- The standard library. + +Source files could be loaded from disk, stored in memory, or come from any other source. +Standard input and output can be virtualised. The standard library can be customised to provide additional functionality. -Let's start by implementing that. +In this minimal example, we define a [`World`] that contains a single source file. ```rust use compose::{ World, - eval_source, - eval::EvalConfig, - eval::Machine, library::Library, - library::repr::Repr, - library::diag::{FileError, FileResult, print_diagnostics}, - syntax::FileId, - syntax::Source, - library::{Value} + library::diag::{FileError, FileResult}, + syntax::{FileId, Source}, }; use std::collections::HashMap; use std::io::{Read, Write}; struct ExampleWorld { - /// The entrypoint - main: FileId, - /// Any other sources that have been loaded - sources: HashMap, + main: Source, library: Library, } impl World for ExampleWorld { fn entry_point(&self) -> FileId { - self.main + self.main.id() } + // This example world only supports a single source, so that is the only source it can return. fn source(&self, file_id: FileId) -> FileResult { - match self.sources.get(&file_id).cloned() { - Some(source) => Ok(source), - None => Err(FileError::NotFound(file_id.path().to_path_buf())), + if file_id == self.main.id() { + Ok(self.main.clone()) + } else { + Err(FileError::NotFound(file_id.path().to_path_buf())) } } @@ -79,6 +89,7 @@ impl World for ExampleWorld { &self.library } + // Write to stdout fn write( &self, f: &mut dyn FnMut(&mut dyn Write) -> std::io::Result<()>, @@ -87,6 +98,7 @@ impl World for ExampleWorld { f(&mut stdout_lock) } + // read from stdin fn read( &self, f: &mut dyn FnMut(&mut dyn Read) -> std::io::Result<()>, @@ -99,62 +111,135 @@ impl World for ExampleWorld { impl ExampleWorld { fn new(main_source: Source) -> Self { Self { - main: main_source.id(), - sources: { - let mut map = HashMap::new(); - map.insert(main_source.id(), main_source); - map - }, - library: Library::empty(), + main: main_source, + library: Library::default(), } } } +``` -fn main() { - // Define/load the source to be interpreted - let source_text = r#" - println("Hello from Compose"); - 2 + 3 // the value of the last expression is returned - "#; +### Loading source code - let file_name = "main.cmps"; - let source = Source::from_string(file_name, source_text); +Compose source code is represented by the [`Source`](compose_syntax::Source) type. - // Define the world that contains the source - let world = ExampleWorld::new(source.clone()); - // Create a VM with access to that world. - let mut vm = Machine::new(&world); +```rust +use compose::syntax::Source; - // evaluate the source - let warned_result = eval_source(&source, &mut vm, &EvalConfig::default()); +let source_text = r#" + println("Hello from Compose"); + 2 + 3 // the value of the last expression is returned +"#; +let source = Source::from_string("main.cmps", source_text); +``` - print_diagnostics(&world, &[], &warned_result.warnings, false) - .expect("Failed to print diagnostics"); +### Evaluating source code - let result = match warned_result.value { - Ok(value) => value, - Err(errors) => { - print_diagnostics(&world, &errors, &[], false) - .expect("Failed to print diagnostics"); - return; - } - }; +```rust +use compose::{ + evaluation::{Machine, eval}, + library::diag::print_diagnostics, + library::repr::Repr, + library::Value, + # syntax::Source, + # syntax::FileId, + # World, + # library::Library, + # library::diag::{FileError, FileResult}, +}; +use std::process::exit; +# use std::collections::HashMap; +# use std::io::{Read, Write}; + +# struct ExampleWorld { +# main: Source, +# library: Library, +# } +# +# impl World for ExampleWorld { +# fn entry_point(&self) -> FileId { +# self.main.id() +# } +# +# // This example world only supports a single, so that is the only source it can return. +# fn source(&self, file_id: FileId) -> FileResult { +# if file_id == self.main.id() { +# Ok(self.main.clone()) +# } else { +# Err(FileError::NotFound(file_id.path().to_path_buf())) +# } +# } +# +# fn library(&self) -> &Library { +# &self.library +# } +# +# // Write to stdout +# fn write( +# &self, +# f: &mut dyn FnMut(&mut dyn Write) -> std::io::Result<()>, +# ) -> std::io::Result<()> { +# let mut stdout_lock = std::io::stdout().lock(); +# f(&mut stdout_lock) +# } +# +# // read from stdin +# fn read( +# &self, +# f: &mut dyn FnMut(&mut dyn Read) -> std::io::Result<()>, +# ) -> std::io::Result<()> { +# let mut stdin_lock = std::io::stdin().lock(); +# f(&mut stdin_lock) +# } +# } +# +# impl ExampleWorld { +# fn new(main_source: Source) -> Self { +# Self { +# main: main_source, +# library: Library::default(), +# } +# } +# } + +# fn main() { +# let source = Source::from_string("main.cmps", "2 + 3"); +let world = ExampleWorld::new(source.clone()); +let mut vm = Machine::new(&world); + +// `eval` evaluates the entrypoint source provided by the `world`. +let result = eval(&mut vm); + +// Print any warnings +print_diagnostics(&world, &[], &result.warnings, false).unwrap(); + +let value = match result.value { + Ok(value) => value, + Err(errors) => { + print_diagnostics(&world, &errors, &[], false).unwrap(); + exit(1); + } +}; - println!("{}", result.repr(&vm)); +// Print the resulting value +println!("{}", value.repr(&vm)); +assert_eq!(Value::Int(5), value); +# } - assert_eq!(Value::Int(5), result); -} ``` + +To learn more about embedding Compose in your application, check out the [embedding] documentation. */ pub mod implementation; pub mod language; +pub mod embedding; +pub mod cli; pub use compose_eval::{eval_source, eval_source_range, test}; pub use compose_library::{SourceResult, World, diag::SourceDiagnostic, diag::Warned}; #[doc(hidden)] -pub use compose_eval as eval; +pub use compose_eval as evaluation; #[doc(hidden)] pub use compose_library as library; #[doc(hidden)]