initial pest grammar and parser

This commit is contained in:
koehr 2025-06-21 08:57:57 +02:00
commit dd42ef23d4
9 changed files with 1780 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

212
Cargo.lock generated Normal file
View file

@ -0,0 +1,212 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "pest"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
dependencies = [
"pest",
"sha2",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "solace"
version = "0.1.0"
dependencies = [
"lazy_static",
"pest",
"pest_derive",
]
[[package]]
name = "syn"
version = "2.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"

9
Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "solace"
version = "0.1.0"
edition = "2024"
[dependencies]
lazy_static = "1.5.0"
pest = "2.8.1"
pest_derive = "2.8.1"

250
README.md Normal file
View file

@ -0,0 +1,250 @@
# Solace
A language to find solace after using JavaScript
## Proposal
A strongly typed programming language that compiles to JavaScript and comes with the following features:
* Error unions: `Error!string`
* Optional values: `?string`
* scoped blocks
* exhaustive pattern matching
* "unreachable" as keyword
* defer statements
* built-in reactivity system (using signals?)
## Syntax
Solace looks like Zig, but without the low-level complexities and added
pattern matching. Typical expressions are very much like in TypeScript and it
supports the same primitive types undefined, string, number, boolean, and
their array versions, as well as Map and Set. It uses none instead of null and
objects are called structs. There is no "new" keyword and there are no classes.
Instead of classes, Solace uses scopes that work just like JavaScript's scopes.
It also looks very similar to TypeScript, if you want:
```solace
fn sum(a: number, b?: number): number {
if b is undefined {
return a
}
return a + b
}
```
// But also supports Optionals on a syntax level:
```solace
fn sum(a: number, b: ?number): number {
if b is none { // here b has to be defined, but can be set to none
return a
}
return a + b
}
```
// And Error unions:
```solace
fn sum(a: number, b?: number): !number {
if b is undefined {
return failure("Second number is missing")
}
return success(a + b)
}
```
Error unions default to Error, so the last example is equivalent to this one:
```solace
fn sum(a: number, b?: number): Error!number {
if b is undefined {
return failure("Second number is missing", Error)
}
return a + b
}
```
A more specific error might be needed, though. Although Solace doesn't really
use classes, it can handle inheritance:
```solace
// The following lines compile to:
// function NumberError(message) {
// this.name = "NumberError"
// this.message = message
// }
// NumberError.prototype = Error
fn NumberError(message: string) extends Error {
_.name = "NumberError"
_.message = message
}
fn sum(a: number, b?: number): NumberError!number {
if b is undefined {
return failure("Second number is missing", Error)
}
return a + b
}
```
Solace also supports initialized default return values, similar to swifts inout parameters.
They are really useful for short functions:
```solace
fn fizzbuzz(until: number) => (output: string[] = []) {
// var output = [] // no need, because it is already defined
for i in 1..=until { // this is an inclusive range
// pattern matching
match i {
_ % 3 == 0 && _ % 5 == 0 -> output.push("fizzbuzz")
_ % 3 == 0 -> output.push("fizz")
_ % 5 == 0 -> output.push("buzz")
_ -> output.push(i.toString())
}
}
// no need, because it's the default return value, but can added for clarity
// return output
}
```
Matches are expressions, which means, they return a value. Also, just for
completeness, match doesn't need a value. So here is the last example changed
to take both into account:
```solace
fn numberToFizzBuzz(n: number): string {
return match {
i % 3 == 0 && i % 5 == 0 -> "fizzbuzz"
i % 3 == 0 -> "fizz"
i % 5 == 0 -> "buzz"
i -> i.toString()
}
}
fn fizzbuzz(until: number) => (output: string[] = []) {
for i in i..=until { // an exclusive range would be i..<until
output.push(numberToFizzBuzz(i))
}
}
```
If can be used as a statement or expression:
```solace
fn twentyIfTrue(x: boolean): ?number {
const value = if x {
20
} else {
none
}
return value
}
```
Of course this could be written with less lines. Here with a named return variable.
```solace
fn twentyIfTrue(x: boolean) => (value: ?number) {
value = if x { 20 } else { none }
// or
value = x ? 20 : none
// or
value = if x: 20
else: none
}
```
An if expression always needs to cover all cases to ensure that the variable
will be assigned. If statements don't have that requirement, but then the
return value needs to be initialized:
```solace
fn twentyIfTrue(x: boolean) => (value: ?number = none) {
if x: value = 20
}
```
Now to the more interesting part: Reactivity and unified handling of values.
```solace
import fs from "node:fs/promises"
fn openFile(path: string) {
// fs.open returns a Promise, and while Solace supports them, it doesn't need
// Promises or async/await, because it has live values.
// Promises (and with them return values of async functions) are translated
// to reactive values if used like this:
live fd = fs.open(path, "rw")
// This defer statements runs after the function finished, but only if the
// given pattern matches, in this case if ok(fd) returns a value.
defer when ok(fd) {
// Because it is a nameless assignment, we use _ to
// access the return value. If you need a name, use:
// defer when ok(fd) |file| { /*...*/ }
_.close() // close file descriptor
}
// This defer statement always runs after everything else finished.
// You can see it like try-finally, for functions.
defer {
console.log("This message will show after the function finished.")
}
// live variables would be defined like this in TypeScript:
// { state: "pending", value: Promise<T>, error: null } |
// { state: "err", value: null, error: Error } |
// { state: "ok", value: T, error: null }
watch fd {
match fd {
// ok(x) returns the value of Result types like live, which is an Error
// union essentially, while err(x) returns the error or null
ok(fd) -> handleFile(_)
err(fd) -> handleError(_)
_ -> continue // matches need to handle all possible cases
}
}
}
```
Here is another way to write the former example:
```solace
import fs from "node:fs/promises"
fn openFile(path: string) {
live fd = fs.open(path, "rw")
// this defer executes only if at the end of the function ok(fd) returns a value
defer when ok(fd) |file| {
console.log("File closed.")
file.close() // close file descriptor
}
// live variables would be defined like this in TypeScript:
// { state: "pending", value: Promise<T>, error: null } |
// { state: "err", value: null, error: Error } |
// { state: "ok", value: T, error: null }
watch fd {
match fd {
// ok(x) returns the value of Result types like live, which is an Error
// union essentially, while err(x) returns the error or null
ok(fd) -> handleFile(_)
err(fd) -> handleError(_)
_ -> continue // matches need to handle all possible cases
}
}
}
```
Here some more loop constructs:
```solace
const myArray: number[] = [23, 42]
for x in myArray: console.log(x)
for y,i in myArray: console.log(`Index ${i} has value ${y}!`)
for i in 0..<23 {
console.log("This will output as long as i is smaller than 23")
}
for i in 0..=23 {
console.log("This will output as long as i is smaller or equal than 23")
}
while i <= 23: console.log("This will output forever, because I forgot to change the value of i")
while i < 23 {
console.log("This will stop when i reaches 22. Currently it has the value", i++)
}
```

561
src/main.rs Normal file
View file

@ -0,0 +1,561 @@
use std::fs;
use pest::iterators::{Pair, Pairs};
use pest::pratt_parser::{Assoc, Op, PrattParser};
use pest::Parser;
use pest_derive::Parser;
#[derive(Parser)]
#[grammar = "solace.pest"]
pub struct SolaceParser;
#[derive(Debug, Clone)]
pub enum Expr {
// Literals
Number(f64),
String(String),
Boolean(bool),
None,
Undefined,
Underscore,
// Variables and calls
Identifier(String),
MemberAccess(Box<Expr>, String),
Index(Box<Expr>, Box<Expr>),
Call(Box<Expr>, Vec<Expr>),
// Operators
Binary(BinaryOp, Box<Expr>, Box<Expr>),
Unary(UnaryOp, Box<Expr>),
Ternary(Box<Expr>, Box<Expr>, Box<Expr>),
Assignment(String, Box<Expr>),
// Control flow
If(Box<Expr>, Box<Block>, Option<Box<Block>>),
Match(Option<Box<Expr>>, Vec<MatchArm>),
// Collections
Array(Vec<Expr>),
// Postfix
PostIncrement(Box<Expr>),
PostDecrement(Box<Expr>),
}
#[derive(Debug, Clone)]
pub enum BinaryOp {
Add, Sub, Mul, Div, Mod,
Eq, Ne, Lt, Gt, Le, Ge,
And, Or,
RangeInclusive, RangeExclusive,
Is,
}
#[derive(Debug, Clone)]
pub enum UnaryOp {
Not, Neg, PreIncrement, PreDecrement,
}
#[derive(Debug, Clone)]
pub struct MatchArm {
pattern: MatchPattern,
body: Expr,
}
#[derive(Debug, Clone)]
pub enum MatchPattern {
Wildcard,
Expression(Expr),
Condition(Vec<(BinaryOp, Expr)>),
}
#[derive(Debug, Clone)]
pub struct Block {
statements: Vec<Statement>,
}
#[derive(Debug, Clone)]
pub enum ReturnType {
Simple(Type),
Named {
name: String,
type_annotation: Type,
default_value: Option<Expr>,
}
}
#[derive(Debug, Clone)]
pub enum Statement {
Import { name: String, from: String },
Function {
name: String,
params: Vec<Param>,
return_type: Option<ReturnType>,
extends: Option<String>,
body: Block,
},
Variable {
kind: VarKind,
name: String,
type_annotation: Option<Type>,
value: Expr,
},
Defer {
condition: Option<Expr>,
binding: Option<String>,
body: Block,
},
Watch {
target: String,
body: Block,
},
Return(Option<Expr>),
If(Expr, Box<Statement>, Option<Box<Statement>>),
For {
var: String,
index: Option<String>,
iterable: Expr,
body: Box<Statement>,
},
While(Expr, Box<Statement>),
Expression(Expr),
Block(Block),
}
#[derive(Debug, Clone)]
pub enum VarKind {
Const, Var, Live,
}
#[derive(Debug, Clone)]
pub struct Param {
name: String,
type_annotation: Option<Type>,
}
#[derive(Debug, Clone)]
pub enum Type {
Primitive(String),
Array(Box<Type>),
Optional(Box<Type>),
ErrorUnion(Option<String>, Box<Type>),
Named(String),
}
pub struct SolacePrattParser {
pratt: PrattParser<Rule>,
}
impl SolacePrattParser {
pub fn new() -> Self {
let pratt = PrattParser::new()
.op(Op::infix(Rule::assign, Assoc::Right))
.op(Op::infix(Rule::question, Assoc::Right) | Op::infix(Rule::colon, Assoc::Right))
.op(Op::infix(Rule::or, Assoc::Left))
.op(Op::infix(Rule::and, Assoc::Left))
.op(Op::infix(Rule::eq, Assoc::Left) | Op::infix(Rule::ne, Assoc::Left) | Op::infix(Rule::is_kw, Assoc::Left))
.op(Op::infix(Rule::lt, Assoc::Left) | Op::infix(Rule::gt, Assoc::Left) |
Op::infix(Rule::le, Assoc::Left) | Op::infix(Rule::ge, Assoc::Left))
.op(Op::infix(Rule::range_inclusive, Assoc::Left) |
Op::infix(Rule::range_exclusive, Assoc::Left))
.op(Op::infix(Rule::plus, Assoc::Left) | Op::infix(Rule::minus, Assoc::Left))
.op(Op::infix(Rule::multiply, Assoc::Left) | Op::infix(Rule::divide, Assoc::Left) |
Op::infix(Rule::modulo, Assoc::Left))
.op(Op::prefix(Rule::not) | Op::prefix(Rule::minus) |
Op::prefix(Rule::increment) | Op::prefix(Rule::decrement))
.op(Op::postfix(Rule::increment) | Op::postfix(Rule::decrement));
SolacePrattParser { pratt }
}
pub fn parse_expr(&self, pairs: Pairs<Rule>) -> Result<Expr, String> {
self.pratt
.map_primary(|primary| self.parse_primary(primary))
.map_infix(|lhs, op, rhs| self.parse_infix(lhs, op, rhs))
.map_prefix(|op, rhs| self.parse_prefix(op, rhs))
.map_postfix(|lhs, op| self.parse_postfix(lhs, op))
.parse(pairs)
}
fn parse_primary(&self, pair: Pair<Rule>) -> Result<Expr, String> {
match pair.as_rule() {
Rule::number_literal => {
let num = pair.as_str().parse::<f64>()
.map_err(|_| "Invalid number")?;
Ok(Expr::Number(num))
}
Rule::string_literal => {
let s = pair.as_str();
Ok(Expr::String(s[1..s.len()-1].to_string()))
}
Rule::boolean_literal => {
Ok(Expr::Boolean(pair.as_str() == "true"))
}
Rule::none_kw => Ok(Expr::None),
Rule::undefined_kw => Ok(Expr::Undefined),
Rule::underscore => Ok(Expr::Underscore),
Rule::identifier => Ok(Expr::Identifier(pair.as_str().to_string())),
Rule::array_literal => {
let mut elements = vec![];
for inner in pair.into_inner() {
if inner.as_rule() == Rule::expression {
elements.push(self.parse_expr(inner.into_inner())?);
}
}
Ok(Expr::Array(elements))
}
Rule::if_expr => self.parse_if_expr(pair),
Rule::match_expr => self.parse_match_expr(pair),
_ => Err(format!("Unexpected primary: {:?}", pair.as_rule()))
}
}
fn parse_infix(&self, lhs: Result<Expr, String>, op: Pair<Rule>, rhs: Result<Expr, String>) -> Result<Expr, String> {
let lhs = lhs?;
let rhs = rhs?;
match op.as_rule() {
Rule::plus => Ok(Expr::Binary(BinaryOp::Add, Box::new(lhs), Box::new(rhs))),
Rule::minus => Ok(Expr::Binary(BinaryOp::Sub, Box::new(lhs), Box::new(rhs))),
Rule::multiply => Ok(Expr::Binary(BinaryOp::Mul, Box::new(lhs), Box::new(rhs))),
Rule::divide => Ok(Expr::Binary(BinaryOp::Div, Box::new(lhs), Box::new(rhs))),
Rule::modulo => Ok(Expr::Binary(BinaryOp::Mod, Box::new(lhs), Box::new(rhs))),
Rule::eq => Ok(Expr::Binary(BinaryOp::Eq, Box::new(lhs), Box::new(rhs))),
Rule::ne => Ok(Expr::Binary(BinaryOp::Ne, Box::new(lhs), Box::new(rhs))),
Rule::lt => Ok(Expr::Binary(BinaryOp::Lt, Box::new(lhs), Box::new(rhs))),
Rule::gt => Ok(Expr::Binary(BinaryOp::Gt, Box::new(lhs), Box::new(rhs))),
Rule::le => Ok(Expr::Binary(BinaryOp::Le, Box::new(lhs), Box::new(rhs))),
Rule::ge => Ok(Expr::Binary(BinaryOp::Ge, Box::new(lhs), Box::new(rhs))),
Rule::and => Ok(Expr::Binary(BinaryOp::And, Box::new(lhs), Box::new(rhs))),
Rule::or => Ok(Expr::Binary(BinaryOp::Or, Box::new(lhs), Box::new(rhs))),
Rule::is_kw => Ok(Expr::Binary(BinaryOp::Is, Box::new(lhs), Box::new(rhs))),
Rule::range_inclusive => Ok(Expr::Binary(BinaryOp::RangeInclusive, Box::new(lhs), Box::new(rhs))),
Rule::range_exclusive => Ok(Expr::Binary(BinaryOp::RangeExclusive, Box::new(lhs), Box::new(rhs))),
Rule::assign => {
if let Expr::Identifier(name) = lhs {
Ok(Expr::Assignment(name, Box::new(rhs)))
} else {
Err("Left side of assignment must be identifier".to_string())
}
}
Rule::question => {
// Handle ternary - need to parse the rest
// This is simplified - in practice you'd need more complex handling
Ok(Expr::Ternary(Box::new(lhs), Box::new(rhs), Box::new(Expr::None)))
}
_ => Err(format!("Unexpected infix operator: {:?}", op.as_rule()))
}
}
fn parse_prefix(&self, op: Pair<Rule>, rhs: Result<Expr, String>) -> Result<Expr, String> {
let rhs = rhs?;
match op.as_rule() {
Rule::not => Ok(Expr::Unary(UnaryOp::Not, Box::new(rhs))),
Rule::minus => Ok(Expr::Unary(UnaryOp::Neg, Box::new(rhs))),
Rule::increment => Ok(Expr::Unary(UnaryOp::PreIncrement, Box::new(rhs))),
Rule::decrement => Ok(Expr::Unary(UnaryOp::PreDecrement, Box::new(rhs))),
_ => Err(format!("Unexpected prefix operator: {:?}", op.as_rule()))
}
}
fn parse_postfix(&self, lhs: Result<Expr, String>, op: Pair<Rule>) -> Result<Expr, String> {
let lhs = lhs?;
match op.as_rule() {
Rule::increment => Ok(Expr::PostIncrement(Box::new(lhs))),
Rule::decrement => Ok(Expr::PostDecrement(Box::new(lhs))),
_ => Err(format!("Unexpected postfix operator: {:?}", op.as_rule()))
}
}
fn parse_if_expr(&self, pair: Pair<Rule>) -> Result<Expr, String> {
let mut inner = pair.into_inner();
let condition = self.parse_expr(inner.next().unwrap().into_inner())?;
let then_block = self.parse_block(inner.next().unwrap())?;
let else_block = inner.next().map(|p| self.parse_block(p)).transpose()?;
Ok(Expr::If(Box::new(condition), Box::new(then_block), else_block.map(Box::new)))
}
fn parse_match_expr(&self, pair: Pair<Rule>) -> Result<Expr, String> {
let mut inner = pair.into_inner();
let target = if let Some(p) = inner.peek() {
if p.as_rule() == Rule::expression {
Some(Box::new(self.parse_expr(inner.next().unwrap().into_inner())?))
} else {
None
}
} else {
None
};
let mut arms = vec![];
for arm_pair in inner {
if arm_pair.as_rule() == Rule::match_arm {
arms.push(self.parse_match_arm(arm_pair)?);
}
}
Ok(Expr::Match(target, arms))
}
fn parse_match_arm(&self, pair: Pair<Rule>) -> Result<MatchArm, String> {
let mut inner = pair.into_inner();
let pattern = self.parse_match_pattern(inner.next().unwrap())?;
let body = self.parse_expr(inner.next().unwrap().into_inner())?;
Ok(MatchArm { pattern, body })
}
fn parse_match_pattern(&self, pair: Pair<Rule>) -> Result<MatchPattern, String> {
// Simplified pattern parsing
match pair.as_rule() {
Rule::underscore => Ok(MatchPattern::Wildcard),
_ => Ok(MatchPattern::Expression(self.parse_expr(pair.into_inner())?))
}
}
fn parse_block(&self, pair: Pair<Rule>) -> Result<Block, String> {
let mut statements = vec![];
for stmt in pair.into_inner() {
statements.push(self.parse_statement(stmt)?);
}
Ok(Block { statements })
}
fn parse_statement(&self, pair: Pair<Rule>) -> Result<Statement, String> {
match pair.as_rule() {
Rule::expression_stmt => {
let expr = self.parse_expr(pair.into_inner())?;
Ok(Statement::Expression(expr))
}
Rule::return_stmt => {
let mut inner = pair.into_inner();
let expr = inner.next().map(|p| self.parse_expr(p.into_inner())).transpose()?;
Ok(Statement::Return(expr))
}
// Add other statement parsing here
_ => Err(format!("Unimplemented statement type: {:?}", pair.as_rule()))
}
}
fn parse_type(&self, pair: Pair<Rule>) -> Result<Type, String> {
match pair.as_rule() {
Rule::type_annotation => {
// Type annotation starts with colon, skip it
let mut inner = pair.into_inner();
inner.next(); // skip colon
self.parse_type_expr(inner.next().unwrap())
}
Rule::type_expr => {
self.parse_type_expr(pair)
}
_ => Err(format!("Expected type annotation or type expression, got {:?}", pair.as_rule()))
}
}
fn parse_type_expr(&self, pair: Pair<Rule>) -> Result<Type, String> {
let inner = pair.into_inner();
let mut current_type = None;
let mut is_optional = false;
let mut error_type = None;
let mut array_depth = 0;
for part in inner {
match part.as_rule() {
Rule::optional_prefix => {
is_optional = true;
}
Rule::error_union_prefix => {
// Could be just "!" or "ErrorType!"
let prefix_inner = part.into_inner();
if let Some(error_name) = prefix_inner.peek() {
error_type = Some(error_name.as_str().to_string());
} else {
error_type = Some("Error".to_string()); // Default error type
}
}
Rule::base_type => {
current_type = Some(self.parse_base_type(part)?);
}
Rule::array_suffix => {
array_depth += 1;
}
_ => {}
}
}
let mut result_type = current_type.ok_or("No base type found")?;
// Apply array suffixes
for _ in 0..array_depth {
result_type = Type::Array(Box::new(result_type));
}
// Apply error union
if let Some(err_type) = error_type {
result_type = Type::ErrorUnion(Some(err_type), Box::new(result_type));
}
// Apply optional
if is_optional {
result_type = Type::Optional(Box::new(result_type));
}
Ok(result_type)
}
fn parse_base_type(&self, pair: Pair<Rule>) -> Result<Type, String> {
let mut inner = pair.into_inner();
let type_part = inner.next().unwrap();
match type_part.as_rule() {
Rule::primitive_type => {
Ok(Type::Primitive(type_part.as_str().to_string()))
}
Rule::identifier => {
Ok(Type::Named(type_part.as_str().to_string()))
}
_ => Err(format!("Unexpected base type: {:?}", type_part.as_rule()))
}
}
fn parse_param_list(&self, pair: Pair<Rule>) -> Result<Vec<Param>, String> {
let mut params = vec![];
for inner in pair.into_inner() {
if inner.as_rule() == Rule::param {
params.push(self.parse_param(inner)?);
}
}
Ok(params)
}
fn parse_param(&self, pair: Pair<Rule>) -> Result<Param, String> {
let mut inner = pair.into_inner();
let name = inner.next().unwrap().as_str().to_string();
let type_annotation = if let Some(type_pair) = inner.next() {
Some(self.parse_type(type_pair)?)
} else {
None
};
Ok(Param { name, type_annotation })
}
fn parse_return_type(&self, pair: Pair<Rule>) -> Result<ReturnType, String> {
let mut inner = pair.into_inner();
let first = inner.next().unwrap();
match first.as_rule() {
Rule::arrow_fat => {
// Named return variable: => (name: Type = default?)
let name = inner.next().unwrap().as_str().to_string();
let type_annotation = self.parse_type(inner.next().unwrap())?;
let default_value = if let Some(expr_pair) = inner.next() {
Some(self.parse_expr(expr_pair.into_inner())?)
} else {
None
};
Ok(ReturnType::Named {
name,
type_annotation,
default_value,
})
}
Rule::type_annotation => {
// Simple return type: : Type
Ok(ReturnType::Simple(self.parse_type(first)?))
}
_ => Err(format!("Unexpected return type: {:?}", first.as_rule()))
}
}
fn parse_function_decl(&self, pair: Pair<Rule>) -> Result<Statement, String> {
let mut inner = pair.into_inner();
// Skip 'fn' keyword
inner.next();
let name = inner.next().unwrap().as_str().to_string();
let params = self.parse_param_list(inner.next().unwrap())?;
let mut extends = None;
let mut return_type = None;
let mut body = None;
while let Some(remaining) = inner.next() {
match remaining.as_rule() {
Rule::extends_kw => {
extends = Some(inner.next().unwrap().as_str().to_string());
}
Rule::return_type => {
return_type = Some(self.parse_return_type(remaining)?);
}
Rule::block => {
body = Some(self.parse_block(remaining)?);
}
_ => {}
}
}
Ok(Statement::Function {
name,
params,
return_type,
extends,
body: body.ok_or("Function body required")?,
})
}
}
/* Usage example
pub fn parse_solace_code(input: &str) -> Result<Vec<Statement>, String> {
let pairs = SolaceParser::parse(Rule::program, input)
.map_err(|e| format!("Parse error: {}", e))?;
let parser = SolacePrattParser::new();
let mut statements = vec![];
for pair in pairs {
if pair.as_rule() == Rule::program {
for stmt_pair in pair.into_inner() {
if stmt_pair.as_rule() != Rule::EOI {
statements.push(parser.parse_statement(stmt_pair)?);
}
}
}
}
Ok(statements)
}
*/
fn main() {
let unparsed_file = fs::read_to_string("test.solace").expect("Cannot read test file");
match SolaceParser::parse(Rule::program, &unparsed_file) {
Ok(mut pairs) => {
let program = pairs.next().unwrap();
println!("Parsing was successful.");
print_parse_tree(program, 0);
}
Err(err) => {
println!("Parse error: {}", err);
}
}
}
fn print_parse_tree(pair: Pair<Rule>, indent: usize) {
let indent_str = " ".repeat(indent);
println!("{}{:?}: \"{}\"", indent_str, pair.as_rule(), pair.as_str());
for inner_pair in pair.into_inner() {
print_parse_tree(inner_pair, indent + 1)
}
}

182
src/main.rs.bak Normal file
View file

@ -0,0 +1,182 @@
use std::fs;
use pest::Parser;
use pest::pratt_parser::{Op, Assoc, PrattParser};
use lazy_static::lazy_static;
use pest_derive::Parser;
#[derive(Parser)]
#[grammar = "solace.pest"]
pub struct SolaceParser;
lazy_static! {
static ref PRATT_PARSER: PrattParser<Rule> = {
use Rule::*;
use Assoc::Left;
PrattParser::new()
// Logical OR (lowest precedence)
.op(Op::infix(or, Left))
// Logical AND
.op(Op::infix(and, Left))
// Equality
.op(Op::infix(eq, Left) | Op::infix(neq, Left))
// Comparison
.op(Op::infix(lt, Left) | Op::infix(gt, Left) |
Op::infix(lte, Left) | Op::infix(gte, Left))
// Range operators
.op(Op::infix(range_inclusive, Left) | Op::infix(range_exclusive, Left))
// Additive
.op(Op::infix(add, Left) | Op::infix(subtract, Left))
// Multiplicative
.op(Op::infix(multiply, Left) | Op::infix(divide, Left) | Op::infix(modulo, Left))
// Unary operators
.op(Op::prefix(not) | Op::prefix(negate))
// Member access (highest precedence)
.op(Op::postfix(member_access))
// Function call (same precedence as member access)
.op(Op::postfix(call))
};
}
pub fn parse_expr(pair: pest::iterators::Pair<Rule>) -> Expr {
PRATT_PARSER
.map_primary(|primary| match primary.as_rule() {
Rule::number => Expr::Number(primary.as_str().parse().unwrap()),
Rule::string => Expr::String(primary.as_str().to_string()),
Rule::boolean => Expr::Boolean(primary.as_str() == "true"),
Rule::identifier => Expr::Ident(primary.as_str().to_string()),
Rule::array_literal => Expr::Array(parse_array(primary)),
Rule::struct_literal => Expr::Struct(parse_struct(primary)),
Rule::expr => parse_expr(primary),
_ => unreachable!("Unexpected primary expression: {:?}", primary),
})
.map_infix(|lhs, op, rhs| {
let op = match op.as_rule() {
Rule::add => BinOp::Add,
Rule::subtract => BinOp::Sub,
Rule::multiply => BinOp::Mul,
Rule::divide => BinOp::Div,
Rule::modulo => BinOp::Mod,
Rule::eq => BinOp::Eq,
Rule::neq => BinOp::Neq,
Rule::lt => BinOp::Lt,
Rule::gt => BinOp::Gt,
Rule::lte => BinOp::Lte,
Rule::gte => BinOp::Gte,
Rule::and => BinOp::And,
Rule::or => BinOp::Or,
Rule::range_inclusive => BinOp::RangeInclusive,
Rule::range_exclusive => BinOp::RangeExclusive,
_ => unreachable!(),
};
Expr::Binary(Box::new(lhs), op, Box::new(rhs))
})
.map_prefix(|op, rhs| {
let op = match op.as_rule() {
Rule::negate => UnOp::Neg,
Rule::not => UnOp::Not,
_ => unreachable!(),
};
Expr::Unary(op, Box::new(rhs))
})
.map_postfix(|lhs, op| {
match op.as_rule() {
Rule::field_access => {
let field = op.into_inner().next().unwrap();
Expr::FieldAccess(Box::new(lhs), field.as_str().to_string())
}
Rule::call => {
let args = op.into_inner()
.map(parse_expr)
.collect();
Expr::Call(Box::new(lhs), args)
}
_ => unreachable!(),
}
})
.parse(pair.into_inner())
}
// Example AST types (simplified)
#[derive(Debug)]
pub enum Expr {
Number(f64),
String(String),
Boolean(bool),
Ident(String),
Array(Vec<Expr>),
Struct(Vec<(String, Expr)>),
Binary(Box<Expr>, BinOp, Box<Expr>),
Unary(UnOp, Box<Expr>),
FieldAccess(Box<Expr>, String),
Call(Box<Expr>, Vec<Expr>),
}
#[derive(Debug)]
pub enum BinOp {
Add, Sub, Mul, Div, Mod,
Eq, Neq, Lt, Gt, Lte, Gte,
And, Or,
RangeInclusive, RangeExclusive,
}
#[derive(Debug)]
pub enum UnOp {
Neg, Not,
}
// Helper functions
fn parse_array(pair: pest::iterators::Pair<Rule>) -> Vec<Expr> {
pair.into_inner()
.filter(|p| p.as_rule() == Rule::expr)
.map(parse_expr)
.collect()
}
fn parse_struct(pair: pest::iterators::Pair<Rule>) -> Vec<(String, Expr)> {
pair.into_inner()
.filter(|p| p.as_rule() == Rule::struct_field)
.map(|f| {
let mut inner = f.into_inner();
let name = inner.next().unwrap().as_str().to_string();
let value = parse_expr(inner.next().unwrap());
(name, value)
})
.collect()
}
fn main() {
let unparsed_file = fs::read_to_string("test.solace").expect("Cannot read test file");
match SolaceParser::parse(Rule::program, &unparsed_file) {
Ok(mut pairs) => {
let program = pairs.next().unwrap();
println!("Parsing was successful.");
print_parse_tree(program, 0);
}
Err(err) => {
println!("Parse error: {}", err);
}
}
}
/* Example usage
pub fn parse(input: &str) -> Result<Vec<Expr>, pest::error::Error<Rule>> {
let pairs = SolaceParser::parse(Rule::program, input)?;
let exprs = pairs
.filter(|p| p.as_rule() == Rule::expr)
.map(parse_expr)
.collect();
Ok(exprs)
}
*/
fn print_parse_tree(pair: Pair<Rule>, indent: usize) {
let indent_str = " ".repeat(indent);
println!("{}{:?}: \"{}\"", indent_str, pair.as_rule(), pair.as_str());
for inner_pair in pair.into_inner() {
print_parse_tree(inner_pair, indent + 1)
}
}

242
src/solace.pest Normal file
View file

@ -0,0 +1,242 @@
// solace.pest - Pest Grammar for Solace Language (Fixed)
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
COMMENT = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" | "//" ~ (!NEWLINE ~ ANY)* }
NEWLINE = _{ "\n" | "\r\n" }
// Keywords
fn_kw = @{ "fn" ~ !identifier_char }
if_kw = @{ "if" ~ !identifier_char }
else_kw = @{ "else" ~ !identifier_char }
match_kw = @{ "match" ~ !identifier_char }
for_kw = @{ "for" ~ !identifier_char }
while_kw = @{ "while" ~ !identifier_char }
defer_kw = @{ "defer" ~ !identifier_char }
when_kw = @{ "when" ~ !identifier_char }
live_kw = @{ "live" ~ !identifier_char }
watch_kw = @{ "watch" ~ !identifier_char }
return_kw = @{ "return" ~ !identifier_char }
const_kw = @{ "const" ~ !identifier_char }
var_kw = @{ "var" ~ !identifier_char }
import_kw = @{ "import" ~ !identifier_char }
from_kw = @{ "from" ~ !identifier_char }
extends_kw = @{ "extends" ~ !identifier_char }
in_kw = @{ "in" ~ !identifier_char }
is_kw = @{ "is" ~ !identifier_char }
none_kw = @{ "none" ~ !identifier_char }
undefined_kw = @{ "undefined" ~ !identifier_char }
failure_kw = @{ "failure" ~ !identifier_char }
success_kw = @{ "success" ~ !identifier_char }
continue_kw = @{ "continue" ~ !identifier_char }
ok_kw = @{ "ok" ~ !identifier_char }
err_kw = @{ "err" ~ !identifier_char }
keyword = @{
(fn_kw | if_kw | else_kw | match_kw | for_kw | while_kw | defer_kw |
when_kw | live_kw | watch_kw | return_kw | const_kw | var_kw |
import_kw | from_kw | extends_kw | in_kw | is_kw | none_kw |
undefined_kw | failure_kw | success_kw | continue_kw | ok_kw | err_kw)
}
// Literals
string_literal = @{ "\"" ~ (!"\"" ~ ("\\\\" | "\\\"" | ANY))* ~ "\"" }
template_literal = @{ "`" ~ (!"`" ~ ("\\`" | ANY))* ~ "`" }
number_literal = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
boolean_literal = @{ ("true" | "false") ~ !identifier_char }
// Identifiers
identifier_char = _{ ASCII_ALPHANUMERIC | "_" }
identifier = @{ !(keyword | ASCII_DIGIT) ~ identifier_char+ }
underscore = @{ "_" }
// Operators
eq = @{ "==" }
ne = @{ "!=" }
le = @{ "<=" }
ge = @{ ">=" }
and = @{ "&&" }
or = @{ "||" }
arrow = @{ "->" }
arrow_fat = @{ "=>" }
plus = { "+" }
minus = { "-" }
multiply = { "*" }
divide = { "/" }
modulo = { "%" }
assign = { "=" }
lt = { "<" }
gt = { ">" }
not = { "!" }
question = { "?" }
colon = { ":" }
dot = { "." }
comma = { "," }
semicolon = { ";" }
pipe = { "|" }
// Range operators
range_inclusive = @{ "..=" }
range_exclusive = @{ "..<" }
range_op = { range_inclusive | range_exclusive }
comparison_op = { le | ge | eq | ne | lt | gt }
// Increment/Decrement
increment = @{ "++" }
decrement = @{ "--" }
// Delimiters
lparen = { "(" }
rparen = { ")" }
lbrace = { "{" }
rbrace = { "}" }
lbracket = { "[" }
rbracket = { "]" }
// Types - Fixed to avoid left recursion
primitive_type = { "string" | "number" | "boolean" | "undefined" }
base_type = { primitive_type | identifier }
// Type suffixes
array_suffix = { lbracket ~ rbracket }
optional_prefix = { question }
error_union_prefix = { identifier? ~ not }
// Type expression - now handles prefixes and suffixes properly
type_expr = {
optional_prefix? ~ error_union_prefix? ~ base_type ~ array_suffix* |
optional_prefix? ~ base_type ~ array_suffix*
}
// Type annotation
type_annotation = { colon ~ type_expr }
// Parameters
optional_suffix = { question }
param = { identifier ~ optional_suffix? ~ type_annotation? }
param_list = { lparen ~ (param ~ (comma ~ param)*)? ~ rparen }
// Return type with named return variable
return_type_simple = { type_annotation }
return_type_named = { arrow_fat ~ lparen ~ identifier ~ type_annotation ~ (assign ~ expression)? ~ rparen }
return_type = { return_type_simple | return_type_named }
// Expressions
primary_expr = {
underscore |
boolean_literal |
number_literal |
string_literal |
template_literal |
none_kw |
undefined_kw |
function_call_expr |
identifier |
lparen ~ expression ~ rparen |
array_literal |
if_expr |
match_expr |
continue_expr
}
// Function calls including ok() and err()
function_call_expr = { (ok_kw | err_kw | failure_kw | success_kw) ~ lparen ~ expression_list? ~ rparen }
array_literal = { lbracket ~ expression_list? ~ rbracket }
expression_list = { expression ~ (comma ~ expression)* }
// Member access and indexing
member_access = { dot ~ identifier }
index_access = { lbracket ~ expression ~ rbracket }
call_suffix = { lparen ~ expression_list? ~ rparen }
postfix_expr = { primary_expr ~ (member_access | index_access | call_suffix | increment | decrement)* }
unary_expr = { (not | minus | increment | decrement)* ~ postfix_expr }
multiplicative_expr = { unary_expr ~ ((multiply | divide | modulo) ~ unary_expr)* }
additive_expr = { multiplicative_expr ~ ((plus | minus) ~ multiplicative_expr)* }
range_expr = { additive_expr ~ (range_op ~ additive_expr)? }
relational_expr = { range_expr ~ (comparison_op ~ range_expr)* }
equality_expr = { relational_expr ~ (is_kw ~ relational_expr)* }
logical_and_expr = { equality_expr ~ (and ~ equality_expr)* }
logical_or_expr = { logical_and_expr ~ (or ~ logical_and_expr)* }
ternary_expr = { logical_or_expr ~ (question ~ expression ~ colon ~ expression)? }
assignment_expr = { lvalue ~ assign ~ expression }
lvalue = { identifier ~ (dot ~ identifier | lbracket ~ expression ~ rbracket)* }
expression = { assignment_expr | ternary_expr }
// If expression and statement
if_expr = { if_kw ~ expression ~ lbrace ~ statement* ~ rbrace ~ (else_kw ~ lbrace ~ statement* ~ rbrace)? }
if_expr_short = { if_kw ~ expression ~ colon ~ expression ~ (else_kw ~ colon ~ expression)? }
// Match expression
match_expr = { match_kw ~ expression? ~ lbrace ~ match_arm* ~ rbrace }
match_arm = { match_pattern ~ arrow ~ (expression | block) }
match_pattern = { expression }
// Continue can be used as an expression in matches
continue_expr = { continue_kw }
// Statements
statement = {
import_stmt |
function_decl |
variable_decl |
defer_stmt |
watch_stmt |
return_stmt |
if_stmt |
for_stmt |
while_stmt |
continue_stmt |
match_stmt |
expression_stmt
}
import_stmt = { import_kw ~ identifier ~ from_kw ~ string_literal }
function_decl = {
fn_kw ~ identifier ~ param_list ~ (extends_kw ~ identifier)? ~ return_type? ~ block
}
variable_decl = {
(const_kw | var_kw | live_kw) ~ identifier ~ type_annotation? ~ assign ~ expression
}
defer_stmt = {
defer_kw ~ (when_kw ~ expression ~ (pipe ~ identifier ~ pipe)?)? ~ block
}
watch_stmt = {
watch_kw ~ identifier ~ block
}
return_stmt = { return_kw ~ expression? }
if_stmt = {
if_kw ~ expression ~ ((colon ~ statement) | block) ~ (else_kw ~ ((colon ~ statement) | block))?
}
for_stmt = {
for_kw ~ identifier ~ (comma ~ identifier)? ~ in_kw ~ expression ~ ((colon ~ statement) | block)
}
while_stmt = {
while_kw ~ expression? ~ ((colon ~ statement) | block)
}
continue_stmt = { continue_kw }
match_stmt = { match_kw ~ expression? ~ lbrace ~ match_arm* ~ rbrace }
expression_stmt = { expression }
// Blocks
block = { lbrace ~ statement* ~ rbrace }
// Program
program = { SOI ~ statement* ~ EOI }

196
src/solace.pest.bak Normal file
View file

@ -0,0 +1,196 @@
//! Solace Grammar
WHITESPACE = _{ " " | "\t" }
COMMENT = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" | "//" ~ (!NEWLINE ~ ANY)* }
// Newline handling for automatic semicolon insertion
NEWLINE = _{ "\n" | "\r\n" }
terminator = _{ ";" | NEWLINE }
// Identifiers
identifier = @{ ASCII_ALPHANUMERIC+ }
// ========== TYPE SYSTEM ==========
type = {
primitive_type |
array_type |
map_type |
set_type |
error_union_type |
optional_type |
identifier
}
primitive_type = { "undefined" | "string" | "number" | "boolean" }
array_type = { type ~ "[]" }
map_type = { "Map<" ~ type ~ "," ~ type ~ ">" }
set_type = { "Set<" ~ type ~ ">" }
error_union_type = { ("Error" ~ "!")? ~ type }
optional_type = { type ~ "?" | "?" ~ type }
// ========== EXPRESSIONS (Pratt-ready) ==========
// Base expression atoms
atom = {
literal |
identifier |
"(" ~ expr ~ ")" |
match_expression |
lambda_expression
}
// Literals
literal = {
number_literal |
string_literal |
boolean_literal |
"undefined" |
"none" |
array_literal |
struct_literal
}
number_literal = @{ ("-")? ~ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
string_literal = @{ "\"" ~ (!"\"" ~ ANY)* ~ "\"" | "'" ~ (!"'" ~ ANY)* ~ "'" }
boolean_literal = { "true" | "false" }
array_literal = { "[" ~ (expr ~ ("," ~ expr)*)? ~ "]" }
struct_literal = { "{" ~ (struct_field ~ ("," ~ struct_field)*)? ~ "}" }
struct_field = { identifier ~ ":" ~ expr }
// Postfix operators (highest precedence)
postfix_expr = {
atom ~
( call |
member_access |
array_access |
range_operator )*
}
call = { "(" ~ (expr ~ ("," ~ expr)*)? ~ ")" }
member_access = { "." ~ identifier }
array_access = { "[" ~ expr ~ "]" }
range_operator = { (range_inclusive | range_exclusive) ~ expr }
// Unary operators
unary_expr = { unary_operator* ~ postfix_expr }
unary_operator = { not | negate }
// Binary operators (will be handled by Pratt parser)
// This is just for grammar completeness - actual precedence handled in Pratt parser
binary_expr = { unary_expr ~ (binary_operator ~ unary_expr)* }
binary_operator = {
add |
substract |
multiply |
divide |
modulo |
eq |
neq |
lt |
gt |
lte |
gte |
and |
or
}
or = { "||" }
and = { "&&" }
eq = { "==" }
neq = { "!=" }
lt = { "<" }
gt = { ">" }
lte = { "<=" }
gte = { ">=" }
add = { "+" }
substract = { "-" }
multiply = { "*" }
divide = { "/" }
modulo = { "%" }
not = { "!" }
negate = { "-" }
range_inclusive = { "..=" }
range_exclusive = { "..<" }
// The main expression rule
expr = { binary_expr }
// Special expressions
match_expression = {
"match" ~ (expr)? ~ "{" ~
(match_case ~ ("," ~ match_case)*)? ~
"}"
}
match_case = { match_pattern ~ "->" ~ expr }
match_pattern = { "_" | identifier | expr }
lambda_expression = { "|" ~ (identifier ~ ("," ~ identifier)*)? ~ "|" ~ "->" ~ expr }
// ========== STATEMENTS ==========
statement = {
variable_declaration ~ terminator |
function_declaration ~ terminator? |
expr_statement ~ terminator |
return_statement ~ terminator |
if_statement ~ terminator? |
for_statement ~ terminator? |
while_statement ~ terminator? |
defer_statement ~ terminator? |
watch_statement ~ terminator? |
block_statement ~ terminator? |
import_statement ~ terminator
}
variable_declaration = {
("const" | "let" | "live") ~ identifier ~ (":" ~ type)? ~ ("=" ~ expr)?
}
function_declaration = {
"fn" ~ identifier ~ "(" ~ (parameter ~ ("," ~ parameter)*)? ~ ")" ~
("=>" ~ "(" ~ parameter ~ ")")? ~
(":" ~ type)? ~
(block | expr)
}
parameter = { identifier ~ ":" ~ type ~ ("?" | "=" ~ expr)? }
return_statement = { "return" ~ expr? }
expr_statement = { expr }
if_statement = {
"if" ~ expr ~ block ~
("else" ~ (if_statement | block))?
}
block = { "{" ~ statement* ~ "}" }
block_statement = { block }
// Loops
for_statement = {
"for" ~
(identifier ~ ",")? ~ identifier ~ "in" ~
(expr | range_operator) ~
(block | expr_statement)
}
while_statement = {
"while" ~ expr? ~ block
}
// Special statements
defer_statement = {
"defer" ~ ("when" ~ expr)? ~
(lambda_expression | block)
}
watch_statement = {
"watch" ~ expr ~ block
}
// Import statement
import_statement = {
"import" ~ identifier ~ "from" ~ string_literal
}
// ========== PROGRAM ==========
program = { SOI ~ (statement | COMMENT)* ~ EOI }

127
test.solace Normal file
View file

@ -0,0 +1,127 @@
/*
* Solace looks like Zig, but without the low-level complexities and added
* pattern matching. Typical expressions are very much like in TypeScript and
* it supports the same primitive types undefined, string, number, boolean,
* and their array versions, as well as Map and Set. It uses none instead of
* null and objects are called structs. There is no new keyword and there are
* no classes. Instead, Solace uses scopes that work like JavaScript's scopes.
*/
// Optional parameters
fn sum(a: number, b?: number): number {
if b is undefined {
return a
}
return a + b
}
// Optional values
fn sum(a: number, b: ?number): number {
if b is none { // b has to be defined, but can be set to none
return a
}
return a + b
}
// Error Unions
fn sum(a: number, b?: number): Error!number {
if b is undefined {
return failure("Second number is missing", Error)
}
return a + b
}
// Error unions default to Error, so the last example could also be written like
fn sum(a: number, b?: number): !number {
if b is undefined {
return failure("Second number is missing")
}
return a + b
}
// Solace also supports initialized default return values, similar to Swift's inout parameters.
// They are really useful for short functions:
fn fizzbuzz(until: number) => (output: string[] = []) {
// var output = [] // no need, because it is already defined
for i in 0..=until {
// pattern matching
match i {
_ % 3 == 0 && _ % 5 == 0 -> output.push("fizzbuzz")
_ % 3 == 0 -> output.push("fizz")
_ % 5 == 0 -> output.push("buzz")
_ -> output.push(i.toString())
}
}
// no need, because it's the default return value, but can added for clarity
// return output
}
// Matches are expressions, which means, they return a value.
// Also, just for completeness, match doesn't need a value. So here is the last example changed to take both into account:
fn numberToFizzBuzz(n: number): string {
return match {
i % 3 == 0 && i % 5 == 0 -> "fizzbuzz"
i % 3 == 0 -> "fizz"
i % 5 == 0 -> "buzz"
i -> i.toString()
}
}
fn fizzbuzz(until: number) => (output: string[] = []) {
for i in 0..=until: output.push(numberToFizzBuzz(i))
}
// Now to the more interesting parts of Solace: Reactivity and unified handling of values.
import fs from "node:fs/promises"
fn openFile(path: string) {
// fs.open returns a Promise, and while Solace supports them, it doesn't need
// Promises or async/await, because it has live values.
// Promises (and with them return values of async functions) are translated
// to reactive values if used like this:
live fd = fs.open(path, "rw")
// A defer statement runs after everything else finished, successful or not.
// You can see it like try-finally, for functions.
defer when ok(fd) {
// Because it is a nameless assignment, we use _ to
// access the return value. If you need a name, use:
// defer when ok(fd) |file| { /*...*/ }
_.close() // close file descriptor
}
// live variables would be defined like this in TypeScript:
// { state: "pending", value: Promise<T>, error: null } |
// { state: "err", value: null, error: Error } |
// { state: "ok", value: T, error: null }
watch fd {
match fd {
// ok(x) returns the value of Result types like live, which is essentially an Error Union,
// while err(x) returns the error or null
ok(fd) -> handleFile(_) // here _ refers to the return value of ok(fd)
err(fd) -> handleError(_) // here _ refers to the error value returned by err(fd)
_ -> continue // matches need to handle all possible cases, and continue just ignores
}
}
}
// here all the different ways to write loops:
const myArray: number[] = [23, 42]
for x in myArray: console.log(x)
for y,i in myArray: console.log(`Index ${i} has value ${y}!`)
for i in 0..<23 {
console.log("This will output as long as i is smaller than 23")
}
for i in 0..=23 {
console.log("This will output as long as i is smaller or equal than 23")
}
while i <= 23: console.log("This will output forever, because I forgot to change the value of i")
while i <= 23 {
console.log("This will output until i reaches 23. It's value is now", i++)
}