From dd42ef23d4cb327b309daf1ac23b36b060a86947 Mon Sep 17 00:00:00 2001 From: koehr Date: Sat, 21 Jun 2025 08:57:57 +0200 Subject: [PATCH] initial pest grammar and parser --- .gitignore | 1 + Cargo.lock | 212 +++++++++++++++++ Cargo.toml | 9 + README.md | 250 ++++++++++++++++++++ src/main.rs | 561 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs.bak | 182 ++++++++++++++ src/solace.pest | 242 +++++++++++++++++++ src/solace.pest.bak | 196 ++++++++++++++++ test.solace | 127 ++++++++++ 9 files changed, 1780 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/main.rs create mode 100644 src/main.rs.bak create mode 100644 src/solace.pest create mode 100644 src/solace.pest.bak create mode 100644 test.solace diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..312c704 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1d5191d --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0895bb --- /dev/null +++ b/README.md @@ -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.. (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, 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, 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++) +} +``` diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e4cce2b --- /dev/null +++ b/src/main.rs @@ -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, String), + Index(Box, Box), + Call(Box, Vec), + + // Operators + Binary(BinaryOp, Box, Box), + Unary(UnaryOp, Box), + Ternary(Box, Box, Box), + Assignment(String, Box), + + // Control flow + If(Box, Box, Option>), + Match(Option>, Vec), + + // Collections + Array(Vec), + + // Postfix + PostIncrement(Box), + PostDecrement(Box), +} + +#[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, +} + +#[derive(Debug, Clone)] +pub enum ReturnType { + Simple(Type), + Named { + name: String, + type_annotation: Type, + default_value: Option, + } +} + +#[derive(Debug, Clone)] +pub enum Statement { + Import { name: String, from: String }, + Function { + name: String, + params: Vec, + return_type: Option, + extends: Option, + body: Block, + }, + Variable { + kind: VarKind, + name: String, + type_annotation: Option, + value: Expr, + }, + Defer { + condition: Option, + binding: Option, + body: Block, + }, + Watch { + target: String, + body: Block, + }, + Return(Option), + If(Expr, Box, Option>), + For { + var: String, + index: Option, + iterable: Expr, + body: Box, + }, + While(Expr, Box), + Expression(Expr), + Block(Block), +} + +#[derive(Debug, Clone)] +pub enum VarKind { + Const, Var, Live, +} + +#[derive(Debug, Clone)] +pub struct Param { + name: String, + type_annotation: Option, +} + +#[derive(Debug, Clone)] +pub enum Type { + Primitive(String), + Array(Box), + Optional(Box), + ErrorUnion(Option, Box), + Named(String), +} + +pub struct SolacePrattParser { + pratt: PrattParser, +} + +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) -> Result { + 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) -> Result { + match pair.as_rule() { + Rule::number_literal => { + let num = pair.as_str().parse::() + .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, op: Pair, rhs: Result) -> Result { + 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, rhs: Result) -> Result { + 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, op: Pair) -> Result { + 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) -> Result { + 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) -> Result { + 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) -> Result { + 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) -> Result { + // 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) -> Result { + 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) -> Result { + 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) -> Result { + 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) -> Result { + 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) -> Result { + 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) -> Result, 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) -> Result { + 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) -> Result { + 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) -> Result { + 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, 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, 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) + } +} + diff --git a/src/main.rs.bak b/src/main.rs.bak new file mode 100644 index 0000000..6d6f313 --- /dev/null +++ b/src/main.rs.bak @@ -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 = { + 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) -> 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), + Struct(Vec<(String, Expr)>), + Binary(Box, BinOp, Box), + Unary(UnOp, Box), + FieldAccess(Box, String), + Call(Box, Vec), +} + +#[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) -> Vec { + pair.into_inner() + .filter(|p| p.as_rule() == Rule::expr) + .map(parse_expr) + .collect() +} + +fn parse_struct(pair: pest::iterators::Pair) -> 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, pest::error::Error> { + 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, 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) + } +} + diff --git a/src/solace.pest b/src/solace.pest new file mode 100644 index 0000000..c094ca7 --- /dev/null +++ b/src/solace.pest @@ -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 } + diff --git a/src/solace.pest.bak b/src/solace.pest.bak new file mode 100644 index 0000000..14bbb6c --- /dev/null +++ b/src/solace.pest.bak @@ -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 } diff --git a/test.solace b/test.solace new file mode 100644 index 0000000..50b7bf1 --- /dev/null +++ b/test.solace @@ -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, 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++) +}