diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000..7cc19b5c --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,26 @@ +name: Rust + +on: + push: + branches: [ master ] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose --workspace diff --git a/.idea/compose.iml b/.idea/compose.iml index 1b1c7035..b33f28b8 100644 --- a/.idea/compose.iml +++ b/.idea/compose.iml @@ -19,6 +19,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index 0ea7f29b..6790fc14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,7 +261,10 @@ version = "0.1.0" dependencies = [ "compose-doc-macros", "compose-error-codes", + "compose-error-codes-doc-tests", "compose-eval", + "compose-library", + "compose-syntax", ] [[package]] @@ -339,6 +342,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "compose-error-codes-doc-tests" +version = "0.1.0" +dependencies = [ + "compose-doc", + "compose-error-codes", + "compose-eval", +] + [[package]] name = "compose-eval" version = "0.1.0" @@ -346,6 +358,7 @@ dependencies = [ "compose-error-codes", "compose-library", "compose-syntax", + "compose-utils", "ecow", "extension-traits", "tap", @@ -384,6 +397,7 @@ dependencies = [ "compose-utils", "ecow", "extension-traits", + "itertools", "unscanny", ] @@ -481,6 +495,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef5eeffa816451f3a6a4cce9cd796e3e5ba6018638d3ce5cc7f87b73bababf60" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -696,6 +716,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "jpeg-decoder" version = "0.1.22" diff --git a/Cargo.toml b/Cargo.toml index 22b2be28..4426096e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "compose-utils", "compose", "compose-error-codes", + "compose-error-codes-doc-tests", "compose-doc-macros", "compose-doc", "compose-codespan-reporting" @@ -36,10 +37,12 @@ compose-macros = { path = "compose-macros" } compose-syntax = { path = "compose-syntax" } compose-utils = { path = "compose-utils" } compose-error-codes = { path = "compose-error-codes" } +compose-error-codes-doc-tests = { path = "compose-error-codes-doc-tests" } compose-editor = { path = "compose-editor" } compose-doc-macros = { path = "compose-doc-macros" } compose-doc = { path = "compose-doc" } compose-codespan-reporting = { path = "compose-codespan-reporting" } +itertools = "0.14.0" [workspace.lints.rust] unsafe_code = "deny" diff --git a/compose-cli/src/explain.rs b/compose-cli/src/explain.rs index df539f77..e294262e 100644 --- a/compose-cli/src/explain.rs +++ b/compose-cli/src/explain.rs @@ -15,7 +15,14 @@ pub(crate) fn explain(code: &str) -> Result<(), CliError> { Some(code) => { let md_text = code.description; - let explained = match compose_doc::transform_markdown(md_text, &Config::ansi()) { + let explained = match compose_doc::transform_markdown( + md_text, + &Config::new() + .with_ansi() + // make sure that the explain command works even if the explanation is technically not fully accurate + .with_output_block_error_mode(compose_doc::ErrorHandlingMode::Ignore) + .with_code_block_error_mode(compose_doc::ErrorHandlingMode::Ignore), + ) { Ok(v) => v, Err(e) => { eprintln!( diff --git a/compose-cli/src/world.rs b/compose-cli/src/world.rs index 6fa4ef97..aa788775 100644 --- a/compose-cli/src/world.rs +++ b/compose-cli/src/world.rs @@ -110,11 +110,11 @@ impl World for SystemWorld { &self.library } - fn write(&self, f: &dyn Fn(&mut dyn Write) -> std::io::Result<()>) -> std::io::Result<()> { + 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: &dyn Fn(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()> { + fn read(&self, f: &mut dyn FnMut(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()> { f(&mut std::io::stdin()) } diff --git a/compose-doc-macros/src/lib.rs b/compose-doc-macros/src/lib.rs index 089e4ef9..724caf3b 100644 --- a/compose-doc-macros/src/lib.rs +++ b/compose-doc-macros/src/lib.rs @@ -1,29 +1,34 @@ +use compose_doc::ErrorHandlingMode; use proc_macro::TokenStream as BoundaryStream; use quote::quote; use syn::spanned::Spanned; -use syn::{parse_macro_input, Attribute, Item}; +use syn::{Attribute, Item, parse_macro_input}; use unindent::unindent; #[proc_macro] pub fn compose_doc(input: BoundaryStream) -> BoundaryStream { let ComposeDocInput { attrs, item } = parse_macro_input!(input as ComposeDocInput); - let lines: Vec<_> = attrs.iter() - .filter_map(extract_doc_line) - .collect(); + let lines: Vec<_> = attrs.iter().filter_map(extract_doc_line).collect(); let markdown = unindent(&lines.join("\n")); // Transform markdown - let transformed = - match compose_doc::transform_markdown(&markdown, &compose_doc::Config::no_color()) { - Ok(t) => t, - Err(e) => { - return syn::Error::new_spanned(&item, format!("{}: {}", e.line, e.message)) - .to_compile_error() - .into(); - } - }; + let transformed = match compose_doc::transform_markdown( + &markdown, + &compose_doc::Config::new() + .with_no_color() + .with_code_block_error_mode(ErrorHandlingMode::EmitAsTests) + .with_output_block_error_mode(ErrorHandlingMode::EmitAsTests), + + ) { + Ok(t) => t, + Err(e) => { + return syn::Error::new_spanned(&item, format!("{}: {}", e.line, e.message)) + .to_compile_error() + .into(); + } + }; let doc_lines = transformed.lines().map(|line| quote! { #[doc = #line] }); @@ -38,7 +43,6 @@ pub fn compose_doc(input: BoundaryStream) -> BoundaryStream { output.into() } - struct ComposeDocInput { attrs: Vec, item: Item, diff --git a/compose-doc/src/lib.rs b/compose-doc/src/lib.rs index 9772d679..068ec978 100644 --- a/compose-doc/src/lib.rs +++ b/compose-doc/src/lib.rs @@ -135,7 +135,7 @@ If a block: - emits diagnostics not marked with `error(...)` or `warn(...)`, or - claims to emit diagnostics that aren’t produced by the code, -then `transform_markdown` will fail with a line-referenced error. +then [`transform_markdown`] will fail with a line-referenced error. --- @@ -143,8 +143,9 @@ then `transform_markdown` will fail with a line-referenced error. ```rust use compose_doc::Config; -let config = Config::ansi(); // diagnostics with colour -let config = Config::no_color(); // plain-text diagnostics + +let config = Config::new().with_ansi(); // diagnostics with colour +let config = Config::new().with_no_color(); // plain-text diagnostics ``` */ mod block; @@ -285,7 +286,7 @@ mod tests { "#}; let output = - transform_markdown(input, &Config::no_color()).expect("failed to transform markdown"); + transform_markdown(input, &Config::new().with_no_color()).expect("failed to transform markdown"); assert_eq!( output.trim(), @@ -332,7 +333,7 @@ mod tests { "#}; let output = - transform_markdown(input, &Config::no_color()).expect("failed to transform markdown"); + transform_markdown(input, &Config::new().with_no_color()).expect("failed to transform markdown"); assert_eq!( output.trim(), diff --git a/compose-doc/src/markdown.rs b/compose-doc/src/markdown.rs index e38a07ae..57083778 100644 --- a/compose-doc/src/markdown.rs +++ b/compose-doc/src/markdown.rs @@ -1,33 +1,59 @@ -use crate::block::{BlockHeader, parse_block_header}; -use crate::diag::{At, Error, diagnostics_to_string, line_starts, offset_to_line}; -use crate::realise::{EvalResult, execute_code_block}; -use compose_error_codes::lookup; +use crate::block::{parse_block_header, BlockHeader}; +use crate::diag::{diagnostics_to_string, line_starts, offset_to_line, At, Error}; +use crate::realise::{eval_code, EvalResult}; +use compose_error_codes::{lookup, ErrorCode}; use compose_library::diag::SourceDiagnostic; use pulldown_cmark::{CodeBlockKind, Event, OffsetIter, Options, Parser, Tag}; +use std::cmp::PartialEq; use std::iter::Peekable; pub struct Config { pub diag_mode: DiagMode, + /// How to handle unexpected warnings and errors in code blocks + pub code_block_error_mode: ErrorHandlingMode, + /// How to handle warnings and errors in output blocks that don't occur + pub output_block_error_mode: ErrorHandlingMode, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)] +pub enum ErrorHandlingMode { + EmitAsTests, + Error, + Ignore, } impl Config { #[must_use] pub const fn new() -> Self { - Self::ansi() - } - - #[must_use] - pub const fn ansi() -> Self { Self { diag_mode: DiagMode::Ansi, + code_block_error_mode: ErrorHandlingMode::Error, + output_block_error_mode: ErrorHandlingMode::Error, } } #[must_use] - pub const fn no_color() -> Self { - Self { - diag_mode: DiagMode::NoColor, - } + pub const fn with_ansi(mut self) -> Self { + self.diag_mode = DiagMode::Ansi; + self + } + + #[must_use] + pub const fn with_no_color(mut self) -> Self { + self.diag_mode = DiagMode::NoColor; + self + } + + #[must_use] + pub const fn with_code_block_error_mode(mut self, mode: ErrorHandlingMode) -> Self { + self.code_block_error_mode = mode; + self + } + + #[must_use] + pub const fn with_output_block_error_mode(mut self, mode: ErrorHandlingMode) -> Self { + self.output_block_error_mode = mode; + self } } @@ -42,6 +68,68 @@ pub enum DiagMode { NoColor, } +pub struct Ir<'a> { + events: Vec>, +} + +impl<'a> Ir<'a> { + fn new() -> Self { + Self { events: Vec::new() } + } + + fn push_other(&mut self, event: Event<'a>) { + if !matches!(self.events.last(), Some(DocIr::Other(_))) { + self.events.push(DocIr::Other(Vec::new())); + } + + if let Some(DocIr::Other(events)) = self.events.last_mut() { + events.push(event); + } else { + unreachable!(); + } + } + + fn push_compose_block(&mut self, block: ComposeBlock) { + self.events.push(DocIr::ComposeBlock(block)); + } + + fn push_output_block(&mut self, block: OutputBlock) { + self.events.push(DocIr::OutputBlock(block)); + } + + fn last_compose_block_mut(&mut self) -> Option<&mut ComposeBlock> { + self.events.iter_mut().rev().find_map(|v| match v { + DocIr::ComposeBlock(block) => Some(block), + _ => None, + }) + } +} + +enum DocIr<'a> { + ComposeBlock(ComposeBlock), + OutputBlock(OutputBlock), + Other(Vec>), +} + +pub struct ComposeBlock { + display: String, + raw: String, + line: usize, + header: String, + output_uses: Vec, + eval: EvalResult, +} + +pub struct OutputBlockDiags { + pub errors: Vec<&'static ErrorCode>, + pub warnings: Vec<&'static ErrorCode>, + pub line_number: usize, +} + +pub struct OutputBlock { + contents: String, +} + /// Transforms Markdown by evaluating fenced `compose` code blocks and replacing/validating /// adjacent `output` blocks. /// @@ -108,7 +196,7 @@ pub enum DiagMode { /// ``` /// "#; /// -/// let rendered = transform_markdown(markdown, &Config::no_color()).unwrap(); +/// let rendered = transform_markdown(markdown, &Config::new().with_no_color()).unwrap(); /// assert_eq!(rendered.trim(), r#" /// ````compose error(E0004) /// let a = 4; @@ -159,13 +247,18 @@ struct BlockContext { last_eval: Option, } +/// Performs a two-pass rewrite of the Markdown, replacing `compose` and `output` blocks with +/// their rendered output. +/// +/// Depending on configuration options, rust doc tests can be emitted for inclusion in generated +/// doc comments. fn rewrite_code_blocks<'a>( line_starts: &[usize], parser: &mut Peekable>, config: &Config, ) -> Result>, Error> { let mut ctx = BlockContext::default(); - let mut events = Vec::new(); + let mut ir = Ir::new(); while let Some((event, offset)) = parser.next() { match &event { @@ -176,26 +269,260 @@ fn rewrite_code_blocks<'a>( match meta.lang.as_str() { "compose" => { let (code, raw) = parse_code_block(parser); - let eval = execute_code_block(&raw, &meta, line)?; - ctx.last_eval = Some(eval); - - events.extend(emit_code_block(info.to_string(), code)); + let eval = eval_code(&raw); + ctx.last_eval = Some(eval.clone()); + + ir.push_compose_block(ComposeBlock { + display: code, + raw, + line, + header: info.to_string(), + output_uses: vec![], + eval, + }); } "output" => { - let code = handle_output_block(parser, &ctx, &meta, line, config)?; - events.extend(emit_code_block("text".to_string(), code)); + let last_compose_block = ir + .last_compose_block_mut() + .ok_or("missing preceding compose block") + .at(line)?; + + let (output, diags) = parse_output_block( + parser, + &last_compose_block.eval, + &meta, + line, + config, + )?; + + last_compose_block.output_uses.push(diags); + + ir.push_output_block(output); } - _ => events.push(event.clone()), + _ => ir.push_other(event.clone()), } } _ => { - events.push(event.clone()); + ir.push_other(event.clone()); } } } + + let mut events = Vec::new(); + for block in ir.events { + match block { + DocIr::ComposeBlock(compose_block) => { + if config.output_block_error_mode == ErrorHandlingMode::Error { + verify_output_block_errors_and_warnings(&compose_block)?; + } + match config.code_block_error_mode { + ErrorHandlingMode::EmitAsTests => { + let meta = + parse_block_header(&compose_block.header).at(compose_block.line)?; + let mut rust_test = + code_block_as_rust_test(&compose_block.raw, &meta, compose_block.line)?; + + if config.output_block_error_mode == ErrorHandlingMode::EmitAsTests { + emit_output_diags_tests(&compose_block.output_uses, &mut rust_test); + } + + events.extend(emit_code_block("rust".to_string(), rust_test)); + } + ErrorHandlingMode::Error => { + let meta = + parse_block_header(&compose_block.header).at(compose_block.line)?; + let (expected_errors, expected_warnings) = + parse_expected_errors_and_warnings(&meta).at(compose_block.line)?; + let eval_result = &compose_block.eval; + + verify_warnings_and_errors( + &eval_result, + &expected_errors, + &expected_warnings, + ) + .at(compose_block.line)?; + + events.extend(emit_code_block( + compose_block.header.to_string(), + compose_block.display, + )); + } + ErrorHandlingMode::Ignore => { + events.extend(emit_code_block( + compose_block.header.to_string(), + compose_block.display, + )); + } + } + } + DocIr::OutputBlock(output) => { + events.extend(emit_code_block("text".to_string(), output.contents)); + } + DocIr::Other(e) => events.extend(e), + } + } + Ok(events) } +/// returns (expected_errors, expected_warnings) +pub(crate) fn parse_expected_errors_and_warnings( + meta: &BlockHeader, +) -> Result<(Vec<&ErrorCode>, Vec<&ErrorCode>), String> { + let expected_errors = meta + .expected_errors + .iter() + .map(|code| lookup(code).ok_or_else(|| format!("{code} is not a valid error code"))) + .collect::, _>>()?; + + let expected_warnings = meta + .expected_warnings + .iter() + .map(|code| lookup(code).ok_or(format!("{code} is not a valid warning code"))) + .collect::, _>>()?; + + Ok((expected_errors, expected_warnings)) +} + +/// Emits tests for error and warning annotations in output blocks. +/// +/// Ensures that the interpreter actually raised warnings and errors the output block wants to show +/// +/// Expects an EvalResult named `result` to be defined. +fn emit_output_diags_tests(output_diags: &[OutputBlockDiags], rust_test: &mut String) { + rust_test.push_str("\n"); + for output_use in output_diags { + for error in &output_use.errors { + rust_test.push_str(&format!( + "# assert!(result.errors().iter().any(|e| e.code.map(|c| c.code) == Some(\"{error}\")), \"Output block expected error code {error} at line {line}\");\n", + error = error.code, line = output_use.line_number)); + } + + for warning in &output_use.warnings { + rust_test.push_str(&format!( + "# assert!(result.warnings().iter().any(|e| e.code.map(|c| c.code) == Some(\"{warning}\")), \"Output block expected warning code {warning} at line {line}\");\n", + warning = warning.code, line = output_use.line_number)); + } + } +} + +fn verify_output_block_errors_and_warnings(compose_block: &ComposeBlock) -> Result<(), Error> { + for output_use in &compose_block.output_uses { + for error in &output_use.errors { + if !compose_block + .eval + .errors + .iter() + .any(|e| e.code.map(|c| c.code) == Some(error.code)) + { + return Err(format!( + "Output block expected error code {error} at line {line}", + error = error.code, + line = output_use.line_number + )) + .at(output_use.line_number); + } + } + + for warning in &output_use.warnings { + if !compose_block + .eval + .errors + .iter() + .any(|w| w.code.map(|c| c.code) == Some(warning.code)) + { + return Err(format!( + "Output block expected warning code {warning} at line {line}", + warning = warning.code, + line = output_use.line_number + )) + .at(output_use.line_number); + } + } + } + Ok(()) +} + +fn verify_warnings_and_errors( + eval_result: &EvalResult, + expected_errors: &[&ErrorCode], + expected_warnings: &[&ErrorCode], +) -> Result<(), String> { + if !eval_result + .errors + .iter() + .map(|e| e.code) + .zip(expected_errors) + .all(|(a, b)| a == Some(b)) + || eval_result.errors.len() != expected_errors.len() + { + let errors_str = diagnostics_to_string( + &eval_result.world, + &eval_result.errors, + &[], + &Config::new().with_no_color(), + ); + return Err(format!( + "expected errors: {expected_errors:?}, got:\n{errors_str}" + )); + } + + if !eval_result + .warnings + .iter() + .map(|e| e.code) + .zip(expected_warnings) + .all(|(a, b)| a == Some(b)) + || eval_result.warnings.len() != expected_warnings.len() + { + let warnings_str = diagnostics_to_string( + &eval_result.world, + &[], + &eval_result.warnings, + &Config::new().with_no_color(), + ); + return Err(format!( + "expected warnings: {expected_warnings:?}, got:\n{warnings_str}" + )); + } + + Ok(()) +} + +fn code_block_as_rust_test(raw: &str, meta: &BlockHeader, line: usize) -> Result { + let (expected_errors, expected_warnings) = parse_expected_errors_and_warnings(meta).at(line)?; + + let mut code = String::new(); + code.push_str("# use ::compose_eval::test::eval_code;\n"); + code.push_str("# use ::compose_error_codes::*;\n"); + code.push_str(&format!( + "# let expected_errors = [{}];\n", + expected_errors + .iter() + .map(|e| format!("lookup(\"{}\").unwrap()", e.code)) + .collect::>() + .join(", ") + )); + code.push_str(&format!( + "# let expected_warnings = [{}];\n", + expected_warnings + .iter() + .map(|e| format!("lookup(\"{}\").unwrap()", e.code)) + .collect::>() + .join(", ") + )); + code.push_str("# let result = eval_code(r#\"\n"); + code.push_str(raw); + if !raw.ends_with('\n') { + code.push('\n'); + } + code.push_str("# \"#)\n"); + code.push_str("# .assert_errors(&expected_errors)\n"); + code.push_str("# .assert_warnings(&expected_warnings);"); + + Ok(code) +} + /// Parses a code block into a display and raw code fn parse_code_block(parser: &mut Peekable>) -> (String /*display*/, String /*raw*/) { let mut display = String::new(); @@ -232,50 +559,58 @@ fn parse_code_block(parser: &mut Peekable>) -> (String /*display* (display, raw) } -fn handle_output_block( +fn parse_output_block( parser: &mut Peekable>, - ctx: &BlockContext, + last_eval: &EvalResult, meta: &BlockHeader, line: usize, config: &Config, -) -> Result { +) -> Result<(OutputBlock, OutputBlockDiags), Error> { + let mut content = String::new(); + while let Some((Event::Text(_), _)) = parser.peek() { parser.next(); // discard any content in the code block } - let eval = ctx - .last_eval - .as_ref() - .ok_or("missing preceding compose block") - .at(line)?; - - let mut out = String::new(); + let mut diags = OutputBlockDiags { + errors: vec![], + warnings: vec![], + line_number: line, + }; for warning in &meta.expected_warnings { - let diag = find_diag(&eval.warnings, warning, line)?; - out.push_str(&diagnostics_to_string( - &eval.world, - &[], - &[diag.clone()], - config, - )); + if let Ok(diag) = find_diag(&last_eval.warnings, warning, line) { + content.push_str(&diagnostics_to_string( + &last_eval.world, + &[], + &[diag.clone()], + config, + )); + } + + diags.warnings.push(lookup(warning).unwrap()); } for error in &meta.expected_errors { - let diag = find_diag(&eval.errors, error, line)?; - out.push_str(&diagnostics_to_string( - &eval.world, - &[diag.clone()], - &[], - config, - )); + if let Ok(diag) = find_diag(&last_eval.errors, error, line) { + content.push_str(&diagnostics_to_string( + &last_eval.world, + &[diag.clone()], + &[], + config, + )); + } + + diags.errors.push(lookup(error).unwrap()); } if meta.stdout { - out.push_str(&eval.stdout); + content.push_str(&last_eval.stdout); } - Ok(out) + let output = OutputBlock { contents: content }; + + Ok((output, diags)) } fn find_diag<'a>( diff --git a/compose-doc/src/realise.rs b/compose-doc/src/realise.rs index e3328310..e6293ebc 100644 --- a/compose-doc/src/realise.rs +++ b/compose-doc/src/realise.rs @@ -1,92 +1,7 @@ -use crate::Config; -use crate::block::BlockHeader; -use crate::diag::{Error, diagnostics_to_string}; use crate::world::DocWorld; -use compose_error_codes::lookup; use compose_eval::{EvalConfig, Machine}; use compose_library::diag::{SourceDiagnostic, Warned}; -pub(crate) fn execute_code_block( - code: &str, - meta: &BlockHeader, - code_block_line_start: usize, -) -> Result { - let expected_errors = match meta - .expected_errors - .iter() - .map(|code| lookup(code).ok_or_else(|| format!("{code} is not a valid error code"))) - .collect::, _>>() - { - Ok(errors) => errors, - Err(err) => { - return Err(Error { - message: err, - line: code_block_line_start, - }); - } - }; - - let expected_warnings = match meta - .expected_warnings - .iter() - .map(|code| lookup(code).ok_or(format!("{code} is not a valid warning code"))) - .collect::, _>>() - { - Ok(warnings) => warnings, - Err(err) => { - return Err(Error { - message: err, - line: code_block_line_start, - }); - } - }; - - let eval_result = eval_code(code); - - if !eval_result - .errors - .iter() - .map(|e| e.code) - .zip(&expected_errors) - .all(|(a, b)| a == Some(b)) - || eval_result.errors.len() != expected_errors.len() - { - let errors_str = diagnostics_to_string( - &eval_result.world, - &eval_result.errors, - &[], - &Config::no_color(), - ); - return Err(Error { - message: format!("expected errors: {expected_errors:?}, got:\n{errors_str}",), - line: code_block_line_start, - }); - } - - if !eval_result - .warnings - .iter() - .map(|e| e.code) - .zip(&expected_warnings) - .all(|(a, b)| a == Some(b)) - || eval_result.warnings.len() != expected_warnings.len() - { - let warnings_str = diagnostics_to_string( - &eval_result.world, - &[], - &eval_result.warnings, - &Config::no_color(), - ); - return Err(Error { - message: format!( - "expected warnings: {expected_warnings:?}, got:\n{warnings_str}", - ), - line: code_block_line_start, - }); - } - - Ok(eval_result) -} pub(crate) fn eval_code(code: &str) -> EvalResult { let world = DocWorld::from_str(code); @@ -117,6 +32,7 @@ pub(crate) fn eval_code(code: &str) -> EvalResult { } } +#[derive(Debug, Clone)] pub(crate) struct EvalResult { pub world: DocWorld, pub stdout: String, diff --git a/compose-doc/src/world.rs b/compose-doc/src/world.rs index a4bd78d7..dfdb3e29 100644 --- a/compose-doc/src/world.rs +++ b/compose-doc/src/world.rs @@ -11,6 +11,16 @@ pub(crate) struct DocWorld { pub stdout: Mutex } +impl Clone for DocWorld { + fn clone(&self) -> Self { + Self { + source: self.source.clone(), + library: self.library.clone(), + stdout: Mutex::new(self.stdout.lock().expect("failed to lock stdout").clone()) + } + } +} + impl DocWorld { pub(crate) fn from_str(text: &str) -> Self { let entrypoint = FileId::new("main.comp"); @@ -38,7 +48,7 @@ impl World for DocWorld { &self.library } - fn write(&self, f: &dyn Fn(&mut dyn Write) -> std::io::Result<()>) -> std::io::Result<()> { + fn write(&self, f: &mut dyn FnMut(&mut dyn Write) -> std::io::Result<()>) -> std::io::Result<()> { let mut buffer: Vec = Vec::new(); f(&mut buffer)?; let output = String::from_utf8(buffer).expect("Invalid UTF-8"); @@ -46,7 +56,7 @@ impl World for DocWorld { Ok(()) } - fn read(&self, f: &dyn Fn(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()> { + fn read(&self, f: &mut dyn FnMut(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()> { f(&mut std::io::stdin()) } } diff --git a/compose-editor/src/editor/mod.rs b/compose-editor/src/editor/mod.rs index 2fc0b71f..c4ce0cc3 100644 --- a/compose-editor/src/editor/mod.rs +++ b/compose-editor/src/editor/mod.rs @@ -132,12 +132,12 @@ impl Editor { } /// Get the current line. - pub fn curr_ln(&self) -> Cow { + pub fn curr_ln(&self) -> Cow<'_, str> { Cow::from(trimmed(self.buf.line(self.focus.ln))) } /// Get the current selection of text. - pub fn curr_sel(&self) -> Option> { + pub fn curr_sel(&self) -> Option> { if let Some(anchor) = self.anchor { let anchor_idx = self.rope_idx(anchor, 0); let focus_idx = self.rope_idx(self.focus, 0); diff --git a/compose-editor/src/renderer/mod.rs b/compose-editor/src/renderer/mod.rs index 6542f243..870495ed 100644 --- a/compose-editor/src/renderer/mod.rs +++ b/compose-editor/src/renderer/mod.rs @@ -96,15 +96,15 @@ impl<'b> RenderData<'b> { Ok(()) } - pub fn line(&self, index: usize) -> Cow { + pub fn line(&self, index: usize) -> Cow<'_, str> { trimmed(self.buf.line(index)).into() } - pub fn last_line(&self) -> Cow { + pub fn last_line(&self) -> Cow<'_, str> { self.line(self.buf.len_lines() - 1) } - pub fn current_line(&self) -> Cow { + pub fn current_line(&self) -> Cow<'_, str> { self.line(self.focus.ln) } } diff --git a/compose-error-codes-doc-tests/Cargo.toml b/compose-error-codes-doc-tests/Cargo.toml new file mode 100644 index 00000000..13a7ddaa --- /dev/null +++ b/compose-error-codes-doc-tests/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "compose-error-codes-doc-tests" +version.workspace = true +edition.workspace = true + +[dependencies] +compose-eval = {workspace = true} +compose-error-codes = {workspace = true} + +[lints] +workspace = true + +[build-dependencies] +compose-error-codes = {workspace = true} +compose-doc = { workspace = true} \ No newline at end of file diff --git a/compose-error-codes-doc-tests/build.rs b/compose-error-codes-doc-tests/build.rs new file mode 100644 index 00000000..8e0a0414 --- /dev/null +++ b/compose-error-codes-doc-tests/build.rs @@ -0,0 +1,42 @@ +use compose_doc::{Config, ErrorHandlingMode}; +use compose_error_codes::ERROR_CODES; +use std::fmt::Write; +use std::fs; +use std::path::Path; + +fn main() { + let mut doctests = String::new(); + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is always set during builds"); + for code in ERROR_CODES { + let converted_md = compose_doc::transform_markdown( + code.description, + &Config::new() + .with_no_color() + .with_code_block_error_mode(ErrorHandlingMode::EmitAsTests) + .with_output_block_error_mode(ErrorHandlingMode::EmitAsTests), + ) + .unwrap_or_else(|e| panic!("failed to convert markdown for {} at {}: {}", code.name, e.line, e.message)); + + let ty_name = format!( + "DOC_{}_{}", + code.code, + code.name.replace(' ', "_").to_uppercase() + ); + + write!( + doctests, + r#" +/** +{converted_md} +*/ +#[allow(non_snake_case, dead_code)] +pub mod {ty_name} {{}} + + + "# + ) + .expect("writing to a string is infallible"); + } + fs::write(Path::new(&out_dir).join("Error_Codes"), doctests) + .expect("failed to write Error_Codes"); +} diff --git a/compose-error-codes-doc-tests/src/lib.rs b/compose-error-codes-doc-tests/src/lib.rs new file mode 100644 index 00000000..acde73ff --- /dev/null +++ b/compose-error-codes-doc-tests/src/lib.rs @@ -0,0 +1,11 @@ +/*! +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 +via `compose_doc::transform_markdown` into documented items with doc-tests. +*/ + +include!(concat!(env!("OUT_DIR"), "/Error_Codes")); diff --git a/compose-error-codes/build.rs b/compose-error-codes/build.rs index e7eb868d..6522e1a6 100644 --- a/compose-error-codes/build.rs +++ b/compose-error-codes/build.rs @@ -20,6 +20,7 @@ fn main() { out.push_str("// This file is @generated by build.rs\n"); let mut lookup_matches = String::new(); + let mut error_codes = String::new(); for file in files { let path = file.path(); @@ -65,6 +66,13 @@ fn main() { "#, ) .expect("writing to a string is infallible"); + + write!( + error_codes, + r#" + &{name}, + "#, + ).expect("writing to a string is infallible"); } write!( @@ -78,12 +86,16 @@ fn main() { _ => return None, }}) }} + + pub const ERROR_CODES: &[&ErrorCode] = &[ + {error_codes} + ]; "# ) .expect("writing to a string is infallible"); - fs::write(Path::new(&out_dir).join("error_codes.rs"), out) - .expect("failed to write error_codes.rs"); + fs::write(Path::new(&out_dir).join("Error_Codes"), out) + .expect("failed to write Error_Codes"); } fn escape(s: &str) -> String { diff --git a/compose-error-codes/src/codes/errors/E0002_invalid_assignment.md b/compose-error-codes/src/codes/errors/E0002_invalid_assignment.md index 371dc573..85a561ad 100644 --- a/compose-error-codes/src/codes/errors/E0002_invalid_assignment.md +++ b/compose-error-codes/src/codes/errors/E0002_invalid_assignment.md @@ -9,7 +9,8 @@ Assignments like `x = y` are **statements**, not expressions. They cannot be use ### Example of erroneous code ```compose error(E0002) -let f = (x, a) => (x = a); +# let x = 1; let a = 2; +let f = x = a; ``` ```output error(E0002) @@ -29,7 +30,7 @@ Assignment (`x = a`) evaluates to the unit type `()` and **cannot be used where This helps avoid common mistakes, such as confusing assignment with comparison: ```compose error(E0002) -if a = b { } // probably meant `a == b` +if (a = b) { } // probably meant `a == b` ``` --- @@ -39,17 +40,20 @@ if a = b { } // probably meant `a == b` #### ✅ If you meant to compare: ```compose -let f = (x, a) => (x == a); +# let x = 1; let a = 2; +let f = x == a; ``` #### ✅ If you meant to assign a value: ```compose -let f = (x, a) => { x = a; }; +# let mut x = 1; let a = 2; +let f = { x = a; }; ``` #### ✅ Or use `let` for rebinding the variable: ```compose -let f = (x, a) => { let x = a; x; }; +# let mut x = 1; let a = 2; +let f = { let x = a; x; }; ``` \ No newline at end of file diff --git a/compose-error-codes/src/codes/errors/E0003_expected_binding_after_let.md b/compose-error-codes/src/codes/errors/E0003_expected_binding_after_let.md index bcaa511d..801fbbed 100644 --- a/compose-error-codes/src/codes/errors/E0003_expected_binding_after_let.md +++ b/compose-error-codes/src/codes/errors/E0003_expected_binding_after_let.md @@ -46,6 +46,7 @@ let x = 5; #### ✅ Use a destructuring pattern: ```compose +# let some_object = { a: 1, b: 2 }; let { a, b } = some_object; ``` diff --git a/compose-error-codes/src/codes/errors/E0005_if_expression_bodies_require_braces.md b/compose-error-codes/src/codes/errors/E0005_if_expression_bodies_require_braces.md index 639a8fe1..17d1a4cc 100644 --- a/compose-error-codes/src/codes/errors/E0005_if_expression_bodies_require_braces.md +++ b/compose-error-codes/src/codes/errors/E0005_if_expression_bodies_require_braces.md @@ -7,8 +7,8 @@ In Compose, the body of an `if` expression must be enclosed in `{}`. Unlike some ### Example ```compose error(E0005) -if true - let a = true; +if (true) + println("it was true"); ``` ```output error(E0005) @@ -18,8 +18,8 @@ if true ✅ **Fix:** ```compose -if true { - let a = true; +if (true) { + println("it was true"); }; ``` diff --git a/compose-error-codes/src/codes/errors/E0006_unterminated_statement.md b/compose-error-codes/src/codes/errors/E0006_unterminated_statement.md index fa122725..8d68a249 100644 --- a/compose-error-codes/src/codes/errors/E0006_unterminated_statement.md +++ b/compose-error-codes/src/codes/errors/E0006_unterminated_statement.md @@ -8,7 +8,8 @@ consistent. ### Example ```compose error(E0006) -let x = 1 let y = 2 +let x = 1 +let y = 2 ``` ```output error(E0006) @@ -18,7 +19,8 @@ let x = 1 let y = 2 ✅ **Fix:** ```compose -let x = 1; let y = 2; +let x = 1; +let y = 2; ``` --- diff --git a/compose-error-codes/src/codes/errors/E0007_missing_equals_after_let_binding.md b/compose-error-codes/src/codes/errors/E0007_missing_equals_after_let_binding.md index 6cb11758..87ea1376 100644 --- a/compose-error-codes/src/codes/errors/E0007_missing_equals_after_let_binding.md +++ b/compose-error-codes/src/codes/errors/E0007_missing_equals_after_let_binding.md @@ -3,7 +3,7 @@ In Compose, a `let` statement introduces a new variable binding. After the binding pattern (e.g., `count`), the parser expects either: - an initializer with `=`, -- or a clear statement terminator: a semicolon (`;`) or a newline. +- or a semicolon (`;`). --- @@ -29,13 +29,6 @@ or let count; 42 ``` -or - -```compose -let count -42 -``` - --- -This requirement avoids ambiguity between incomplete bindings and separate expressions. Use `=` to initialize, or a semicolon or newline to end the declaration. +This requirement avoids ambiguity between incomplete bindings and separate expressions. Use `=` to initialise, or a semicolon to end the declaration. diff --git a/compose-error-codes/src/codes/errors/E0009_args_missing_commas.md b/compose-error-codes/src/codes/errors/E0009_args_missing_commas.md index e39c1cde..14af81fc 100644 --- a/compose-error-codes/src/codes/errors/E0009_args_missing_commas.md +++ b/compose-error-codes/src/codes/errors/E0009_args_missing_commas.md @@ -17,6 +17,8 @@ add(x y z) ✅ **Fix:** ```compose +# let add = { x, y, z => x + y + z }; +# let x = 1; let y = 2; let z = 3; add(x, y, z) ``` diff --git a/compose-error-codes/src/codes/errors/E0010_uncaptured_variable.md b/compose-error-codes/src/codes/errors/E0010_uncaptured_variable.md index 7fc3e485..503f7bc1 100644 --- a/compose-error-codes/src/codes/errors/E0010_uncaptured_variable.md +++ b/compose-error-codes/src/codes/errors/E0010_uncaptured_variable.md @@ -10,7 +10,7 @@ In Compose, closures must explicitly declare any variables they use from the out ```compose error(E0010) let x = 1; let y = 2; -let f = () => x + y; +let f = { => x + y }; ``` ```output error(E0010) @@ -22,7 +22,7 @@ let f = () => x + y; ```compose # let x = 1; # let y = 2; -let f = |x, y| () => x + y; +let f = { |x, y| => x + y }; ``` --- @@ -43,7 +43,7 @@ Captured variables can be annotated to control how the closure accesses them: ```compose let x = box::new(42); -let f = |ref x| () => print(*x); +let f = { |ref x| => print(*x) }; ``` --- diff --git a/compose-error-codes/src/codes/errors/E0011_unbound_variable.md b/compose-error-codes/src/codes/errors/E0011_unbound_variable.md index 1adcf215..99b3a9df 100644 --- a/compose-error-codes/src/codes/errors/E0011_unbound_variable.md +++ b/compose-error-codes/src/codes/errors/E0011_unbound_variable.md @@ -5,7 +5,7 @@ This error occurs when you try to use a variable that has not been declared in t ### Example: ```compose error(E0011) -y = 4; // ❌ Error: `y` is unbound +y = 4; ``` ```output error(E0011) diff --git a/compose-error-codes/src/codes/errors/E0012_predicate_must_return_boolean.md b/compose-error-codes/src/codes/errors/E0012_predicate_must_return_boolean.md index 9e5f0cf3..26acb558 100644 --- a/compose-error-codes/src/codes/errors/E0012_predicate_must_return_boolean.md +++ b/compose-error-codes/src/codes/errors/E0012_predicate_must_return_boolean.md @@ -7,7 +7,7 @@ Methods like `all`, `filter`, `find`, and similar take a **predicate function**: ### Example ```compose error(E0012) -(1..5).iter().any(x => 3); +(1..5).iter().any { x => 3 }; ``` #### Error: @@ -18,7 +18,7 @@ Methods like `all`, `filter`, `find`, and similar take a **predicate function**: ✅ **Fix:** ```compose -(1..5).iter().any(x => x == 3); +(1..5).iter().any { x => x == 3 }; ``` --- diff --git a/compose-error-codes/src/codes/errors/E0301_array_destructuring_wrong_number_of_elements.md b/compose-error-codes/src/codes/errors/E0301_array_destructuring_wrong_number_of_elements.md index 0b74960a..c93ee54d 100644 --- a/compose-error-codes/src/codes/errors/E0301_array_destructuring_wrong_number_of_elements.md +++ b/compose-error-codes/src/codes/errors/E0301_array_destructuring_wrong_number_of_elements.md @@ -56,31 +56,8 @@ let [a, b, c] = [1, 2, 3]; --- -#### **Case 3: Unknown-length array, pattern without `..`** -```compose error(E0301) -# let get_array = { => [1, 2, 3] }; -let arr = get_array(); -let [a, b] = arr; -``` - -```output error(E0301) -// autogenerated -``` - -* The compiler cannot know the length of `arr` at compile time. -* **Error**: exact-length destructuring is not allowed for unknown-length arrays. - -**Fix:** Use `..` to allow variable-length arrays: - -```compose -# let arr = [1, 2, 3]; -let [a, b, ..] = arr; -``` - ---- - -#### **Case 4: Too few elements when using a `..rest` binding** +#### **Case 4: Too few elements when using a `..name` binding** ```compose error(E0301) let [a, b, ..c] = [1]; diff --git a/compose-error-codes/src/codes/errors/E0302_map_destructuring_uncovered_keys.md b/compose-error-codes/src/codes/errors/E0302_map_destructuring_uncovered_keys.md new file mode 100644 index 00000000..dca5dc7f --- /dev/null +++ b/compose-error-codes/src/codes/errors/E0302_map_destructuring_uncovered_keys.md @@ -0,0 +1,65 @@ +## E0302: map destructuring does not cover all keys + +### What this means + +This error occurs when a map destructuring pattern does not account for **every key** in the map being destructured. + +In Compose, map destructuring is **exhaustive** by default: +if a map contains keys that are not explicitly destructured, the pattern is rejected. + +--- + +### Example that triggers this error + +```compose error(E0302) +let { a } = { a: 1, b: 2 }; +``` + +The map contains the keys `a` and `b`, but the pattern only destructures `a`. +Since `b` is not handled, this results in an error. + +--- + +### Why this is an error + +Allowing partial map destructuring without being explicit would make it unclear whether: + +* extra keys are intentionally ignored, or +* a key was forgotten by mistake + +To avoid silent bugs, Compose requires you to **explicitly choose** what happens to the remaining keys. + +--- + +### How to fix it + +You have two options. + +#### Ignore remaining keys + +Use `..` to explicitly discard any keys not listed in the pattern: + +```compose +let { a, .. } = { a: 1, b: 2 }; +``` + +#### Capture remaining keys + +Use `..name` to bind the remaining keys into a new map: + +```compose +let { a, ..rest } = { a: 1, b: 2 }; +``` + +Here, `rest` will be a map containing `{ b: 2 }`. + +--- + +### Summary + +* Map destructuring in Compose is exhaustive by default +* All keys must be handled explicitly +* Use `..` to ignore remaining keys +* Use `..name` to capture remaining keys into a map + +This design favors clarity and prevents accidental data loss. \ No newline at end of file diff --git a/compose-error-codes/src/codes/errors/E0303_map_destructuring_missing_key_in_value.md b/compose-error-codes/src/codes/errors/E0303_map_destructuring_missing_key_in_value.md new file mode 100644 index 00000000..06ed2c62 --- /dev/null +++ b/compose-error-codes/src/codes/errors/E0303_map_destructuring_missing_key_in_value.md @@ -0,0 +1,82 @@ +## E0303: missing key in map pattern + +### What this means + +This error occurs when a map destructuring pattern **requires a key that is not present** in the map being destructured. + +In Compose, map patterns list the keys that must exist. +If any required key is missing from the value, destructuring fails. + +--- + +### Example that triggers this error + +```compose error(E0303) +let { a, b } = { a: 1 }; +``` + +The pattern requires the keys `a` and `b`, but the map only contains `a`. +Since `b` is not present, the pattern cannot be matched. + +--- + +### Why this is an error + +Map destructuring is **strict**: + +* Every key named in the pattern must exist in the map +* Missing keys would otherwise result in uninitialized bindings + +Rather than implicitly assigning a default value or ignoring missing keys, Compose reports an error to ensure correctness. + +--- + +### How to fix it + +You have several options, depending on your intent. + +#### Provide the missing key + +```compose +let { a, b } = { a: 1, b: 2 }; +``` + +#### Remove the key from the pattern + +```compose +let { a } = { a: 1 }; +``` + +#### Conditionally match the map + +If the map may or may not contain the key, use a **pattern test** instead of direct destructuring. + +Patterns can be used in both `match` expressions **and** `is` expressions: + +```compose +let map = { a: 1 }; + +if (map is { a, b }) { + println(a, b); +} +``` + +The `is` expression only returns true if the pattern matches. +Inside the body of the `if` expression, all pattern bindings (`a` and `b`) are guaranteed to exist. + +The same pattern can also be used in a `match` expression: + +```compose +# let map = { a: 1 }; +match (map) { + { a, b } => println(a, b), + _ => (), +} +``` + +### Summary + +* Keys listed in a map pattern are required +* Direct destructuring is unconditional +* Use `if … is` or `match` to conditionally match maps +* This error ensures missing data is handled explicitly diff --git a/compose-error-codes/src/codes/errors/E0304_type_patterns_not_allowed_in_let_bindings.md b/compose-error-codes/src/codes/errors/E0304_type_patterns_not_allowed_in_let_bindings.md new file mode 100644 index 00000000..f9db2fc7 --- /dev/null +++ b/compose-error-codes/src/codes/errors/E0304_type_patterns_not_allowed_in_let_bindings.md @@ -0,0 +1,55 @@ +## E0304: Type patterns are not allowed in `let` bindings + +This error occurs when a `let` binding uses a *type pattern*, such as: + +```compose error(E0304) +let Int x = 1; +``` + +In Compose, **`let` bindings do not perform type checks**. A binding like `let x = value;` simply introduces a name and assigns it a value. +Writing a type before the binding name is **not** treated as a type annotation. + +### Why this is not allowed + +Although Compose supports *type patterns* (for example `Int x`), those patterns are only meaningful in contexts where **control flow can branch**, such as `match` or `is` expressions. A plain `let` binding has no way to handle the case where the value does *not* match the type, so allowing type patterns there would be misleading. + +In other words: + +* `let Int x = ...` looks like a static type annotation +* but Compose does not use `let` for type checking +* and failed type matches must be handled explicitly + +To avoid unexpected runtime errors, type patterns are not allowed in `let` bindings. + +### How to fix this + +If you simply want to bind a value: + +```compose +let x = 1; +``` + +If you want to **check the type of a value**, use a `match` expression: + +```compose +# let value = 1; +match (value) { + Int x => println(x), + _ => panic("expected an Int"), +} +``` + +Or use an `is` expression: + +```compose +# let value = 1; +if (value is Int x) { + println(x); +} +``` + +### Summary + +* `let` bindings introduce names, they do not check types +* `let Int x = ...` is **not** a type annotation in Compose +* use `match` or `is` when you need to branch on a value’s type \ No newline at end of file diff --git a/compose-error-codes/src/codes/errors/E0311_match_arm_patterns_bind_different_variables.md b/compose-error-codes/src/codes/errors/E0311_match_arm_patterns_bind_different_variables.md new file mode 100644 index 00000000..fc0dda8d --- /dev/null +++ b/compose-error-codes/src/codes/errors/E0311_match_arm_patterns_bind_different_variables.md @@ -0,0 +1,81 @@ +## E0311: Match arm patterns bind different variables + +### Error + +```text +patterns within a match arm have differing bindings +``` + +--- + +### Meaning + +All patterns joined with `|` in a `match` arm share the same body. +That body must be valid for **every** pattern. + +Therefore, **each pattern must bind the same variables**. + +--- + +### Example + +```compose error(E0311) +# let value = [0,1]; +match (value) { + [0, a] | [1, b] => a + b +} +``` + +```output error(E0311) +``` + +This is invalid: + +* `[0, a]` binds `a` +* `[1, b]` binds `b` +* the body requires both + +No execution binds both variables. + +--- + +### Why this is rejected + +A `|`-joined arm behaves like multiple patterns with a single body: + +```text +p1 | p2 => body +``` + +Since `body` is shared, it may only reference variables bound by **all** patterns. + +--- + +### Fixes + +**Use the same bindings:** + +```compose +# let value = [0, 1]; +match (value) { + [0, x] | [1, x] => x +} +``` + +**Or split the arms:** + +```compose +# let value = [0, 1]; +match (value) { + [0, a] => a, + [1, b] => b, +} +``` + +--- + +### Summary + +* `|`-joined patterns must bind identical variables +* the arm body must work for every pattern +* rename bindings or split the arm to resolve the error diff --git a/compose-error-codes/src/codes/warnings/W0001_used_uninitialized_variable.md b/compose-error-codes/src/codes/warnings/W0001_used_uninitialized_variable.md index 061f500e..7dfa6a03 100644 --- a/compose-error-codes/src/codes/warnings/W0001_used_uninitialized_variable.md +++ b/compose-error-codes/src/codes/warnings/W0001_used_uninitialized_variable.md @@ -60,7 +60,7 @@ a // evaluates to () Uninitialized variables default to `()`, which is often unintentional. For example: -```compose warning +```compose let total; println("Total is", total); ``` diff --git a/compose-error-codes/src/lib.rs b/compose-error-codes/src/lib.rs index 7e128b0c..fc9e5d7f 100644 --- a/compose-error-codes/src/lib.rs +++ b/compose-error-codes/src/lib.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -include!(concat!(env!("OUT_DIR"), "/error_codes.rs")); +include!(concat!(env!("OUT_DIR"), "/Error_Codes")); #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct ErrorCode { diff --git a/compose-eval/Cargo.toml b/compose-eval/Cargo.toml index bca3a396..69d61818 100644 --- a/compose-eval/Cargo.toml +++ b/compose-eval/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" compose-library = { workspace = true } compose-syntax = { workspace = true } compose-error-codes = { workspace = true } +compose-utils = { workspace = true } ecow = { workspace = true } extension-traits = { workspace = true } -tap = {workspace = true} +tap = { workspace = true } diff --git a/compose-eval/src/evaluated.rs b/compose-eval/src/evaluated.rs new file mode 100644 index 00000000..d6b66f78 --- /dev/null +++ b/compose-eval/src/evaluated.rs @@ -0,0 +1,85 @@ +use compose_library::Value; +use compose_syntax::Span; +use crate::Machine; +use crate::vm::Tracked; + +#[derive(Debug, Clone, PartialEq)] +pub struct Evaluated { + pub value: Value, + /// Whether the value is allowed to be mutated. + /// + /// True for any expression except for reading or dereferencing immutable values. + pub mutable: bool, + /// The span of the binding this value is related to + pub origin: Option, +} + +impl Evaluated { + pub fn new(value: Value, mutable: bool) -> Self { + Self { value, mutable, origin: None } + } + + pub fn mutable(value: Value) -> Self { + Self::new(value, true) + } + + pub fn immutable(value: Value) -> Self { + Self::new(value, false) + } + + pub fn unit() -> Self { + Self::new(Value::unit(), true) + } + + pub fn spanned(self, span: Span) -> Self { + Self { + value: self.value.spanned(span), + ..self + } + } + + pub fn with_origin(self, origin: Span) -> Self { + Self { origin: Some(origin), ..self } + } + + pub fn with_value(self, value: Value) -> Self { + Self { value, ..self } + } + + pub fn make_mutable(self) -> Self { + Self { mutable: true, ..self } + } + + pub fn value(&self) -> &Value { + &self.value + } + + pub fn into_value(self) -> Value { + self.value + } +} + +impl Tracked for Evaluated { + fn track_tmp_root(self, vm: &mut Machine) -> Self { + Self { + value: self.value.track_tmp_root(vm), + ..self + } + } +} + +pub trait ValueEvaluatedExtensions { + fn mutable(self) -> Evaluated; + #[expect(unused)] + fn immutable(self) -> Evaluated; +} + +impl ValueEvaluatedExtensions for Value { + fn mutable(self) -> Evaluated { + Evaluated::new(self, true) + } + + fn immutable(self) -> Evaluated { + Evaluated::new(self, false) + } +} \ No newline at end of file diff --git a/compose-eval/src/expression/array.rs b/compose-eval/src/expression/array.rs index 5e9f03d8..b59b1090 100644 --- a/compose-eval/src/expression/array.rs +++ b/compose-eval/src/expression/array.rs @@ -1,7 +1,8 @@ -use crate::{Eval, Evaluated, Machine, ValueEvaluatedExtensions}; +use crate::{Eval, Machine}; use compose_library::diag::SourceResult; use compose_library::{ArrayValue, IntoValue, Value, Vm}; use compose_syntax::ast; +use crate::evaluated::{Evaluated, ValueEvaluatedExtensions}; impl<'a> Eval for ast::Array<'a> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/expression/assignment.rs b/compose-eval/src/expression/assignment.rs index 4c3d46e2..a18cddd1 100644 --- a/compose-eval/src/expression/assignment.rs +++ b/compose-eval/src/expression/assignment.rs @@ -1,9 +1,10 @@ use crate::access::Access; -use crate::{Eval, Evaluated, Machine}; +use crate::{Eval, Machine}; use compose_library::diag::{bail, At, SourceResult}; use compose_library::{ops, Value}; use compose_syntax::ast; use compose_syntax::ast::{AssignOp, AstNode}; +use crate::evaluated::Evaluated; impl Eval for ast::Assignment<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/expression/atomic.rs b/compose-eval/src/expression/atomic.rs index 474cb83a..da30d9ee 100644 --- a/compose-eval/src/expression/atomic.rs +++ b/compose-eval/src/expression/atomic.rs @@ -1,8 +1,9 @@ use crate::vm::Machine; -use crate::{Eval, Evaluated, ValueEvaluatedExtensions}; +use crate::Eval; use compose_library::diag::SourceResult; -use compose_library::{IntoValue, Value}; +use compose_library::IntoValue; use compose_syntax::ast; +use crate::evaluated::{Evaluated, ValueEvaluatedExtensions}; impl Eval for ast::Int<'_> { fn eval(self, _vm: &mut Machine) -> SourceResult { @@ -24,7 +25,7 @@ impl Eval for ast::Bool<'_> { #[cfg(test)] mod tests { - use super::*; + use compose_library::Value; use crate::test::assert_eval; #[test] diff --git a/compose-eval/src/expression/binary.rs b/compose-eval/src/expression/binary.rs index 51691832..3a49e426 100644 --- a/compose-eval/src/expression/binary.rs +++ b/compose-eval/src/expression/binary.rs @@ -1,9 +1,10 @@ use crate::vm::Machine; -use crate::{Eval, Evaluated, ValueEvaluatedExtensions}; +use crate::Eval; use compose_library::diag::{bail, At, SourceResult}; use compose_library::{ops, Value}; use compose_syntax::ast; use compose_syntax::ast::{AstNode, BinOp}; +use crate::evaluated::{Evaluated, ValueEvaluatedExtensions}; impl Eval for ast::Binary<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { @@ -26,6 +27,11 @@ impl Eval for ast::Binary<'_> { ), }; + // The lhs expression might introduce flow bindings used in rhs, + // we make sure to run in a flow scope that last at least as long + // as this entire binary expression + let vm = &mut vm.in_flow_scope_guard(); + let l = self.lhs(); let lhs = l.eval(vm)?; @@ -37,7 +43,9 @@ impl Eval for ast::Binary<'_> { let r = self.rhs(); let rhs = r.eval(vm)?; - Ok(op(&lhs.value, &rhs.value, &vm.heap).at(self.span())?.mutable()) + Ok(op(&lhs.value, &rhs.value, &vm.heap) + .at(self.span())? + .mutable()) } } diff --git a/compose-eval/src/expression/bindings.rs b/compose-eval/src/expression/bindings.rs index 0b6481ce..8661805c 100644 --- a/compose-eval/src/expression/bindings.rs +++ b/compose-eval/src/expression/bindings.rs @@ -1,15 +1,17 @@ +use crate::expression::pattern; +use crate::expression::pattern::{PatternContext, PatternMatchResult}; use crate::vm::{ErrorMode, Machine}; -use crate::{Eval, Evaluated}; -use compose_error_codes::E0301_ARRAY_DESTRUCTURING_WRONG_NUMBER_OF_ELEMENTS; -use compose_library::diag::{bail, error, At, SourceDiagnostic, SourceResult, Spanned}; -use compose_library::{diag, ArrayValue, BindingKind, IntoValue, Value, Visibility, Vm}; +use crate::Eval; +use compose_library::diag::{bail, At, SourceResult}; +use compose_library::{BindingKind, Visibility}; use compose_syntax::ast; -use compose_syntax::ast::{AstNode, DestructuringItem, Expr, Pattern}; -use ecow::{eco_format, eco_vec}; +use compose_syntax::ast::AstNode; +use crate::evaluated::Evaluated; -impl<'a> Eval for ast::Ident<'a> { +impl Eval for ast::Ident<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { let span = self.span(); + let binding = vm.frames.top.scopes.get(&self).at(span)?; let mutable = binding.is_mutable(); @@ -52,150 +54,20 @@ impl<'a> Eval for ast::LetBinding<'a> { return Ok(Evaluated::unit()); } - destructure_pattern(vm, self.pattern(), value.value, binding_kind, visibility)?; - - Ok(Evaluated::unit()) - } -} - -pub fn destructure_pattern( - vm: &mut Machine, - pattern: Pattern, - value: Value, - binding_kind: BindingKind, - visibility: Visibility, -) -> SourceResult<()> { - destructure_impl(vm, pattern, value, &mut |vm, expr, value| match expr { - Expr::Ident(ident) => { - let name = ident.get().clone(); - let spanned = value - .named(Spanned::new(name, ident.span())) - // Now that the names have been added, make sure any deferred errors are resolved - .resolved()?; - - vm.define(ident, spanned, binding_kind, visibility)?; - - Ok(()) - } - _ => Err(eco_vec![diag::SourceDiagnostic::error( - expr.span(), - "cannot destructure pattern", - )]), - }) -} - -fn destructure_impl( - vm: &mut Machine, - pattern: Pattern, - value: Value, - bind: &mut impl Fn(&mut Machine, Expr, Value) -> SourceResult<()>, -) -> SourceResult<()> { - match pattern { - Pattern::Single(expr) => bind(vm, expr, value)?, - Pattern::PlaceHolder(_) => {} // A placeholder means we discard the value, no need to bind - Pattern::Destructuring(destruct) => match value { - Value::Array(value) => destructure_array(vm, destruct, value, bind)?, - Value::Map(value) => destructure_map(vm, destruct, value, bind)?, - _ => bail!(pattern.span(), "cannot destructure {}", value.ty()), - } - }; - - Ok(()) -} - -fn destructure_array(vm: &mut Machine, destruct: ast::Destructuring, value: ArrayValue, bind: &mut impl Fn(&mut Machine, Expr, Value) -> SourceResult<()>) -> SourceResult<()> { - let arr = value.heap_ref().get_unwrap(&vm.heap).clone(); - - let len = arr.len(); - let mut index = 0; - - for p in destruct.items() { - match p { - DestructuringItem::Pattern(pat) => { - let Some(v) = arr.get(index) else { - bail!(wrong_number_of_elements(destruct, len)) - }; - - destructure_impl(vm, pat, v.clone(), bind)?; - index += 1; - } - DestructuringItem::Named(named) => { - bail!(named.span(), "cannot destructure a named pattern from an array") - } - DestructuringItem::Spread(spread) => { - // The number of elements that have not been bound by a destructuring item and will be bound by the spread - let sink_size = (1 + len).checked_sub(destruct.items().count()); - - // The items that will be bound by the spread - let sunk_items = sink_size.and_then(|n| arr.get(index..index + n)); - - let (Some(sink_size), Some(sunk_items)) = (sink_size, sunk_items) else { - bail!(wrong_number_of_elements(destruct, len)) - }; - - if let Some(expr) = spread.sink_expr() { - let sunk_arr = ArrayValue::from(vm.heap_mut(), sunk_items.to_vec()).into_value(); - bind(vm, expr, sunk_arr)?; - } - index += sink_size; - } - } - } - - // require all items to be bound - if index != len { - bail!(wrong_number_of_elements(destruct, len)) - } - - Ok(()) -} - -#[allow(unused)] -fn destructure_map(vm: &mut Machine, destruct: ast::Destructuring, value: compose_library::MapValue, bind: &mut impl Fn(&mut Machine, Expr, Value) -> SourceResult<()>) -> SourceResult<()> { - - unimplemented!("destructuring maps") -} - - -/// The error message when the number of elements of the destructuring and the -/// array is mismatched. -#[cold] -fn wrong_number_of_elements( - destruct: ast::Destructuring, - len: usize, -) -> SourceDiagnostic { - let mut count = 0; - let mut spread = false; - - for p in destruct.items() { - match p { - DestructuringItem::Pattern(_) => count += 1, - DestructuringItem::Spread(_) => spread = true, - DestructuringItem::Named(_) => {} + let matched = pattern::destructure_pattern( + vm, + self.pattern(), + value.value, + PatternContext::LetBinding, + binding_kind, + visibility, + )?; + + match matched { + PatternMatchResult::NotMatched(err) => bail!(err), + PatternMatchResult::Matched => Ok(Evaluated::unit()), } } - - let quantifier = if len > count { "too many" } else { "not enough" }; - let expected = match (spread, count) { - (true, 1) => "at least 1 element".into(), - (true, c) => eco_format!("at least {c} elements"), - (false, 0) => "an empty array".into(), - (false, 1) => "a single element".into(), - (false, c) => eco_format!("{c} elements",), - }; - - let mut err = error!( - destruct.span(), "{quantifier} elements to destructure"; - hint: "the provided array has a length of {len}, \ - but the pattern expects {expected}"; - code: &E0301_ARRAY_DESTRUCTURING_WRONG_NUMBER_OF_ELEMENTS; - ); - - if len > count { - err.hint("use `..` to ignore the remaining elements, or `..rest` to bind them"); - } - - err } #[cfg(test)] @@ -203,7 +75,7 @@ mod tests { use super::*; use crate::test::{assert_eval, assert_eval_with_vm, eval_code_with_vm, TestWorld}; use compose_error_codes::{E0004_MUTATE_IMMUTABLE_VARIABLE, W0001_USED_UNINITIALIZED_VARIABLE}; - use compose_library::{BindingKind, UnitValue}; + use compose_library::{BindingKind, UnitValue, Value}; #[test] fn test_let_binding() { @@ -276,7 +148,7 @@ mod tests { assert_eval_with_vm(&mut vm, &world, "let a"); // reading emits warning let result = eval_code_with_vm(&mut vm, &world, "a") - .assert_warnings(&[W0001_USED_UNINITIALIZED_VARIABLE]) + .assert_warnings(&[&W0001_USED_UNINITIALIZED_VARIABLE]) .assert_no_errors() .get_value(); @@ -366,6 +238,6 @@ mod tests { assert_eq!(binding.read(), &Value::Int(3)); eval_code_with_vm(&mut vm, &world, "a = 4") - .assert_errors(&[E0004_MUTATE_IMMUTABLE_VARIABLE]); + .assert_errors(&[&E0004_MUTATE_IMMUTABLE_VARIABLE]); } } diff --git a/compose-eval/src/expression/block.rs b/compose-eval/src/expression/block.rs index dc855898..2d67207f 100644 --- a/compose-eval/src/expression/block.rs +++ b/compose-eval/src/expression/block.rs @@ -1,6 +1,7 @@ -use crate::{Eval, Evaluated, Machine}; +use crate::{Eval, Machine}; use compose_library::diag::SourceResult; use compose_syntax::ast::CodeBlock; +use crate::evaluated::Evaluated; impl Eval for CodeBlock<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { @@ -9,7 +10,7 @@ impl Eval for CodeBlock<'_> { let statements = self.statements(); - vm.in_scope(|vm| { + vm.in_lexical_scope(|vm| { for statement in statements { result = statement.eval(vm)?; if vm.flow.is_some() { diff --git a/compose-eval/src/expression/call.rs b/compose-eval/src/expression/call.rs index 1e6ab5fe..5a563818 100644 --- a/compose-eval/src/expression/call.rs +++ b/compose-eval/src/expression/call.rs @@ -1,11 +1,12 @@ use crate::vm::ErrorMode; -use crate::{Eval, Evaluated, Machine}; -use compose_library::diag::{At, SourceResult, Spanned, Trace, TracePoint, bail}; +use crate::{Eval, Machine}; +use compose_library::diag::{bail, At, SourceResult, Spanned, Trace, TracePoint}; use compose_library::{Arg, Args, Func, NativeScope, Type, UnboundItem, Value}; use compose_syntax::ast::AstNode; -use compose_syntax::{Label, Span, ast}; -use ecow::{EcoString, EcoVec, eco_format}; +use compose_syntax::{ast, Label, Span}; +use ecow::{eco_format, EcoString, EcoVec}; use extension_traits::extension; +use crate::evaluated::Evaluated; impl Eval for ast::FuncCall<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/expression/captures_visitor.rs b/compose-eval/src/expression/captures_visitor.rs new file mode 100644 index 00000000..d9ede792 --- /dev/null +++ b/compose-eval/src/expression/captures_visitor.rs @@ -0,0 +1,465 @@ +use compose_library::{Binding, Library, Scope, Scopes, Value}; +use compose_syntax::ast::{Arg, AstNode, Expr, Ident, ParamKind, Statement}; +use compose_syntax::{ast, Span}; +use ecow::EcoString; +use std::collections::HashMap; +use std::path::PathBuf; + +/// Visits a closure and determines which variables are captured implicitly. +#[derive(Debug)] +pub struct CapturesVisitor<'a> { + /// The external scope that variables might be captured from. + external: &'a Scopes<'a>, + /// The internal scope of variables defined within the closure. + internal: Scopes<'a>, + /// The variables that are captured. + captures: HashMap, +} + +impl<'a> CapturesVisitor<'a> { + pub fn new(external: &'a Scopes<'a>, library: Option<&'a Library>, existing: &Scope) -> Self { + let mut visitor = Self { + external, + internal: Scopes::new(library), + captures: HashMap::new(), + }; + + for (k, v) in existing.bindings() { + visitor + .internal + .top_lexical_mut() + .bind(k.clone(), v.clone()); + } + + visitor + } + pub(crate) fn visit_lambda(&mut self, closure: ast::Lambda<'a>) { + for param in closure.params().children() { + match param.kind() { + ParamKind::Pos(pat) => { + for ident in pat.bindings() { + self.bind(ident); + } + } + ParamKind::Named(named) => { + self.bind(named.name()); + } + } + } + + for capture in closure.captures().children() { + self.bind(capture.binding()); + } + + for statement in closure.statements() { + self.visit_statement(statement); + } + } + + fn visit_statement(&mut self, statement: Statement<'a>) { + self.internal.enter_flow(); + match statement { + Statement::Let(let_binding) => { + if let Some(init) = let_binding.initial_value() { + self.visit_expr(init) + } + for binding in let_binding.pattern().bindings() { + self.bind(binding); + } + } + Statement::Expr(expr) => self.visit_expr(expr), + Statement::Assign(assign) => { + self.visit_expr(assign.lhs()); + self.visit_expr(assign.rhs()); + } + Statement::Break(brk) => { + if let Some(expr) = brk.value() { + self.visit_expr(expr); + } + } + Statement::Return(ret) => { + if let Some(expr) = ret.value() { + self.visit_expr(expr); + } + } + Statement::Continue(_) => {} + Statement::ModuleImport(import) => { + if let Some(alias) = import.alias() { + self.bind(alias); + } else { + let path = PathBuf::from(import.source().as_str()); + if let Some(stem) = path.file_stem() { + self.bind_eco_str(stem.to_string_lossy().into(), import.source_span()); + } + } + } + } + self.internal.exit_flow(); + } + + fn visit_expr(&mut self, expr: Expr<'a>) { + match expr { + Expr::Ident(ident) => self.capture(ident), + Expr::CodeBlock(block) => self.visit_code_block(block), + Expr::FieldAccess(access) => { + self.visit_expr(access.target()); + } + Expr::Lambda(closure) => { + for param in closure.params().children() { + if let ParamKind::Named(named) = param.kind() { + self.visit_expr(named.expr()); + } + } + + for capture in closure.captures().children() { + self.capture(capture.binding()); + } + + // NOTE: For now we do not try to analyse the body of the closure. + // This is because the closure might try to recursively call itself + // and in simple ast walking, that is really hard to resolve correctly. + // Any capture errors in the body will be caught when the outer body is evaluated. + } + + Expr::ForLoop(for_loop) => { + // Created in outer scope + self.internal.enter_flow(); + self.visit_expr(for_loop.iterable()); + + self.internal.enter_lexical(); + let pattern = for_loop.binding(); + for ident in pattern.bindings() { + self.bind(ident); + } + + for stmt in for_loop.body().statements() { + self.visit_statement(stmt); + } + + self.internal.exit_lexical(); + self.internal.exit_flow(); + } + Expr::IsExpression(is) => { + self.visit_expr(is.expr()); + + for ident in is.pattern().bindings() { + self.bind_flow(ident); + } + } + Expr::Unary(unary) => self.visit_expr(unary.expr()), + Expr::Unit(_) => {} + Expr::Binary(binary) => { + self.visit_expr(binary.lhs()); + self.visit_expr(binary.rhs()); + } + Expr::Int(_) => {} + Expr::Str(_) => {} + Expr::Bool(_) => {} + Expr::FuncCall(call) => { + self.visit_expr(call.callee()); + for arg in call.args().items() { + match arg { + Arg::Pos(expr) => self.visit_expr(expr), + Arg::Named(named) => self.visit_expr(named.expr()), + } + } + } + Expr::PathAccess(path) => self.visit_expr(path.target()), + Expr::Parenthesized(paren) => self.visit_expr(paren.expr()), + Expr::Conditional(cond) => { + self.internal.enter_flow(); + self.visit_expr(cond.condition().expr()); + self.visit_code_block(cond.consequent()); + self.internal.exit_flow(); + + for alternates in cond.cond_alternates() { + self.internal.enter_flow(); + self.visit_expr(alternates.condition().expr()); + self.visit_code_block(alternates.consequent()); + self.internal.exit_flow(); + } + + if let Some(else_) = cond.cond_else() { + self.visit_code_block(else_.consequent()) + } + } + Expr::WhileLoop(while_) => { + self.internal.enter_flow(); + self.visit_expr(while_.condition().expr()); + self.visit_code_block(while_.body()); + self.internal.exit_flow(); + } + Expr::Array(arr) => { + for expr in arr.elements() { + self.visit_expr(expr); + } + } + Expr::Range(r) => { + if let Some(start) = r.start() { + self.visit_expr(start); + } + if let Some(end) = r.end() { + self.visit_expr(end); + } + } + Expr::Map(map) => { + for entry in map.entries() { + match entry.key() { + Expr::Ident(_) => {} + expr => self.visit_expr(expr), + } + self.visit_expr(entry.value()); + } + } + Expr::IndexAccess(index) => { + self.visit_expr(index.target()); + self.visit_expr(index.index()); + } + Expr::MatchExpression(match_) => { + self.internal.enter_flow(); + self.visit_expr(match_.expr()); + + for arm in match_.match_arms() { + self.internal.enter_flow(); + for binding in arm.patterns().flat_map(|pat| pat.bindings()) { + self.bind(binding); + } + if let Some(match_guard) = arm.guard() { + self.visit_expr(match_guard); + } + self.visit_expr(arm.expr()); + self.internal.exit_flow(); + } + + self.internal.exit_flow(); + } + } + } + + fn visit_code_block(&mut self, block: ast::CodeBlock<'a>) { + self.internal.enter_lexical(); + for child in block.statements() { + self.visit_statement(child); + } + self.internal.exit_lexical(); + } + + fn bind(&mut self, ident: Ident) { + self.internal.top_lexical_mut().bind( + ident.get().clone(), + Binding::new(Value::unit(), ident.span()), + ); + } + + fn bind_eco_str(&mut self, name: EcoString, span: Span) { + self.internal + .top_lexical_mut() + .bind(name, Binding::new(Value::unit(), span)); + } + + fn bind_flow(&mut self, ident: Ident) { + if let Some(flow) = self.internal.top_flow_mut() { + flow.bind( + ident.get().clone(), + Binding::new(Value::unit(), ident.span()), + ); + } + } + + fn capture(&mut self, ident: Ident<'a>) { + if self.internal.get(&ident).is_ok() { + // Was defined internally, no need to capture + return; + } + + // If the value does not exist in the external scope, it is not captured. + if self.external.get(&ident).is_ok() { + self.captures + .entry(ident.get().clone()) + .or_insert(ident.span()); + } + } + + pub(crate) fn finish(self) -> HashMap { + self.captures + } +} + +#[cfg(test)] +mod tests { + use crate::expression::captures_visitor::CapturesVisitor; + use crate::test::{print_diagnostics, TestWorld}; + use compose_library::diag::SourceDiagnostic; + use compose_library::{Scope, Scopes}; + use compose_syntax::ast; + + #[track_caller] + fn test(text: &str, expected_names: &[&str]) { + let mut scopes = Scopes::new(None); + scopes.top_lexical_mut().define("f", 0i64); + scopes.top_lexical_mut().define("x", 0i64); + scopes.top_lexical_mut().define("y", 0i64); + scopes.top_lexical_mut().define("z", 0i64); + + let mut existing = Scope::new_lexical(); + existing.define("a", 0i64); + existing.define("b", 0i64); + existing.define("c", 0i64); + let mut visitor = CapturesVisitor::new(&scopes, None, &existing); + let world = TestWorld::from_str(text); + + + let source = world.entrypoint_src(); + let mut fail = false; + for node in source.nodes() { + let errors = node + .errors() + .into_iter() + .map(SourceDiagnostic::from) + .collect::>(); + if !errors.is_empty() { + print_diagnostics(&TestWorld::new(), &errors, &[]); + fail = true; + } + visitor.visit_statement(node.cast::().expect("expected a statement")); + } + + if fail { + panic!("failed to parse"); + } + + let captures = visitor.finish(); + let mut names: Vec<_> = captures.iter().map(|(k, ..)| k).collect(); + names.sort(); + + assert_eq!(names, expected_names); + } + + #[test] + fn captures_identifier_from_let_initializer() { + test("let t = x;", &["x"]); + } + + #[test] + fn captures_self_reference_in_let_initializer() { + test("let x = x;", &["x"]); + } + + #[test] + fn let_without_initializer_captures_nothing() { + test("let x;", &[]); + } + + #[test] + fn does_not_capture_defined_variables() { + test("let x = 2; x + y;", &["y"]); + } + + #[test] + fn captures_identifiers_from_expression() { + test("x + y", &["x", "y"]); + } + + #[test] + fn assignment_captures_lhs_and_rhs() { + test("x += y;", &["x", "y"]); + } + + #[test] + fn simple_assignment_captures_lhs_and_rhs() { + test("x = y;", &["x", "y"]); + } + + #[test] + fn closure_definition_does_not_capture_from_body() { + test("let f = { => x + y; }", &[]); + } + + #[test] + fn closure_with_capture_list_captures_explicit_names() { + test("let f = { |x| => x + y; }", &["x"]); + } + + #[test] + fn closure_body_referencing_outer_name_does_not_force_capture() { + test("let f = { |x| => f(); }", &["x"]); + } + + #[test] + fn closure_does_not_capture_positional_parameters() { + test("let f = { x, y, z => f(); }", &[]); + } + + #[test] + fn closure_with_named_parameters_captures_default_values() { + test("let f = { x: x, y: y, z: z => f(); }", &["x", "y", "z"]); + } + + #[test] + fn for_loop_captures_iterable_and_body_uses() { + test("for (x in y) { x + z; };", &["y", "z"]); + } + + #[test] + fn for_loop_binding_does_not_escape_and_requires_capture() { + test("for (x in y) { x; }; x", &["x", "y"]); + } + + #[test] + fn block_expression_captures_inner_identifier() { + test("{ x; };", &["x"]); + } + + #[test] + fn block_local_binding_prevents_capture() { + test("{ let x; x; };", &[]); + } + + #[test] + fn block_binding_does_not_escape_and_requires_capture() { + test("{ let x; x; }; x;", &["x"]); + } + + #[test] + fn field_access_captures_receiver_and_arguments() { + test("x.y.f(z);", &["x", "z"]); + } + + #[test] + fn parenthesized_expression_captures_identifiers() { + test("(x + z);", &["x", "z"]); + } + + #[test] + fn nested_parenthesized_closure_does_not_capture_inner_bindings() { + test("(({ x => x + y }) + y);", &["y"]); + } + + #[test] + fn if_flow_binding_does_not_require_capture_of_bound_names() { + test( + "if ([1, 2] is [x, y] && y == 2) { x + y + z; }", + &["z"], + ); + } + + #[test] + fn match_arm_binding_does_not_require_capture() { + test("match (1) { Int x => x + y };", &["y"]); + } + + #[test] + fn match_guard_binding_does_not_require_capture() { + test("match (1) { Int x if x > 0 => y };", &["y"]); + } + + #[test] + fn match_guard_flow_binding_does_not_require_capture() { + test("match (1) { Int x if x is Int y => y };", &[]); + } + + #[test] + fn while_flow_binding_does_not_require_capture() { + test("while (x is [first, ..]) { first }", &["x"]); + } +} diff --git a/compose-eval/src/expression/closure.rs b/compose-eval/src/expression/closure.rs index 671e9a76..991fea10 100644 --- a/compose-eval/src/expression/closure.rs +++ b/compose-eval/src/expression/closure.rs @@ -1,33 +1,35 @@ +use crate::expression::captures_visitor::CapturesVisitor; +use crate::expression::pattern::{destructure_pattern, PatternContext, PatternMatchResult}; use crate::vm::{FlowEvent, TrackedContainer}; -use crate::{Eval, Evaluated, Machine, ValueEvaluatedExtensions}; -use compose_library::diag::{IntoSourceDiagnostic, SourceResult, Spanned, bail, error}; +use crate::{Eval, Machine}; +use compose_library::diag::{bail, error, IntoSourceDiagnostic, SourceResult, Spanned}; use compose_library::{ - Args, Binding, BindingKind, Closure, Func, IntoValue, Library, Scope, Scopes, Value, + Args, Binding, BindingKind, Closure, Func, IntoValue, Scope, Value, VariableAccessError, Visibility, }; -use compose_syntax::ast::{AstNode, Expr, Ident, Param, ParamKind}; -use compose_syntax::{Label, Span, SyntaxNode, ast}; -use ecow::{EcoString, EcoVec}; -use std::collections::HashMap; +use compose_syntax::ast::{AstNode, Expr, Ident, Param, ParamKind, Pattern}; +use compose_syntax::{ast, Label}; +use ecow::EcoVec; +use crate::evaluated::{Evaluated, ValueEvaluatedExtensions}; impl Eval for ast::Lambda<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { - let guard = vm.temp_root_guard(); + let vm = &mut vm.temp_root_guard(); let mut defaults = Vec::new(); for param in self.params().children() { - if let ast::ParamKind::Named(named) = param.kind() { - defaults.push(named.expr().eval(guard.vm)?.value); + if let ParamKind::Named(named) = param.kind() { + defaults.push(named.expr().eval(vm)?.value); } } let captured = { let mut errors = EcoVec::new(); - let mut scope = Scope::new(); + let mut scope = Scope::new_lexical(); for capture in self.captures().children() { let span = capture.binding().span(); let name = capture.binding().get(); - let binding = match guard.vm.get(&capture.binding()).cloned() { + let binding = match vm.get(&capture.binding()).cloned() { Ok(v) => v, Err(e) => { let VariableAccessError::Unbound(unbound) = e else { @@ -48,7 +50,7 @@ impl Eval for ast::Lambda<'_> { } }; - let value = match validate_capture(capture, &binding, guard.vm) { + let value = match validate_capture(capture, &binding, vm) { Ok(v) => v, Err(e) => { errors.extend(e.into_iter()); @@ -74,8 +76,8 @@ impl Eval for ast::Lambda<'_> { let unresolved_captures = { let mut visitor = CapturesVisitor::new( - &guard.vm.frames.top.scopes, - Some(guard.vm.engine.world.library()), + &vm.frames.top.scopes, + Some(vm.engine.world.library()), &captured, ); visitor.visit_lambda(self); @@ -95,7 +97,7 @@ impl Eval for ast::Lambda<'_> { unresolved_captures, }; - if !guard.vm.context.closure_capture.should_defer() { + if !vm.context.closure_capture.should_defer() { closure.resolve_captures()? } @@ -171,7 +173,7 @@ fn define( //noinspection RsUnnecessaryQualifications - False Positive pub fn eval_lambda(closure: &Closure, vm: &mut Machine, args: Args) -> SourceResult { - let guard = vm.temp_root_guard(); + let vm = &mut vm.temp_root_guard(); let ast_closure = closure .node .cast::() @@ -180,10 +182,9 @@ pub fn eval_lambda(closure: &Closure, vm: &mut Machine, args: Args) -> SourceRes let statements = ast_closure.statements(); // Make sure a gc round is aware that the args are reachable - guard.vm.track_tmp_root(&args); + vm.track_tmp_root(&args); - let result = guard - .vm + let result = vm .with_frame(move |vm| { let mut args = args; if let Some(Spanned { value, span }) = &closure.name { @@ -201,10 +202,30 @@ pub fn eval_lambda(closure: &Closure, vm: &mut Machine, args: Args) -> SourceRes for p in params.children() { match p.kind() { ast::ParamKind::Pos(pattern) => match pattern { - ast::Pattern::Single(Expr::Ident(ident)) => { + Pattern::Single(Expr::Ident(ident)) => { define(vm, ident, args.expect(&ident)?, p)?; } - pattern => bail!(pattern.span(), "Patterns not supported in closures yet"), + pattern => { + let Some(v) = args.eat()? else { + bail!(pattern.span(), "missing argument for this parameter"); + }; + let binding_kind = match p.is_mut() { + false => BindingKind::Param, + true => BindingKind::ParamMut, + }; + + match destructure_pattern( + vm, + pattern, + v, + PatternContext::Parameter, + binding_kind, + Visibility::Private, + )? { + PatternMatchResult::NotMatched(err) => bail!(err), + PatternMatchResult::Matched => {} + } + } }, ast::ParamKind::Named(named) => { let name = named.name(); @@ -240,170 +261,16 @@ pub fn eval_lambda(closure: &Closure, vm: &mut Machine, args: Args) -> SourceRes SourceResult::Ok(output) }) - .track_tmp_root(guard.vm); + .track_tmp_root(vm); - guard.vm.maybe_gc(); + vm.maybe_gc(); result } -/// Visits a closure and determines which variables are captured implicitly. -#[derive(Debug)] -pub struct CapturesVisitor<'a> { - /// The external scope that variables might be captured from. - external: &'a Scopes<'a>, - /// The internal scope of variables defined within the closure. - internal: Scopes<'a>, - /// The variables that are captured. - captures: HashMap, -} - -impl<'a> CapturesVisitor<'a> { - pub fn new(external: &'a Scopes<'a>, library: Option<&'a Library>, existing: &Scope) -> Self { - let mut inst = Self { - external, - internal: Scopes::new(library), - captures: HashMap::new(), - }; - - for (k, v) in existing.bindings() { - inst.internal.top.bind(k.clone(), v.clone()); - } - - inst - } - pub(crate) fn visit_lambda(&mut self, closure: ast::Lambda<'a>) { - for param in closure.params().children() { - match param.kind() { - ParamKind::Pos(pat) => { - for ident in pat.bindings() { - self.bind(ident); - } - } - ParamKind::Named(named) => { - self.bind(named.name()); - } - } - } - - for capture in closure.captures().children() { - self.visit(capture.to_untyped()); - } - - for statement in closure.statements() { - self.visit(statement.to_untyped()); - } - } - - pub fn visit(&mut self, node: &'a SyntaxNode) { - if let Some(ast::Statement::Let(let_binding)) = node.cast() { - if let Some(init) = let_binding.initial_value() { - self.visit(init.to_untyped()) - } - - for ident in let_binding.pattern().bindings() { - self.bind(ident); - } - return; - } - - let expr = match node.cast::() { - Some(expr) => expr, - None => { - if let Some(named) = node.cast::() { - // Don't capture the name of a named parameter. - self.visit(named.expr().to_untyped()); - return; - } - - Expr::default() - } - }; - - match expr { - Expr::Ident(ident) => self.capture(ident), - Expr::CodeBlock(_) => { - self.internal.enter(); - for child in node.children() { - self.visit(child); - } - self.internal.exit(); - } - Expr::FieldAccess(access) => { - self.visit(access.target().to_untyped()); - } - Expr::Lambda(closure) => { - for param in closure.params().children() { - if let ast::ParamKind::Named(named) = param.kind() { - self.visit(named.expr().to_untyped()); - } - } - - for capture in closure.captures().children() { - self.visit(capture.to_untyped()); - } - - // NOTE: For now we do not try to analyse the body of the closure. - // This is because the closure might try to recursively call itself - // and in simple ast walking, that is really hard to resolve correctly. - // Any errors in the body will be caught when the outer body is evaluated. - } - - Expr::ForLoop(for_loop) => { - // Created in outer scope - self.visit(for_loop.iterable().to_untyped()); - - self.internal.enter(); - let pattern = for_loop.binding(); - for ident in pattern.bindings() { - self.bind(ident); - } - - self.visit(for_loop.body().to_untyped()); - self.internal.exit(); - } - - _ => { - // If not an expression or named, just go over all the children - for child in node.children() { - self.visit(child); - } - } - } - } - - fn bind(&mut self, ident: Ident) { - self.internal.top.bind( - ident.get().clone(), - Binding::new(Value::unit(), ident.span()), - ); - } - - fn capture(&mut self, ident: Ident<'a>) { - if self.internal.get(&ident).is_ok() { - // Was defined internally, no need to capture - return; - } - - // If the value does not exist in the external scope, it is not captured. - if self.external.get(&ident).is_ok() { - self.captures - .entry(ident.get().clone()) - .or_insert(ident.span()); - } - } - - fn finish(self) -> HashMap { - self.captures - } -} - #[cfg(test)] mod tests { - use crate::expression::closure::CapturesVisitor; use crate::test::*; - use compose_library::{Scope, Scopes}; - use compose_syntax::{FileId, parse}; #[test] fn capturing() { @@ -449,81 +316,4 @@ mod tests { ); } - #[track_caller] - fn test(scopes: &Scopes, existing_scope: &Scope, text: &str, expected_names: &[&str]) { - let mut visitor = CapturesVisitor::new(scopes, None, existing_scope); - let nodes = parse(text, FileId::new("test.comp")); - - for node in &nodes { - assert!(node.errors().is_empty(), "node has errors: {:#?}", node.errors()); - visitor.visit(node); - } - - let captures = visitor.finish(); - let mut names: Vec<_> = captures.iter().map(|(k, ..)| k).collect(); - names.sort(); - - assert_eq!(names, expected_names); - } - - #[test] - fn test_captures_visitor() { - let mut scopes = Scopes::new(None); - scopes.top.define("f", 0i64); - scopes.top.define("x", 0i64); - scopes.top.define("y", 0i64); - scopes.top.define("z", 0i64); - let s = &scopes; - - let mut existing = Scope::new(); - existing.define("a", 0i64); - existing.define("b", 0i64); - existing.define("c", 0i64); - let e = &existing; - - test(s, e, "{ x => x * 2; }", &[]); - - // let binding - test(s, e, "let t = x;", &["x"]); - test(s, e, "let x = x;", &["x"]); - test(s, e, "let x;", &[]); - test(s, e, "let x = 2; x + y;", &["y"]); - test(s, e, "x + y", &["x", "y"]); - - // assignment - test(s, e, "x += y;", &["x", "y"]); - test(s, e, "x = y;", &["x", "y"]); - - // closure definition - // Closure bodies are ignored - test(s, e, "let f = { => x + y; }", &[]); - // with capture - test(s, e, "let f = { |x| => x + y; }", &["x"]); - test(s, e, "let f = { |x| => f(); }", &["x"]); - // with params - test(s, e, "let f = { x, y, z => f(); }", &[]); - // named params - test( - s, - e, - "let f = { x: x, y: y, z: z => f(); }", - &["x", "y", "z"], - ); - - // for loop - test(s, e, "for (x in y) { x + z; };", &["y", "z"]); - test(s, e, "for (x in y) { x; }; x", &["x", "y"]); - - // block - test(s, e, "{ x; };", &["x"]); - test(s, e, "{ let x; x; };", &[]); - test(s, e, "{ let x; x; }; x;", &["x"]); - - // field access - test(s, e, "x.y.f(z);", &["x", "z"]); - - // parenthesized - test(s, e, "(x + z);", &["x", "z"]); - test(s, e, "(({ x => x + y }) + y);", &["y"]); - } } diff --git a/compose-eval/src/expression/control_flow.rs b/compose-eval/src/expression/control_flow.rs index de400a02..667948eb 100644 --- a/compose-eval/src/expression/control_flow.rs +++ b/compose-eval/src/expression/control_flow.rs @@ -1,20 +1,26 @@ -use crate::expression::bindings::destructure_pattern; +use crate::expression::pattern::{destructure_pattern, PatternContext, PatternMatchResult}; use crate::vm::{FlowEvent, Tracked}; -use crate::{Eval, Evaluated, Machine}; +use crate::{Eval, Machine}; use compose_library::diag::{At, SourceResult}; -use compose_library::{BindingKind, IterValue, Value, ValueIterator, Visibility}; +use compose_library::{bail, BindingKind, IterValue, Value, ValueIterator, Visibility}; use compose_syntax::ast; use compose_syntax::ast::AstNode; +use crate::evaluated::Evaluated; impl Eval for ast::Conditional<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { - if eval_condition(self.condition(), vm)? { - return self.consequent().eval(vm); + { + let vm_in_flow_scope = &mut vm.new_flow_scope_guard(); + if eval_condition(self.condition().expr(), vm_in_flow_scope)? { + return self.consequent().eval(vm_in_flow_scope); + } } for alternate in self.cond_alternates() { - if eval_condition(alternate.condition(), vm)? { - return alternate.consequent().eval(vm); + let vm_in_flow_scope = &mut vm.new_flow_scope_guard(); + + if eval_condition(alternate.condition().expr(), vm_in_flow_scope)? { + return alternate.consequent().eval(vm_in_flow_scope); } } @@ -30,19 +36,23 @@ impl Eval for ast::WhileLoop<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { let mut output = Value::unit(); let flow = vm.flow.take(); - while eval_condition(self.condition(), vm)? { - output = self.body().eval(vm)?.value; + loop { + let vm_in_flow_scope = &mut vm.new_flow_scope_guard(); + if !eval_condition(self.condition().expr(), vm_in_flow_scope)? { + break; + } + output = self.body().eval(vm_in_flow_scope)?.value; - match &vm.flow { + match &vm_in_flow_scope.flow { None => {} Some(FlowEvent::Break(_, value)) => { if let Some(value) = value { output = value.clone(); } - vm.flow = None; + vm_in_flow_scope.flow = None; break; } - Some(FlowEvent::Continue(_)) => vm.flow = None, + Some(FlowEvent::Continue(_)) => vm_in_flow_scope.flow = None, Some(FlowEvent::Return(..)) => break, } } @@ -58,54 +68,55 @@ impl Eval for ast::WhileLoop<'_> { impl Eval for ast::ForLoop<'_> { //noinspection RsUnnecessaryQualifications - False positive fn eval(self, vm: &mut Machine) -> SourceResult { - let root_guard = vm.temp_root_guard(); + let vm = &mut vm.temp_root_guard(); let mut output = Value::unit(); let pattern = self.binding(); let iterable_expr = self.iterable(); let iterator = { - let value = iterable_expr - .eval(root_guard.vm)? - .track_tmp_root(root_guard.vm); - IterValue::try_from_value(value.value, value.mutable, root_guard.vm) + let value = iterable_expr.eval(vm)?.track_tmp_root(vm); + IterValue::try_from_value(value.value, value.mutable, &mut **vm) .at(iterable_expr.span())? - .track_tmp_root(root_guard.vm) + .track_tmp_root(vm) }; let body = self.body(); - let flow = root_guard.vm.flow.take(); + let flow = vm.flow.take(); - while let Some(v) = iterator.next(root_guard.vm)? { - root_guard.vm.in_scope(|vm| { - destructure_pattern( + while let Some(v) = iterator.next(&mut **vm)? { + vm.in_lexical_scope(|vm| { + if let PatternMatchResult::NotMatched(err) = destructure_pattern( vm, pattern, v, + PatternContext::ForLoopBinding, BindingKind::Immutable { first_assign: None }, Visibility::Private, - )?; + )? { + bail!(err); + } output = body.eval(vm)?.value; SourceResult::Ok(()) })?; - match &root_guard.vm.flow { + match &vm.flow { None => {} Some(FlowEvent::Break(_, value)) => { if let Some(value) = value { output = value.clone(); } - root_guard.vm.flow = None; + vm.flow = None; break; } - Some(FlowEvent::Continue(_)) => root_guard.vm.flow = None, + Some(FlowEvent::Continue(_)) => vm.flow = None, Some(FlowEvent::Return(..)) => break, } } if let Some(flow) = flow { - root_guard.vm.flow = Some(flow); + vm.flow = Some(flow); } Ok(Evaluated::mutable(output)) @@ -114,8 +125,9 @@ impl Eval for ast::ForLoop<'_> { /// Evaluates the condition expression and ensures it's a boolean. #[inline] -fn eval_condition(cond: ast::Condition<'_>, vm: &mut Machine) -> SourceResult { - let cond_expr = cond.expr(); - let cond_value = cond_expr.eval(vm)?; - cond_value.value.cast::().at(cond_expr.span()) +pub fn eval_condition(expr: ast::Expr<'_>, vm: &mut Machine) -> SourceResult { + let cond_value = expr.eval(vm)?; + let as_bool = cond_value.value.cast::().at(expr.span())?; + + Ok(as_bool) } diff --git a/compose-eval/src/expression/field_access.rs b/compose-eval/src/expression/field_access.rs index 5314c6d1..46169184 100644 --- a/compose-eval/src/expression/field_access.rs +++ b/compose-eval/src/expression/field_access.rs @@ -1,7 +1,8 @@ -use crate::{Eval, Evaluated, Machine}; +use crate::{Eval, Machine}; use compose_library::diag::{At, SourceResult}; use compose_syntax::ast; use compose_syntax::ast::AstNode; +use crate::evaluated::Evaluated; impl Eval for ast::FieldAccess<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/expression/index_access.rs b/compose-eval/src/expression/index_access.rs index cb1d19f6..3fa483a3 100644 --- a/compose-eval/src/expression/index_access.rs +++ b/compose-eval/src/expression/index_access.rs @@ -1,9 +1,10 @@ use crate::vm::Tracked; -use crate::{Eval, Evaluated, Machine}; +use crate::{Eval, Machine}; use compose_library::diag::SourceResult; use compose_library::{IntoValue, Vm}; use compose_syntax::ast; use compose_syntax::ast::AstNode; +use crate::evaluated::Evaluated; impl Eval for ast::IndexAccess<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/expression/map.rs b/compose-eval/src/expression/map.rs index a02c7d7b..c4c36762 100644 --- a/compose-eval/src/expression/map.rs +++ b/compose-eval/src/expression/map.rs @@ -1,9 +1,10 @@ -use crate::{Eval, Evaluated, Machine}; +use crate::{Eval, Machine}; use compose_library::diag::{bail, SourceResult}; use compose_library::{MapValue, Value, Vm}; use compose_syntax::ast; use compose_syntax::ast::{AstNode, Expr}; use std::collections::HashMap; +use crate::evaluated::Evaluated; impl Eval for ast::MapLiteral<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/expression/match_expression.rs b/compose-eval/src/expression/match_expression.rs new file mode 100644 index 00000000..a6f18056 --- /dev/null +++ b/compose-eval/src/expression/match_expression.rs @@ -0,0 +1,68 @@ +use crate::expression::pattern::{destructure_into_flow, PatternMatchResult}; +use crate::{Eval, Machine}; +use compose_library::Value; +use compose_library::diag::{bail, SourceResult}; +use compose_library::repr::Repr; +use compose_syntax::ast::{AstNode, MatchExpression, Pattern}; +use compose_utils::trace_fn; +use crate::evaluated::Evaluated; + +impl Eval for MatchExpression<'_> { + fn eval(self, vm: &mut Machine) -> SourceResult { + trace_fn!("eval_match_expression"); + let vm = &mut vm.new_flow_scope_guard(); + let value = self.expr().eval(vm)?.value; + + for arm in self.match_arms() { + trace_fn!("eval_match_arm"); + let vm_in_arm_flow_scope = &mut vm.new_flow_scope_guard(); + + if !try_patterns(&value, vm_in_arm_flow_scope, arm.patterns())? { + continue; + } + + if let Some(guard) = arm.guard() { + match guard.eval(vm_in_arm_flow_scope)?.value { + Value::Bool(true) => {} // matched, carry on evaluating the arm + // Continue matching arms if the guard evaluates to false. + Value::Bool(false) => continue, + other => { + bail!(guard.span(), "match guard must evaluate to a boolean"; + label_message: "guard evaluated to `{}`", other.repr(&mut **vm_in_arm_flow_scope); + note: "expected `Bool`, found `{}`", other.ty(); + hint: "consider removing the guard to match any value") + } + } + } + + let result = arm.expr().eval(vm_in_arm_flow_scope)?; + + return Ok(result); + } + + // no matches + bail!(self.span(), "no match arm applies to this value"; + note: "the value: `{}` is not covered by any match arm", value.repr(&mut **vm); + hint: "consider adding a default `_ => ...` arm to handle all remaining cases" + ) + } +} + +fn try_patterns<'a>( + value: &Value, + vm: &mut Machine, + patterns: impl IntoIterator>, +) -> SourceResult { + for pattern in patterns { + match destructure_into_flow(vm, value.clone(), pattern)? { + PatternMatchResult::Matched => { + return Ok(true); + } + PatternMatchResult::NotMatched(_) => { + continue; + } + } + } + + Ok(false) +} diff --git a/compose-eval/src/expression/mod.rs b/compose-eval/src/expression/mod.rs index 452545e9..ff2d30e7 100644 --- a/compose-eval/src/expression/mod.rs +++ b/compose-eval/src/expression/mod.rs @@ -1,6 +1,6 @@ -use crate::vm::{Machine, Tracked}; -use crate::{Eval, Evaluated}; -use compose_library::diag::{SourceResult}; +use crate::vm::Machine; +use crate::Eval; +use compose_library::diag::SourceResult; use compose_syntax::ast::{AstNode, Expr}; mod assignment; @@ -19,8 +19,12 @@ mod array; mod range; mod map; mod index_access; +mod pattern; +mod match_expression; +mod captures_visitor; pub use closure::eval_lambda; +use crate::evaluated::Evaluated; impl Eval for Expr<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { @@ -28,7 +32,6 @@ impl Eval for Expr<'_> { let v = match self { Expr::Int(i) => i.eval(vm), Expr::Binary(b) => b.eval(vm), - Expr::LetBinding(l) => l.eval(vm), Expr::Ident(i) => i.eval(vm), Expr::CodeBlock(c) => c.eval(vm), Expr::Unit(_) => Ok(Evaluated::unit()), @@ -47,9 +50,10 @@ impl Eval for Expr<'_> { Expr::Map(m) => m.eval(vm), Expr::Lambda(l) => l.eval(vm), Expr::IndexAccess(i) => i.eval(vm), + Expr::MatchExpression(m) => m.eval(vm), + Expr::IsExpression(i) => i.eval(vm), }? - .spanned(span) - .track_tmp_root(vm); + .spanned(span); Ok(v) } diff --git a/compose-eval/src/expression/parenthesized.rs b/compose-eval/src/expression/parenthesized.rs index 0963acb7..9edadb70 100644 --- a/compose-eval/src/expression/parenthesized.rs +++ b/compose-eval/src/expression/parenthesized.rs @@ -1,6 +1,7 @@ -use crate::{Eval, Evaluated, Machine}; +use crate::{Eval, Machine}; use compose_library::diag::SourceResult; use compose_syntax::ast; +use crate::evaluated::Evaluated; impl Eval for ast::Parenthesized<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/expression/path_access.rs b/compose-eval/src/expression/path_access.rs index 2b3df87d..70e5dded 100644 --- a/compose-eval/src/expression/path_access.rs +++ b/compose-eval/src/expression/path_access.rs @@ -1,7 +1,8 @@ -use crate::{Eval, Evaluated, Machine}; +use crate::{Eval, Machine}; use compose_library::diag::SourceResult; use compose_syntax::ast; use compose_syntax::ast::AstNode; +use crate::evaluated::Evaluated; impl Eval for ast::PathAccess<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/expression/pattern.rs b/compose-eval/src/expression/pattern.rs new file mode 100644 index 00000000..76b04a88 --- /dev/null +++ b/compose-eval/src/expression/pattern.rs @@ -0,0 +1,703 @@ +use crate::evaluated::Evaluated; +use crate::{Eval, Machine}; +use compose_error_codes::{E0007_MISSING_EQUALS_AFTER_LET_BINDING, E0301_ARRAY_DESTRUCTURING_WRONG_NUMBER_OF_ELEMENTS, E0302_MAP_DESTRUCTURING_UNCOVERED_KEYS, E0303_MAP_DESTRUCTURING_MISSING_KEY_IN_VALUE, E0304_TYPE_PATTERNS_NOT_ALLOWED_IN_LET_BINDINGS}; +use compose_library::diag::{At, SourceDiagnostic, SourceResult, Spanned}; +use compose_library::ops::Comparison; +use compose_library::{ + ArrayValue, Binding, BindingKind, DebugRepr, IntoValue, MapValue, Type, Value, Visibility, Vm, + bail, error, +}; +use compose_syntax::ast::{AstNode, DestructuringItem, Expr, Ident, LiteralPattern, Pattern}; +use compose_syntax::{Span, ast}; +use compose_utils::trace_fn; +use ecow::{EcoString, EcoVec, eco_format}; +use std::cmp::PartialEq; +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use tap::Tap; + +impl Eval for LiteralPattern<'_> { + fn eval(self, vm: &mut Machine) -> SourceResult { + self.to_untyped() + .cast::() + .expect("any literal is a valid expression") + .eval(vm) + } +} + +impl Eval for ast::IsExpression<'_> { + fn eval(self, vm: &mut Machine) -> SourceResult { + trace_fn!("eval_is_expression"); + let vm = &mut vm.in_flow_scope_guard(); + let expr = self.expr(); + let value = expr.eval(vm)?.value; + + let pat = self.pattern(); + + let matched = match destructure_into_flow(vm, value, pat)? { + PatternMatchResult::NotMatched(_) => false, + PatternMatchResult::Matched => true, + }; + + Ok(Evaluated::immutable(matched.into_value())) + } +} + +pub fn destructure_into_flow( + vm: &mut Machine, + value: Value, + pat: Pattern, +) -> SourceResult { + destructure_impl( + vm, + pat, + value, + PatternContext::FlowBinding, + MatchPath::new(), + &mut |vm, expr, value| { + match expr { + Expr::Ident(ident) => { + let name = ident.get(); + let spanned = value + .named(Spanned::new(name.clone(), ident.span())) + // Now that the names have been added, make sure any deferred errors are resolved + .resolved()?; + + vm.scopes_mut() + .top_flow_mut() + .expect("running in a flow") + .try_bind(name.clone(), Binding::new(spanned, expr.span())) + .at(expr.span())?; + + Ok(()) + } + _ => bail!(expr.span(), "cannot destructure pattern",), + } + }, + ) +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum MatchPathSegment { + ArrayIndex(usize), + MapKey(EcoString), +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct MatchPath { + segments: EcoVec, +} + +impl MatchPath { + pub fn new() -> Self { + Self { + segments: EcoVec::new(), + } + } + + pub fn is_empty(&self) -> bool { + self.segments.is_empty() + } + + pub fn with_segment(mut self, segment: MatchPathSegment) -> Self { + self.segments.push(segment); + self + } +} + +impl Default for MatchPath { + fn default() -> Self { + Self::new() + } +} + +impl Display for MatchPathSegment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MatchPathSegment::ArrayIndex(index) => write!(f, "[{}]", index), + MatchPathSegment::MapKey(key) => write!(f, ".{}", key), + } + } +} + +impl Display for MatchPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.segments.iter().try_for_each(|s| write!(f, "{}", s)) + } +} + +pub enum PatternMatchResult { + Matched, + NotMatched(SourceDiagnostic), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PatternContext { + LetBinding, + ForLoopBinding, + FlowBinding, + Parameter, +} + +pub fn destructure_pattern( + vm: &mut Machine, + pattern: Pattern, + value: Value, + ctx: PatternContext, + binding_kind: BindingKind, + visibility: Visibility, +) -> SourceResult { + trace_fn!("eval_destructure_pattern"); + destructure_impl( + vm, + pattern, + value, + ctx, + MatchPath::new(), + &mut |vm, expr, value| match expr { + Expr::Ident(ident) => { + let name = ident.get().clone(); + let spanned = value + .named(Spanned::new(name, ident.span())) + // Now that the names have been added, make sure any deferred errors are resolved + .resolved()?; + + vm.define(ident, spanned, binding_kind, visibility)?; + + Ok(()) + } + _ => bail!(expr.span(), "cannot destructure pattern",), + }, + ) +} + +pub fn destructure_impl( + vm: &mut Machine, + pattern: Pattern, + value: Value, + ctx: PatternContext, + match_path: MatchPath, + bind: &mut impl Fn(&mut Machine, Expr, Value) -> SourceResult<()>, +) -> SourceResult { + trace_fn!("eval_destructure_impl"); + match pattern { + Pattern::Single(expr) => { + if let Some(ident) = expr.cast::() + && let Some(expected_ty) = get_type(vm, ident) + { + if &value.ty() != expected_ty { + return Ok(PatternMatchResult::NotMatched( + error!(ident.span(), "Type pattern does not match"; + note: "expected `{}`, found `{}`", expected_ty, value.ty()), + )); + } + } + + bind(vm, expr, value)?; + Ok(PatternMatchResult::Matched) + } + Pattern::TypedPattern(typed_pattern) if ctx == PatternContext::LetBinding => { + // Types in Compose are resolved at runtime, not during parsing. + // This means `let x 1` is initially parsed as a typed pattern (`x 1`). + // We resolve the ambiguity here: + // - if `x` resolves to a type → disallowed typed pattern + // - otherwise → assume a missing `=` and emit E0007 + let ty = typed_pattern.ty(); + let is_type = get_type(vm, ty).is_some(); + if is_type { + let pat_text = typed_pattern.pattern().to_text(); + let ty_text = ty.get(); + bail!( + typed_pattern.span(), + "type patterns are not allowed in `let` bindings"; + note: "`let {ty_text} {pat_text} = ...` is not a type annotation in Compose"; + hint: "use a `match` or `is` expression to check the type of a value"; + code: &E0304_TYPE_PATTERNS_NOT_ALLOWED_IN_LET_BINDINGS; + ); + } else { + let binding_text = ty.get(); + bail!(ty.span().after(), "expected `=` after binding name"; + label_message: "expected `=` here"; + hint: "if you meant to initialise the binding, add `=`: `let {binding_text} = ...;`"; + hint: "or, if you meant to leave it uninitialised, add a semicolon `let {binding_text};`"; + code: &E0007_MISSING_EQUALS_AFTER_LET_BINDING) + } + } + Pattern::TypedPattern(typed_pattern) => { + let ty_ident = typed_pattern.ty(); + + let expected_ty = get_type(vm, ty_ident); + let Some(expected_ty) = expected_ty else { + bail!(typed_pattern.ty().span(), "Type in typed pattern does not exist";) + }; + + if &value.ty() != expected_ty { + return Ok(PatternMatchResult::NotMatched( + error!(ty_ident.span(), "Type pattern does not match"; + note: "expected `{}`, found `{}`", expected_ty, value.ty()), + )); + } + + destructure_impl(vm, typed_pattern.pattern(), value, ctx, match_path, bind) + } + Pattern::PlaceHolder(_) => Ok(PatternMatchResult::Matched), // A placeholder means we discard the value, no need to bind + Pattern::Destructuring(destruct) => match value { + Value::Array(value) => destructure_array(vm, destruct, value, ctx, match_path, bind), + Value::Map(value) => destructure_map(vm, destruct, value, ctx, match_path, bind), + _ => bail!(pattern.span(), "cannot destructure {}", value.ty()), + }, + Pattern::LiteralPattern(lit) if ctx == PatternContext::LetBinding => bail!( + lit.span(), + "literal patterns are not allowed in `let` bindings"; + note: "a `let` binding must introduce at least one variable" + ), + Pattern::LiteralPattern(lit) if ctx == PatternContext::ForLoopBinding => bail!( + lit.span(), + "literal patterns are not allowed in `let` bindings"; + note: "a `for` llop binding must introduce at least one variable" + ), + Pattern::LiteralPattern(lit) => { + let literal_value = lit.eval(vm)?.value; + + if literal_value.not_equals(&value, &vm.heap).at(lit.span())? { + return Ok(PatternMatchResult::NotMatched(error!( + lit.span(), + "literal pattern did not match"; + label_message: "expected `{}`, got `{}`", literal_value.debug_repr(vm), value.debug_repr(vm); + note: "while matching the value at path {}", match_path; + ))); + } + + Ok(PatternMatchResult::Matched) + } + } +} + +fn get_type<'a>(vm: &mut Machine<'a>, ty_ident: Ident) -> Option<&'a Type> { + match vm + .engine + .world + .library() + .global + .scope() + .get(ty_ident.get()) + .map(|b| b.read())? + { + Value::Type(t) => Some(t), + _ => None, + } +} + +fn destructure_array( + vm: &mut Machine, + destruct: ast::Destructuring, + value: ArrayValue, + ctx: PatternContext, + match_path: MatchPath, + bind: &mut impl Fn(&mut Machine, Expr, Value) -> SourceResult<()>, +) -> SourceResult { + let arr = value.heap_ref().get_unwrap(&vm.heap).clone(); + + let len = arr.len(); + let mut index = 0; + + for p in destruct.items() { + match p { + DestructuringItem::Pattern(pat) => { + let Some(v) = arr.get(index) else { + return Ok(PatternMatchResult::NotMatched(wrong_number_of_elements( + destruct, + len, + &match_path, + ))); + }; + + let matched = destructure_impl( + vm, + pat, + v.clone(), + ctx, + match_path + .clone() + .with_segment(MatchPathSegment::ArrayIndex(index)), + bind, + )?; + + if let PatternMatchResult::NotMatched(err) = matched { + return Ok(PatternMatchResult::NotMatched(err)); + } + index += 1; + } + DestructuringItem::Named(named) => { + return Ok(PatternMatchResult::NotMatched(error!( + named.span(), + "cannot destructure a named pattern from an array" + ))); + } + DestructuringItem::Spread(spread) => { + // The number of elements that have not been bound by a destructuring item and will be bound by the spread + let sink_size = (1 + len).checked_sub(destruct.items().count()); + + // The items that will be bound by the spread + let sunk_items = sink_size.and_then(|n| arr.get(index..index + n)); + + let (Some(sink_size), Some(sunk_items)) = (sink_size, sunk_items) else { + return Ok(PatternMatchResult::NotMatched(wrong_number_of_elements( + destruct, + len, + &match_path, + ))); + }; + + if let Some(expr) = spread.sink_expr() { + let sunk_arr = + ArrayValue::from(vm.heap_mut(), sunk_items.to_vec()).into_value(); + bind(vm, expr, sunk_arr)?; + } + index += sink_size; + } + } + } + + // require all items to be bound + if index != len { + return Ok(PatternMatchResult::NotMatched(wrong_number_of_elements( + destruct, + len, + &match_path, + ))); + } + + Ok(PatternMatchResult::Matched) +} + +#[allow(unused)] +fn destructure_map( + vm: &mut Machine, + destruct: ast::Destructuring, + value: MapValue, + ctx: PatternContext, + match_path: MatchPath, + bind: &mut impl Fn(&mut Machine, Expr, Value) -> SourceResult<()>, +) -> SourceResult { + let map = value.heap_ref().get_unwrap(&vm.heap).clone(); + + let mut used: HashSet<&str> = HashSet::new(); + let mut spread = None; + + for p in destruct.items() { + match p { + DestructuringItem::Named(named) => { + let name = named.name().get(); + used.insert(name); + + let Some(v) = map.get(name) else { + return Ok(PatternMatchResult::NotMatched(missing_key_err( + name, + named.span(), + &match_path, + ))); + }; + + let matched = destructure_impl( + vm, + named.pattern(), + v.clone(), + ctx, + match_path + .clone() + .with_segment(MatchPathSegment::MapKey(name.clone())), + bind, + )?; + if let PatternMatchResult::NotMatched(err) = matched { + return Ok(PatternMatchResult::NotMatched(err)); + } + } + DestructuringItem::Pattern(Pattern::Single(Expr::Ident(ident))) => { + used.insert(ident.get()); + let Some(v) = map.get(ident.get()) else { + return Ok(PatternMatchResult::NotMatched(missing_key_err( + ident.get(), + ident.span(), + &match_path, + ))); + }; + + bind(vm, Expr::Ident(ident.clone()), v.clone())?; + } + DestructuringItem::Spread(s) => { + spread = Some(s); + } + DestructuringItem::Pattern(_) => { + let mut err = error!( + destruct.span(), + "cannot destructure an unnamed pattern from a map" + ); + + if !match_path.is_empty() { + err.note(eco_format!( + "while matching the pattern at path {}", + match_path + )) + } + + return Ok(PatternMatchResult::NotMatched(err)); + } + } + } + + if let Some(spread) = spread + && spread.sink_expr().is_some() + { + let map = HashMap::from_iter( + map.iter() + .filter(|(k, _)| !used.contains(k.as_str())) + .map(|(k, v)| (k.clone(), v.clone())), + ); + let value = MapValue::from(vm.heap_mut(), map).into_value(); + bind(vm, spread.sink_expr().unwrap(), value)?; + } + + if spread.is_none() && used.len() != map.len() { + let key_hint = { + let missing_keys = map + .keys() + .filter(|k| !used.contains(k.as_str())) + .map(|k| k.as_str()) + .collect::>() + .tap_mut(|v| v.sort_unstable()); + format_keyset(&missing_keys) + }; + let mut err = error!( + destruct.span(), + "map destructuring does not cover all keys"; + label_message: "unmatched keys: {key_hint}"; + hint: "add `..` to ignore the remaining keys"; + hint: "or use `..name` to bind them into a map"; + code: &E0302_MAP_DESTRUCTURING_UNCOVERED_KEYS, + ); + + if !match_path.is_empty() { + err.note(eco_format!( + "while matching the pattern at path {}", + match_path + )) + } + + return Ok(PatternMatchResult::NotMatched(err)); + } + + Ok(PatternMatchResult::Matched) +} + +fn missing_key_err(name: &str, span: Span, match_path: &MatchPath) -> SourceDiagnostic { + let mut err = error!( + span, "missing key in map pattern"; + label_message: "key `{}` is not present in the map", name; + code: &E0303_MAP_DESTRUCTURING_MISSING_KEY_IN_VALUE; + ); + + if !match_path.is_empty() { + err.note(eco_format!( + "while matching the pattern at path {}", + match_path + )) + } + + err +} + +/// Returns a string describing a keyset. lists the first 3 keys, and lists the count of remaining keys. +fn format_keyset(missing_keys: &[&str]) -> EcoString { + const LISTED_KEYS_NUM: usize = 3; + let named_keys = missing_keys + .iter() + .take(LISTED_KEYS_NUM) + .copied() + .collect::>() + .join(", "); + let rest_len = missing_keys.len().saturating_sub(LISTED_KEYS_NUM); + + let more = if rest_len > 0 { + &eco_format!(", and {rest_len} more") + } else { + "" + }; + eco_format!("{}{}", named_keys, more) +} + +/// Returns a diagnostic indicating that the number of elements in the array does not match the number of elements in the destructuring pattern. +#[cold] +fn wrong_number_of_elements( + destruct: ast::Destructuring, + len: usize, + match_path: &MatchPath, +) -> SourceDiagnostic { + let mut count = 0; + let mut spread = false; + + for p in destruct.items() { + match p { + DestructuringItem::Pattern(_) => count += 1, + DestructuringItem::Spread(_) => spread = true, + DestructuringItem::Named(_) => {} + } + } + + let quantifier = if len > count { + "too many" + } else { + "not enough" + }; + let expected = match (spread, count) { + (true, 1) => "at least 1 element".into(), + (true, c) => eco_format!("at least {c} elements"), + (false, 0) => "an empty array".into(), + (false, 1) => "a single element".into(), + (false, c) => eco_format!("{c} elements",), + }; + + let mut err = error!( + destruct.span(), "{quantifier} elements to destructure"; + hint: "the provided array has a length of {len}, \ + but the pattern expects {expected}"; + code: &E0301_ARRAY_DESTRUCTURING_WRONG_NUMBER_OF_ELEMENTS; + ); + + if len > count { + err.hint("use `..` to ignore the remaining elements, or `..name` to bind them"); + } + + if !match_path.is_empty() { + err.note(eco_format!( + "while matching the pattern at path {}", + match_path + )) + } + + err +} + +#[cfg(test)] +mod tests { + use crate::test::{assert_eval, eval_code}; + use compose_error_codes::{ + E0301_ARRAY_DESTRUCTURING_WRONG_NUMBER_OF_ELEMENTS, E0302_MAP_DESTRUCTURING_UNCOVERED_KEYS, + E0303_MAP_DESTRUCTURING_MISSING_KEY_IN_VALUE, + }; + + #[test] + fn simple_map_destructuring() { + assert_eval( + r#" + let { a, b } = { a: 1, b: 2 }; + assert::eq(a, 1); + assert::eq(b, 2); + "#, + ); + } + + #[test] + fn simple_array_destructuring() { + assert_eval( + r#" + let [a, b] = [1, 2]; + assert::eq(a, 1); + assert::eq(b, 2); + "#, + ); + } + + #[test] + fn named_map_destructuring() { + assert_eval( + r#" + let { a: x, b: y } = { a: 1, b: 2 }; + assert::eq(x, 1); + assert::eq(y, 2); + "#, + ); + } + + #[test] + fn nested_named_map_destructuring() { + assert_eval( + r#" + let { a: { x: z } } = { a: { x: 1 } }; + assert::eq(z, 1); + "#, + ); + } + + #[test] + fn spread_map_destructuring() { + assert_eval( + r#" + let { a, ..rest } = { a: 1, b: 2 }; + assert::eq(a, 1); + assert::eq(rest.get("b"), 2); + "#, + ); + } + + #[test] + fn spread_array_destructuring() { + assert_eval( + r#" + let [a, ..rest] = [1, 2]; + assert::eq(a, 1); + assert::eq(rest, [2]); + "#, + ); + } + + #[test] + fn literal_pattern() { + assert_eval( + r#" + assert([1, 2] is [1, _]); + "#, + ); + } + + #[test] + fn map_error_with_unmapped_keys() { + eval_code( + r#" + let { a } = { a: 1, c: 2 }; + "#, + ) + .assert_errors(&[&E0302_MAP_DESTRUCTURING_UNCOVERED_KEYS]); + } + + #[test] + fn map_error_with_nonexistent_key() { + // TODO: Add error code for nonexistent key + eval_code( + r#" + let { a } = { b: 1 }; + "#, + ) + .assert_errors(&[&E0303_MAP_DESTRUCTURING_MISSING_KEY_IN_VALUE]); + } + + #[test] + fn array_error_with_uncovered_elements() { + eval_code( + r#" + let [a] = [1, 2]; + "#, + ) + .assert_errors(&[&E0301_ARRAY_DESTRUCTURING_WRONG_NUMBER_OF_ELEMENTS]); + } + + #[test] + fn array_error_with_overcovered_elements() { + eval_code( + r#" + let [a, b] = [1]; + "#, + ) + .assert_errors(&[&E0301_ARRAY_DESTRUCTURING_WRONG_NUMBER_OF_ELEMENTS]); + } +} diff --git a/compose-eval/src/expression/range.rs b/compose-eval/src/expression/range.rs index f010525d..59b5d9cd 100644 --- a/compose-eval/src/expression/range.rs +++ b/compose-eval/src/expression/range.rs @@ -1,8 +1,9 @@ -use crate::{Eval, Evaluated, Machine}; +use crate::{Eval, Machine}; use compose_library::diag::{At, SourceResult}; use compose_library::{RangeValue, Value}; use compose_syntax::ast; use compose_syntax::ast::AstNode; +use crate::evaluated::Evaluated; impl Eval for ast::Range<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/expression/unary.rs b/compose-eval/src/expression/unary.rs index a1a33a43..de6fbf4c 100644 --- a/compose-eval/src/expression/unary.rs +++ b/compose-eval/src/expression/unary.rs @@ -1,8 +1,9 @@ use crate::access::Access; -use crate::{Eval, Evaluated, Machine}; -use compose_library::diag::{At, SourceResult, StrResult, bail}; -use compose_library::{Heap, Value, ops}; +use crate::{Eval, Machine}; +use compose_library::diag::{bail, At, SourceResult, StrResult}; +use compose_library::{ops, Heap, Value}; use compose_syntax::ast::{AstNode, UnOp, Unary}; +use crate::evaluated::Evaluated; impl Eval for Unary<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { diff --git a/compose-eval/src/lib.rs b/compose-eval/src/lib.rs index e75ea260..6cf511b5 100644 --- a/compose-eval/src/lib.rs +++ b/compose-eval/src/lib.rs @@ -1,103 +1,157 @@ +/*! +The Compose Interpreter + + +*/ + mod access; +mod evaluated; mod expression; mod statement; pub mod test; mod vm; pub use crate::vm::Machine; -use crate::vm::Tracked; +pub use evaluated::Evaluated; +use compose_library::diag::{error, SourceDiagnostic, SourceResult, Warned}; use compose_library::Value; -use compose_library::diag::{SourceDiagnostic, SourceResult, Warned, error}; use compose_syntax::ast::Statement; -use compose_syntax::{Source, Span}; -use ecow::{EcoVec, eco_vec}; +use compose_syntax::Source; +use ecow::{eco_vec, EcoVec}; use std::cmp::min; use std::ops::Range; -pub trait Eval { - fn eval(self, vm: &mut Machine) -> SourceResult; -} +/** +Represents a language construct that can be evaluated. Examples are expressions and statements. -#[derive(Debug, Clone, PartialEq)] -pub struct Evaluated { - pub value: Value, - /// Whether the value is allowed to be mutated. - /// - /// True for any expression except for reading or dereferencing immutable values. - pub mutable: bool, - /// The span of the binding this value is related to - pub origin: Option, -} +Evaluation may: + - Have side effects like writing to stdout or writing to a file. + - Allocate on the VM-managed heap. + - Trigger flow control (break, continue, return). + - Introduce lexical or flow bindings. -impl Evaluated { - pub fn new(value: Value, mutable: bool) -> Self { - Self { value, mutable, origin: None } - } +# Contract - pub fn mutable(value: Value) -> Self { - Self::new(value, true) - } +## Scope management +Implementors that enter **lexical** or **flow** scopes must leave these scopes before returning from [`Eval::eval`]. - pub fn immutable(value: Value) -> Self { - Self::new(value, false) - } +To make this less error-prone, [`Machine`] provides several helpers: +- [`Machine::in_lexical_scope`] +- [`Machine::new_lexical_scope_guard`] +- [`Machine::in_flow_scope_guard`] +- [`Machine::new_flow_scope_guard`] - pub fn unit() -> Self { - Self::new(Value::unit(), true) - } +To read more about the rationale behind scope usage see [`compose_library::Scopes`]. - pub fn spanned(self, span: Span) -> Self { - Self { - value: self.value.spanned(span), - ..self - } - } +*Example: Evaluating a code block* - pub fn with_origin(self, origin: Span) -> Self { - Self { origin: Some(origin), ..self } - } +```rust,ignore +use compose_eval::{Eval, Machine}; +use compose_library::diag::SourceResult; +use compose_syntax::ast::CodeBlock; +use crate::Evaluated; - pub fn with_value(self, value: Value) -> Self { - Self { value, ..self } - } +impl Eval for CodeBlock<'_> { + fn eval(self, vm: &mut Machine) -> SourceResult { + let flow = vm.flow.take(); + let mut result = Evaluated::unit(); - pub fn make_mutable(self) -> Self { - Self { mutable: true, ..self } - } + let statements = self.statements(); - pub fn value(&self) -> &Value { - &self.value - } + // in_lexical_scope enters a scope for the duration of the closure + vm.in_lexical_scope(|vm| { + for statement in statements { + result = statement.eval(vm)?; + if vm.flow.is_some() { + break; + } + } + SourceResult::Ok(()) + })?; - pub fn into_value(self) -> Value { - self.value + if let Some(flow) = flow { + vm.flow = Some(flow); + } + + Ok(result) } } +``` -impl Tracked for Evaluated { - fn track_tmp_root(self, vm: &mut Machine) -> Self { - Self { - value: self.value.track_tmp_root(vm), - ..self - } + +## Any interaction with the outside world should go through the [`compose_library::World`] trait object in the [`Machine::engine`]. + +This is required to uphold the sandbox in which compose is evaluated and allows implementations to work in +any situation is used in. + +*Example: writing to stdout* + +```rust,ignore +# use compose_eval::{Eval, Machine, Evaluated}; +# use compose_syntax::{Span}; +# use compose_library::diag::{At, SourceResult}; +struct WriteToStdout { span: Span } + +impl Eval for WriteToStdout { + fn eval(self, vm: &mut Machine) -> SourceResult { + vm + .engine + .world + .write(&mut |write| write!(write, "Hello, world from Compose!")) + .map_err(|e| format!("An error occurred while writing to stdout: {}", e)) + .at(self.span)?; + + Ok(Evaluated::unit()) } } +``` -pub trait ValueEvaluatedExtensions { - fn mutable(self) -> Evaluated; - fn immutable(self) -> Evaluated; -} +## Temporary values need to be rooted while performing GC -impl ValueEvaluatedExtensions for Value { - fn mutable(self) -> Evaluated { - Evaluated::new(self, true) - } +A value returned from an evaluation may not yet be rooted yet. Any values on the stack within the [`Eval::eval`] +function must be rooted while performing GC. + +Use [`Machine::temp_root_guard`] or [`Machine::temp_root_scope`] to open a temporary root scope. Then track +any temporary values with [`Machine::track_tmp_root`]. + +*Example: GC after evaluating a statement* - fn immutable(self) -> Evaluated { - Evaluated::new(self, false) +```rust,ignore +use compose_eval::{Eval, Machine, Evaluated}; +use compose_library::{Binding, IntoValue, Module, diag::SourceResult}; +use compose_syntax::{ast}; + +impl Eval for ast::Statement<'_> { + fn eval(self, vm: &mut Machine) -> SourceResult { + // This might return a value with a heap allocation. + // The VM might not yet know about it if it is not bound in any scope. + // But the value might still be used after this statement. + let result = match self { + ast::Statement::Expr(e) => e.eval(vm), + // ... + _ => unimplemented!() + }; + + // Temporarily root the value to ensure it is not GCed before we use it. + vm.temp_root_scope(|vm| { + if let Ok(result) = &result { + vm.track_tmp_root(result.value()); + } + + vm.maybe_gc(); + Ok(()) + })?; + + result } } +``` +*/ + +pub trait Eval { + fn eval(self, vm: &mut Machine) -> SourceResult; +} #[derive(Default)] pub struct EvalConfig { diff --git a/compose-eval/src/statement.rs b/compose-eval/src/statement.rs index ad42fe81..1ef28c10 100644 --- a/compose-eval/src/statement.rs +++ b/compose-eval/src/statement.rs @@ -1,27 +1,39 @@ +use crate::evaluated::Evaluated; use crate::vm::FlowEvent; -use crate::{eval, Eval, EvalConfig, Evaluated, Machine}; -use compose_library::diag::{bail, error, SourceResult, Trace, TracePoint, Warned}; +use crate::{Eval, EvalConfig, Machine, eval}; +use compose_library::diag::{SourceResult, Trace, TracePoint, Warned, bail, error}; use compose_library::{Binding, IntoValue, Module}; use compose_syntax::ast::{AstNode, BreakStatement}; -use compose_syntax::{ast, FileId}; -use ecow::{eco_vec, EcoString}; +use compose_syntax::{FileId, ast}; +use ecow::{EcoString, eco_vec}; use std::path::PathBuf; impl Eval for ast::Statement<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { - // trace_log!("eval statement: {:#?}", self); - let guard = vm.temp_root_guard(); - let result = match self { - ast::Statement::Expr(e) => e.eval(guard.vm), - ast::Statement::Let(l) => l.eval(guard.vm), - ast::Statement::Assign(a) => a.eval(guard.vm), - ast::Statement::Break(b) => b.eval(guard.vm), - ast::Statement::Return(r) => r.eval(guard.vm), - ast::Statement::Continue(c) => c.eval(guard.vm), - ast::Statement::ModuleImport(i) => i.eval(guard.vm), + let result = { + // Flow scope can be dropped after evaluating the statement, this way it can be GCd + let vm = &mut vm.new_flow_scope_guard(); + match self { + ast::Statement::Expr(e) => e.eval(vm), + ast::Statement::Let(l) => l.eval(vm), + ast::Statement::Assign(a) => a.eval(vm), + ast::Statement::Break(b) => b.eval(vm), + ast::Statement::Return(r) => r.eval(vm), + ast::Statement::Continue(c) => c.eval(vm), + ast::Statement::ModuleImport(i) => i.eval(vm), + } }; - guard.vm.maybe_gc(); + let vm = &mut vm.temp_root_guard(); + vm.temp_root_scope(|vm| { + if let Ok(result) = &result { + vm.track_tmp_root(result.value()); + } + + vm.maybe_gc(); + Ok(()) + })?; + result } } @@ -58,7 +70,7 @@ impl Eval for ast::ReturnStatement<'_> { } } -impl Eval for ast::Continue<'_> { +impl Eval for ast::ContinueStatement<'_> { fn eval(self, vm: &mut Machine) -> SourceResult { vm.flow = Some(FlowEvent::Continue(self.span())); Ok(Evaluated::unit()) @@ -82,11 +94,14 @@ impl Eval for ast::ModuleImport<'_> { let as_path = PathBuf::from(source.as_str()); let stem = match as_path.file_stem() { Some(stem) => stem, - None => bail!(self.source_span(), "could not resolve module name from path") + None => bail!( + self.source_span(), + "could not resolve module name from path" + ), }; EcoString::from(stem.to_string_lossy().as_ref()) } - Some(name) => name.get().to_owned() + Some(name) => name.get().to_owned(), }; let id = FileId::new(path); @@ -98,15 +113,16 @@ impl Eval for ast::ModuleImport<'_> { )) })?; - let module = vm.with_frame(|vm| { - let Warned { value, warnings } = eval(&source, vm, &EvalConfig::default()); - value?; - vm.sink_mut().warnings.extend(warnings); - + let module = vm + .with_frame(|vm| { + let Warned { value, warnings } = eval(&source, vm, &EvalConfig::default()); + value?; + vm.sink_mut().warnings.extend(warnings); - let module = Module::new(name.clone(), vm.frames.top.scopes.top.clone()); - SourceResult::Ok(module.into_value()) - }).trace(|| TracePoint::Import, self.source_span())?; + let module = Module::new(name.clone(), vm.scopes().top_lexical().clone()); + SourceResult::Ok(module.into_value()) + }) + .trace(|| TracePoint::Import, self.source_span())?; vm.try_bind(name, Binding::new(module, self.span()))?; diff --git a/compose-eval/src/test/flow_variables.rs b/compose-eval/src/test/flow_variables.rs new file mode 100644 index 00000000..1eaa9cd2 --- /dev/null +++ b/compose-eval/src/test/flow_variables.rs @@ -0,0 +1,28 @@ +#[cfg(test)] +use { + crate::test::{assert_eval, eval_code}, + compose_error_codes::E0011_UNBOUND_VARIABLE +}; + +#[test] +fn is_expression() { + assert_eval( + r#" + let two_is_even = 2 is Int x && x % 2 == 0; + + assert(two_is_even); + "#, + ); +} + +#[test] +fn is_expression_flow_scope_does_not_leak() { + eval_code( + r#" + let two_is_even = 2 is Int x && x % 2 == 0; + + x; // should not be in scope here + "#, + ) + .assert_errors(&[&E0011_UNBOUND_VARIABLE]); +} diff --git a/compose-eval/src/test/mod.rs b/compose-eval/src/test/mod.rs index 418f03ce..497b9d11 100644 --- a/compose-eval/src/test/mod.rs +++ b/compose-eval/src/test/mod.rs @@ -3,9 +3,7 @@ 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, -}; +use compose_library::diag::{FileError, FileResult, SourceDiagnostic, SourceResult, Warned, write_diagnostics, write_diagnostics_to_string}; use compose_library::{Library, Value, World, library}; use compose_syntax::{FileId, Source}; use ecow::{EcoVec, eco_format, eco_vec}; @@ -15,6 +13,7 @@ use std::io::{Read, Write}; use std::sync::Mutex; use tap::pipe::Pipe; +mod flow_variables; #[cfg(test)] mod iterators; #[cfg(test)] @@ -24,6 +23,7 @@ pub struct TestWorld { sources: Mutex>, entrypoint: FileId, library: Library, + stdout: Mutex, } impl Clone for TestWorld { @@ -32,6 +32,7 @@ impl Clone for TestWorld { sources: Mutex::new(self.sources.lock().unwrap().clone()), entrypoint: self.entrypoint, library: self.library.clone(), + stdout: Mutex::new(String::new()), } } } @@ -64,6 +65,7 @@ impl TestWorld { sources: Mutex::new(sources), entrypoint, library: library(), + stdout: Mutex::new(String::new()), } } @@ -107,16 +109,23 @@ impl World for TestWorld { &self.library } - fn write(&self, f: &dyn Fn(&mut dyn Write) -> std::io::Result<()>) -> std::io::Result<()> { - f(&mut std::io::stdout()) + fn write( + &self, + f: &mut dyn FnMut(&mut dyn Write) -> std::io::Result<()>, + ) -> std::io::Result<()> { + let mut buffer: Vec = Vec::new(); + f(&mut buffer)?; + let output = String::from_utf8(buffer).expect("Invalid UTF-8"); + self.stdout.lock().expect("failed to lock stdout").push_str(&output); + Ok(()) } - fn read(&self, f: &dyn Fn(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()> { + fn read(&self, f: &mut dyn FnMut(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()> { f(&mut std::io::stdin()) } } -fn print_diagnostics( +pub fn print_diagnostics( world: &TestWorld, errors: &[SourceDiagnostic], warnings: &[SourceDiagnostic], @@ -185,6 +194,17 @@ impl TestResult { self } + pub fn errors(&self) -> &[SourceDiagnostic] { + match &self.value { + Ok(_) => &[], + Err(errors) => errors, + } + } + + pub fn warnings(&self) -> &[SourceDiagnostic] { + &self.warnings + } + #[track_caller] pub fn assert_no_warnings(self) -> Self { if !self.warnings.is_empty() { @@ -195,24 +215,40 @@ impl TestResult { } #[track_caller] - pub fn assert_errors(self, expected_errors: &[ErrorCode]) -> Self { + pub fn assert_errors(self, expected_errors: &[&ErrorCode]) -> Self { match &self.value { + Ok(_) if expected_errors.is_empty() => {} Ok(_) => panic!("expected errors, but got none"), Err(errors) => { - if expected_errors.is_empty() { - panic!("expected no errors, but got: {:?}", errors) - } - if errors + let missing_expected_errors = expected_errors .iter() + .filter(|e| !errors.iter().any(|c| c.code == Some(*e))) .map(|e| e.code) - .zip(expected_errors.iter().map(Some)) - .any(|(a, b)| a != b) - { - print_diagnostics(&self.world, errors, &self.warnings); - panic!( - "expected errors: {:?}, but got: {:?}", - expected_errors, errors - ) + .collect::>(); + let unexpected_errors = errors + .iter() + .filter(|d| { + !expected_errors + .iter() + .any(|e| Some(e.code) == d.code.map(|c| c.code)) + }) + .cloned() + .collect::>(); + + let mut error = String::new(); + if !missing_expected_errors.is_empty() { + error.push_str("expected errors did not occur: "); + error.push_str(&missing_expected_errors.join(", ")); + error.push_str("\n"); + } + + if !unexpected_errors.is_empty() { + error.push_str("unexpected errors occurred: \n"); + error.push_str(write_diagnostics_to_string(&self.world, &unexpected_errors, &[]).as_str()); + } + + if !missing_expected_errors.is_empty() || !unexpected_errors.is_empty() { + panic!("{}", error); } } } @@ -221,13 +257,13 @@ impl TestResult { } #[track_caller] - pub fn assert_warnings(self, expected_warnings: &[ErrorCode]) -> Self { + pub fn assert_warnings(self, expected_warnings: &[&ErrorCode]) -> Self { if self .warnings .iter() .map(|e| e.code) .zip(expected_warnings.iter().map(Some)) - .any(|(a, b)| a != b) + .any(|(a, b)| a.as_ref() != b) { print_diagnostics(&self.world, &self.warnings, &self.warnings); panic!( @@ -255,6 +291,16 @@ impl TestResult { Err(errors) => errors.clone(), } } + + pub fn assert_stdout(&self, expected: &str) { + let stdout = self.world.stdout.lock().expect("failed to lock stdout").clone(); + assert_eq!(stdout, expected); + } + + pub fn assert_stdout_predicate(&self, predicate: impl FnOnce(&str) -> bool) { + let stdout = self.world.stdout.lock().expect("failed to lock stdout").clone(); + assert!(predicate(&stdout)); + } } #[must_use] diff --git a/compose-eval/src/vm/mod.rs b/compose-eval/src/vm/mod.rs index 440182cb..181e81ed 100644 --- a/compose-eval/src/vm/mod.rs +++ b/compose-eval/src/vm/mod.rs @@ -1,20 +1,20 @@ mod stack; -//noinspection RsUnusedImport - false positive, actually used use crate::expression::eval_lambda; use crate::vm::stack::{StackFrames, TrackMarker}; -use compose_library::diag::{At, SourceDiagnostic, SourceResult, error}; +use compose_library::diag::{error, At, SourceDiagnostic, SourceResult}; use compose_library::{ Args, Binding, BindingKind, Engine, Func, FuncKind, Heap, IntoValue, Routines, Scopes, Sink, SyntaxContext, Trace, UntypedRef, Value, VariableAccessError, Visibility, Vm, World, }; use compose_syntax::ast::AstNode; -use compose_syntax::{Span, ast}; +use compose_syntax::{ast, Span}; +use compose_utils::{defer, trace_fn}; use ecow::EcoString; pub use stack::Tracked; pub use stack::TrackedContainer; use std::fmt::Debug; -use std::ops::{Deref, DerefMut}; +use std::ops::{DerefMut}; pub struct Machine<'a> { pub frames: StackFrames<'a>, @@ -49,78 +49,6 @@ impl<'a> Vm<'a> for Machine<'a> { } } -impl<'a> Machine<'a> { - pub(crate) fn sink_mut(&mut self) -> &mut Sink { - &mut self.engine.sink - } - - pub(crate) fn in_scope(&mut self, f: impl FnOnce(&mut Machine<'a>) -> T) -> T { - self.frames.top.scopes.enter(); - let result = f(self); - self.frames.top.scopes.exit(); - result - } - - pub fn world(&self) -> &dyn World { - self.engine.world - } - - pub fn syntax_ctx<'w>(&self) -> SyntaxContext<'w> - where - 'a: 'w, - { - SyntaxContext { - world: self.engine.world, - } - } -} - -impl Debug for Machine<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Vm") - .field("frames", &self.frames) - .field("flow", &self.flow) - .field("sink", &self.engine) - .field("heap", &self.heap) - .finish() - } -} - -#[derive(Debug, Clone)] -pub enum FlowEvent { - Continue(Span), - Break(Span, Option), - Return(Span, Option), -} - -impl Trace for FlowEvent { - fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { - match self { - FlowEvent::Continue(_) => {} - FlowEvent::Break(_, Some(value)) => value.visit_refs(f), - FlowEvent::Break(_, None) => {} - FlowEvent::Return(_, Some(value)) => value.visit_refs(f), - FlowEvent::Return(_, None) => {} - } - } -} - -impl FlowEvent { - pub(crate) fn forbidden(&self) -> SourceDiagnostic { - match *self { - Self::Break(span, _) => { - error!(span, "cannot break outside of a loop") - } - Self::Return(span, _) => { - error!(span, "cannot return outside of a function") - } - Self::Continue(span) => { - error!(span, "cannot continue outside of a loop") - } - } - } -} - impl<'a> Machine<'a> { pub fn new(world: &'a dyn World) -> Self { Self { @@ -153,24 +81,142 @@ impl<'a> Machine<'a> { }; self.heap.maybe_gc(&roots); } -} + + pub fn sink_mut(&mut self) -> &mut Sink { + &mut self.engine.sink + } -#[derive(Debug)] -pub struct VmRoots<'a> { - pub frames: &'a StackFrames<'a>, - pub flow: &'a Option, -} + /// Executes `f` inside a new **lexical scope**. + /// + /// A lexical scope controls name bindings and shadowing. A fresh lexical + /// scope is pushed before `f` is executed and is always popped afterwards, + /// even if `f` introduces new bindings. + /// + /// # Scope behavior + /// - Always creates a new lexical scope + /// - Scope is exited immediately after `f` returns + pub fn in_lexical_scope(&mut self, f: impl FnOnce(&mut Machine<'a>) -> T) -> T { + trace_fn!("in_lexical_scope"); + self.frames.top.scopes.enter_lexical(); + let result = f(self); + self.frames.top.scopes.exit_lexical(); + result + } -impl Trace for VmRoots<'_> { - fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { - self.frames.visit_refs(f); - if let Some(flow) = self.flow { - flow.visit_refs(f); + /// Executes `f` within a **flow scope**. + /// + /// A flow scope tracks temporary variables created within expressions. + /// For example `x is Int y && y > 0` introduces a temporary variable `y` + /// that is only valid within this expression. + /// + /// # Scope behaviour + /// - Reuses the current flow scope if one exists + /// - Otherwise creates a temporary flow scope + /// - The flow scope is exited only if this function created it + pub fn in_flow_scope(&mut self, f: impl FnOnce(&mut Self) -> T) -> T { + let top_was_flow = self.scopes().top_flow().is_some(); + if !top_was_flow { + self.scopes_mut().enter_flow(); + } + + let result = f(self); + if !top_was_flow { + self.scopes_mut().exit_flow(); + } + + result + } + + /// Enters a **flow scope** and returns a guard that exits it when dropped. + /// + /// A flow scope tracks temporary variables created within expressions. + /// For example `x is Int y && y > 0` introduces a temporary variable `y` + /// that is only valid within this expression. + /// + /// If a flow scope is already active, it is reused and the guard becomes + /// a no-op when dropped. Otherwise, a new flow scope is created and will be + /// exited when the guard is dropped. + /// + /// This is the guard-based equivalent of [`Machine::in_flow_scope`], useful when + /// control flow makes closure-based APIs inconvenient. + /// + /// # Drop behaviour + /// - Scope exit is guaranteed when the guard is dropped + /// - Safe to use with early returns or error propagation + #[must_use] + pub fn in_flow_scope_guard(&mut self) -> impl DerefMut { + let _trace_guard = ::compose_utils::TraceFnGuard::new("in_flow_scope_guard", None); + let top_was_flow = self.scopes().top_flow().is_some(); + if !top_was_flow { + self.scopes_mut().enter_flow(); + } + + defer(self, move |vm| { + if !top_was_flow { + vm.scopes_mut().exit_flow(); + } + drop(_trace_guard); + }) + } + + /// Enters a new **lexical scope** and returns a guard that exits it on drop. + /// + /// Lexical scopes control variable bindings and shadowing. This always + /// creates a fresh lexical scope and guarantees that it is exited when + /// the guard is dropped. + /// + /// This is the guard-based alternative to [`Machine::in_lexical_scope`]. + /// + /// # Drop behaviour + /// - Always exits the lexical scope on drop + /// - Safe across early returns and error paths + #[must_use] + pub fn new_lexical_scope_guard(&mut self) -> impl DerefMut { + let _trace_guard = ::compose_utils::TraceFnGuard::new("lexical_scope_guard", None); + self.scopes_mut().enter_lexical(); + defer(self, move |vm| { + vm.scopes_mut().exit_lexical(); + drop(_trace_guard); + }) + } + + /// Enters a **new flow scope**, unconditionally, and returns a guard. + /// + /// A flow scope tracks temporary variables created within expressions. + /// For example `x is Int y && y > 0` introduces a temporary variable `y` + /// that is only valid within this expression. + /// + /// Unlike [`Machine::in_flow_scope_guard`], this function always creates a fresh + /// flow scope, even if one is already active. This is useful when a + /// construct requires an isolated flow environment (e.g. match expressions). + /// + /// # Drop behaviour + /// - Always exits the flow scope on drop + /// - Caller location is tracked for debugging + #[track_caller] + #[must_use] + pub fn new_flow_scope_guard(&mut self) -> impl DerefMut { + let _trace_guard = ::compose_utils::TraceFnGuard::new("new_flow_scope_guard", None); + self.scopes_mut().enter_flow(); + defer(self, move |vm| { + vm.scopes_mut().exit_flow(); + drop(_trace_guard); + }) + } + + pub fn world(&self) -> &dyn World { + self.engine.world + } + + pub fn syntax_ctx<'w>(&self) -> SyntaxContext<'w> + where + 'a: 'w, + { + SyntaxContext { + world: self.engine.world, } } -} -impl<'a> Machine<'a> { pub fn track_tmp_root(&mut self, value: &impl Trace) { self.frames.top.track(value); } @@ -193,16 +239,25 @@ impl<'a> Machine<'a> { } /// Automatically forgets any tracked values during `f` after f is finished - pub fn temp_root_scope( + pub fn temp_root_scope( &mut self, - f: impl FnOnce(&mut Machine<'a>) -> SourceResult, - ) -> SourceResult { + f: impl FnOnce(&mut Machine<'a>) -> SourceResult, + ) -> SourceResult { let marker = self.temp_root_marker(); let result = f(self); self.pop_temp_roots(marker); result } + pub fn temp_root_guard(&mut self) -> impl DerefMut { + let marker = self.temp_root_marker(); + // trace_log!("pushing temp roots: {:?}", marker); + defer(self, move |vm| { + // trace_log!("popping temp roots: {:?}", marker); + vm.pop_temp_roots(marker) + }) + } + pub fn define( &mut self, var: ast::Ident, @@ -220,17 +275,17 @@ impl<'a> Machine<'a> { pub fn try_bind(&mut self, name: EcoString, binding: Binding) -> SourceResult<&mut Binding> { let span = binding.span(); - self.frames.top.scopes.top.try_bind(name, binding).at(span) + self.scopes_mut().top_lexical_mut().try_bind(name, binding).at(span) } pub fn bind(&mut self, name: EcoString, binding: Binding) -> &mut Binding { - self.frames.top.scopes.top.bind(name, binding) + self.scopes_mut().top_lexical_mut().bind(name, binding) } - fn scopes(&self) -> &Scopes<'a> { + pub(crate) fn scopes(&self) -> &Scopes<'a> { &self.frames.top.scopes } - fn scopes_mut(&mut self) -> &mut Scopes<'a> { + pub(crate) fn scopes_mut(&mut self) -> &mut Scopes<'a> { &mut self.frames.top.scopes } @@ -241,40 +296,86 @@ impl<'a> Machine<'a> { pub fn get_mut(&mut self, name: &str) -> Result<&mut Binding, VariableAccessError> { self.scopes_mut().get_mut(name) } + + /// Run f with closure capture errors deferred. + /// + /// This makes the caller responsible for either handling or emitting the unresolved errors. + pub fn with_closure_capture_errors_mode( + &mut self, + mode: ErrorMode, + f: impl FnOnce(&mut Machine) -> SourceResult, + ) -> SourceResult { + let old = self.context.closure_capture; + self.context.closure_capture = mode; + let result = f(self); + self.context.closure_capture = old; + result + } } -pub struct TempRootGuard<'a, 'b> { - pub marker: TrackMarker, - pub vm: &'b mut Machine<'a>, +impl Debug for Machine<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Vm") + .field("frames", &self.frames) + .field("flow", &self.flow) + .field("sink", &self.engine) + .field("heap", &self.heap) + .finish() + } } -impl<'a, 'b> Drop for TempRootGuard<'a, 'b> { - fn drop(&mut self) { - self.vm.pop_temp_roots(self.marker); +#[derive(Debug, Clone)] +pub enum FlowEvent { + Continue(Span), + Break(Span, Option), + Return(Span, Option), +} + +impl Trace for FlowEvent { + fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { + match self { + FlowEvent::Continue(_) => {} + FlowEvent::Break(_, Some(value)) => value.visit_refs(f), + FlowEvent::Break(_, None) => {} + FlowEvent::Return(_, Some(value)) => value.visit_refs(f), + FlowEvent::Return(_, None) => {} + } } } -impl<'a> Machine<'a> { - pub fn temp_root_guard<'b>(&'b mut self) -> TempRootGuard<'a, 'b> { - let marker = self.temp_root_marker(); - TempRootGuard { marker, vm: self } +impl FlowEvent { + pub(crate) fn forbidden(&self) -> SourceDiagnostic { + match *self { + Self::Break(span, _) => { + error!(span, "cannot break outside of a loop") + } + Self::Return(span, _) => { + error!(span, "cannot return outside of a function") + } + Self::Continue(span) => { + error!(span, "cannot continue outside of a loop") + } + } } } -impl<'a, 'b> Deref for TempRootGuard<'a, 'b> { - type Target = Machine<'a>; - fn deref(&self) -> &Self::Target { - self.vm - } +#[derive(Debug)] +pub struct VmRoots<'a> { + pub frames: &'a StackFrames<'a>, + pub flow: &'a Option, } -impl<'a, 'b> DerefMut for TempRootGuard<'a, 'b> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.vm +impl Trace for VmRoots<'_> { + fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { + self.frames.visit_refs(f); + if let Some(flow) = self.flow { + flow.visit_refs(f); + } } } + pub fn routines() -> Routines { Routines {} } @@ -299,18 +400,4 @@ impl ErrorMode { } impl Machine<'_> { - /// Run f with closure capture errors deferred. - /// - /// This makes the caller responsible for either handling or emitting the unresolved errors. - pub fn with_closure_capture_errors_mode( - &mut self, - mode: ErrorMode, - f: impl FnOnce(&mut Machine) -> SourceResult, - ) -> SourceResult { - let old = self.context.closure_capture; - self.context.closure_capture = mode; - let result = f(self); - self.context.closure_capture = old; - result - } } diff --git a/compose-library/src/diag.rs b/compose-library/src/diag.rs index 7eb82db1..57014e86 100644 --- a/compose-library/src/diag.rs +++ b/compose-library/src/diag.rs @@ -1,5 +1,5 @@ pub use compose_codespan_reporting; -use compose_codespan_reporting::term::termcolor::WriteColor; +use compose_codespan_reporting::term::termcolor::{NoColor, WriteColor}; use compose_codespan_reporting::{diagnostic, term}; use compose_syntax::{ FileId, Fix, FixDisplay, Label, LabelType, PatchEngine, Span, SyntaxError, SyntaxErrorSeverity, @@ -17,7 +17,7 @@ pub fn write_diagnostics( errors: &[SourceDiagnostic], warnings: &[SourceDiagnostic], writer: &mut dyn WriteColor, - config: &term::Config, + config: &Config, ) -> Result<(), compose_codespan_reporting::files::Error> { for diag in warnings.iter().chain(errors) { let mut diagnostic = match diag.severity { @@ -68,10 +68,10 @@ pub fn write_diagnostics( diagnostic .notes - .extend(diag.hints.iter().map(|h| format!("help: {h}"))); + .extend(diag.notes.iter().map(|n| format!("note: {n}"))); diagnostic .notes - .extend(diag.notes.iter().map(|n| format!("note: {n}"))); + .extend(diag.hints.iter().map(|h| format!("help: {h}"))); if let Some(code) = &diag.code { diagnostic = diagnostic.with_code(code.code).with_note(eco_format!( @@ -98,6 +98,26 @@ pub fn write_diagnostics( Ok(()) } +pub fn write_diagnostics_to_string( + world: &dyn World, + errors: &[SourceDiagnostic], + warnings: &[SourceDiagnostic], +) -> String { + let mut diags_buffer = vec![]; + let color_writer = &mut NoColor::new(&mut diags_buffer); + let config = Config::default(); + + let mut output = String::new(); + + write_diagnostics(world, warnings, errors, color_writer, &config) + .expect("failed to write diagnostics"); + + output.push_str( + &String::from_utf8(diags_buffer).expect("failed to convert diagnostics to string"), + ); + output.trim_end().to_string() +} + fn diag_label(diag: &SourceDiagnostic) -> Option> { let id = diag.span.id()?; let range = diag.span.range()?; @@ -144,7 +164,7 @@ fn create_label(label: &Label) -> Option> { /// ``` #[macro_export] #[doc(hidden)] -macro_rules! __bail { +macro_rules! bail { // For bail!("just a {}", "string") ( $fmt:literal $(, $arg:expr)* @@ -176,27 +196,16 @@ macro_rules! __bail { }; } -/// Construct an [`EcoString`], [`HintedString`] or [`SourceDiagnostic`] with +/// Construct an [`EcoString`] or [`SourceDiagnostic`] with /// severity `Error`. #[macro_export] #[doc(hidden)] -macro_rules! __error { +macro_rules! error { // For bail!("just a {}", "string"). ($fmt:literal $(, $arg:expr)* $(,)?) => { $crate::diag::eco_format!($fmt, $($arg),*).into() }; - // For bail!("a hinted {}", "string"; hint: "some hint"; hint: "...") - ( - $fmt:literal $(, $arg:expr)* - $(; hint: $hint:literal $(, $hint_arg:expr)*)* - $(,)? - ) => { - $crate::diag::HintedString::new( - $crate::diag::eco_format!($fmt, $($arg),*) - ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))* - }; - // For bail!(span, ...) ( $span:expr, $fmt:literal $(, $arg:expr)* @@ -240,7 +249,7 @@ macro_rules! __error { /// ``` #[macro_export] #[doc(hidden)] -macro_rules! __warning { +macro_rules! warning { ( $span:expr, $fmt:literal $(, $arg:expr)* @@ -266,11 +275,12 @@ macro_rules! __warning { #[rustfmt::skip] #[doc(inline)] pub use { - crate::__bail as bail, - crate::__error as error, - crate::__warning as warning, + bail, + error, + warning, ecow::{eco_format, EcoString}, }; +use compose_codespan_reporting::term::Config; use compose_error_codes::ErrorCode; use compose_library::World; diff --git a/compose-library/src/foundations/global_funcs/assertions.rs b/compose-library/src/foundations/global_funcs/assertions.rs index 61f6eb96..f2498ac3 100644 --- a/compose-library/src/foundations/global_funcs/assertions.rs +++ b/compose-library/src/foundations/global_funcs/assertions.rs @@ -1,10 +1,10 @@ -use crate::foundations::ops::Comparison; use crate::diag::bail; -use compose_library::{Value, Vm}; +use crate::foundations::ops::Comparison; use compose_library::diag::StrResult; +use compose_library::repr::Repr; +use compose_library::{Value, Vm}; use compose_macros::{func, scope}; use ecow::EcoString; -use compose_library::repr::Repr; #[func(scope)] pub fn assert(cond: bool, #[named] message: Option) -> StrResult<()> { @@ -21,8 +21,12 @@ pub fn assert(cond: bool, #[named] message: Option) -> StrResult<()> #[scope] impl assert { #[func] - pub fn eq(vm: &dyn Vm, left: Value, right: Value, #[named] message: Option) -> StrResult<()> { - + pub fn eq( + vm: &dyn Vm, + left: Value, + right: Value, + #[named] message: Option, + ) -> StrResult<()> { if !left.equals(&right, vm.heap())? { let l_repr = left.repr(vm); let r_repr = right.repr(vm); @@ -38,10 +42,16 @@ impl assert { } #[func] - pub fn ne(vm: &dyn Vm, left: Value, right: Value) -> StrResult<()> { + pub fn ne( + vm: &dyn Vm, + left: Value, + right: Value, + #[named] message: Option, + ) -> StrResult<()> { if left.equals(&right, vm.heap())? { bail!( - "assertion `left != right` failed\n{:>7} = {left:?}\n{:>7} = {right:?}", + "assertion `left != right` failed{}\n{:>7} = {left:?}\n{:>7} = {right:?}", + message.map(|msg| format!(": {}", msg)).unwrap_or_default(), "left:", "right:" ) diff --git a/compose-library/src/foundations/global_funcs/mod.rs b/compose-library/src/foundations/global_funcs/mod.rs index abfd3cd7..1860ca5b 100644 --- a/compose-library/src/foundations/global_funcs/mod.rs +++ b/compose-library/src/foundations/global_funcs/mod.rs @@ -11,22 +11,22 @@ use compose_library::repr::Repr; use compose_library::vm::Vm; #[func] -pub fn panic(msg: Value) -> StrResult<()> { - bail!("Panic: {:?}", msg) +pub fn panic(vm: &mut dyn Vm, msg: Value) -> StrResult<()> { + bail!("Panic: {}", msg.repr(vm)); } #[func] pub fn print(vm: &mut dyn Vm, #[variadic] print_args: Vec) -> StrResult<()> { vm.engine() .world - .write(&|wtr: &mut dyn Write| write!(wtr, "{}", join_args(&print_args, vm))) + .write(&mut |wtr: &mut dyn Write| write!(wtr, "{}", join_args(&print_args, vm))) .map_err(|e| e.to_string().into()) } #[func] pub fn println(vm: &mut dyn Vm, #[variadic] print_args: Vec) -> StrResult<()> { vm.engine() .world - .write(&|wtr: &mut dyn Write| writeln!(wtr, "{}", join_args(&print_args, vm))) + .write(&mut |wtr: &mut dyn Write| writeln!(wtr, "{}", join_args(&print_args, vm))) .map_err(|e| e.to_string().into()) } diff --git a/compose-library/src/foundations/iterator/iter_combinators.rs b/compose-library/src/foundations/iterator/iter_combinators.rs index 8be2546b..d3b835b4 100644 --- a/compose-library/src/foundations/iterator/iter_combinators.rs +++ b/compose-library/src/foundations/iterator/iter_combinators.rs @@ -1,12 +1,12 @@ -use crate::IterValue; use crate::diag::StrResult; -use compose_library::diag::{At, SourceResult, bail}; +use crate::IterValue; +use compose_library::diag::{bail, At, SourceResult}; +use compose_library::support::eval_predicate; use compose_library::vm::Vm; -use compose_library::{Args, Func, Trace, UntypedRef, Value, ValueIterator}; +use compose_library::{Args, ArrayValue, Func, IntoValue, Trace, UntypedRef, Value, ValueIterator}; use std::iter; use std::ops::DerefMut; use std::sync::{Arc, Mutex}; -use compose_library::support::eval_predicate; #[derive(Debug, Clone)] pub struct TakeIter { @@ -23,22 +23,22 @@ impl TakeIter { } } -impl PartialEq for TakeIter { - fn eq(&self, other: &Self) -> bool { - if self.inner != other.inner { - return false; - } - - let take_a = self.take.lock().expect("mutex poisoned"); - let take_b = other.take.lock().expect("mutex poisoned"); - - if *take_a != *take_b { - return false; - } - - true - } -} +// impl PartialEq for TakeIter { +// fn eq(&self, other: &Self) -> bool { +// if self.inner != other.inner { +// return false; +// } +// +// let take_a = self.take.lock().expect("mutex poisoned"); +// let take_b = other.take.lock().expect("mutex poisoned"); +// +// if *take_a != *take_b { +// return false; +// } +// +// true +// } +// } impl Trace for TakeIter { fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { @@ -71,24 +71,24 @@ pub struct SkipIter { pub(crate) skip: Arc>, } -impl PartialEq for SkipIter { - fn eq(&self, other: &Self) -> bool { - if self.inner != other.inner { - return false; - } - - { - let skip_a = self.skip.lock().expect("mutex poisoned"); - let skip_b = self.skip.lock().expect("mutex poisoned"); - - if *skip_a != *skip_b { - return false; - } - } - - true - } -} +// impl PartialEq for SkipIter { +// fn eq(&self, other: &Self) -> bool { +// if self.inner != other.inner { +// return false; +// } +// +// { +// let skip_a = self.skip.lock().expect("mutex poisoned"); +// let skip_b = self.skip.lock().expect("mutex poisoned"); +// +// if *skip_a != *skip_b { +// return false; +// } +// } +// +// true +// } +// } impl Trace for SkipIter { fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { @@ -110,7 +110,7 @@ impl ValueIterator for SkipIter { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct TakeWhileIter { pub(crate) inner: IterValue, pub(crate) predicate: Arc, @@ -153,7 +153,7 @@ impl ValueIterator for TakeWhileIter { // nth method cannot be optimized here, so we just fall back to the default } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct FilterIter { pub(crate) inner: IterValue, pub(crate) predicate: Arc, @@ -169,7 +169,7 @@ impl Trace for FilterIter { impl ValueIterator for FilterIter { fn next(&self, vm: &mut dyn Vm) -> SourceResult> { loop { - let item = match self.inner.next(vm, ) { + let item = match self.inner.next(vm) { Ok(Some(item)) => item, Ok(None) => return Ok(None), Err(err) => return Err(err), @@ -186,7 +186,7 @@ impl ValueIterator for FilterIter { // Fall back to the default implementation } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct MapIter { pub(crate) inner: IterValue, pub(crate) map: Arc, @@ -231,23 +231,23 @@ impl StepByIter { } } -impl PartialEq for StepByIter { - fn eq(&self, other: &Self) -> bool { - if self.step != other.step { - return false; - } - - if *self.first_step.lock().unwrap() != *other.first_step.lock().unwrap() { - return false; - } - - if self.inner != other.inner { - return false; - } - - true - } -} +// impl PartialEq for StepByIter { +// fn eq(&self, other: &Self) -> bool { +// if self.step != other.step { +// return false; +// } +// +// if *self.first_step.lock().unwrap() != *other.first_step.lock().unwrap() { +// return false; +// } +// +// if self.inner != other.inner { +// return false; +// } +// +// true +// } +// } #[derive(Debug, Clone)] pub struct StepByIter { @@ -292,3 +292,38 @@ impl Trace for StepByIter { self.inner.visit_refs(f); } } + +#[derive(Debug, Clone)] +pub struct EnumerateIter { + pub(crate) inner: IterValue, + pub(crate) index: Arc>, +} + +impl Trace for EnumerateIter { + fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { + self.inner.visit_refs(f) + } +} + +impl ValueIterator for EnumerateIter { + fn next(&self, vm: &mut dyn Vm) -> SourceResult> { + self.nth(vm, 0) + } + + fn nth(&self, vm: &mut dyn Vm, n: usize) -> SourceResult> { + let mut index = self.index.lock().expect("index poisoned"); + + let index_cpy = *index; + *index = *index + n + 1; // plus one because n is 0 indexed. (for 0 we do yield an item, so index should be incremented by 1) + + drop(index); + + let Some(inner_value) = self.inner.nth(vm, n)? else { + return Ok(None); + }; + + Ok(Some( + ArrayValue::from(vm.heap_mut(), vec![index_cpy.into_value(), inner_value]).into_value(), + )) + } +} diff --git a/compose-library/src/foundations/iterator/mod.rs b/compose-library/src/foundations/iterator/mod.rs index 69379657..6843e836 100644 --- a/compose-library/src/foundations/iterator/mod.rs +++ b/compose-library/src/foundations/iterator/mod.rs @@ -23,11 +23,18 @@ pub use range_iter::*; pub use string_iter::*; #[ty(scope, cast, name = "Iterator")] -#[derive(Debug, Clone, PartialEq, Copy)] +#[derive(Debug, Clone, Copy)] pub struct IterValue { iter: HeapRef, } +impl PartialEq for IterValue { + fn eq(&self, _other: &Self) -> bool { + // Iterators are not comparable. + false + } +} + impl Trace for IterValue { fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { f(self.iter.key()) @@ -102,7 +109,7 @@ pub fn requires_mutable_iter(value: Value) -> Result {} Iter::StepBy(iter) => iter.visit_refs(f), Iter::Filter(filter) => filter.visit_refs(f), + Iter::Enumerate(enumerate) => enumerate.visit_refs(f), } } } @@ -143,6 +152,7 @@ impl Iter { Iter::Range(r) => r.next(vm), Iter::StepBy(s) => s.next(vm), Iter::Filter(f) => f.next(vm), + Iter::Enumerate(e) => e.next(vm), } } @@ -157,6 +167,7 @@ impl Iter { Iter::Range(r) => r.nth(vm, n), Iter::StepBy(s) => s.nth(vm, n), Iter::Filter(f) => f.nth(vm, n), + Iter::Enumerate(e) => e.nth(vm, n), } } } @@ -246,6 +257,17 @@ impl IterValue { ) } + #[func] + fn enumerate(self, vm: &mut dyn Vm) -> Self { + IterValue::new( + Iter::Enumerate(EnumerateIter { + inner: self, + index: Arc::new(Mutex::new(0)), + }), + vm, + ) + } + #[func] fn find(&mut self, vm: &mut dyn Vm, predicate: Func) -> SourceResult> { while let Some(v) = self.next(vm)? { diff --git a/compose-library/src/foundations/scope.rs b/compose-library/src/foundations/scope.rs index 876f5c56..615c4b3a 100644 --- a/compose-library/src/foundations/scope.rs +++ b/compose-library/src/foundations/scope.rs @@ -7,13 +7,13 @@ use compose_error_codes::{ use compose_library::diag::{StrResult, bail}; use compose_library::{Func, NativeFunc, UntypedRef}; use compose_syntax::{Label, Span}; +use compose_utils::trace_log; use ecow::{EcoString, eco_format, eco_vec}; use indexmap::IndexMap; use indexmap::map::Entry; use std::collections::HashSet; use std::fmt::Debug; use std::hash::Hash; -use std::iter; use std::sync::LazyLock; use strsim::jaro_winkler; use tap::Pipe; @@ -22,46 +22,135 @@ pub trait NativeScope { fn scope() -> &'static Scope; } -pub static EMPTY_SCOPE: LazyLock = LazyLock::new(Scope::new); +pub static EMPTY_SCOPE: LazyLock = LazyLock::new(Scope::new_lexical); -#[derive(Default, Clone)] +/** +Manages Bindings, Lexical Scopes, and Flow Scopes as well as access to the standard library. + +Scopes are organised as a stack of [`Scope`]s. Each scope contains a map of variable names to [`Binding`]s. + +# Scope kinds + +Compose has two kinds of scopes: lexical and flow. Lexical scopes are used to define long-lived variables +and work very similarly to scopes in other languages. In general, a lexical scope is created when a block +is entered, and destroyed when it is exited. Flow scopes are used to define temporary variables that are +only valid within a particular expression or statement. This shows up in pattern matching like +`x is Int y && y > 0`, where `y` is a flow variable. + +These scope kinds live in a single stack with flow and lexical scopes interleaved. +New flow bindings are always bound to the top flow `Scope`, and can only be bound when the top scope is a flow scope. +Lexical bindings are bound to the top lexical `Scope`, but it can be below other flow scopes. + +# At least one lexical scope is present +At least one lexical scope must be present in the stack at all times. +This 'bottom' scope represents the global scope for a module. +*/ +#[derive(Clone)] pub struct Scopes<'a> { - /// The current scope. - pub top: Scope, - /// The rest of the scopes. pub stack: Vec, pub lib: Option<&'a Library>, } -impl Debug for Scopes<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Scopes") - .field("top", &self.top) - .field("stack", &self.stack) - .finish() - } -} - impl<'a> Scopes<'a> { pub fn new(lib: Option<&'a Library>) -> Self { Self { - top: Scope::new(), - stack: Vec::new(), + stack: vec![Scope::new_lexical()], lib, } } - pub fn enter(&mut self) { - self.stack.push(std::mem::take(&mut self.top)); + pub fn top_lexical(&self) -> &Scope { + self.stack + .iter() + .rev() + .find(|s| s.kind() == ScopeKind::Lexical) + .expect("At least one lexical scope must be present") + } + + pub fn top_lexical_mut(&mut self) -> &mut Scope { + self.stack + .iter_mut() + .rev() + .find(|s| s.kind() == ScopeKind::Lexical) + .expect("At least one lexical scope must be present") } - pub fn exit(&mut self) { - self.top = self.stack.pop().expect("Scope stack underflow"); + pub fn top_flow(&self) -> Option<&Scope> { + match self.stack.last() { + Some(s) if s.kind() == ScopeKind::Flow => Some(s), + _ => None, + } + } + + pub fn top_flow_mut(&mut self) -> Option<&mut Scope> { + match self.stack.last_mut() { + Some(s) if s.kind() == ScopeKind::Flow => Some(s), + _ => None, + } + } + + pub fn clear_top_lexical(&mut self) { + let position_from_end = self + .stack + .iter() + .rev() + .position(|s| s.kind() == ScopeKind::Lexical) + .expect("At least one lexical scope must be present"); + + let top_lexical_pos = self.stack.len() - position_from_end - 1; + self.stack.truncate(top_lexical_pos + 1); // clear any flow scopes above it + + self.stack + .last_mut() + .expect("At least one lexical scope must be present") + .map + .clear(); + } + + pub fn enter_lexical(&mut self) { + self.stack.push(Scope::new_lexical()); + trace_log!("entered_lexical"); + } + + #[track_caller] + pub fn exit_lexical(&mut self) { + let position_from_end = self + .stack + .iter() + .rev() + .position(|s| s.kind() == ScopeKind::Lexical) + .expect("At least one lexical scope must be present"); + let position = self.stack.len() - position_from_end - 1; + self.stack.truncate(position); + // check that there is still a lexical scope present + self.stack + .iter() + .rev() + .find(|s| s.kind() == ScopeKind::Lexical) + .expect("At least one lexical scope must be present"); + trace_log!("exited_lexical"); + } + + pub fn enter_flow(&mut self) { + self.stack.push(Scope::new_flow()); + } + + #[track_caller] + pub fn exit_flow(&mut self) -> Scope { + match self.stack.pop() { + Some(s) if s.kind() == ScopeKind::Flow => s, + Some(s) => panic!( + "At least one flow scope must be present: {:?}, {:?}", + s, self.stack + ), + None => panic!("At least one scope must be present"), + } } pub fn get(&self, name: &str) -> Result<&Binding, VariableAccessError> { - iter::once(&self.top) - .chain(self.stack.iter().rev()) + self.stack + .iter() + .rev() .chain(self.lib.iter().map(|lib| lib.global.scope())) .find_map(|scope| scope.get(name)) .ok_or_else(|| self.unbound_error(name.into())) @@ -74,8 +163,9 @@ impl<'a> Scopes<'a> { // So we call `get(name)?` here to handle errors, then search mutably afterward. let _ = self.get(name)?; - iter::once(&mut self.top) - .chain(self.stack.iter_mut().rev()) + self.stack + .iter_mut() + .rev() .find_map(|scope| scope.get_mut(name)) .ok_or_else( || match self.lib.and_then(|base| base.global.scope().get(name)) { @@ -93,8 +183,9 @@ impl<'a> Scopes<'a> { } fn unbound_error(&self, name: EcoString) -> VariableAccessError { - let all_idents = iter::once(&self.top) - .chain(&self.stack) + let all_idents = self + .stack + .iter() .chain(self.lib.iter().map(|lib| lib.global.scope())) .flat_map(|scope| scope.map.keys()); @@ -106,6 +197,20 @@ impl<'a> Scopes<'a> { } } +impl Default for Scopes<'_> { + fn default() -> Self { + Self::new(None) + } +} + +impl Debug for Scopes<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Scopes") + .field("stack", &self.stack) + .finish() + } +} + #[derive(Debug, Clone)] pub enum VariableAccessError { Unbound(UnBoundError), @@ -194,14 +299,25 @@ impl At for Result { } } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ScopeKind { + /// A flow scope tracks temporary variables created within expressions. + /// For example, `x is Int y && y > 0` introduces a temporary variable `y` + /// that is only valid within this expression. + Flow, + #[default] + Lexical, +} + #[derive(Debug, Default, Clone)] +/// Stores Bindings within a scope. pub struct Scope { + kind: ScopeKind, map: IndexMap, } impl Trace for Scopes<'_> { fn visit_refs(&self, f: &mut dyn FnMut(UntypedRef)) { - self.top.visit_refs(f); for scope in self.stack.iter() { scope.visit_refs(f); } @@ -265,13 +381,27 @@ impl Scope { pub fn bindings(&self) -> &IndexMap { &self.map } + + pub fn kind(&self) -> ScopeKind { + self.kind + } } impl Scope { - pub fn new() -> Self { + pub fn new_lexical() -> Self { Default::default() } + /// A flow scope tracks temporary variables created within expressions. + /// For example, `x is Int y && y > 0` introduces a temporary variable `y` + /// that is only valid within this expression. + pub fn new_flow() -> Self { + Self { + kind: ScopeKind::Flow, + ..Default::default() + } + } + pub fn get(&self, name: &str) -> Option<&Binding> { self.map.get(name) } diff --git a/compose-library/src/foundations/value.rs b/compose-library/src/foundations/value.rs index f62580e3..63893ca3 100644 --- a/compose-library/src/foundations/value.rs +++ b/compose-library/src/foundations/value.rs @@ -69,15 +69,8 @@ impl Value { pub fn pipe(self, vm: &mut dyn Vm, transform: Func) -> SourceResult { vm.call_func(&transform, Args::new(transform.span, iter::once(self))) } -} - -impl Value { - pub fn is_box(&self) -> bool { - matches!(self, Value::Box(_)) - } -} -impl Value { + #[func] pub fn ty(&self) -> Type { match self { Value::Int(_) => Type::of::(), @@ -94,6 +87,15 @@ impl Value { Value::Module(_) => Type::of::(), } } +} + +impl Value { + pub fn is_box(&self) -> bool { + matches!(self, Value::Box(_)) + } +} + +impl Value { pub fn unit() -> Value { Value::Unit(UnitValue) @@ -240,6 +242,19 @@ impl Repr for Value { } } +pub trait DebugRepr { + fn debug_repr(&self, vm: &dyn Vm) -> EcoString; +} + +impl DebugRepr for Value { + fn debug_repr(&self, vm: &dyn Vm) -> EcoString { + match self { + Value::Str(v) => eco_format!("\"{v}\""), + other => other.repr(vm), + } + } +} + impl Default for Value { fn default() -> Self { Value::Unit(UnitValue) diff --git a/compose-library/src/gc/mod.rs b/compose-library/src/gc/mod.rs index 479cb71a..cd0b6da9 100644 --- a/compose-library/src/gc/mod.rs +++ b/compose-library/src/gc/mod.rs @@ -217,7 +217,6 @@ macro_rules! impl_heap_obj { }; } -pub(crate) use impl_heap_obj; use crate::diag::StrResult; impl_heap_obj!(Value, Value); @@ -226,7 +225,7 @@ impl_heap_obj!(Array, Array); impl_heap_obj!(Map, Map); heap_enum! { - #[derive(Debug, Clone, PartialEq)] + #[derive(Debug, Clone)] pub enum HeapItem { Value(Value), Iter(Iter), diff --git a/compose-library/src/lib.rs b/compose-library/src/lib.rs index d584cf46..aeb0cae7 100644 --- a/compose-library/src/lib.rs +++ b/compose-library/src/lib.rs @@ -8,8 +8,6 @@ pub mod repr; mod engine; mod gc; mod vm; -mod modules; - pub use engine::*; pub use foundations::*; pub use gc::*; @@ -41,17 +39,20 @@ pub struct Routines { pub fn library() -> Library { - let mut global = Scope::new(); + 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::(); diff --git a/compose-library/src/modules/json/mod.rs b/compose-library/src/modules/json/mod.rs deleted file mode 100644 index 5cfabad0..00000000 --- a/compose-library/src/modules/json/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::collections::HashMap; - -/// because normal values contain heap references they cannot properly implement SerDe's serialize and -/// deserialize. This enum is a `dereferenced` version -pub enum SerializableValue { - Int(i64), - Bool(bool), - Unit, - Str(String), - Array(Vec), - Map(HashMap), -} - - - - - diff --git a/compose-library/src/modules/mod.rs b/compose-library/src/modules/mod.rs deleted file mode 100644 index 09dbc788..00000000 --- a/compose-library/src/modules/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod json; \ No newline at end of file diff --git a/compose-library/src/world.rs b/compose-library/src/world.rs index bf9e55c4..ea6938f8 100644 --- a/compose-library/src/world.rs +++ b/compose-library/src/world.rs @@ -5,21 +5,71 @@ use compose_syntax::{FileId, Source, Span}; use std::io::{Read, Write}; use std::ops::Range; +/** +Defines how Compose interacts with the outside world. + +[`World`] is an abstraction layer between the Compose runtime and its external environment. + +It is responsible for: + +- Loading source files and defining the program entrypoint. +- Providing access to a standard library. +- Provide I/O primitives for stdout. + +This trait allows Compose to be embedded in different environments +(e.g. CLI tools, editors, tests, or sandboxes) without hard-coding +filesystem or I/O behaviour. +*/ pub trait World { - /// The entrypoint file of the program to execute + /// Returns the [`FileId`] of the entrypoint of the program. fn entry_point(&self) -> FileId; + /// Returns the Source identified by the given [`FileId`] + /// + /// # Errors + /// + /// Returns an error if the source cannot be located or loaded. fn source(&self, file_id: FileId) -> FileResult; + /// Returns a reference to the standard library available to the program. fn library(&self) -> &Library; - fn write(&self, f: &dyn Fn(&mut dyn Write) -> std::io::Result<()>) -> std::io::Result<()>; - fn read(&self, f: &dyn Fn(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()>; + /// Provides write access to the program's output stream. + /// + /// The provided closure is given exclusive access to a writer + /// for the duration of the call. + fn write( + &self, + f: &mut dyn FnMut(&mut dyn Write) -> std::io::Result<()>, + ) -> std::io::Result<()>; + + /// Provides read access to the program's input stream. + /// + /// The provided closure is given exclusive access to the reader + /// for the duration of the call. + fn read(&self, f: &mut dyn FnMut(&mut dyn Read) -> std::io::Result<()>) -> std::io::Result<()>; + /// Provides read and write access to the programs input and output stream. + /// + /// The provided closure is given exclusive access to the reader and writer + /// for the duration of the call. + fn with_io( + &self, + f: &mut dyn FnMut(&mut dyn Read, &mut dyn Write) -> std::io::Result<()>, + ) -> std::io::Result<()> { + self.read(&mut |r| self.write(&mut |w| f(r, w))) + } + + /// Returns a human-readable name for the given file identifier. + /// + /// By default, this is derived from the file's path. fn name(&self, id: FileId) -> String { id.path().0.display().to_string() } + /// Attempts to retrieve the source associated with the given span. + /// + /// This is primarily used for diagnostics and error reporting. fn related_source(&self, span: Span) -> Option { self.source(span.id()?).ok() } @@ -29,13 +79,15 @@ pub struct SyntaxContext<'a> { pub world: &'a dyn World, } - impl<'a> Files<'a> for &dyn World { type FileId = FileId; type Name = String; type Source = Source; - fn name(&'a self, id: Self::FileId) -> Result { + fn name( + &'a self, + id: Self::FileId, + ) -> Result { Ok(World::name(*self, id)) } diff --git a/compose-macros/src/scope.rs b/compose-macros/src/scope.rs index 68d2d0e7..09fef59c 100644 --- a/compose-macros/src/scope.rs +++ b/compose-macros/src/scope.rs @@ -52,7 +52,7 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result { impl #foundations::NativeScope for #self_ty { fn scope() -> &'static #foundations::Scope { static SCOPE: ::std::sync::LazyLock<#foundations::Scope> = ::std::sync::LazyLock::new(|| { - let mut scope = #foundations::Scope::new(); + let mut scope = #foundations::Scope::new_lexical(); #(#definitions;)* scope }); diff --git a/compose-syntax/Cargo.toml b/compose-syntax/Cargo.toml index 7b048e6c..0d820e93 100644 --- a/compose-syntax/Cargo.toml +++ b/compose-syntax/Cargo.toml @@ -9,5 +9,6 @@ compose-utils = { path = "../compose-utils" } compose-error-codes = { path = "../compose-error-codes"} unscanny = "0.1.0" extension-traits = { workspace = true } +itertools = { workspace = true } [dev-dependencies] \ No newline at end of file diff --git a/compose-syntax/src/ast.rs b/compose-syntax/src/ast.rs index 36f00cf8..10667e5c 100644 --- a/compose-syntax/src/ast.rs +++ b/compose-syntax/src/ast.rs @@ -16,6 +16,8 @@ mod range; mod map; mod module; mod index_access; +mod pattern; +mod match_expression; use ecow::EcoString; use crate::node::SyntaxNode; @@ -39,6 +41,8 @@ pub use range::*; pub use map::*; pub use module::*; pub use index_access::*; +pub use pattern::*; +pub use match_expression::*; pub trait AstNode<'a>: Sized { fn from_untyped(node: &'a SyntaxNode) -> Option; diff --git a/compose-syntax/src/ast/bindings.rs b/compose-syntax/src/ast/bindings.rs index cd548d67..2d3affda 100644 --- a/compose-syntax/src/ast/bindings.rs +++ b/compose-syntax/src/ast/bindings.rs @@ -1,4 +1,4 @@ -use crate::ast::func::Pattern; +use crate::ast::pattern::Pattern; use crate::ast::{node, AstNode, Expr, Ident}; use crate::kind::SyntaxKind; use crate::Span; @@ -30,7 +30,7 @@ impl<'a> LetBinding<'a> { } pub fn is_mut(self) -> bool { - self.0.children().any(|n| n.kind() == SyntaxKind::Mut) + self.0.children().any(|n| n.kind() == SyntaxKind::MutKW) } pub fn eq_span(self) -> Span { @@ -44,7 +44,7 @@ impl<'a> LetBinding<'a> { pub fn mut_span(self) -> Option { self.0 .children() - .find(|&n| n.kind() == SyntaxKind::Mut) + .find(|&n| n.kind() == SyntaxKind::MutKW) .map(|n| n.span()) } @@ -53,13 +53,13 @@ impl<'a> LetBinding<'a> { } pub fn is_public(self) -> bool { - self.0.children().any(|n| n.kind() == SyntaxKind::Pub) + self.0.children().any(|n| n.kind() == SyntaxKind::PubKW) } pub fn pub_span(self) -> Option { self.0 .children() - .find(|&n| n.kind() == SyntaxKind::Pub) + .find(|&n| n.kind() == SyntaxKind::PubKW) .map(|n| n.span()) } } diff --git a/compose-syntax/src/ast/control_flow.rs b/compose-syntax/src/ast/control_flow.rs index 1a514d67..83fa9ba0 100644 --- a/compose-syntax/src/ast/control_flow.rs +++ b/compose-syntax/src/ast/control_flow.rs @@ -1,5 +1,6 @@ use crate::ast::macros::node; -use crate::ast::{CodeBlock, Expr, Pattern}; +use crate::ast::{CodeBlock, Expr}; +use crate::ast::pattern::Pattern; use crate::kind::SyntaxKind; use crate::SyntaxNode; @@ -59,7 +60,7 @@ impl<'a> ForLoop<'a> { pub fn iterable(self) -> Expr<'a> { self.0 .children() - .skip_while(|n| n.kind() != SyntaxKind::In) + .skip_while(|n| n.kind() != SyntaxKind::InKW) .find_map(SyntaxNode::cast) .unwrap_or_default() } diff --git a/compose-syntax/src/ast/expr.rs b/compose-syntax/src/ast/expr.rs index 2bccdfb5..59943139 100644 --- a/compose-syntax/src/ast/expr.rs +++ b/compose-syntax/src/ast/expr.rs @@ -1,12 +1,13 @@ -use crate::SyntaxNode; use crate::ast::atomics::Unit; use crate::ast::control_flow::Conditional; use crate::ast::map::MapLiteral; +use crate::ast::match_expression::MatchExpression; use crate::ast::range::Range; use crate::ast::unary::Unary; -use crate::ast::{Array, AstNode, Binary, ForLoop, Ident, IndexAccess, Int, Lambda, Parenthesized, WhileLoop}; -use crate::ast::{Bool, CodeBlock, FieldAccess, FuncCall, LetBinding, PathAccess, Str}; +use crate::ast::{Array, AstNode, Binary, ForLoop, Ident, IndexAccess, Int, IsExpression, Lambda, Parenthesized, WhileLoop}; +use crate::ast::{Bool, CodeBlock, FieldAccess, FuncCall, PathAccess, Str}; use crate::kind::SyntaxKind; +use crate::SyntaxNode; /// An expression. The base of Compose. Any "statement" is an expression. /// @@ -25,7 +26,6 @@ pub enum Expr<'a> { Ident(Ident<'a>), Binary(Binary<'a>), Int(Int<'a>), - LetBinding(LetBinding<'a>), CodeBlock(CodeBlock<'a>), Str(Str<'a>), Bool(Bool<'a>), @@ -41,6 +41,8 @@ pub enum Expr<'a> { Map(MapLiteral<'a>), Lambda(Lambda<'a>), IndexAccess(IndexAccess<'a>), + MatchExpression(MatchExpression<'a>), + IsExpression(IsExpression<'a>), } impl<'a> AstNode<'a> for Expr<'a> { @@ -51,7 +53,6 @@ impl<'a> AstNode<'a> for Expr<'a> { SyntaxKind::Ident => Some(Self::Ident(Ident::from_untyped(node)?)), SyntaxKind::Binary => Some(Self::Binary(Binary::from_untyped(node)?)), SyntaxKind::Int => Some(Self::Int(Int::from_untyped(node)?)), - SyntaxKind::LetBinding => Some(Self::LetBinding(LetBinding::from_untyped(node)?)), SyntaxKind::CodeBlock => Some(Self::CodeBlock(CodeBlock::from_untyped(node)?)), SyntaxKind::Str => Some(Self::Str(Str::from_untyped(node)?)), SyntaxKind::Bool => Some(Self::Bool(Bool::from_untyped(node)?)), @@ -69,6 +70,8 @@ impl<'a> AstNode<'a> for Expr<'a> { SyntaxKind::MapLiteral => Some(Self::Map(MapLiteral::from_untyped(node)?)), SyntaxKind::Lambda => Some(Self::Lambda(Lambda::from_untyped(node)?)), SyntaxKind::IndexAccess => Some(Self::IndexAccess(IndexAccess::from_untyped(node)?)), + SyntaxKind::MatchExpression => Some(Self::MatchExpression(MatchExpression::from_untyped(node)?)), + SyntaxKind::IsExpression => Some(Self::IsExpression(IsExpression::from_untyped(node)?)), _ => None, } } @@ -80,7 +83,6 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Ident(ident) => ident.to_untyped(), Self::Binary(binary) => binary.to_untyped(), Self::Int(int) => int.to_untyped(), - Self::LetBinding(let_binding) => let_binding.to_untyped(), Self::CodeBlock(code_block) => code_block.to_untyped(), Self::Str(str) => str.to_untyped(), Self::Bool(bool) => bool.to_untyped(), @@ -96,6 +98,8 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Map(map) => map.to_untyped(), Self::Lambda(lambda) => lambda.to_untyped(), Self::IndexAccess(index_access) => index_access.to_untyped(), + Self::MatchExpression(match_expression) => match_expression.to_untyped(), + Self::IsExpression(is_expression) => is_expression.to_untyped(), } } } diff --git a/compose-syntax/src/ast/func.rs b/compose-syntax/src/ast/func.rs index e7d3e9eb..c3297037 100644 --- a/compose-syntax/src/ast/func.rs +++ b/compose-syntax/src/ast/func.rs @@ -1,6 +1,7 @@ -use crate::ast::{AstNode, Expr, Ident, Statement, node}; +use crate::ast::{node, AstNode, Expr, Ident, Statement}; use crate::kind::SyntaxKind; use crate::{Span, SyntaxNode}; +use crate::ast::pattern::Pattern; node! { struct Lambda @@ -43,24 +44,24 @@ impl<'a> Capture<'a> { } pub fn is_ref(self) -> bool { - self.0.children().any(|n| n.kind() == SyntaxKind::Ref) + self.0.children().any(|n| n.kind() == SyntaxKind::RefKW) } pub fn ref_span(self) -> Option { self.0 .children() - .find(|n| n.kind() == SyntaxKind::Ref) + .find(|n| n.kind() == SyntaxKind::RefKW) .map(|n| n.span()) } pub fn is_mut(self) -> bool { - self.0.children().any(|n| n.kind() == SyntaxKind::Mut) + self.0.children().any(|n| n.kind() == SyntaxKind::MutKW) } pub fn mut_span(self) -> Option { self.0 .children() - .find(|n| n.kind() == SyntaxKind::Mut) + .find(|n| n.kind() == SyntaxKind::MutKW) .map(|n| n.span()) } } @@ -85,24 +86,24 @@ impl<'a> Param<'a> { } pub fn is_ref(self) -> bool { - self.0.children().any(|n| n.kind() == SyntaxKind::Ref) + self.0.children().any(|n| n.kind() == SyntaxKind::RefKW) } pub fn ref_span(self) -> Option { self.0 .children() - .find(|n| n.kind() == SyntaxKind::Ref) + .find(|n| n.kind() == SyntaxKind::RefKW) .map(|n| n.span()) } pub fn is_mut(self) -> bool { - self.0.children().any(|n| n.kind() == SyntaxKind::Mut) + self.0.children().any(|n| n.kind() == SyntaxKind::MutKW) } pub fn mut_span(self) -> Option { self.0 .children() - .find(|n| n.kind() == SyntaxKind::Mut) + .find(|n| n.kind() == SyntaxKind::MutKW) .map(|n| n.span()) } } @@ -137,47 +138,6 @@ impl<'a> AstNode<'a> for ParamKind<'a> { } } -#[derive(Debug, Clone, Copy)] -pub enum Pattern<'a> { - Single(Expr<'a>), - PlaceHolder(Underscore<'a>), - Destructuring(Destructuring<'a>), -} - -impl<'a> Pattern<'a> { - pub fn bindings(self) -> Vec> { - match self { - Pattern::Single(Expr::Ident(i)) => vec![i], - Pattern::Destructuring(v) => v.bindings(), - _ => vec![], - } - } -} - -impl<'a> AstNode<'a> for Pattern<'a> { - fn from_untyped(node: &'a SyntaxNode) -> Option { - match node.kind() { - SyntaxKind::Underscore => Some(Self::PlaceHolder(Underscore(node))), - SyntaxKind::Destructuring => Some(Self::Destructuring(Destructuring(node))), - _ => node.cast().map(Self::Single), - } - } - - fn to_untyped(&self) -> &'a SyntaxNode { - match self { - Self::Single(e) => e.to_untyped(), - Self::PlaceHolder(u) => u.to_untyped(), - Self::Destructuring(d) => d.to_untyped(), - } - } -} - -impl Default for Pattern<'_> { - fn default() -> Self { - Self::Single(Expr::default()) - } -} - node! { struct Underscore } @@ -203,81 +163,6 @@ impl<'a> Named<'a> { } } -node! { - struct Destructuring -} - -impl<'a> Destructuring<'a> { - pub fn items(self) -> impl DoubleEndedIterator> { - self.0.children().filter_map(SyntaxNode::cast) - } - - pub fn bindings(self) -> Vec> { - self.items() - .flat_map(|binding| match binding { - DestructuringItem::Named(named) => named.pattern().bindings(), - DestructuringItem::Pattern(pattern) => pattern.bindings(), - DestructuringItem::Spread(spread) => { - spread.sink_ident().into_iter().collect() - } - }) - .collect() - } -} - -pub enum DestructuringItem<'a> { - Pattern(Pattern<'a>), - Named(Named<'a>), - Spread(Spread<'a>), -} - -node! { - struct Spread -} - -impl<'a> Spread<'a> { - /// The spread expression. - /// - /// This should only be accessed if this `Spread` is contained in an - /// `ArrayItem`, `MapItem`, or `Arg`. - pub fn expr(self) -> Expr<'a> { - self.0.cast_first() - } - - /// The sink identifier, if present. - /// - /// This should only be accessed if this `Spread` is contained in a - /// `Param` or binding `DestructuringItem`. - pub fn sink_ident(self) -> Option> { - self.0.try_cast_first() - } - - /// The sink expressions, if present. - /// - /// This should only be accessed if this `Spread` is contained in a - /// `DestructuringItem`. - pub fn sink_expr(self) -> Option> { - self.0.try_cast_first() - } -} - -impl<'a> AstNode<'a> for DestructuringItem<'a> { - fn from_untyped(node: &'a SyntaxNode) -> Option { - match node.kind() { - SyntaxKind::Named => Some(Self::Named(Named::from_untyped(node)?)), - SyntaxKind::Spread => Some(Self::Spread(Spread(node))), - _ => node.cast().map(Self::Pattern), - } - } - - fn to_untyped(&self) -> &'a SyntaxNode { - match self { - Self::Named(n) => n.to_untyped(), - Self::Pattern(p) => p.to_untyped(), - Self::Spread(s) => s.to_untyped(), - } - } -} #[cfg(test)] mod tests { diff --git a/compose-syntax/src/ast/map.rs b/compose-syntax/src/ast/map.rs index 25cd596b..485c27e1 100644 --- a/compose-syntax/src/ast/map.rs +++ b/compose-syntax/src/ast/map.rs @@ -36,7 +36,7 @@ mod tests { #[test] fn test_map_with_identifier_keys() { assert_ast! { - "#{ a: 1, b: 2 }", + "{ a: 1, b: 2 }", map as MapLiteral { map.entries() => [ entry as MapEntry { @@ -63,7 +63,7 @@ mod tests { #[test] fn test_map_with_string_keys() { assert_ast! { - "#{ \"a\": 1, \"b\": 2 }", + "{ \"a\": 1, \"b\": 2 }", map as MapLiteral { map.entries() => [ entry as MapEntry { @@ -90,7 +90,7 @@ mod tests { #[test] fn test_map_with_shorthand_keys() { assert_ast! { - "#{ a, b, }", + "{ a:, b, }", map as MapLiteral { map.entries() => [ entry as MapEntry { diff --git a/compose-syntax/src/ast/match_expression.rs b/compose-syntax/src/ast/match_expression.rs new file mode 100644 index 00000000..8639b57f --- /dev/null +++ b/compose-syntax/src/ast/match_expression.rs @@ -0,0 +1,46 @@ +use crate::ast::macros::node; +use crate::ast::Expr; +use crate::ast::Pattern; +use crate::{SyntaxKind, SyntaxNode}; + +node! { + struct MatchExpression +} + +impl<'a> MatchExpression<'a> { + pub fn expr(self) -> Expr<'a> { + self.0.cast_first() + } + + pub fn match_arms(self) -> impl DoubleEndedIterator> { + self.0.children().filter_map(SyntaxNode::cast) + } +} + +node! { + struct MatchArm +} + +impl<'a> MatchArm<'a> { + pub fn patterns(self) -> impl Iterator> { + self.0 + .children() + .take_while(|node| !matches!(node.kind(), SyntaxKind::Arrow | SyntaxKind::IfKW)) + .filter_map(SyntaxNode::cast) + } + + pub fn guard(self) -> Option> { + self.0 + .children() + // Dont look past the arrow + .take_while(|node| !matches!(node.kind(), SyntaxKind::Arrow)) + // Get the expression after the `if` keyword + .skip_while(|node| !matches!(node.kind(), SyntaxKind::IfKW)) + .skip(1) // skip the `if` keyword + .find_map(SyntaxNode::cast) + } + + pub fn expr(self) -> Expr<'a> { + self.0.cast_last() + } +} diff --git a/compose-syntax/src/ast/module.rs b/compose-syntax/src/ast/module.rs index 52f48cb5..c16a7623 100644 --- a/compose-syntax/src/ast/module.rs +++ b/compose-syntax/src/ast/module.rs @@ -22,7 +22,7 @@ impl<'a> ModuleImport<'a> { .children() .take_while(|n| n.kind() != SyntaxKind::Colon); while let Some(child) = children.next() { - if child.kind() == SyntaxKind::As { + if child.kind() == SyntaxKind::AsKW { return children.next().and_then(SyntaxNode::cast); } } @@ -47,7 +47,7 @@ impl<'a> ImportItem<'a> { pub fn alias(self) -> Option> { self.0 .children() - .skip_while(|n| n.kind() != SyntaxKind::As) + .skip_while(|n| n.kind() != SyntaxKind::AsKW) .find_map(SyntaxNode::cast) } } diff --git a/compose-syntax/src/ast/pattern.rs b/compose-syntax/src/ast/pattern.rs new file mode 100644 index 00000000..832a8821 --- /dev/null +++ b/compose-syntax/src/ast/pattern.rs @@ -0,0 +1,198 @@ +use crate::ast::macros::node; +use crate::ast::{AstNode, Expr, Ident, Named, Underscore, Unit}; +use crate::{Span, SyntaxKind, SyntaxNode}; + +#[derive(Debug, Clone, Copy)] +pub enum Pattern<'a> { + Single(Expr<'a>), + PlaceHolder(Underscore<'a>), + Destructuring(Destructuring<'a>), + LiteralPattern(LiteralPattern<'a>), + TypedPattern(TypedPattern<'a>), +} + +impl<'a> Pattern<'a> { + pub fn bindings(self) -> Vec> { + match self { + Pattern::Single(Expr::Ident(i)) => vec![i], + Pattern::Destructuring(v) => v.bindings(), + Pattern::TypedPattern(typed) => typed.pattern().bindings(), + Pattern::PlaceHolder(_) => vec![], + Pattern::LiteralPattern(_) => vec![], + Pattern::Single(_) => vec![], + } + } +} + +impl<'a> AstNode<'a> for Pattern<'a> { + fn from_untyped(node: &'a SyntaxNode) -> Option { + match node.kind() { + SyntaxKind::Underscore => Some(Self::PlaceHolder(Underscore::from_untyped(node)?)), + SyntaxKind::Destructuring => { + Some(Self::Destructuring(Destructuring::from_untyped(node)?)) + } + SyntaxKind::TypedPattern => Some(Self::TypedPattern(TypedPattern::from_untyped(node)?)), + _ => match LiteralPattern::from_untyped(node) { + Some(lit) => Some(Self::LiteralPattern(lit)), + None => node.cast().map(Self::Single), + }, + } + } + + fn to_untyped(&self) -> &'a SyntaxNode { + match self { + Self::Single(e) => e.to_untyped(), + Self::PlaceHolder(u) => u.to_untyped(), + Self::Destructuring(d) => d.to_untyped(), + Self::LiteralPattern(lit) => lit.to_untyped(), + Self::TypedPattern(typed_binding) => typed_binding.to_untyped(), + } + } +} + +impl Default for Pattern<'_> { + fn default() -> Self { + Self::Single(Expr::default()) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum LiteralPattern<'a> { + Int(Expr<'a>), + Float(Expr<'a>), + Str(Expr<'a>), + Bool(Expr<'a>), + Unit(Unit<'a>), +} + +impl<'a> AstNode<'a> for LiteralPattern<'a> { + fn from_untyped(node: &'a SyntaxNode) -> Option { + match node.kind() { + SyntaxKind::Int => Some(Self::Int(Expr::from_untyped(node)?)), + SyntaxKind::Float => Some(Self::Float(Expr::from_untyped(node)?)), + SyntaxKind::Str => Some(Self::Str(Expr::from_untyped(node)?)), + SyntaxKind::Bool => Some(Self::Bool(Expr::from_untyped(node)?)), + SyntaxKind::Unit => Some(Self::Unit(Unit::from_untyped(node)?)), + _ => None, + } + } + + fn to_untyped(&self) -> &'a SyntaxNode { + match self { + Self::Int(i) => i.to_untyped(), + Self::Float(f) => f.to_untyped(), + Self::Str(s) => s.to_untyped(), + Self::Bool(b) => b.to_untyped(), + Self::Unit(u) => u.to_untyped(), + } + } +} + +node! { + struct Destructuring +} + +node! { + struct Spread +} + +impl<'a> Destructuring<'a> { + pub fn items(self) -> impl DoubleEndedIterator> { + self.0.children().filter_map(SyntaxNode::cast) + } + + pub fn bindings(self) -> Vec> { + self.items() + .flat_map(|binding| match binding { + DestructuringItem::Named(named) => named.pattern().bindings(), + DestructuringItem::Pattern(pattern) => pattern.bindings(), + DestructuringItem::Spread(spread) => spread.sink_ident().into_iter().collect(), + }) + .collect() + } +} + +node! { + struct TypedPattern +} + +impl<'a> TypedPattern<'a> { + pub fn ty(self) -> Ident<'a> { + self.0.cast_first() + } + + pub fn pattern(self) -> Pattern<'a> { + self.0.cast_last() + } +} + +pub enum DestructuringItem<'a> { + Pattern(Pattern<'a>), + Named(Named<'a>), + Spread(Spread<'a>), +} + +impl<'a> Spread<'a> { + /// The spread expression. + /// + /// This should only be accessed if this `Spread` is contained in an + /// `ArrayItem`, `MapItem`, or `Arg`. + pub fn expr(self) -> Expr<'a> { + self.0.cast_first() + } + + /// The sink identifier, if present. + /// + /// This should only be accessed if this `Spread` is contained in a + /// `Param` or binding `DestructuringItem`. + pub fn sink_ident(self) -> Option> { + self.0.try_cast_first() + } + + /// The sink expressions, if present. + /// + /// This should only be accessed if this `Spread` is contained in a + /// `DestructuringItem`. + pub fn sink_expr(self) -> Option> { + self.0.try_cast_first() + } +} + +impl<'a> AstNode<'a> for DestructuringItem<'a> { + fn from_untyped(node: &'a SyntaxNode) -> Option { + match node.kind() { + SyntaxKind::Named => Some(Self::Named(Named::from_untyped(node)?)), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), + _ => node.cast().map(Self::Pattern), + } + } + + fn to_untyped(&self) -> &'a SyntaxNode { + match self { + Self::Named(n) => n.to_untyped(), + Self::Pattern(p) => p.to_untyped(), + Self::Spread(s) => s.to_untyped(), + } + } +} + +node! { + struct IsExpression +} + +impl<'a> IsExpression<'a> { + pub fn expr(self) -> Expr<'a> { + self.0.cast_first() + } + + pub fn pattern(self) -> Pattern<'a> { + self.0.cast_last() + } + + pub fn is_span(self) -> Span { + self.0 + .children() + .find_map(|n| (n.kind() == SyntaxKind::IsKW).then(|| n.span())) + .expect("IsExpression must contain an `is` keyword") + } +} diff --git a/compose-syntax/src/ast/statement.rs b/compose-syntax/src/ast/statement.rs index 919cf01f..535f7d34 100644 --- a/compose-syntax/src/ast/statement.rs +++ b/compose-syntax/src/ast/statement.rs @@ -11,7 +11,7 @@ pub enum Statement<'a> { Assign(Assignment<'a>), Break(BreakStatement<'a>), Return(ReturnStatement<'a>), - Continue(Continue<'a>), + Continue(ContinueStatement<'a>), ModuleImport(ModuleImport<'a>) } @@ -24,7 +24,7 @@ impl<'a> AstNode<'a> for Statement<'a> { } SyntaxKind::BreakStatement => Some(Statement::Break(BreakStatement::from_untyped(node)?)), SyntaxKind::ReturnStatement => Some(Statement::Return(ReturnStatement::from_untyped(node)?)), - SyntaxKind::Continue => Some(Statement::Continue(Continue::from_untyped(node)?)), + SyntaxKind::ContinueStatement => Some(Statement::Continue(ContinueStatement::from_untyped(node)?)), SyntaxKind::ModuleImport => Some(Statement::ModuleImport(ModuleImport::from_untyped(node)?)), _ => Expr::from_untyped(node).map(Statement::Expr), } @@ -64,5 +64,5 @@ impl<'a> ReturnStatement<'a> { } node! { - struct Continue + struct ContinueStatement } diff --git a/compose-syntax/src/file.rs b/compose-syntax/src/file.rs index 80899f0b..a9b51a60 100644 --- a/compose-syntax/src/file.rs +++ b/compose-syntax/src/file.rs @@ -12,19 +12,6 @@ static INTERNER: LazyLock> = LazyLock::new(|| { }) }); -/// Resets the interner for file ids. -/// -/// # Warning -/// -/// This will break existing file ids and cause any reading of paths from invalidated file ids to panic! -/// Only use this if you know you will not use any of the existing file ids. -pub fn reset_interner() { - *INTERNER.write().unwrap() = FileInterner { - from_id: Vec::new(), - to_id: HashMap::new(), - } -} - struct FileInterner { from_id: Vec, to_id: HashMap, diff --git a/compose-syntax/src/kind.rs b/compose-syntax/src/kind.rs index 8135575f..94715ddf 100644 --- a/compose-syntax/src/kind.rs +++ b/compose-syntax/src/kind.rs @@ -6,7 +6,7 @@ pub enum SyntaxKind { Apostrophe, Args, Arrow, - As, + AsKW, Assignment, At, Backtick, @@ -14,7 +14,7 @@ pub enum SyntaxKind { BangEq, Binary, Bool, - Break, + BreakKW, Capture, CaptureList, CodeBlock, @@ -26,7 +26,7 @@ pub enum SyntaxKind { Conditional, ConditionalAlternate, ConditionalElse, - Continue, + ContinueKW, DestructureAssignment, Destructuring, DocComment, @@ -36,15 +36,15 @@ pub enum SyntaxKind { DotsEq, DoubleQuote, Ellipsis, - Else, + ElseKW, End, - Enum, + EnumKW, Eq, EqEq, Error, FieldAccess, Float, - For, + ForKW, ForLoop, FuncCall, Gt, @@ -54,23 +54,23 @@ pub enum SyntaxKind { Hat, HatEq, Ident, - If, - Import, - In, + IfKW, + ImportKW, + InKW, IndexAccess, Int, LeftBrace, LeftBracket, LeftParen, - Let, + LetKW, LetBinding, - Loop, + LoopKW, Lt, LtEq, LtLt, Minus, MinusEq, - Mut, + MutKW, Named, NewLine, Param, @@ -83,9 +83,9 @@ pub enum SyntaxKind { PipePipe, Plus, PlusEq, - Pub, - Ref, - Return, + PubKW, + RefKW, + ReturnKW, RightBrace, RightBracket, RightParen, @@ -100,18 +100,25 @@ pub enum SyntaxKind { Unary, Underscore, Unit, - While, + WhileKW, WhileLoop, Array, Range, MapLiteral, MapEntry, + MatchKW, BreakStatement, ReturnStatement, Lambda, ImportItem, ModuleImport, Spread, + ContinueStatement, + MatchArm, + MatchExpression, + IsKW, + TypedPattern, + IsExpression, } impl SyntaxKind { @@ -123,15 +130,14 @@ impl SyntaxKind { } pub(crate) fn descriptive_name(&self) -> &'static str { - match self { - SyntaxKind::Amp => "&", + match self { SyntaxKind::Amp => "&", SyntaxKind::AmpAmp => "&&", SyntaxKind::AmpersandEq => "&=", SyntaxKind::Apostrophe => "'", SyntaxKind::Args => "argument list", SyntaxKind::Array => "array", SyntaxKind::Arrow => "=>", - SyntaxKind::As => "as", + SyntaxKind::AsKW => "as", SyntaxKind::Assignment => "assignment", SyntaxKind::At => "@", SyntaxKind::Backtick => "`", @@ -139,7 +145,7 @@ impl SyntaxKind { SyntaxKind::BangEq => "!=", SyntaxKind::Binary => "binary expression", SyntaxKind::Bool => "boolean literal", - SyntaxKind::Break => "break", + SyntaxKind::BreakKW => "break", SyntaxKind::BreakStatement => "break statement", SyntaxKind::CaptureList => "capture group", SyntaxKind::Capture => "captured variable", @@ -152,7 +158,8 @@ impl SyntaxKind { SyntaxKind::Conditional => "if expression", SyntaxKind::ConditionalAlternate => "else if expression", SyntaxKind::ConditionalElse => "else expression", - SyntaxKind::Continue => "continue", + SyntaxKind::ContinueKW => "continue", + SyntaxKind::ContinueStatement => "continue statement", SyntaxKind::DestructureAssignment => "destructuring assignment", SyntaxKind::Destructuring => "destructuring", SyntaxKind::DocComment => "doc comment", @@ -162,15 +169,15 @@ impl SyntaxKind { SyntaxKind::DotsEq => "..=", SyntaxKind::DoubleQuote => "\"", SyntaxKind::Ellipsis => "...", - SyntaxKind::Else => "else", + SyntaxKind::ElseKW => "else", SyntaxKind::End => "end of file", - SyntaxKind::Enum => "enum", + SyntaxKind::EnumKW => "enum", SyntaxKind::Eq => "=", SyntaxKind::EqEq => "==", SyntaxKind::Error => "error", SyntaxKind::FieldAccess => "field access", SyntaxKind::Float => "float literal", - SyntaxKind::For => "for", + SyntaxKind::ForKW => "for", SyntaxKind::ForLoop => "for loop", SyntaxKind::FuncCall => "function call", SyntaxKind::Gt => ">", @@ -180,17 +187,19 @@ impl SyntaxKind { SyntaxKind::Hat => "^", SyntaxKind::HatEq => "^=", SyntaxKind::Ident => "identifier", - SyntaxKind::If => "if", - SyntaxKind::Import => "import", - SyntaxKind::In => "in", + SyntaxKind::IfKW => "if", + SyntaxKind::ImportKW => "import", + SyntaxKind::InKW => "in", SyntaxKind::IndexAccess => "index access", SyntaxKind::Int => "integer literal", + SyntaxKind::IsKW => "is", + SyntaxKind::IsExpression => "is expression", SyntaxKind::LeftBrace => "{", SyntaxKind::LeftBracket => "[", SyntaxKind::LeftParen => "(", - SyntaxKind::Let => "let", + SyntaxKind::LetKW => "let", SyntaxKind::LetBinding => "let binding", - SyntaxKind::Loop => "loop", + SyntaxKind::LoopKW => "loop", SyntaxKind::Lt => "<", SyntaxKind::LtEq => "<=", SyntaxKind::LtLt => "<<", @@ -200,10 +209,10 @@ impl SyntaxKind { SyntaxKind::MinusEq => "-=", SyntaxKind::ModuleImport => "module import", SyntaxKind::ImportItem => "import item", - SyntaxKind::Mut => "mut", + SyntaxKind::MutKW => "mut", SyntaxKind::Named => "named binding", SyntaxKind::NewLine => "newline", - SyntaxKind::Param => "parameter", + SyntaxKind::Param => "parameter", SyntaxKind::Params => "parameter list", SyntaxKind::Parenthesized => "parenthesized expression", SyntaxKind::PathAccess => "path", @@ -213,10 +222,10 @@ impl SyntaxKind { SyntaxKind::PipePipe => "||", SyntaxKind::Plus => "+", SyntaxKind::PlusEq => "+=", - SyntaxKind::Pub => "pub", + SyntaxKind::PubKW => "pub", SyntaxKind::Range => "range", - SyntaxKind::Ref => "ref", - SyntaxKind::Return => "return", + SyntaxKind::RefKW => "ref", + SyntaxKind::ReturnKW => "return", SyntaxKind::ReturnStatement => "return statement", SyntaxKind::RightBrace => "}", SyntaxKind::RightBracket => "]", @@ -234,8 +243,12 @@ impl SyntaxKind { SyntaxKind::Unary => "unary expression", SyntaxKind::Underscore => "_", SyntaxKind::Unit => "()", - SyntaxKind::While => "while", + SyntaxKind::WhileKW => "while", SyntaxKind::WhileLoop => "while loop", + SyntaxKind::MatchKW => "match", + SyntaxKind::MatchArm => "match arm", + SyntaxKind::MatchExpression => "match expression", + SyntaxKind::TypedPattern => "type binding pattern", } } @@ -274,24 +287,25 @@ impl SyntaxKind { pub(crate) fn is_keyword(&self) -> bool { matches!( self, - Self::As - | Self::Break - | Self::Continue - | Self::Else - | Self::Enum - | Self::For - | Self::If - | Self::Import - | Self::In - | Self::Let + Self::AsKW + | Self::BreakKW + | Self::ContinueKW + | Self::ElseKW + | Self::EnumKW + | Self::ForKW + | Self::IfKW + | Self::ImportKW + | Self::InKW + | Self::LetKW | Self::LetBinding - | Self::Loop - | Self::Mut - | Self::Ref - | Self::Return + | Self::LoopKW + | Self::MutKW + | Self::RefKW + | Self::ReturnKW | Self::Unit - | Self::While - | Self::Pub + | Self::WhileKW + | Self::PubKW + | Self::MatchKW ) } } diff --git a/compose-syntax/src/lexer.rs b/compose-syntax/src/lexer.rs index c488de96..f55ad3ab 100644 --- a/compose-syntax/src/lexer.rs +++ b/compose-syntax/src/lexer.rs @@ -307,24 +307,26 @@ fn is_space(c: char) -> bool { fn keyword(ident: &str) -> Option { Some(match ident { - "as" => SyntaxKind::As, - "break" => SyntaxKind::Break, - "continue" => SyntaxKind::Continue, - "else" => SyntaxKind::Else, - "enum" => SyntaxKind::Enum, + "as" => SyntaxKind::AsKW, + "break" => SyntaxKind::BreakKW, + "continue" => SyntaxKind::ContinueKW, + "else" => SyntaxKind::ElseKW, + "enum" => SyntaxKind::EnumKW, "false" => SyntaxKind::Bool, - "for" => SyntaxKind::For, - "if" => SyntaxKind::If, - "import" => SyntaxKind::Import, - "in" => SyntaxKind::In, - "let" => SyntaxKind::Let, - "loop" => SyntaxKind::Loop, - "mut" => SyntaxKind::Mut, - "ref" => SyntaxKind::Ref, - "pub" => SyntaxKind::Pub, - "return" => SyntaxKind::Return, + "for" => SyntaxKind::ForKW, + "if" => SyntaxKind::IfKW, + "import" => SyntaxKind::ImportKW, + "in" => SyntaxKind::InKW, + "let" => SyntaxKind::LetKW, + "loop" => SyntaxKind::LoopKW, + "mut" => SyntaxKind::MutKW, + "ref" => SyntaxKind::RefKW, + "pub" => SyntaxKind::PubKW, + "return" => SyntaxKind::ReturnKW, "true" => SyntaxKind::Bool, - "while" => SyntaxKind::While, + "while" => SyntaxKind::WhileKW, + "match" => SyntaxKind::MatchKW, + "is" => SyntaxKind::IsKW, _ => return None, }) } diff --git a/compose-syntax/src/node.rs b/compose-syntax/src/node.rs index 6cef5a8d..58c28d7b 100644 --- a/compose-syntax/src/node.rs +++ b/compose-syntax/src/node.rs @@ -5,71 +5,42 @@ use crate::set::SyntaxSet; use crate::span::Span; use compose_error_codes::ErrorCode; use compose_utils::trace_log; -use ecow::{eco_vec, EcoString, EcoVec}; +use ecow::{EcoString, EcoVec, eco_vec}; use std::fmt::{Debug, Formatter}; use std::ops::Range; use std::sync::Arc; +/// A node in the untyped syntax tree. +/// +/// A node can either be +/// - a leaf node representing a single token, +/// - an inner node containing one or more children +/// - or, an error node containing a syntax error. #[derive(Clone, Eq, PartialEq, Hash)] pub struct SyntaxNode(Repr); +// Constructors impl SyntaxNode { - pub(crate) fn erroneous(&self) -> bool { - match &self.0 { - Repr::Leaf(_) => false, - Repr::Inner(i) => i.erroneous, - Repr::Error(e) => e.error.severity == SyntaxErrorSeverity::Error, - } - } - pub fn errors(&self) -> Vec { - if !self.erroneous() { - return vec![]; - } - - if let Repr::Error(node) = &self.0 { - vec![node.error.clone()] - } else { - self.children() - .filter(|node| node.erroneous()) - .flat_map(|node| node.errors()) - .collect() - } - } - - pub fn warnings(&self) -> Vec { - match &self.0 { - Repr::Error(node) => { - if node.error.severity == SyntaxErrorSeverity::Warning { - vec![node.error.clone()] - } else { - vec![] - } - } - Repr::Inner(i) => i.children.iter().flat_map(|node| node.warnings()).collect(), - Repr::Leaf(_) => vec![], - } + pub(crate) fn error(error: SyntaxError, text: impl Into) -> Self { + Self(Repr::Error(Arc::new(ErrorNode::new(error, text)))) } - - pub fn error_mut(&mut self) -> Option<&mut SyntaxError> { - match &mut self.0 { - Repr::Error(e) => Some(&mut Arc::make_mut(e).error), - _ => None, - } + pub(crate) fn leaf(kind: SyntaxKind, text: impl Into, span: Span) -> Self { + Self(Repr::Leaf(LeafNode::new(kind, text, span))) } - pub(crate) fn descendents(&self) -> usize { - match &self.0 { - Repr::Leaf(_) | Repr::Error(_) => 1, - Repr::Inner(i) => i.descendents, - } + pub(crate) fn inner(kind: SyntaxKind, children: Vec) -> Self { + Self(Repr::Inner(Arc::new(InnerNode::new(kind, children)))) } - pub(crate) fn len(&self) -> usize { - match &self.0 { - Repr::Leaf(l) => l.len(), - Repr::Inner(i) => i.len, - Repr::Error(e) => e.len(), + pub(crate) const fn placeholder(kind: SyntaxKind) -> Self { + if matches!(kind, SyntaxKind::Error) { + panic!("cannot create error placeholder"); } + Self(Repr::Leaf(LeafNode { + kind, + text: EcoString::new(), + span: Span::detached(), + })) } } @@ -102,28 +73,6 @@ impl SyntaxNode { } impl SyntaxNode { - pub(crate) fn error(error: SyntaxError, text: impl Into) -> Self { - Self(Repr::Error(Arc::new(ErrorNode::new(error, text)))) - } - pub(crate) fn leaf(kind: SyntaxKind, text: impl Into, span: Span) -> Self { - Self(Repr::Leaf(LeafNode::new(kind, text, span))) - } - - pub(crate) fn inner(kind: SyntaxKind, children: Vec) -> Self { - Self(Repr::Inner(Arc::new(InnerNode::new(kind, children)))) - } - - pub(crate) const fn placeholder(kind: SyntaxKind) -> Self { - if matches!(kind, SyntaxKind::Error) { - panic!("cannot create error placeholder"); - } - Self(Repr::Leaf(LeafNode { - kind, - text: EcoString::new(), - span: Span::detached(), - })) - } - pub fn kind(&self) -> SyntaxKind { match &self.0 { Repr::Leaf(l) => l.kind, @@ -216,6 +165,67 @@ impl SyntaxNode { } } +impl SyntaxNode { + pub(crate) fn erroneous(&self) -> bool { + match &self.0 { + Repr::Leaf(_) => false, + Repr::Inner(i) => i.erroneous, + Repr::Error(e) => e.error.severity == SyntaxErrorSeverity::Error, + } + } + pub fn errors(&self) -> Vec { + if !self.erroneous() { + return vec![]; + } + + if let Repr::Error(node) = &self.0 { + vec![node.error.clone()] + } else { + self.children() + .filter(|node| node.erroneous()) + .flat_map(|node| node.errors()) + .collect() + } + } + + pub fn warnings(&self) -> Vec { + match &self.0 { + Repr::Error(node) => { + if node.error.severity == SyntaxErrorSeverity::Warning { + vec![node.error.clone()] + } else { + vec![] + } + } + Repr::Inner(i) => i.children.iter().flat_map(|node| node.warnings()).collect(), + Repr::Leaf(_) => vec![], + } + } + + pub fn error_mut(&mut self) -> Option<&mut SyntaxError> { + match &mut self.0 { + Repr::Error(e) => Some(&mut Arc::make_mut(e).error), + _ => None, + } + } + + pub(crate) fn descendents(&self) -> usize { + match &self.0 { + Repr::Leaf(_) | Repr::Error(_) => 1, + Repr::Inner(i) => i.descendents, + } + } + + pub(crate) fn len(&self) -> usize { + match &self.0 { + Repr::Leaf(l) => l.len(), + Repr::Inner(i) => i.len, + Repr::Error(e) => e.len(), + } + } +} + + impl Default for SyntaxNode { fn default() -> Self { Self::leaf(SyntaxKind::End, EcoString::new(), Span::detached()) diff --git a/compose-syntax/src/parser/control_flow.rs b/compose-syntax/src/parser/control_flow.rs index 947e6031..7f87a5df 100644 --- a/compose-syntax/src/parser/control_flow.rs +++ b/compose-syntax/src/parser/control_flow.rs @@ -1,6 +1,6 @@ use crate::kind::SyntaxKind; use crate::parser::expressions::code_expression; -use crate::parser::patterns::pattern; +use crate::parser::pattern::pattern; use crate::parser::statements::code; use crate::parser::Parser; use crate::set::syntax_set; @@ -12,7 +12,7 @@ use std::collections::HashSet; pub(crate) fn conditional(p: &mut Parser) { trace_fn!("parse_conditional"); let m = p.marker(); - p.assert(SyntaxKind::If); + p.assert(SyntaxKind::IfKW); condition(p); @@ -21,11 +21,11 @@ pub(crate) fn conditional(p: &mut Parser) { return; } - while p.at(SyntaxKind::Else) { + while p.at(SyntaxKind::ElseKW) { trace_fn!("parse_else_maybe_if"); let else_marker = p.marker(); - p.assert(SyntaxKind::Else); - if p.eat_if(SyntaxKind::If) { + p.assert(SyntaxKind::ElseKW); + if p.eat_if(SyntaxKind::IfKW) { trace_fn!("parse_else_if"); condition(p); @@ -51,7 +51,7 @@ pub(crate) fn conditional(p: &mut Parser) { pub fn while_loop(p: &mut Parser) { trace_fn!("parse_while_loop"); let m = p.marker(); - p.assert(SyntaxKind::While); + p.assert(SyntaxKind::WhileKW); condition(p); @@ -62,9 +62,7 @@ pub fn while_loop(p: &mut Parser) { pub fn for_loop(p: &mut Parser) { trace_fn!("parse_for_loop"); let m = p.marker(); - p.assert(SyntaxKind::For); - - let left_paren_marker = p.marker(); + p.assert(SyntaxKind::ForKW); let wrapped = p.eat_if(SyntaxKind::LeftParen); @@ -73,15 +71,15 @@ pub fn for_loop(p: &mut Parser) { .with_label_message("Expected an opening `(` after the `for` keyword"); } - pattern(p, true, &mut HashSet::new(), None); + pattern(p, true, false, &mut HashSet::new()); - p.expect(SyntaxKind::In); + p.expect(SyntaxKind::InKW); // parse the iterable code_expression(p); if wrapped { - p.expect_closing_delimiter(left_paren_marker, SyntaxKind::RightParen); + p.expect_closing_delimiter(m, SyntaxKind::RightParen); } parse_control_flow_block(p, ControlFlow::For); @@ -132,10 +130,10 @@ fn parse_control_flow_block(p: &mut Parser, flow: ControlFlow) -> bool { // the left brace isnt missing, something very unexpected is happening // recover until the end of the entire statement if possible // then give up - p.recover_until(syntax_set!(RightBrace, End, Else)); + p.recover_until(syntax_set!(RightBrace, End, ElseKW)); p.eat_if(SyntaxKind::RightBrace); // eat elses as well - while p.eat_if(SyntaxKind::Else) { + while p.eat_if(SyntaxKind::ElseKW) { p.recover_until(syntax_set!(RightBrace, End)); } @@ -196,7 +194,7 @@ mod tests { } "#, WhileLoop [ - While("while") + WhileKW("while") Condition [ LeftParen("(") Bool("true") @@ -223,7 +221,7 @@ mod tests { } "#, Conditional [ - If("if") + IfKW("if") Condition [ LeftParen("(") Bool("true") @@ -241,7 +239,7 @@ mod tests { do_thing(); "#, Conditional [ - If("if") + IfKW("if") Condition [ ... ] CodeBlock [ Error(E0005_IF_EXPRESSION_BODIES_REQUIRE_BRACES) @@ -266,7 +264,7 @@ mod tests { } "#, Conditional [ - If("if") + IfKW("if") Condition [ LeftParen("(") Ident("cond1") @@ -274,8 +272,8 @@ mod tests { ] CodeBlock [...] ConditionalAlternate [ - Else("else") - If("if") + ElseKW("else") + IfKW("if") Condition [ LeftParen("(") Ident("cond2") @@ -284,10 +282,11 @@ mod tests { CodeBlock [...] ] ConditionalElse [ - Else("else") + ElseKW("else") CodeBlock [...] ] ] ); } } + diff --git a/compose-syntax/src/parser/expressions.rs b/compose-syntax/src/parser/expressions.rs index 5da0912c..c5dbb1cd 100644 --- a/compose-syntax/src/parser/expressions.rs +++ b/compose-syntax/src/parser/expressions.rs @@ -1,17 +1,19 @@ use crate::ast::{AssignOp, BinOp}; use crate::kind::SyntaxKind; use crate::parser::control_flow::{conditional, for_loop, while_loop}; -use crate::parser::funcs::block_or_lambda; -use crate::parser::{funcs, statements, ExprContext}; +use crate::parser::funcs::lambda; +use crate::parser::pattern::{match_expr, pattern}; +use crate::parser::{ExprContext, funcs, statements}; use crate::parser::{Marker, Parser}; use crate::precedence::{Precedence, PrecedenceTrait}; -use crate::set::{syntax_set, SyntaxSet, UNARY_OP}; -use crate::{ast, set, Label}; +use crate::set::{SyntaxSet, UNARY_OP, syntax_set}; +use crate::{Label, ast, set}; use compose_error_codes::{ E0001_UNCLOSED_DELIMITER, E0002_INVALID_ASSIGNMENT, E0008_EXPECTED_EXPRESSION, }; use compose_utils::trace_fn; use ecow::eco_format; +use std::collections::HashSet; pub fn code_expression(p: &mut Parser) { code_expr_prec(p, ExprContext::Expr, Precedence::Lowest); @@ -32,7 +34,9 @@ pub fn code_expr_prec(p: &mut Parser, ctx: ExprContext, min_prec: Precedence) { loop { trace_fn!("parse_code_expr_prec loop", "{:?}", p.current()); - if p.at_set(syntax_set!(LeftParen, LeftBrace)) { + + // Simple function call `foo(args)` + if p.at(SyntaxKind::LeftParen) { if Precedence::Call < min_prec { break; } @@ -41,12 +45,39 @@ pub fn code_expr_prec(p: &mut Parser, ctx: ExprContext, min_prec: Precedence) { continue; } + // maybe a trailing lambda? `foo { args => ... }` + if p.at(SyntaxKind::LeftBrace) { + let mut scanner = p.scanner(); + scanner + .next() + .expect("we know the next token is an opening brace"); // enter opening `{` + if scanner + .level_contains_kind(SyntaxKind::Arrow) + .unwrap_or(false) + { + if Precedence::Call < min_prec { + break; + } + funcs::args(p); + p.wrap(m, SyntaxKind::FuncCall); + continue; + } + } + let at_field_or_index = p.at(SyntaxKind::Dot) || p.at(SyntaxKind::LeftBracket); if ctx.is_atomic() && !at_field_or_index { break; } + if p.eat_if(SyntaxKind::IsKW) { + if Precedence::Is < min_prec { + break; + } + pattern(p, false, false, &mut HashSet::new()); + p.wrap(m, SyntaxKind::IsExpression) + } + // handle path access `a::b::c` if p.eat_if(SyntaxKind::ColonColon) { if Precedence::Path < min_prec { @@ -160,13 +191,16 @@ fn primary_expr(p: &mut Parser, ctx: ExprContext) { let m = p.marker(); match p.current() { // `_ = something` + SyntaxKind::MatchKW => match_expr(p), SyntaxKind::Underscore if !ctx.is_atomic() && p.peek() == SyntaxKind::Eq => { p.assert(SyntaxKind::Underscore); p.assert(SyntaxKind::Eq); code_expression(p); p.wrap(m, SyntaxKind::DestructureAssignment); } - SyntaxKind::LeftBrace => { block_or_lambda(p); }, + SyntaxKind::LeftBrace => { + expr_with_braces(p); + } SyntaxKind::DotsEq | SyntaxKind::Dots => { p.eat(); // for ..= an expression on the rhs is required @@ -175,20 +209,25 @@ fn primary_expr(p: &mut Parser, ctx: ExprContext) { } p.wrap(m, SyntaxKind::Range); } - SyntaxKind::Hash if p.peek() == SyntaxKind::LeftBrace => map(p), SyntaxKind::LeftBracket => array(p), - SyntaxKind::If => conditional(p), - SyntaxKind::While => while_loop(p), - SyntaxKind::For => for_loop(p), + SyntaxKind::IfKW => conditional(p), + SyntaxKind::WhileKW => while_loop(p), + SyntaxKind::ForKW => for_loop(p), SyntaxKind::LeftParen if p.peek_at(SyntaxKind::RightParen) => { p.assert(SyntaxKind::LeftParen); p.assert(SyntaxKind::RightParen); p.wrap(m, SyntaxKind::Unit) } - SyntaxKind::LeftParen => { parenthesized(p); }, - SyntaxKind::Import => import(p), + SyntaxKind::LeftParen => { + parenthesized(p); + } + SyntaxKind::ImportKW => import(p), // Already fully handled in the lexer - SyntaxKind::Int | SyntaxKind::Float | SyntaxKind::Bool | SyntaxKind::Str | SyntaxKind::Ident => p.eat(), + SyntaxKind::Int + | SyntaxKind::Float + | SyntaxKind::Bool + | SyntaxKind::Str + | SyntaxKind::Ident => p.eat(), _ => err_expected_expression( p, Some(syntax_set!( @@ -202,19 +241,103 @@ fn primary_expr(p: &mut Parser, ctx: ExprContext) { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ExprWithBraces { + Block, + Lambda, + MapLiteral, + DestructuringAssignment, +} + +/// Parses an expression surrounded by braces. +/// +/// Block syntax: { $(stmt)* } +/// Lambda syntax: { $(|capture_list|)? $(param),* $(,)? => $(stmt)* } +/// Map Literal Syntax: { $(ident,)* $(ident: expr),* } +/// note: a map literal with just a single ident must contain a `,` to be parsed as a map +/// Map destructuring reassignment syntax { $(ident $(: ident)?),* } = expr +/// +/// Disambiguation rules (in order): +/// - `=>` at top level → lambda +/// - `}` followed by `=` → destructuring assignment +/// - top-level `:` → map literal +/// - otherwise → block +pub fn expr_with_braces(p: &mut Parser) { + trace_fn!("expr_with_braces"); + debug_assert!(p.at(SyntaxKind::LeftBrace)); + + let m = p.marker(); + + let mut scanner = p.scanner(); + scanner + .next() + .expect("we know the next token is an opening brace"); // enter opening `{` + let starting_delim_depth = scanner.delim_depth(); + + let mut contains_colon = false; + + let expr_kind = loop { + let delim_depth = scanner.delim_depth(); + let node = match scanner.next() { + Ok(Some(node)) => node, + Ok(None) if contains_colon => break ExprWithBraces::MapLiteral, + Ok(None) => break ExprWithBraces::Block, + Err(_) => break ExprWithBraces::Block, + }; + if delim_depth > starting_delim_depth { + // within a nested delim, no need to check + continue; + } + if delim_depth < starting_delim_depth { + // left the current delim somehow without finding a closing delim + p.recover_until_node(&node); + p.expect_closing_delimiter(m, SyntaxKind::RightBrace); + break ExprWithBraces::Block; + } + + match node.kind() { + SyntaxKind::Arrow => break ExprWithBraces::Lambda, + SyntaxKind::Colon => { + contains_colon = true; + continue; + } + // { ... } = expr is unambiguously a destructuring assignment because lambdas and blocks are + // not allowed as the lhs of an assignment + SyntaxKind::RightBrace if scanner.at(SyntaxKind::Eq) => { + break ExprWithBraces::DestructuringAssignment; + } + SyntaxKind::RightBrace if contains_colon => break ExprWithBraces::MapLiteral, + SyntaxKind::RightBrace => break ExprWithBraces::Block, + _ => continue, + }; + }; + + match expr_kind { + ExprWithBraces::Block => block(p), + ExprWithBraces::MapLiteral => map(p), + ExprWithBraces::Lambda => lambda(p), + ExprWithBraces::DestructuringAssignment => destructure_map_assignment(p), + }; +} + +fn destructure_map_assignment(_p: &mut Parser) { + trace_fn!("destructure_map_assignment"); + unimplemented!("destructure map assignment") +} + fn import(p: &mut Parser) { let m = p.marker(); - p.assert(SyntaxKind::Import); + p.assert(SyntaxKind::ImportKW); if !p.at(SyntaxKind::Str) { p.insert_error_here("expected a string literal after `import`"); } code_expr_prec(p, ExprContext::AtomicExpr, Precedence::Lowest); - if p.eat_if(SyntaxKind::As) { - if !p.eat_if(SyntaxKind::Ident) { - p.insert_error_here("expected an identifier after `as`"); - } + if p.eat_if(SyntaxKind::AsKW) { + if !p.eat_if(SyntaxKind::Ident) { + p.insert_error_here("expected an identifier after `as`"); + } } if p.at(SyntaxKind::LeftBrace) { @@ -240,7 +363,7 @@ fn import_item(p: &mut Parser) { code_expression(p); - if p.eat_if(SyntaxKind::As) { + if p.eat_if(SyntaxKind::AsKW) { if !p.eat_if(SyntaxKind::Ident) { p.insert_error_here("expected an identifier after `as` in import item"); } @@ -250,27 +373,42 @@ fn import_item(p: &mut Parser) { } fn map(p: &mut Parser) { + trace_fn!("parse_map"); let m = p.marker(); - - p.assert(SyntaxKind::Hash); p.assert(SyntaxKind::LeftBrace); + // Empty map literals are denoted by `{ : }` to avoid ambiguity with empty blocks + if p.eat_if(SyntaxKind::Colon) { + p.eat_if(SyntaxKind::Comma); // { :, } is also allowed + + if !p.eat_if(SyntaxKind::RightBrace) { + p.insert_error_here("expected a `}` after the empty map literal") + .with_label_message("insert a `}` here"); + } + + p.wrap(m, SyntaxKind::MapLiteral); + return; + } + while !p.current().is_terminator() { let entry_marker = p.marker(); let key_type = map_key(p).unwrap_or(MapKeyType::Identifier); // fallback to identifier as it is the most lenient if !p.eat_if(SyntaxKind::Colon) { + // short-hand notation does not require a colon if key_type != MapKeyType::Identifier { p.insert_error_here("expected a colon after the map key") .with_label_message("insert a `:` here") .with_hint("if you meant to use a dynamic expression as a key, wrap it in square brackets") - .with_hint("if you meant to use shorthand syntax for a map entry, only identifiers can be used as keys: `#{ x, y, z }` is equivalent to `#{ x: x, y: y, z: z }`"); + .with_hint("if you meant to use shorthand syntax for a map entry, only identifiers can be used as keys: `{ x, y, z }` is equivalent to `{ x: x, y: y, z: z }`"); } - - // short-hand notation } else { - code_expression(p); + if !p.at_set(set::EXPR) && key_type == MapKeyType::Identifier { + // allow a trailing colon after identifier + } else { + code_expression(p); + } } p.wrap(entry_marker, SyntaxKind::MapEntry); @@ -341,7 +479,7 @@ fn map_key(p: &mut Parser) -> Result { _ => { p.insert_error_here("expected a map key") .with_label_message("expected a key here") - .with_note(r#"map keys must be `identifiers`, `"strings"` or dynamic expressions in `[brackets]` (e.g. `[expr]: value`)"#); + .with_note(r#"map keys must be `identifiers`, `"strings"` or dynamic expressions in `[brackets]` (e.g. `[expr]: value`)"#); p.eat(); Err(()) @@ -431,14 +569,17 @@ pub(in crate::parser) fn err_unclosed_delim( closing_delim_label = None; } else { closing_delim_label = Some(eco_format!( - "expected closing `{}`, but found `{}` instead", + "expected closing `{}`, but found `{}`", expected_closing.descriptive_name(), p.current_text() )); } + let closing_span = p.current_span(); - p[open_marker] - .convert_to_error("unclosed delimiter") + let open_span = p[open_marker].span(); + + let err = p + .insert_error_at(open_span, "unclosed delimiter") .with_code(&E0001_UNCLOSED_DELIMITER) // label on the opening delimiter .with_label_message(eco_format!( @@ -455,10 +596,7 @@ pub(in crate::parser) fn err_unclosed_delim( )); if let Some(closing_delim_label) = closing_delim_label { - p.last_err() - .expect("was just inserted") - // label on (or near) the closing delimiter - .with_label(Label::primary(closing_span, closing_delim_label)); + err.with_label(Label::primary(closing_span, closing_delim_label)); } } @@ -473,7 +611,7 @@ mod tests { assert_parse_tree!( "import \"foo.bar\"", ModuleImport [ - Import("import") + ImportKW("import") Str("\"foo.bar\"") ] ); @@ -484,9 +622,9 @@ mod tests { assert_parse_tree!( "import \"foo.bar\" as bar", ModuleImport [ - Import("import") + ImportKW("import") Str("\"foo.bar\"") - As("as") + AsKW("as") Ident("bar") ] ) @@ -497,9 +635,9 @@ mod tests { assert_parse_tree!( "import \"foo.bar\" as bar { x, y, z }", ModuleImport [ - Import("import") + ImportKW("import") Str("\"foo.bar\"") - As("as") + AsKW("as") Ident("bar") LeftBrace("{") ImportItem [ @@ -570,7 +708,7 @@ mod tests { ) "#, CodeBlock [ - Error(E0001_UNCLOSED_DELIMITER) + LeftBrace("{") FuncCall [ Ident("println") Args [ LeftParen("(") Str("\"hello\"") RightParen(")") ] @@ -579,6 +717,7 @@ mod tests { Ident("do_other_stuff") Args [ LeftParen("(") RightParen(")") ] ] + Error(E0001_UNCLOSED_DELIMITER) RightParen(")") ] ); @@ -1026,9 +1165,8 @@ mod tests { #[test] fn parse_map_literal() { assert_parse_tree!( - "#{a: 1, b: 2}", + "{a: 1, b: 2}", MapLiteral [ - Hash("#") LeftBrace("{") MapEntry [ Ident("a") @@ -1049,9 +1187,8 @@ mod tests { #[test] fn parse_map_literal_with_trailing_comma() { assert_parse_tree!( - "#{a: 1, b: 2,}", + "{a: 1, b: 2,}", MapLiteral [ - Hash("#") LeftBrace("{") MapEntry [ ... ] Comma(",") @@ -1066,9 +1203,8 @@ mod tests { #[test] fn parse_map_literal_string_keys() { assert_parse_tree!( - r#"#{"a": 1, "b": 2,}"#, + r#"{"a": 1, "b": 2,}"#, MapLiteral [ - Hash("#") LeftBrace("{") MapEntry [ Str("\"a\"") @@ -1090,12 +1226,12 @@ mod tests { #[test] fn parse_map_literal_shorthand() { assert_parse_tree!( - "#{a, b}", + "{a:, b}", MapLiteral [ - Hash("#") LeftBrace("{") MapEntry [ Ident("a") + Colon(":") ] Comma(",") MapEntry [ @@ -1105,4 +1241,76 @@ mod tests { ] ) } + + #[test] + fn parse_expr_with_braces() { + assert_parse_tree!( + "{ x: { x: }, y: { y: { 2 }} => { println(x, { y }) } }()", + FuncCall [ + Lambda [ + LeftBrace("{") + Params [ + Param [ + Named [ + Ident("x") + Colon(":") + MapLiteral [ + LeftBrace("{") + MapEntry [ + Ident("x") + Colon(":") + ] + RightBrace("}") + ] + ] + ] + Comma(",") + Param [ + Named [ + Ident("y") + Colon(":") + MapLiteral [ + LeftBrace("{") + MapEntry [ + Ident ("y") + Colon (":") + CodeBlock [ + LeftBrace ("{") + Int ("2") + RightBrace ("}") + ] + ] + RightBrace ("}") + ] + ] + ] + ] + Arrow ("=>") + CodeBlock [ + LeftBrace ("{") + FuncCall [ + Ident ("println") + Args [ + LeftParen ("(") + Ident ("x") + Comma (",") + CodeBlock [ + LeftBrace ("{") + Ident ("y") + RightBrace ("}") + ] + RightParen (")") + ] + ] + RightBrace ("}") + ] + RightBrace ("}") + ] + Args [ + LeftParen("(") + RightParen(")") + ] + ] + ) + } } diff --git a/compose-syntax/src/parser/funcs.rs b/compose-syntax/src/parser/funcs.rs index 13a1acd1..fd0c9363 100644 --- a/compose-syntax/src/parser/funcs.rs +++ b/compose-syntax/src/parser/funcs.rs @@ -1,12 +1,11 @@ use crate::kind::SyntaxKind; -use crate::parser::expressions::block; use crate::parser::statements::code; -use crate::parser::{expressions, patterns}; use crate::parser::{ExprContext, Parser}; +use crate::parser::{expressions, pattern}; use crate::precedence::Precedence; use crate::scanner::Delimiter; use crate::set; -use crate::set::{syntax_set, ARG_RECOVER}; +use crate::set::{ARG_RECOVER, syntax_set}; use compose_error_codes::E0009_ARGS_MISSING_COMMAS; use compose_utils::trace_fn; use std::collections::HashSet; @@ -37,19 +36,20 @@ pub fn args(p: &mut Parser) { if p.at(SyntaxKind::LeftBrace) { // trailing lambda - lambda(p); + let mut scanner = p.scanner(); + scanner.next().expect("we know there is an opening `{`"); + if scanner + .level_contains_kind(SyntaxKind::Arrow) + .unwrap_or(false) + { + lambda(p); + } } p.wrap(m, SyntaxKind::Args); } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum BlockOrLambda { - Block, - Lambda, -} - -fn lambda(p: &mut Parser) { +pub(crate) fn lambda(p: &mut Parser) { trace_fn!("parse_lambda"); let m = p.marker(); @@ -99,6 +99,10 @@ fn lambda(p: &mut Parser) { p.expect(SyntaxKind::Arrow); + if p.at(SyntaxKind::RightBrace) { + p.insert_error_here("lambda body cannot be empty"); + } + code(p, syntax_set!(RightBrace)); p.expect_closing_delimiter(m, SyntaxKind::RightBrace); @@ -106,38 +110,12 @@ fn lambda(p: &mut Parser) { p.wrap(m, SyntaxKind::Lambda) } -/// Parses a block or lambda -/// -/// Block syntax: { $(stmt)* } -/// Lambda syntax { $(|capture_list|)? $(param),* $(,)? => $(stmt)* } -pub fn block_or_lambda(p: &mut Parser) -> Option { - trace_fn!("parse_block_or_lambda"); - let mut scanner = p.scanner(); - scanner.next(); // skip the opening `{` - scanner.enter(Delimiter::LeftBrace); - - let contains_arrow = { - trace_fn!("parse_block_or_lambda_contains_arrow"); - scanner - .level_contains_kind(SyntaxKind::Arrow) - .unwrap_or(false) - }; - - if contains_arrow { - lambda(p); - Some(BlockOrLambda::Lambda) - } else { - block(p); - Some(BlockOrLambda::Block) - } -} - fn arg(p: &mut Parser) { let m_mods = p.marker(); let mut had_modifiers = false; - had_modifiers |= p.eat_if(SyntaxKind::Ref); - had_modifiers |= p.eat_if(SyntaxKind::Mut); + had_modifiers |= p.eat_if(SyntaxKind::RefKW); + had_modifiers |= p.eat_if(SyntaxKind::MutKW); let m = p.marker(); expressions::code_expression(p); @@ -149,7 +127,7 @@ fn arg(p: &mut Parser) { if had_modifiers { p[m_mods].convert_to_error("argument modifiers like `ref` and `mut` go before the expression in named arguments") - .with_label_message("help: move the modifiers to the right of the `=`"); + .with_label_message("help: move the modifiers to the right of the `=`"); } expressions::code_expression(p); @@ -187,8 +165,8 @@ fn captures(p: &mut Parser) -> bool { fn capture(p: &mut Parser) { let m = p.marker(); - p.eat_if(SyntaxKind::Ref); - p.eat_if(SyntaxKind::Mut); + p.eat_if(SyntaxKind::RefKW); + p.eat_if(SyntaxKind::MutKW); let ident_m = p.marker(); // Parse a full expression, even though we only care about an identifier. @@ -247,13 +225,13 @@ fn param<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) { trace_fn!("parse_param"); let m = p.marker(); - p.eat_if(SyntaxKind::Ref); - p.eat_if(SyntaxKind::Mut); + p.eat_if(SyntaxKind::RefKW); + p.eat_if(SyntaxKind::MutKW); let was_at_pat = p.at_set(set::PATTERN); let pat_m = p.marker(); - patterns::pattern(p, false, seen, Some("parameter")); + pattern::pattern(p, false, false, seen); // Parse named params like `a: 1` if p.eat_if(SyntaxKind::Colon) { @@ -321,13 +299,17 @@ mod tests { #[test] fn test_parse_closure_discard_param() { assert_parse_tree!( - "{ _ => }", + "{ _ => () }", Lambda [ LeftBrace("{") Params [ Param [ Underscore("_") ] ] Arrow("=>") + Unit [ + LeftParen("(") + RightParen(")") + ] RightBrace("}") ] ); @@ -336,7 +318,7 @@ mod tests { #[test] fn test_parse_closure_named_param() { assert_parse_tree!( - "{a: b => }", + "{a: b => () }", Lambda [ LeftBrace("{") Params [ @@ -345,6 +327,10 @@ mod tests { ] ] Arrow("=>") + Unit [ + LeftParen("(") + RightParen(")") + ] RightBrace("}") ] ); @@ -425,7 +411,7 @@ mod tests { FuncCall [ Ident("f") Args [ - Error(E0001_UNCLOSED_DELIMITER) + LeftParen("(") Ident("a") Comma(",") Ident("b") @@ -437,6 +423,7 @@ mod tests { Plus("+") Int("2") ] + Error(E0001_UNCLOSED_DELIMITER) ] ] ); @@ -450,7 +437,7 @@ mod tests { LeftBrace("{") Params [ Param [ - Ref("ref") + RefKW("ref") Ident("a") ] ] @@ -465,7 +452,7 @@ mod tests { LeftBrace("{") Params [ Param [ - Mut("mut") + MutKW("mut") Ident("a") ] ] @@ -479,8 +466,8 @@ mod tests { LeftBrace("{") Params [ Param [ - Ref("ref") - Mut("mut") + RefKW("ref") + MutKW("mut") Ident("a") ] ] @@ -498,11 +485,11 @@ mod tests { Pipe("|") Capture [ Ident("a") ] Comma(",") - Capture [ Ref("ref") Ident("b") ] + Capture [ RefKW("ref") Ident("b") ] Comma(",") - Capture [ Mut("mut") Ident("c") ] + Capture [ MutKW("mut") Ident("c") ] Comma(",") - Capture [ Ref("ref") Mut("mut") Ident("d") ] + Capture [ RefKW("ref") MutKW("mut") Ident("d") ] Pipe("|") ] Params [ diff --git a/compose-syntax/src/parser/mod.rs b/compose-syntax/src/parser/mod.rs index d412540c..715f41d6 100644 --- a/compose-syntax/src/parser/mod.rs +++ b/compose-syntax/src/parser/mod.rs @@ -1,7 +1,7 @@ mod control_flow; mod expressions; mod funcs; -mod patterns; +mod pattern; mod statements; use crate::file::FileId; @@ -242,7 +242,7 @@ impl<'s> Parser<'s> { /// Panics if the current token does not match `kind`. Use for invariant assumptions. #[track_caller] pub(crate) fn assert(&mut self, kind: SyntaxKind) { - assert_eq!(self.current(), kind, "Expected {:?}", kind); + assert_eq!(self.current(), kind, "Expected {:?} at {:?}", kind, self.token.node); self.eat(); } @@ -280,6 +280,19 @@ impl<'s> Parser<'s> { self.last_err().unwrap() } + pub(crate) fn insert_error_at(&mut self, span: Span, message: impl Into) -> &mut SyntaxError { + let text = span.range().and_then(|r| self.get_text(r)).unwrap_or_default(); + + let error = SyntaxNode::error( + SyntaxError::new(message.into(), span), + text, + ); + trace_log!("inserting error: {:?}, text: {:?}, span: {:?}", error, text, span); + self.nodes.push(error); + + self.last_err().unwrap() + } + pub(crate) fn insert_error(&mut self, error: SyntaxError) -> &mut SyntaxError { trace_log!("inserting error: {:?}", error); let span = error.span; @@ -510,6 +523,10 @@ impl<'s> Parser<'s> { self.nodes.last() } + pub(crate) fn last_node_mut(&mut self) -> Option<&mut SyntaxNode> { + self.nodes.last_mut() + } + pub(crate) fn get_text(&self, range: Range) -> Option<&'s str> { self.text.get(range) } diff --git a/compose-syntax/src/parser/pattern.rs b/compose-syntax/src/parser/pattern.rs new file mode 100644 index 00000000..fec3641d --- /dev/null +++ b/compose-syntax/src/parser/pattern.rs @@ -0,0 +1,418 @@ +use crate::kind::SyntaxKind; +use crate::parser::expressions::code_expression; +use crate::parser::Parser; +use crate::parser::{expressions, ExprContext}; +use crate::precedence::Precedence; +use crate::set::syntax_set; +use crate::set; +use compose_error_codes::E0311_MATCH_ARM_PATTERNS_BIND_DIFFERENT_VARIABLES; +use compose_utils::trace_fn; +use ecow::eco_format; +use itertools::Itertools; +use std::collections::HashSet; +use std::mem; + +pub fn match_expr(p: &mut Parser) { + trace_fn!("parse_match"); + let m = p.marker(); + p.assert(SyntaxKind::MatchKW); + + let wrapped = p.eat_if(SyntaxKind::LeftParen); + if !wrapped { + p.insert_error_here("condition expressions require parentheses") + .with_label_message("Expected an opening `(` before the condition"); + + p.recover_until(syntax_set!(LeftBrace)); + + // error is not recoverable within the parens, try to recover until after and abort + return; + } + + if wrapped { + code_expression(p); + p.expect_closing_delimiter(m, SyntaxKind::RightParen); + } + + let open_marker = p.marker(); + p.expect(SyntaxKind::LeftBrace); + + while !p.current().is_terminator() { + // match arm + let arm_m = p.marker(); + + // patterns + let mut first_bindings = HashSet::new(); + pattern(p, false, false, &mut first_bindings); + while p.eat_if(SyntaxKind::Pipe) { + let mut current_bindings = HashSet::new(); + pattern(p, false, false, &mut current_bindings); + + if current_bindings != first_bindings { + insert_pattern_bindings_mismatch_error( + p, + &mut first_bindings, + &mut current_bindings, + ); + } + } + + // guard + if p.eat_if(SyntaxKind::IfKW) { + code_expression(p); + } + + if !p.eat_if(SyntaxKind::Arrow) { + p.insert_error_here("expected `=>` in match arm") + .with_label_message("expected `=>` here"); + + if !p.at_set(set::EXPR) { + // If the `=>` is not missing, just eat a token to make progress + p.eat(); + } + continue; + } + + code_expression(p); + + p.wrap(arm_m, SyntaxKind::MatchArm); + + if !p.current().is_terminator() { + p.expect_or_recover(SyntaxKind::Comma, syntax_set!(RightBracket)); + } + } + + p.expect_closing_delimiter(open_marker, SyntaxKind::RightBrace); + + p.wrap(m, SyntaxKind::MatchExpression) +} + +fn insert_pattern_bindings_mismatch_error( + p: &mut Parser, + first_bindings: &mut HashSet<&str>, + current_bindings: &mut HashSet<&str>, +) { + let span = p.last_node().unwrap().span(); + + let mut introduced = current_bindings.difference(&first_bindings).peekable(); + let mut missing = first_bindings.difference(¤t_bindings).peekable(); + + let mut diff_note = eco_format!("pattern bindings mismatch"); + + if missing.peek().is_some() { + diff_note.push_str(&eco_format!( + "\n - not bound here: `{}`", + missing.join("`, `") + )) + } + if introduced.peek().is_some() { + diff_note.push_str(&eco_format!( + "\n - bound only here: `{}`", + introduced.join("`, `") + )); + }; + + p.insert_error_at(span, "patterns within a match arm have differing bindings") + .with_code(&E0311_MATCH_ARM_PATTERNS_BIND_DIFFERENT_VARIABLES) + .with_hint("all patterns joined with `|` must bind the same variables, because the arm body must work for every pattern") + .with_hint(diff_note); +} + +/// Parses a binding or reassignment pattern. +pub fn pattern<'s>( + p: &mut Parser<'s>, + reassignment: bool, + in_typed_pattern: bool, + seen: &mut HashSet<&'s str>, +) { + trace_fn!("parse_pattern"); + match p.current() { + // Literals + SyntaxKind::Int | SyntaxKind::Float | SyntaxKind::Str | SyntaxKind::Bool => p.eat(), + SyntaxKind::LeftParen if p.peek_at(SyntaxKind::RightParen) => { + let m = p.marker(); + p.assert(SyntaxKind::LeftParen); + p.assert(SyntaxKind::RightParen); + p.wrap(m, SyntaxKind::Unit); + } + + SyntaxKind::Underscore => p.eat(), + SyntaxKind::LeftBracket => destructure_array(p, seen), + SyntaxKind::LeftBrace => destructure_map(p, seen), + _ => pattern_leaf(p, seen, reassignment, in_typed_pattern), + } +} + +fn destructure_map<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) { + trace_fn!("parse_destructure_map"); + let m = p.marker(); + p.assert(SyntaxKind::LeftBrace); + + let mut sink = false; + while !p.current().is_terminator() { + if !p.at_set(set::DESTRUCTURING_ITEM) { + p.unexpected("expected a destructuring item", None); + continue; + } + + destructuring_item(p, seen, &mut sink); + + if !p.current().is_terminator() { + p.expect_or_recover(SyntaxKind::Comma, syntax_set!(RightBrace)); + } + } + + p.expect_closing_delimiter(m, SyntaxKind::RightBrace); + + p.wrap(m, SyntaxKind::Destructuring); +} + + + +fn destructure_array<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) { + trace_fn!("parse_destructure_array"); + + let m = p.marker(); + p.assert(SyntaxKind::LeftBracket); + + let mut sink = false; + while !p.current().is_terminator() { + if !p.at_set(set::DESTRUCTURING_ITEM) { + p.unexpected("expected a destructuring item", None); + continue; + } + + destructuring_item(p, seen, &mut sink); + + if !p.current().is_terminator() { + p.expect_or_recover(SyntaxKind::Comma, syntax_set!(RightBracket)); + } + } + + p.expect_closing_delimiter(m, SyntaxKind::RightBracket); + + p.wrap(m, SyntaxKind::Destructuring); +} + +fn destructuring_item<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>, sink: &mut bool) { + let m = p.marker(); + + if p.eat_if(SyntaxKind::Dots) { + // sink + if p.at_set(set::PATTERN_LEAF) { + pattern_leaf(p, seen, false, false); + } + p.wrap(m, SyntaxKind::Spread); + if mem::replace(sink, true) { + p[m].convert_to_error("duplicate spread operator (`..`) in destructuring pattern"); + } + return; + } + + // parse normal array element or map key + pattern(p, false, false, seen); + + if p.eat_if(SyntaxKind::Colon) { + pattern(p, true, false, seen); + + if p[m].kind() != SyntaxKind::Ident { + p[m].expected("identifier after `:` in destructuring pattern") + } + + p.wrap(m, SyntaxKind::Named) + } +} + +fn pattern_leaf<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>, reassignment: bool, in_typed_pattern: bool) { + trace_fn!("parse_pattern_leaf"); + if p.current().is_keyword() { + p.token.node.expected("pattern"); + p.eat(); + return; + } else if !p.at_set(set::PATTERN_LEAF) { + p.insert_error_here("expected a pattern"); + return; + } + + if p.eat_if(SyntaxKind::Underscore) { + return; + } + + let m = p.marker(); + let mut binding_text = p.current_text(); + + // Parse a full atomic expression, even though we only care about an identifier. + // This way the entire expression can be marked as an error if it is not. + expressions::code_expr_prec(p, ExprContext::AtomicExpr, Precedence::Lowest); + + let last_node = p.last_node().expect("was just parsed"); + let mut span = last_node.span(); + let last_kind = last_node.kind(); + + if !reassignment && last_node.kind() != SyntaxKind::Ident { + p.insert_error_at(span, "pattern leaf must be an identifier or `_`") + .with_label_message("expected this to be an identifier"); + return; + } + + // Do not allow nested typed bindings + let at_typed_binding = last_kind == SyntaxKind::Ident && p.at_set(set::PATTERN) && !in_typed_pattern; + + if at_typed_binding { + binding_text = p.current_text(); + span = p.current_span(); + + pattern(p, reassignment, true, seen); + p.wrap(m, SyntaxKind::TypedPattern); + } + + if !seen.insert(binding_text) && reassignment { + p.insert_error_at(span, "duplicate binding") + .with_label_message("this binding already appears in this pattern"); + } +} + +fn parenthesized_or_destructuring( + p: &mut Parser, + _reassignment: bool, + _seen: &mut HashSet<&str>, + _dupe: Option<&str>, +) { + trace_fn!("parse_destructuring"); + p.assert(SyntaxKind::At); + + unimplemented!("destructuring") +} + +#[cfg(test)] +mod tests { + use crate::assert_parse_tree; + use compose_error_codes::E0311_MATCH_ARM_PATTERNS_BIND_DIFFERENT_VARIABLES; + + #[test] + fn test_destructuring_underscore() { + assert_parse_tree!("let _ = 2", + LetBinding [ + LetKW("let") + Underscore("_") + Eq("=") + Int("2") + ] + ); + } + + #[test] + fn test_destructuring_array() { + assert_parse_tree!("let [a, b, ..c] = 2", + LetBinding [ + LetKW("let") + Destructuring [ + LeftBracket("[") + Ident("a") + Comma(",") + Ident("b") + Comma(",") + Spread [ + Dots("..") + Ident("c") + ] + RightBracket("]") + ] + Eq("=") + Int("2") + ] + ) + } + + #[test] + fn test_match_expression() { + assert_parse_tree!( + r#" + match ([1, 2, 3]) { + [] => "empty", + [_, _, ..rest] if condition => 7, + 3 | 4 | 5 => 8, + } + "#, + MatchExpression [ + MatchKW("match") + LeftParen("(") + Array [ + LeftBracket("[") + Int("1") + Comma(",") + Int("2") + Comma(",") + Int("3") + RightBracket("]") + ] + RightParen(")") + LeftBrace("{") + MatchArm [ + Destructuring [ + LeftBracket("[") + RightBracket("]") + ] + Arrow("=>") + Str("\"empty\"") + ] + Comma(",") + MatchArm [ + Destructuring [ + LeftBracket("[") + Underscore("_") + Comma(",") + Underscore("_") + Comma(",") + Spread [ + Dots("..") + Ident("rest") + ] + RightBracket("]") + ] + IfKW("if") + Ident("condition") + Arrow("=>") + Int("7") + ] + Comma(",") + MatchArm [ + Int("3") + Pipe("|") + Int("4") + Pipe("|") + Int("5") + Arrow("=>") + Int("8") + ] + Comma(",") + RightBrace("}") + ] + ) + } + + #[test] + fn test_match_expression_different_bindings_within_arm_error() { + assert_parse_tree!( + r#" + match (1) { + x | y => x + }"#, + MatchExpression [ + MatchKW("match") + LeftParen("(") + Int("1") + RightParen(")") + LeftBrace("{") + MatchArm [ + Ident("x") + Pipe("|") + Ident("y") + Error(E0311_MATCH_ARM_PATTERNS_BIND_DIFFERENT_VARIABLES) + Arrow("=>") + Ident("x") + ] + RightBrace("}") + ] + ) + } +} diff --git a/compose-syntax/src/parser/patterns.rs b/compose-syntax/src/parser/patterns.rs deleted file mode 100644 index 856b6f0a..00000000 --- a/compose-syntax/src/parser/patterns.rs +++ /dev/null @@ -1,175 +0,0 @@ -use crate::kind::SyntaxKind; -use crate::parser::Parser; -use crate::parser::{ExprContext, expressions}; -use crate::precedence::Precedence; -use crate::set::syntax_set; -use crate::{SyntaxError, set}; -use compose_utils::trace_fn; -use ecow::eco_format; -use std::collections::HashSet; -use std::mem; - -/// Parses a binding or reassignment pattern. -pub fn pattern<'s>( - p: &mut Parser<'s>, - reassignment: bool, - seen: &mut HashSet<&'s str>, - dupe: Option<&'s str>, -) { - trace_fn!("parse_pattern"); - match p.current() { - SyntaxKind::Underscore => p.eat(), - SyntaxKind::LeftBracket => destructure_array(p, seen), - _ => pattern_leaf(p, reassignment, seen, dupe), - } -} - -fn destructure_array<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) { - trace_fn!("parse_destructure_array"); - - let m = p.marker(); - p.assert(SyntaxKind::LeftBracket); - - let mut sink = false; - while !p.current().is_terminator() { - if !p.at_set(set::DESTRUCTURING_ITEM) { - p.unexpected("expected a destructuring item", None); - continue; - } - - destructuring_item(p, seen, &mut sink); - - if !p.current().is_terminator() { - p.expect_or_recover(SyntaxKind::Comma, syntax_set!(RightBracket)); - } - } - - p.expect_closing_delimiter(m, SyntaxKind::RightBracket); - - p.wrap(m, SyntaxKind::Destructuring); -} - -fn destructuring_item<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>, sink: &mut bool) { - let m = p.marker(); - - if p.eat_if(SyntaxKind::Dots) { - // sink - if p.at_set(set::PATTERN_LEAF) { - pattern_leaf(p, false, seen, None); - } - p.wrap(m, SyntaxKind::Spread); - if mem::replace(sink, true) { - p[m].convert_to_error("duplicate spread operator (`..`) in destructuring pattern"); - } - return; - } - - // parse normal array element or map key - pattern(p, false, seen, None); - - if p.eat_if(SyntaxKind::Colon) { - pattern(p, true, seen, None); - - if p[m].kind() != SyntaxKind::Ident { - p[m].expected("identifier after `:` in destructuring pattern") - } - - p.wrap(m, SyntaxKind::Named) - } -} - -fn pattern_leaf<'s>( - p: &mut Parser<'s>, - reassignment: bool, - seen: &mut HashSet<&'s str>, - dupe: Option<&'s str>, -) { - trace_fn!("parse_pattern_leaf"); - if p.current().is_keyword() { - p.token.node.expected("pattern"); - p.eat(); - return; - } else if !p.at_set(set::PATTERN_LEAF) { - p.insert_error_here("expected a pattern"); - return; - } - - if p.eat_if(SyntaxKind::Underscore) { - return; - } - - let m = p.marker(); - let text = p.current_text(); - - // Parse a full atomic expression, even though we only care about an identifier. - // This way the entire expression can be marked as an error if it is not. - expressions::code_expr_prec(p, ExprContext::AtomicExpr, Precedence::Lowest); - - // If the pattern is not a reassignment, it can only be an identifier - if !reassignment { - let node = &mut p[m]; - if node.kind() != SyntaxKind::Ident { - let span = node.span(); - p.insert_error(SyntaxError::new("expected a pattern", span)); - return; - } - if !seen.insert(text) { - node.convert_to_error(eco_format!( - "duplicate {binding}: {text}", - binding = dupe.unwrap_or("binding") - )); - } - } -} - -fn parenthesized_or_destructuring( - p: &mut Parser, - _reassignment: bool, - _seen: &mut HashSet<&str>, - _dupe: Option<&str>, -) { - trace_fn!("parse_destructuring"); - p.assert(SyntaxKind::At); - - unimplemented!("destructuring") -} - -#[cfg(test)] -mod tests { - use crate::assert_parse_tree; - - #[test] - fn test_destructuring_underscore() { - assert_parse_tree!("let _ = 2", - LetBinding [ - Let("let") - Underscore("_") - Eq("=") - Int("2") - ] - ); - } - - #[test] - fn test_destructuring_array() { - assert_parse_tree!("let [a, b, ..c] = 2", - LetBinding [ - Let("let") - Destructuring [ - LeftBracket("[") - Ident("a") - Comma(",") - Ident("b") - Comma(",") - Spread [ - Dots("..") - Ident("c") - ] - RightBracket("]") - ] - Eq("=") - Int("2") - ] - ) - } -} diff --git a/compose-syntax/src/parser/statements.rs b/compose-syntax/src/parser/statements.rs index ccfea994..16027697 100644 --- a/compose-syntax/src/parser/statements.rs +++ b/compose-syntax/src/parser/statements.rs @@ -2,7 +2,7 @@ use crate::fix::FixBuilder; use crate::kind::SyntaxKind; use crate::parser::expressions::{code_expr_prec, code_expression}; use crate::parser::Parser; -use crate::parser::{patterns, ExprContext}; +use crate::parser::{pattern, ExprContext}; use crate::precedence::Precedence; use crate::set::{syntax_set, SyntaxSet, ASSIGN_OP}; use crate::{set, PatchEngine, SyntaxNode}; @@ -17,21 +17,22 @@ use std::collections::HashSet; pub(super) fn statement(p: &mut Parser) { trace_fn!("parse_statement"); - if p.at(SyntaxKind::Let) || (p.at(SyntaxKind::Pub) && p.peek_at(SyntaxKind::Let)) { + if p.at(SyntaxKind::LetKW) || (p.at(SyntaxKind::PubKW) && p.peek_at(SyntaxKind::LetKW)) { let_binding(p); return; } - if p.at(SyntaxKind::Break) { + if p.at(SyntaxKind::BreakKW) { break_statement(p); return; } - if p.eat_if(SyntaxKind::Continue) { + if p.at(SyntaxKind::ContinueKW) { + continue_statement(p); return; } - if p.at(SyntaxKind::Return) { + if p.at(SyntaxKind::ReturnKW) { return_statement(p); return; } @@ -50,9 +51,17 @@ pub(super) fn statement(p: &mut Parser) { } } +pub fn continue_statement(p: &mut Parser) { + let m = p.marker(); + + p.assert(SyntaxKind::ContinueKW); + + p.wrap(m, SyntaxKind::ContinueStatement); +} + pub fn return_statement(p: &mut Parser) { let m = p.marker(); - p.assert(SyntaxKind::Return); + p.assert(SyntaxKind::ReturnKW); if p.at_set(set::EXPR) { code_expression(p); @@ -63,7 +72,7 @@ pub fn return_statement(p: &mut Parser) { pub fn break_statement(p: &mut Parser) { let m = p.marker(); - p.assert(SyntaxKind::Break); + p.assert(SyntaxKind::BreakKW); if p.at_set(set::EXPR) { code_expression(p); @@ -75,10 +84,10 @@ pub fn break_statement(p: &mut Parser) { pub fn let_binding(p: &mut Parser) { trace_fn!("parse_let_binding"); let m = p.marker(); - p.eat_if(SyntaxKind::Pub); - p.assert(SyntaxKind::Let); + p.eat_if(SyntaxKind::PubKW); + p.assert(SyntaxKind::LetKW); - let was_mut = p.eat_if(SyntaxKind::Mut); + let was_mut = p.eat_if(SyntaxKind::MutKW); if !p.at_set(set::PATTERN) { let got = p.current(); @@ -101,21 +110,21 @@ pub fn let_binding(p: &mut Parser) { // eat tokens until we find an `=` or the end of this statement p.recover_until(syntax_set!(Eq, End, NewLine, RightBrace)); } else { - patterns::pattern(p, false, &mut HashSet::new(), None); + pattern::pattern(p, false, false, &mut HashSet::new()); } if p.eat_if(SyntaxKind::Eq) { code_expression(p); } else if p.at_set(set::ATOMIC_EXPR) && !p.had_leading_newline() { let pattern_text = p.last_text().to_owned(); - p.insert_error_before("expected `=` after a binding") + p.insert_error_before("expected `=` after binding name") .with_label_message("expected `=` here") .with_code(&E0007_MISSING_EQUALS_AFTER_LET_BINDING) .with_hint(eco_format!("if you meant to initialize the binding, add `=`: `let {}{} = ...`", if was_mut { "mut " } else { "" }, pattern_text, )) - .with_hint(eco_format!("if you meant to leave it uninitialized, add a semicolon or newline: `let {pattern_text};` or place the next expression on a new line")); + .with_hint(eco_format!("if you meant to leave it uninitialized, add a semicolon: `let {pattern_text};`")); // Assume that the user meant to initialize the binding. code_expression(p); @@ -174,7 +183,7 @@ mod tests { fn test_parse_let_binding() { assert_parse_tree!("let x = 1;", LetBinding [ - Let("let") + LetKW("let") Ident("x") Eq("=") Int("1") @@ -186,8 +195,8 @@ mod tests { fn test_parse_let_mut_binding() { assert_parse_tree!("let mut x = 1;", LetBinding [ - Let("let") - Mut("mut") + LetKW("let") + MutKW("mut") Ident("x") Eq("=") Int("1") @@ -199,37 +208,12 @@ mod tests { fn test_parse_let_binding_uninitialized() { assert_parse_tree!("let x;", LetBinding [ - Let("let") + LetKW("let") Ident("x") ] ); } - #[test] - fn test_parse_let_binding_missing_eq() { - assert_parse_tree!("let x 1;", - LetBinding [ - Let("let") - Ident("x") - Error(E0007_MISSING_EQUALS_AFTER_LET_BINDING) - Int("1") - ] - ); - } - - #[test] - fn test_parse_let_mut_binding_missing_eq() { - assert_parse_tree!("let mut x 1;", - LetBinding [ - Let("let") - Mut("mut") - Ident("x") - Error(E0007_MISSING_EQUALS_AFTER_LET_BINDING) - Int("1") - ] - ); - } - #[test] fn test_parse_assignment() { assert_parse_tree!("x = 1;", @@ -259,7 +243,7 @@ mod tests { for input in inputs { assert_parse_tree!(input, LetBinding [ - Let("let") + LetKW("let") Ident("x") Eq("=") Int("1") @@ -274,7 +258,7 @@ mod tests { ] ] LetBinding [ - Let("let") + LetKW("let") Ident("y") Eq("=") Ident("x") @@ -287,7 +271,7 @@ mod tests { fn test_parse_statements_unterminated() { assert_parse_tree!("let x = 1 x += x * 2 let y = x", LetBinding [ - Let("let") + LetKW("let") Ident("x") Eq("=") Int("1") @@ -304,7 +288,7 @@ mod tests { ] Error(E0006_UNTERMINATED_STATEMENT) LetBinding [ - Let("let") + LetKW("let") Ident("y") Eq("=") Ident("x") diff --git a/compose-syntax/src/precedence.rs b/compose-syntax/src/precedence.rs index 96ce3e20..d122b42e 100644 --- a/compose-syntax/src/precedence.rs +++ b/compose-syntax/src/precedence.rs @@ -5,6 +5,7 @@ pub enum Precedence { Range, // .. or ..= LogicalOr, // || LogicalAnd, // && + Is, // ` is ` Equals, // == LessGreater, // > or < BitwiseOr, // | diff --git a/compose-syntax/src/scanner.rs b/compose-syntax/src/scanner.rs index 668841ef..2df6ef67 100644 --- a/compose-syntax/src/scanner.rs +++ b/compose-syntax/src/scanner.rs @@ -1,3 +1,4 @@ +use crate::set::SyntaxSet; use crate::{Lexer, SyntaxKind, SyntaxNode}; use ecow::{EcoString, eco_format}; @@ -56,7 +57,20 @@ pub struct Scanner<'a> { delimiters: Vec, } -impl<'a> Scanner<'a> {} +pub enum ScanResult { + /// The current token is a match + Match, + /// The current token is not a match, but the scanner should continue scanning. + Continue, + /// The scanner should stop scanning. + BreakScan, +} + +impl ScanResult { + pub fn from_bool(b: bool) -> Self { + if b { Self::Match } else { Self::Continue } + } +} impl<'a> Scanner<'a> { pub fn new(lexer: Lexer<'a>) -> Self { @@ -66,6 +80,14 @@ impl<'a> Scanner<'a> { } } + pub fn delimiters(&self) -> &[Delimiter] { + &self.delimiters + } + + pub fn delim_depth(&self) -> usize { + self.delimiters.len() + } + /// jump to a specific offset in the containing lexer pub fn with_offset(mut self, byte_index: usize) -> Self { self.lexer.jump(byte_index); @@ -94,19 +116,47 @@ impl<'a> Scanner<'a> { Ok(()) } - /// Iterates through the current level of delimiters (does not enter nested delims) and checks if + pub fn at(&self, syntax_kind: SyntaxKind) -> bool { + let (kind, _) = self.lexer.clone().next(); + kind == syntax_kind + } + + /// Iterates through the current level of delimiters (does not check within delims) and checks if /// the given `kind` is contained within pub fn level_contains_kind(&mut self, expected_kind: SyntaxKind) -> Result { - self.find_in_matching_delims(expected_kind).map(|opt| opt.is_some()) + self.find_in_matching_delims(expected_kind) + .map(|opt| opt.is_some()) + } + + /// Iterates through the current level of delimiters (does not check within nested delims) and checks if + /// any of the given `kind`s within the syntax set is contained within + pub fn level_contains_set(&mut self, syntax_set: SyntaxSet) -> Result { + self.find_set_in_matching_delims(syntax_set) + .map(|opt| opt.is_some()) } + pub(crate) fn find_in_matching_delims( &mut self, expected_kind: SyntaxKind, + ) -> Result, EcoString> { + self.find_set_in_matching_delims(SyntaxSet::new().add(expected_kind)) + } + + pub(crate) fn find_set_in_matching_delims( + &mut self, + expected_kinds: SyntaxSet, ) -> Result, EcoString> { let current_level = self.delimiters.len(); self.scan_until(|delim_stack, node| { - delim_stack.len() == current_level && node.kind() == expected_kind + if delim_stack.len() < current_level { + return ScanResult::BreakScan; + } + if delim_stack.len() == current_level && expected_kinds.contains(node.kind()) { + ScanResult::Match + } else { + ScanResult::Continue + } }) } @@ -115,34 +165,38 @@ impl<'a> Scanner<'a> { let current_level = self.delimiters.len(); debug_assert!(current_level > 0, "open a delimiter before calling this"); let exit_level = current_level - 1; - self.scan_until(|delim_stack, _| delim_stack.len() == exit_level) + self.scan_until(|delim_stack, _| ScanResult::from_bool(delim_stack.len() == exit_level)) } /// Scans until the predicate yields true. Returns the yielded node if any pub fn scan_until( &mut self, - predicate: impl Fn(&[Delimiter], &SyntaxNode) -> bool, + predicate: impl Fn(&[Delimiter], &SyntaxNode) -> ScanResult, ) -> Result, EcoString> { - while let Some(node) = self.next() { - if let Some(delimiter) = Delimiter::from_kind(node.kind()) { - if delimiter.is_opening() { - self.enter(delimiter); - } - if delimiter.is_closing() { - self.exit(delimiter)?; - } - } - if predicate(&self.delimiters, &node) { - return Ok(Some(node)); + while let Some(node) = self.next()? { + match predicate(&self.delimiters, &node) { + ScanResult::Match => return Ok(Some(node)), + ScanResult::Continue => continue, + ScanResult::BreakScan => break, } } Ok(None) } - pub fn next(&mut self) -> Option { + pub fn next(&mut self) -> Result, EcoString> { match self.lexer.next() { - (SyntaxKind::End, _) => None, - (_, node) => Some(node), + (SyntaxKind::End, _) => Ok(None), + (_, node) => { + if let Some(delimiter) = Delimiter::from_kind(node.kind()) { + if delimiter.is_opening() { + self.enter(delimiter); + } + if delimiter.is_closing() { + self.exit(delimiter)?; + } + } + Ok(Some(node)) + } } } } @@ -206,7 +260,7 @@ mod tests { let closing_delim = s.matching_closing_delim().unwrap().unwrap(); assert_eq!(closing_delim.kind(), SyntaxKind::RightParen); // check it is the right `)` - assert_eq!(s.next().unwrap().kind(), SyntaxKind::Ellipsis); - assert_eq!(s.next().unwrap().kind(), SyntaxKind::Ident); + assert_eq!(s.next().unwrap().unwrap().kind(), SyntaxKind::Ellipsis); + assert_eq!(s.next().unwrap().unwrap().kind(), SyntaxKind::Ident); } } diff --git a/compose-syntax/src/set.rs b/compose-syntax/src/set.rs index 671fa050..e831ad08 100644 --- a/compose-syntax/src/set.rs +++ b/compose-syntax/src/set.rs @@ -38,7 +38,7 @@ macro_rules! syntax_set { pub(crate) use syntax_set; -pub const STMT: SyntaxSet = syntax_set![Let].union(ATOMIC_EXPR); +pub const STMT: SyntaxSet = syntax_set![LetKW].union(ATOMIC_EXPR); pub const ARG_RECOVER: SyntaxSet = syntax_set![ Comma, @@ -69,14 +69,14 @@ pub const ATOMIC_EXPR: SyntaxSet = syntax_set![ LeftBrace, LeftBracket, LeftParen, - If, - While, - Loop, - For, - Break, - Continue, - Import, - Return, + IfKW, + WhileKW, + LoopKW, + ForKW, + BreakKW, + ContinueKW, + ImportKW, + ReturnKW, Unit, Int, Float, @@ -96,19 +96,22 @@ pub const PATTERN_LEAF: SyntaxSet = syntax_set![ Bool, ]; +pub const LITERAL: SyntaxSet = syntax_set![Int, Float, Str, Bool]; + pub const PATTERN: SyntaxSet = syntax_set![ + LeftBrace, LeftParen, LeftBracket, Underscore, Ident, -]; +].union(LITERAL); pub const DESTRUCTURING_ITEM: SyntaxSet = PATTERN.add(SyntaxKind::Dots); pub const STMT_TERMINATOR: SyntaxSet = syntax_set![RightBrace]; -pub const PARAM: SyntaxSet = PATTERN.union(syntax_set![Ref, Mut]); +pub const PARAM: SyntaxSet = PATTERN.union(syntax_set![RefKW, MutKW]); -pub const CAPTURE: SyntaxSet = syntax_set![Ident, Ref, Mut]; +pub const CAPTURE: SyntaxSet = syntax_set![Ident, RefKW, MutKW]; pub const CAPTURE_RECOVER: SyntaxSet = syntax_set![Comma, Pipe].union(CAPTURE); diff --git a/compose-syntax/src/test_utils.rs b/compose-syntax/src/test_utils.rs index 6239fca4..c08e5ba3 100644 --- a/compose-syntax/src/test_utils.rs +++ b/compose-syntax/src/test_utils.rs @@ -465,7 +465,7 @@ impl NodesTester { /// FuncCall [ /// Ident("f") /// Args [ -/// Error(compose_error_codes::E0001_UNCLOSED_DELIMITER) +/// LeftParen("(") /// Ident("a") /// Comma(",") /// Ident("b") @@ -477,6 +477,7 @@ impl NodesTester { /// Plus("+") /// Int("2") /// ] +/// Error(compose_error_codes::E0001_UNCLOSED_DELIMITER) /// ] /// ] /// ); @@ -491,8 +492,8 @@ impl NodesTester { /// LeftBrace("{") /// Params [ /// Param [ -/// Ref("ref") -/// Mut("mut") +/// RefKW("ref") +/// MutKW("mut") /// Ident("a") /// ] /// ] diff --git a/compose-utils/src/lib.rs b/compose-utils/src/lib.rs index 43ae10c4..8a467393 100644 --- a/compose-utils/src/lib.rs +++ b/compose-utils/src/lib.rs @@ -1,7 +1,7 @@ mod trace; use std::hash::Hash; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; pub use trace::*; @@ -37,3 +37,37 @@ impl Hash for Static { state.write_usize(std::ptr::from_ref(self.0) as _); } } + + +/// Automatically calls a deferred function when the returned handle is dropped. +pub fn defer( + thing: &mut T, + deferred: F, +) -> impl DerefMut { + struct DeferHandle<'a, T, F: FnOnce(&mut T)> { + thing: &'a mut T, + deferred: Option, + } + + impl<'a, T, F: FnOnce(&mut T)> Drop for DeferHandle<'a, T, F> { + fn drop(&mut self) { + std::mem::take(&mut self.deferred).expect("deferred function")(self.thing); + } + } + + impl Deref for DeferHandle<'_, T, F> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.thing + } + } + + impl DerefMut for DeferHandle<'_, T, F> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.thing + } + } + + DeferHandle { thing, deferred: Some(deferred) } +} diff --git a/compose/Cargo.toml b/compose/Cargo.toml index 946e2d53..a774a098 100644 --- a/compose/Cargo.toml +++ b/compose/Cargo.toml @@ -5,8 +5,11 @@ edition.workspace = true [dependencies] compose-eval = { workspace = true} -compose-error-codes = {workspace = true} compose-doc-macros = {workspace = true} +compose-error-codes-doc-tests = { workspace = true} +compose-error-codes = { workspace = true} +compose-syntax = {workspace = true} +compose-library = {workspace = true} [lints] workspace = true diff --git a/compose/src/docs/mod.rs b/compose/src/docs/mod.rs deleted file mode 100644 index 1d3950be..00000000 --- a/compose/src/docs/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod introduction; -mod basics; - -pub use introduction::*; -pub use basics::*; - -pub use compose_error_codes as error_codes; \ No newline at end of file diff --git a/compose/src/implementation/mod.rs b/compose/src/implementation/mod.rs new file mode 100644 index 00000000..79323eca --- /dev/null +++ b/compose/src/implementation/mod.rs @@ -0,0 +1,12 @@ +/*! +Compose language implementation + + +*/ + +#[doc(inline)] +pub use compose_syntax as syntax; +#[doc(inline)] +pub use compose_eval as eval; +#[doc(inline)] +pub use compose_library as library; diff --git a/compose/src/language/Error_Codes.rs b/compose/src/language/Error_Codes.rs new file mode 100644 index 00000000..2ed5f57b --- /dev/null +++ b/compose/src/language/Error_Codes.rs @@ -0,0 +1 @@ +pub use compose_error_codes_doc_tests::*; diff --git a/compose/src/docs/basics.rs b/compose/src/language/basics.rs similarity index 95% rename from compose/src/docs/basics.rs rename to compose/src/language/basics.rs index e3148f3e..e7f2556d 100644 --- a/compose/src/docs/basics.rs +++ b/compose/src/language/basics.rs @@ -19,7 +19,7 @@ compose_doc!( ```compose error(E0004) let x = 5; - x = 6; // ❌ Error: cannot assign to immutable variable + x = 6; ``` ```output error(E0004) @@ -60,12 +60,12 @@ compose_doc!( > assert::eq(x, 1); > ``` - Reading an uninitialized variable is allowed, but emits a warning and produces the unit value () - You should avoid relying on this behaviour. + Reading an uninitialized variable is allowed and resolves to `()` (the unit value). + However, this produces a warning and relying on this behaviour should be avoided. ```compose warn(W0001) let x; - assert::eq(x, ()); // ⚠️ Warning: x was used before being assigned a value + assert::eq(x, ()); ``` ```output warn(W0001) @@ -85,7 +85,7 @@ compose_doc!( assert::eq(outer, 10); assert::eq(inner, 20); }; - assert::eq(inner, 10); // ❌ Error: `inner` is not visible here + assert::eq(inner, 10); // inner is out of scope here ``` ```output error(E0011) diff --git a/compose/src/docs/introduction.rs b/compose/src/language/introduction.rs similarity index 92% rename from compose/src/docs/introduction.rs rename to compose/src/language/introduction.rs index d2761e16..588a6118 100644 --- a/compose/src/docs/introduction.rs +++ b/compose/src/language/introduction.rs @@ -189,16 +189,16 @@ compose_doc! { Both `while` and `for` loops can also be used as expressions. You can return a value from a loop using `break`: ```compose - // let result = { - // let mut i = 0; - // while true { - // if i == 5 { - // break i * 2; // returns 10 - // }; - // i = i + 1; - // }; - // }; - // assert::eq(result, 10); + let result = { + let mut i = 0; + while (true) { + if (i == 5) { + break i * 2; // returns 10 + }; + i = i + 1; + }; + }; + assert::eq(result, 10); ``` If no `break` with value occurs, the result of the loop is `unit`. @@ -238,8 +238,8 @@ compose_doc! { | Arithmetic | ✅ | `1 + 2 * 3` | | Function call | ✅ | `add(1, 2)` | | Block `{ ... }` | ✅ | `{ let x = 2; x + 1 }` | - | `if`/`else` | ✅ | `if x > 0 { "yes" } else { "no" }` | - | `while` / `for` | ✅ via `break` | `while cond { break 5; }` | + | `if`/`else` | ✅ | `if (x > 0) { "yes" } else { "no" }` | + | `while` / `for` | ✅ via `break` | `while (cond) { break 5; }` | | Closure | ✅ | `(x) => x + 1` | In Compose, the idea is simple: **if it does something, it probably returns something too.** @@ -248,7 +248,7 @@ compose_doc! { | Previous | Next | |---------------------|--------------------------------------------------------------------| - | [Back](crate::docs) | [Next: Variables and scopes](crate::docs::C2_Variables_and_Scopes) | + | [Back](crate::language) | [Next: Variables and scopes](crate::language::C2_Variables_and_Scopes) | */ pub mod C1_Overview {} diff --git a/compose/src/language/mod.rs b/compose/src/language/mod.rs new file mode 100644 index 00000000..4f8bafe4 --- /dev/null +++ b/compose/src/language/mod.rs @@ -0,0 +1,9 @@ +mod introduction; +mod basics; +mod patterns; +#[allow(non_snake_case)] +pub mod Error_Codes; + +pub use introduction::*; +pub use basics::*; +pub use patterns::*; diff --git a/compose/src/language/patterns.rs b/compose/src/language/patterns.rs new file mode 100644 index 00000000..34a93840 --- /dev/null +++ b/compose/src/language/patterns.rs @@ -0,0 +1,87 @@ +use compose_doc_macros::compose_doc; + +compose_doc! { +/// # Pattern Matching in Compose +/// +/// Compose provides powerful tools for inspecting and decomposing values using +/// destructuring, `is` expressions, and `match` expressions. These let you +/// ergonomically check a value's structure or type, bind parts of it to variables, and +/// conditionally execute code based on patterns. +/// +/// ## Destructuring with `let` +/// +/// You can destructure arrays and other composite values directly in +/// `let` bindings: +/// +/// ```compose +/// let array = [1, "a string", 3]; +/// +/// // Destructure first element and the rest of the array +/// let [first, ..rest] = array; +/// assert::eq(first, 1); +/// assert::eq(rest.len(), 2); +/// +/// let map = { a: 1, b: 2 }; +/// let { a, b } = map; +/// assert::eq(a, 1); +/// assert::eq(b, 2); +/// ``` +/// +/// Here, `first` is bound to the first element, and `rest` captures the remaining elements. +/// +/// ## `is` Expressions +/// +/// Use `is` to check the structure or type of a value and optionally bind variables +/// when the check succeeds: +/// +/// ```compose +/// # let array = [1, "a string", 3]; +/// if (array is [_, String s, ..] && s.len() == 8) { +/// assert::eq(s, "a string"); // s is accessible within the if body +/// }; +/// ``` +/// +/// The pattern `[_, String s, ..]` matches any array whose second element is a string, +/// binding it to `s`. +/// +/// ## `match` Expressions +/// +/// `match` lets you handle multiple patterns and conditions: +/// +/// ```compose +/// # let array = [1, "a string", 3]; +/// match (array) { +/// [_, _] => "any 2 elements", +/// [Int x, Int y, ..] => "array starting with 2 ints, bound to x and y", +/// [Int x, ..] if x % 2 == 0 => "array starting with an even int, bound to x", +/// [1, "a string", ..] => "array starting with 1 and a string", +/// other => "anything else, bound to variable `other`", +/// }; +/// ``` +/// +/// Notes: +/// - Patterns are checked in order, and the first match is taken. +/// - Variables bound in a pattern are available in the corresponding branch body. +/// - Guards (`if` clauses) can add additional conditions to a pattern. +/// +/// ## Destructuring in Function Parameters +/// +/// Patterns can also be used in function parameters for concise extraction: +/// +/// ```compose +/// # let array = [1, "a string", 3]; +/// let first = { [first, ..] => first }; +/// assert::eq(first(array), 1); +/// ``` +/// +/// This defines a function that takes an array, destructures it, and returns the +/// first element. +/// +/// ## Summary +/// +/// - Use `let` for destructuring values in local bindings. +/// - Use `is` for conditional type/structure checks with optional bindings. +/// - Use `match` for multi-case branching with pattern matching. +/// - Destructuring can also appear in function parameters for concise, readable code. + pub mod C5_Patterns {} +} diff --git a/compose/src/lib.rs b/compose/src/lib.rs index 6f333e32..0a88c44e 100644 --- a/compose/src/lib.rs +++ b/compose/src/lib.rs @@ -3,32 +3,35 @@ ```rust # compose::test::assert_eval(r#" -println("Hello, world! From Compose!"); +println("'Hello, world' from Compose!"); # "#); ``` -**Compose** is a lightweight, expression-oriented programming language designed to be clear, predictable, and structurally robust. Inspired by modern language design principles, Compose aims to offer a seamless blend of expressiveness and simplicity—without the overhead of complex ownership models or verbose syntax. +A functional flavoured interpreted programming language with rust-like syntax. -Whether you're scripting quick logic or building more sophisticated constructs, Compose is designed to guide you gently, offering: +Features: +- Expression focused: blocks and control flow are expressed as expressions. +- Functions as first class citizens +- High quality diagnostics +- Variables are immutable by default +- Garbage collection +- Portable: runs on any platform that supports rust -* **Clear syntax**: Minimal punctuation and consistent structure help you focus on *what* you're expressing, not *how* to express it. -* **Safe references**: Compose uses a `box` system to handle heap-allocated data with clarity and safety. References (`ref`, `ref mut`) are only allowed to refer to boxed values, which helps avoid subtle lifetime bugs. -* **No move semantics**: All values are either copyable or clone-on-write, simplifying reasoning about variable behavior and reducing surprises. -* **Robust error reporting**: Compose comes with detailed, context-aware error messages that point you to the real problem—often with suggestions to fix it. -* **A strong, consistent AST and CST**: Behind the scenes, Compose uses a resilient parser and a fault-tolerant concrete syntax tree, making tooling and analysis more reliable and powerful. -* **First-class closures**: Functions and closures are fully supported, and closures can capture boxed variables by reference, safely enabling functional patterns. +**This language is still in development, and APIs are subject to change. Developed as a learning +project, the language is not intended for production use.** -To learn more, check out the [Overview](docs::C1_Overview) section. +To learn about the language, see the [language] module. -## Getting Started - -- [Docs](docs) - - [Overview](docs::C1_Overview) - - [Variables and Scopes](docs::C2_Variables_and_Scopes) - - [Functions](docs::C4_Functions_and_closures) +To learn about the implementation, see the [implementation] docs. */ -pub mod docs; +pub mod language; +pub mod implementation; pub use compose_eval::{eval, eval_range, test}; -pub use compose_error_codes as error_codes; +#[doc(hidden)] +pub use compose_syntax as syntax; +#[doc(hidden)] +pub use compose_eval as eval; +#[doc(hidden)] +pub use compose_library as library; diff --git a/examples/scratch.cmps b/examples/scratch.cmps index 9566bb84..08d61817 100644 --- a/examples/scratch.cmps +++ b/examples/scratch.cmps @@ -1,3 +1,2 @@ -let [a, b, ..c, d] = (1..7).iter().to_array(); - -println(a, b, c, d); +let value = 1; +value = 5;