initial pest grammar and parser
This commit is contained in:
commit
dd42ef23d4
9 changed files with 1780 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
212
Cargo.lock
generated
Normal file
212
Cargo.lock
generated
Normal 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
9
Cargo.toml
Normal 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
250
README.md
Normal 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
561
src/main.rs
Normal 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
182
src/main.rs.bak
Normal 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
242
src/solace.pest
Normal 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
196
src/solace.pest.bak
Normal 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
127
test.solace
Normal 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++)
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue