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