Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 55 additions & 206 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,239 +1,88 @@
# Compose

> **Work in Progress** – Compose is an experimental programming language and interpreter, built as a learning project to explore language design, fault-tolerant parsing, and high-quality diagnostics.

---

## 🚧 Status

Compose is **not production-ready**. Expect breaking changes, incomplete features, and rough edges.

That said, the project already includes a working parser, interpreter, and a growing standard library.

---

## ✨ Goals and Features

* **Fault-Tolerant Parsing**
Compose’s parser is designed to recover gracefully from syntax errors, building a complete **Concrete Syntax Tree (CST)** even when source code contains mistakes. This enables:

* Detailed error reporting
* Non-blocking analysis tools
* IDE-like features in the future

* **Helpful Diagnostics**
Compose provides **precise, user-friendly diagnostics** with labeled spans, suggestions, and contextual hints. Examples:

```rust
error[E0009]: expected a comma between the function arguments
┌─ main.cmps:1:14
1 │ let x = foo(a c = d)
│ ^ help: insert a comma here
= help: for more information about this error, try `compose explain E0009`

error[E0002]: assignments are not allowed in expression contexts
┌─ main.cmps:1:17
1 │ let x = foo(a c = d)
│ ^ assignment is not allowed here
= help: if you meant to compare `c` and `d`, use `==` instead of `=`
= help: if you meant to assign to `c`, wrap the statement in a block: `{ c = ... }`
= help: or introduce a new variable with `let`: `{ let c = ... }`
= note: assignments like `c = ...` are only valid as standalone statements
= help: for more information about this error, try `compose explain E0002`

error[E0006]: expected a semicolon after a statement
┌─ main.cmps:1:21
1 │ let x = foo(a c = d)
│ ^ help: insert a semicolon here
= suggested fix: write a semicolon to terminate this statement:
`let x = foo(a c = d);`
= help: for more information about this error, try `compose explain E0006`

error: condition expressions require parentheses
┌─ main.cmps:2:12
2 │ let y = if true { 1 } else { 0 }
│ ^^^^ Expected an opening `(` before the condition
```
# The Compose Programming Language

```rust
error[E0004]: cannot reassign to a variable declared as immutable
┌─ main.cmps:3:1
1 │ let a;
│ - was defined as immutable here
2 │ a = "some value";
│ - first assignment occurred here
3 │ a = "another value";
│ ^ cannot reassign an immutable variable
= help: make the variable mutable by writing `let mut`
= note: variables are immutable by default
# compose::test::assert_eval(r#"
println("'Hello, world' from Compose!");
# "#);
```

* **Interpreter Design Inspired by Typst**
Architecture and runtime are heavily inspired by the [Typst](https://typst.app) compiler.

* **Influenced by "Writing an Interpreter in Go"**
Thorsten Ball’s book provided foundational parsing and interpreter concepts.

---

## 🌟 Key Features of Compose

Compose is **expressive, fault-tolerant, and developer-friendly**. Highlights:

### 🧱 Statements

* **Expression Statements:** Useful for side effects.

```compose
3 + 4; // evaluates but does nothing
println("hi"); // prints "hi"
```

* **Let Bindings:** Immutable by default; mutable with `let mut`.

```compose
let x = 42;
let mut y = 5;
y = 6; // allowed
```
Compose is a functionally flavoured interpreted programming language with Rust-like syntax.

* **Assignments:** Must target existing variables. Compose provides clear errors for undeclared or immutable targets.
Features:
- Expression focused: blocks and control flow (`if`, `match`, loops) are expressions and produce values.
- Functions as first-class citizens
- High quality diagnostics
- Variables are immutable by default
- Garbage collection
- Portable: runs on any platform that supports rust

---
<div class="warning">

### ✨ Expressions
Compose is being developed as a hobby project and is not intended for production use.

Compose treats almost everything as an expression, making code **flexible and composable**.
</div>

* **Literals & Variables:**
For more thorough documentation about the language, embedding the language in your application, or using the CLI check out the [docs](https://dutch-raptor.github.io/compose/compose/).

```compose
42; true; "hello"; x;
```
# CLI Quick start

* **Arithmetic & Logical Operators:**
If you want to try out Compose, the easiest way is to use the CLI.

```compose
1 + 2 * 3;
x > 5 && y != 0;
```
The `compose` CLI lets you run Compose programs, explore them interactively, and inspect errors.

* **Functions & Closures:** First-class values, can be assigned, passed, or returned.

```compose
let add = { x, y => x + y; };
add(1, 2); // returns 3
## Installation

let make_adder = { x => { y => x + y; } };
let add_one = make_adder(1);
add_one(2); // returns 3
```
Install the latest version from GitHub using [Cargo](https://doc.rust-lang.org/cargo/):

* **Blocks:** Return the value of the last expression.

```compose
let result = { let y = 3; y + 1; }; // 4
```

* **Conditionals:** `if` expressions produce a value.

```compose
let x = if a > 10 { "big" } else { "small" };
```

* **Loops:** `while` and `for` can return a value using `break`.

```compose
let result = {
let mut i = 0;
while true {
if i == 5 { break i * 2; }
i += 1;
}
}; // 10
```

---

### 🗂 Expression Summary

| Type | Returns a Value? | Example |
| ------------------ | ---------------- | ---------------------------------- |
| Literal | ✅ | `42`, `"hi"` |
| Variable Reference | ✅ | `x` |
| Arithmetic | ✅ | `1 + 2 * 3` |
| Function Call | ✅ | `add(1, 2)` |
| Block `{ ... }` | ✅ | `{ let x = 2; x + 1 }` |
| `if` / `else` | ✅ | `if x > 0 { "yes" } else { "no" }` |
| `while` / `for` | ✅ via `break` | `while cond { break 5; }` |
| Closure | ✅ | `{ x => x + 1 }` |

> **Philosophy:** if it does something, it probably returns something too.

---

## 🔍 Why Compose Exists

Compose is a **learning exercise**:
```bash
cargo install --git https://github.com/Dutch-Raptor/compose.git
```

* Explore **language design**, parsing, and interpreter internals.
* Build **robust developer tools** with IDE-quality feedback.
* Experiment with:
After installation, the `compose` executable will be available on your PATH:

* Error recovery that doesn’t compromise correctness
* Clear diagnostics with fix suggestions
* Garbage Collection
* Compile-time checked documentation
```bash
compose --version
```

---
## Usage

## 🛠️ Current State
Create a new file called `hello.cmps` with the following contents:

* ✅ Hand-written fault-tolerant parser (CST-based)
* ✅ Interpreter with value/reference model
* ✅ Support for arrays, maps, functions, closures, boxed values
* ✅ Support for cyclic data structures in runtime
* ✅ Garbage collector (mark-and-sweep)
* 🚧 Import system (single-evaluation, no cycles, partial)
* 🚧 Iterators (some combinators implemented)
* 🚧 Standard library growth
* 🚧 Error message improvements and linter-like suggestions
```rust
println("Hello, from Compose!")
```

---
and run it:

## 📚 References & Inspirations
```bash
compose file hello.cmps
```

* [Typst](https://typst.app): Clean interpreter & VM design
* *Writing an Interpreter in Go* by Thorsten Ball
* [codespan-reporting](http://github.com/brendanzab/codespan) and [Spade Lang fork](https://gitlab.com/spade-lang/codespan)
This should print `Hello, from Compose!` to the console.

---
### Run a file

## 🔮 Roadmap Ideas
```bash
compose file examples/hello.cmps
```

* IDE integration via LSP
* More expressive types and pattern matching
* Inline documentation and REPL
* Educational visualizations for debugging
### Start a REPL

---
```bash
compose repl
```

## 🤝 Contributing
Load a file in the REPL:

Primarily a **personal learning project**, but feedback and discussions are welcome.
```bash
compose repl --from examples/prelude.cmps
```

---
### Explain an error

## License
```bash
compose explain E0003
```

Compose is licensed under the [Apache License 2.0](LICENSE).
You are free to use, modify, and distribute Compose in personal or commercial projects, provided you include the license text and notices.
This gives examples, explanations, and suggested fixes.
2 changes: 1 addition & 1 deletion compose-cli/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub fn file(args: FileArgs) -> Result<(), CliError> {
crate::print_diagnostics(&world, &[], &warnings).unwrap();
}

let Warned { value, warnings } = compose_eval::eval(&source, &mut vm, &EvalConfig::default());
let Warned { value, warnings } = compose_eval::eval_source(&source, &mut vm, &EvalConfig::default());

if let Err(err) = value {
crate::print_diagnostics(&world, &err, &warnings).unwrap();
Expand Down
4 changes: 2 additions & 2 deletions compose-cli/src/repl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ fn eval_initial_pass(vm: &mut Machine, world: &SystemWorld) {
// Do not return early if there are any errors, as we want to print all diagnostics.
for i in 0..source.nodes().len() {
let Warned { value, warnings } =
compose_eval::eval_range(&source, i..i + 1, vm, &EvalConfig::default());
compose_eval::eval_source_range(&source, i..i + 1, vm, &EvalConfig::default());
crate::print_diagnostics(world, &[], &warnings).unwrap();
if let Err(err) = value {
crate::print_diagnostics(world, &err, &warnings).unwrap();
Expand Down Expand Up @@ -212,7 +212,7 @@ pub fn eval_repl_input(vm: &mut Machine, world: &SystemWorld, input: &str, args:
crate::print_diagnostics(world, &[], &syntax_warnings).unwrap();
}

let Warned { value, warnings } = compose_eval::eval_range(
let Warned { value, warnings } = compose_eval::eval_source_range(
&source,
len_before_edit..len_after_edit,
vm,
Expand Down
2 changes: 1 addition & 1 deletion compose-doc/src/realise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub(crate) fn eval_code(code: &str) -> EvalResult {
let world = DocWorld::from_str(code);
let mut vm = Machine::new(&world);

let Warned { value, warnings } = compose_eval::eval(
let Warned { value, warnings } = compose_eval::eval_source(
&world.source,
&mut vm,
&EvalConfig {
Expand Down
4 changes: 3 additions & 1 deletion compose-eval/src/expression/array.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::{Eval, Machine};
use compose_library::diag::SourceResult;
use compose_library::{ArrayValue, IntoValue, Value, Vm};
use compose_library::{ Value, Vm};
use compose_library::foundations::cast::IntoValue;
use compose_library::foundations::types::ArrayValue;
use compose_syntax::ast;
use crate::evaluated::{Evaluated, ValueEvaluatedExtensions};

Expand Down
3 changes: 2 additions & 1 deletion compose-eval/src/expression/assignment.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::access::Access;
use crate::{Eval, Machine};
use compose_library::diag::{bail, At, SourceResult};
use compose_library::{ops, Value};
use compose_library::{Value};
use compose_library::foundations::ops;
use compose_syntax::ast;
use compose_syntax::ast::{AssignOp, AstNode};
use crate::evaluated::Evaluated;
Expand Down
2 changes: 1 addition & 1 deletion compose-eval/src/expression/atomic.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::vm::Machine;
use crate::Eval;
use compose_library::diag::SourceResult;
use compose_library::IntoValue;
use compose_library::foundations::cast::IntoValue;
use compose_syntax::ast;
use crate::evaluated::{Evaluated, ValueEvaluatedExtensions};

Expand Down
3 changes: 2 additions & 1 deletion compose-eval/src/expression/binary.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::vm::Machine;
use crate::Eval;
use compose_library::diag::{bail, At, SourceResult};
use compose_library::{ops, Value};
use compose_library::{Value};
use compose_library::foundations::ops;
use compose_syntax::ast;
use compose_syntax::ast::{AstNode, BinOp};
use crate::evaluated::{Evaluated, ValueEvaluatedExtensions};
Expand Down
8 changes: 5 additions & 3 deletions compose-eval/src/expression/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::expression::pattern::{PatternContext, PatternMatchResult};
use crate::vm::{ErrorMode, Machine};
use crate::Eval;
use compose_library::diag::{bail, At, SourceResult};
use compose_library::{BindingKind, Visibility};
use compose_library::foundations::scope::{BindingKind, Visibility};
use compose_syntax::ast;
use compose_syntax::ast::AstNode;
use crate::evaluated::Evaluated;
Expand Down Expand Up @@ -72,10 +72,12 @@ impl<'a> Eval for ast::LetBinding<'a> {

#[cfg(test)]
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, Value};
use compose_library::foundations::scope::BindingKind;
use compose_library::foundations::types::UnitValue;
use compose_library::Value;
use crate::Machine;

#[test]
fn test_let_binding() {
Expand Down
Loading