Compare commits

..

22 Commits

Author SHA1 Message Date
25e3941315 Clean up examples 2024-02-19 21:19:27 -05:00
900de8ca4b Edit README; Improve bench script; Optimize 2024-02-19 20:44:26 -05:00
c4b51a1ef9 Change README outline; Fix bench script 2024-02-19 19:23:20 -05:00
939b7464c6 Use GitHub theme for example pics 2024-02-19 18:15:03 -05:00
64fb20c45e Use GitHub theme for example pic 2024-02-19 18:05:55 -05:00
e3b55092b3 Increment cargo version 2024-02-19 17:59:16 -05:00
bd4983b821 Write README; Use GitHub theme for example pics 2024-02-19 17:57:25 -05:00
a1500bf262 Write README 2024-02-19 17:22:14 -05:00
1585145ff4 Write docs; Update logging and error messages 2024-02-19 17:04:13 -05:00
fb3cd6e6da Add image to README 2024-02-19 15:35:27 -05:00
69347ad435 Update grammar and highlight queries 2024-02-19 15:26:49 -05:00
ca72fe04f1 Start new example; Start new syntax features 2024-02-19 15:04:33 -05:00
eaf26fec5e Begin reqriting README 2024-02-19 11:24:54 -05:00
0eac67eb3a Pass enum tests 2024-02-19 11:13:04 -05:00
37fd722fa6 Fix garbage collection bug 2024-02-18 16:43:47 -05:00
255843cb3b Fix type checking bugs 2024-02-18 15:52:47 -05:00
927a2cfbf9 Fix tests 2024-02-18 15:44:57 -05:00
88d05f0dc9 Clean up 2024-02-18 15:19:30 -05:00
0805b96809 Add type argument syntax 2024-02-18 15:07:53 -05:00
a5f3127bcf Fix command tests and parsing 2024-02-18 11:38:35 -05:00
01bdaa308d Fix test 2024-02-18 10:59:49 -05:00
dbf9ab0d00 Fix function test 2024-02-18 10:55:54 -05:00
47 changed files with 28034 additions and 25834 deletions

2
Cargo.lock generated
View File

@ -369,7 +369,7 @@ dependencies = [
[[package]]
name = "dust-lang"
version = "0.4.1"
version = "0.4.2"
dependencies = [
"cc",
"clap",

View File

@ -1,7 +1,7 @@
[package]
name = "dust-lang"
description = "General purpose programming language"
version = "0.4.1"
version = "0.4.2"
repository = "https://git.jeffa.io/jeff/dust.git"
edition = "2021"
license = "MIT"

224
README.md
View File

@ -1,188 +1,100 @@
# Dust
!!! Dust is an experimental project under active development. !!!
High-level programming language with effortless concurrency, automatic memory management, type safety and strict error handling.
Dust is a general purpose programming language that emphasises concurrency and correctness.
A basic dust program:
```dust
output("Hello world!")
```
Dust can do two (or more) things at the same time with effortless concurrency:
```dust
async {
output('will this one finish first?')
output('or will this one?')
}
```
You can use Dust to run complex operations simply and safely. You can even invoke other programs, run them at the same time, capture their output, and pipe them together.
```dust
# Run each statment in this block in its own thread.
async {
# Invoke another program and capture its output.
ip_info = ^ip address;
# Pipe the output to another program.
^ls -1 --all --long docs/ | ^rg .md | ^echo;
# This block is not async and the statements will be run in order.
{
file = fs:read_file('Cargo.toml')
# This loop will run each iteration in its own thread. If one of them
# reaches a "break" statement, they will all stop.
async for line in str:lines(file) {
if str:contains(line, 'author') {
output(line)
break
}
}
}
}
```
Dust is an interpreted, strictly typed language with first class functions, embracing concurrency by allowing any group of statements to be executed in parallel. Dust includes built-in tooling to import and export data in a variety of formats, including JSON, TOML, YAML and CSV. Dust aims to be panic-free. That means that the interpreter will only fail to run a program due to an intended error, such as a type error or syntax error. If your program passes the these checks, it will run correctly.
![Dust version of an example from The Rust Programming Language.](https://git.jeffa.io/jeff/dust/raw/branch/main/docs/assets/example_0.png)
<!--toc:start-->
- [Dust](#dust)
- [Features](#features)
- [Usage](#usage)
- [Dust Language](#dust-language)
- [Installation](#installation)
- [Benchmarks](#benchmarks)
- [Implementation](#implementation)
- [Acknowledgements](#acknowledgements)
- [Easy to Read and Write](#easy-to-read-and-write)
- [Effortless Concurrency](#effortless-concurrency)
- [Helpful Errors](#helpful-errors)
- [Static analysis](#static-analysis)
- [Debugging](#debugging)
- [Automatic Memory Management](#automatic-memory-management)
- [Error Handling](#error-handling)
- [Installation and Usage](#installation-and-usage)
<!--toc:end-->
## Features
- Simplicity: Dust is designed to be easy to learn.
- Speed: Dust is built on [Tree Sitter] and [Rust] to prioritize performance and correctness. See [Benchmarks] below.
- Concurrency: Safe, effortless parallel code using thread pools.
- Safety: Written in safe, stable Rust.
- Correctness: Type checking makes it easy to write good code.
### Easy to Read and Write
## Installation
Dust has simple, easy-to-learn syntax.
### Cargo
You must have the default rust toolchain installed and up-to-date. Install [rustup] if it is not already installed. Run `cargo install dust-lang` then run `dust` to start the interactive shell.
### Build From Source
To build from source, clone the repository and build the parser. To do so, enter the `tree-sitter-dust` directory and run `tree-sitter-generate`. In the project root, run `cargo run` to start the shell. If you get errors about a linking with C, read them carefully to determine which prerequisites are needed.
On Fedora, you can install these prerequisites with:
```sh
sudo dnf group install -y 'C Development Tools and Libraries' && sudo dnf install -y cmake
```js
output('Hello world!')
```
## Usage
### Effortless Concurrency
After installation, the command line interpreter can be given source code to run or it can launch the command-line shell. As with intepreters like `sh` and `bash`, you can use the `-c` flag to pass source code directly.
Write multi-threaded code as easily as you would write code for a single thread.
```sh
dust -c "output('Hello world!')"
# Output: Hello world!
```js
async {
output('Will this one print first?')
output('Or will this one?')
output('Who knows! Each "output" will run in its own thread!')
}
```
Or just provide a path to the source file.
### Helpful Errors
```sh
dust examples/hello_world.ds
Dust shows you exactly where your code went wrong and suggests changes.
![Example of syntax error output.](https://git.jeffa.io/jeff/dust/raw/branch/main/docs/assets/syntax_error.png)
### Static analysis
Your code is always validated for safety before it is run.
![Example of type error output.](https://git.jeffa.io/jeff/dust/raw/branch/main/docs/assets/type_error.png)
Dust
### Debugging
Just set the environment variable `DUST_LOG=info` and Dust will tell you exactly what your code is doing while it's doing it. If you set `DUST_LOG=trace`, it will output detailed logs about parsing, abstraction, validation, memory management and runtime. Here are some of the logs from the end of a simple [fizzbuzz example](https://git.jeffa.io/jeff/dust/src/branch/main/examples/fizzbuzz.ds).
![Example of debug output.](https://git.jeffa.io/jeff/dust/raw/branch/main/docs/assets/debugging.png)
### Automatic Memory Management
Thanks to static analysis, Dust knows exactly how many times each variable is used. This allows Dust to free memory as soon as the variable will no longer be used, without any help from the user.
### Error Handling
Runtime errors are no problem with Dust. The `Result` type represents the output of an operation that might fail. The user must decide what to do in the case of an error.
```dust
match io:stdin() {
Result::Ok(input) -> output("We read this input: " + input)
Result::Error(message) -> output("We got this error: " + message)
}
```
Run `dust --help` to see the available commands and options.
## Installation and Usage
```txt
General purpose programming language
There are two ways to compile Dust. **It is best to clone the repository and compile the latest code**, otherwise the program may be a different version than the one shown on GitHub. Either way, you must have `rustup`, `cmake` and a C compiler installed.
Usage: dust [OPTIONS] [PATH]
To install from the git repository:
Arguments:
[PATH] Location of the file to run
Options:
-c, --command <COMMAND> Dust source code to evaluate
-i, --input <INPUT> Data to assign to the "input" variable
-p, --input-path <INPUT_PATH> A path to file whose contents will be assigned to the "input" variable
-t, --tree Show the syntax tree
-h, --help Print help
-V, --version Print version
```fish
git clone https://git.jeffa.io/jeff/dust
cd dust
cargo run --release
```
## Dust Language
To install with cargo:
See the [Language Reference](/docs/language.md) for more information.
```fish
cargo install dust-lang
dust
```
## Benchmarks
Dust is at an early development stage and these tests are overly simple. Better benchmarks are needed to get a realistic idea of how Dust performs real work. For now, these tests are just for fun.
The examples given were tested using [Hyperfine] on a single-core cloud instance with 1024 MB RAM. Each test was run 1000 times. The test script is shown below. Each test asks the program to read a JSON file and count the objects. Dust is a command line shell, programming language and data manipulation tool so three appropriate targets were chosen for comparison: nushell, NodeJS and jq. The programs produced identical output with the exception that NodeJS printed in color.
## Development Status
For the first test, a file with four entries was used.
| Command | Mean [ms] | Min [ms] | Max [ms]
|:---|---:|---:|---:|
| Dust | 3.1 ± 0.5 | 2.4 | 8.4 |
| jq | 33.7 ± 2.2 | 30.0 | 61.8 |
| NodeJS | 226.4 ± 13.1 | 197.6 | 346.2 |
| Nushell | 51.6 ± 3.7 | 45.4 | 104.3 |
The second set of data is from the GitHub API, it consists of 100 commits from the jq GitHub repo.
| Command | Mean [ms] | Min [ms] | Max [ms] |
|:---|---:|---:|---:|
| Dust | 6.8 ± 0.6 | 5.7 | 12.0 | 2.20 ± 0.40 |
| jq | 43.3 ± 3.6 | 37.6 | 81.6 | 13.95 ± 2.49 |
| NodeJS | 224.9 ± 12.3 | 194.8 | 298.5 |
| Nushell | 59.2 ± 5.7 | 49.7 | 125.0 | 19.11 ± 3.55 |
This data came from CERN, it is a massive file of 100,000 entries.
| Command | Mean [ms] | Min [ms] | Max [ms] |
|:---|---:|---:|---:|
| Dust | 1080.8 ± 38.7 | 975.3 | 1326.6 |
| jq | 1305.3 ± 64.3 | 1159.7 | 1925.1 |
| NodeJS | 1850.5 ± 72.5 | 1641.9 | 2395.1 |
| Nushell | 1850.5 ± 86.2 | 1625.5 | 2400.7 |
The tests were run after 5 warmup runs and the cache was cleared before each run.
```sh
hyperfine \
--shell none \
--warmup 5 \
--prepare "rm -rf /root/.cache" \
--runs 1000 \
--parameter-list data_path seaCreatures.json,jq_data.json,dielectron.json \
--export-markdown test_output.md \
"dust -c '(length (from_json input))' -p {data_path}" \
"jq 'length' {data_path}" \
"node --eval \"require('node:fs').readFile('{data_path}',(err,data)=>{console.log(JSON.parse(data).length)})\"" \
"nu -c 'open {data_path} | length'"
```
## Implementation
Dust is formally defined as a Tree Sitter grammar in the tree-sitter-dust directory. Tree sitter generates a parser, written in C, from a set of rules defined in JavaScript. Dust itself is a rust binary that calls the C parser using FFI.
Tests are written in three places: in the Rust library, in Dust as examples and in the Tree Sitter test format. Generally, features are added by implementing and testing the syntax in the tree-sitter-dust repository, then writing library tests to evaluate the new syntax. Implementation tests run the Dust files in the "examples" directory and should be used to demonstrate and verify that features work together.
Tree Sitter generates a concrete syntax tree, which Dust traverses to create an abstract syntax tree that can run the Dust code. The CST generation is an extra step but it allows easy testing of the parser, defining the language in one file and makes the syntax easy to modify and expand. Because it uses Tree Sitter, developer-friendly features like syntax highlighting and code navigation are already available in any text editor that supports Tree Sitter.
## Acknowledgements
Dust began as a fork of [evalexpr]. Some of the original code is still in place but the project has dramatically changed and no longer uses any of its parsing or interpreting.
[Tree Sitter]: https://tree-sitter.github.io/tree-sitter/
[Rust]: https://rust-lang.org
[evalexpr]: https://github.com/ISibboI/evalexpr
[rustup]: https://rustup.rs
[Hyperfine]: https://github.com/sharkdp/hyperfine
Currently, Dust is being prepared for version 1.0. Until then, there may be breaking changes to the language and CLI.

BIN
docs/assets/debugging.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/assets/example_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/assets/type_error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

26
examples/guessing_game.ds Normal file
View File

@ -0,0 +1,26 @@
# This is a Dust version of an example from the Rust Book.
#
# https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html
output("Guess the number.")
secret_number = int:random_range(0..=100)
loop {
output("Please input your guess.")
input = io:stdin():expect("Failed to read line.")
guess = int:parse(input)
output("You guessed: " + guess)
match cmp(guess, secret_number) {
Ordering::Less -> output("Too small!")
Ordering::Greater -> output("Too big!")
Ordering::Equal -> {
output("You win!")
break
}
}
}

View File

@ -2,7 +2,7 @@ data = json:parse(fs:read_file('examples/assets/jq_data.json'))
new_data = []
for commit_data in data {
for commit_data in data as collection {
new_data += {
message = commit_data:commit:message
name = commit_data:commit:committer:name

View File

@ -11,7 +11,7 @@ hyperfine \
--shell none \
--parameter-list data_path examples/assets/seaCreatures.json \
--warmup 3 \
"dust -c 'length(json:parse(input))' -p {data_path}" \
"target/release/dust -c 'length(json:parse(fs:read_file(\"{data_path}\")))'" \
"jq 'length' {data_path}" \
"node --eval \"require('node:fs').readFile('{data_path}', (err, data)=>{console.log(JSON.parse(data).length)})\"" \
"nu -c 'open {data_path} | length'"
@ -20,7 +20,7 @@ hyperfine \
--shell none \
--parameter-list data_path examples/assets/jq_data.json \
--warmup 3 \
"dust -c 'length(json:parse(input))' -p {data_path}" \
"target/release/dust -c 'length(json:parse(fs:read_file(\"{data_path}\")))'" \
"jq 'length' {data_path}" \
"node --eval \"require('node:fs').readFile('{data_path}', (err, data)=>{console.log(JSON.parse(data).length)})\"" \
"nu -c 'open {data_path} | length'"
@ -29,7 +29,7 @@ hyperfine \
--shell none \
--parameter-list data_path dielectron.json \
--warmup 3 \
"dust -c 'length(json:parse(input))' -p {data_path}" \
"target/release/dust -c 'length(json:parse(fs:read_file(\"{data_path}\")))'" \
"jq 'length' {data_path}" \
"node --eval \"require('node:fs').readFile('{data_path}', (err, data)=>{console.log(JSON.parse(data).length)})\"" \
"nu -c 'open {data_path} | length'"

View File

@ -37,9 +37,15 @@ impl AbstractTree for As {
fn validate(&self, _source: &str, context: &Context) -> Result<(), ValidationError> {
let initial_type = self.expression.expected_type(context)?;
if let Type::List(item_type) = &self.r#type {
if self.r#type.accepts(&initial_type) {
return Ok(());
}
if let Type::ListOf(item_type) = &self.r#type {
match &initial_type {
Type::List(expected_item_type) => {
Type::ListOf(expected_item_type) => {
println!("{item_type} {expected_item_type}");
if !item_type.accepts(&expected_item_type) {
return Err(ValidationError::TypeCheck {
expected: self.r#type.clone(),
@ -77,7 +83,9 @@ impl AbstractTree for As {
fn run(&self, source: &str, context: &Context) -> Result<Value, RuntimeError> {
let value = self.expression.run(source, context)?;
let converted_value = if let Type::List(_) = self.r#type {
let converted_value = if self.r#type.accepts(&value.r#type()?) {
return Ok(value);
} else if let Type::ListOf(_) = self.r#type {
match value {
Value::List(list) => Value::List(list),
Value::String(string) => {

View File

@ -60,6 +60,8 @@ impl AbstractTree for Assignment {
self.statement.expected_type(context)?
};
log::info!("Setting type: {} <{}>", self.identifier, r#type);
context.set_type(self.identifier.clone(), r#type)?;
}
@ -78,7 +80,7 @@ impl AbstractTree for Assignment {
}
}
AssignmentOperator::PlusEqual => {
if let Type::List(expected) = type_specification.inner() {
if let Type::ListOf(expected) = type_specification.inner() {
let actual = self.identifier.expected_type(context)?;
if !expected.accepts(&actual) {
@ -107,7 +109,7 @@ impl AbstractTree for Assignment {
match self.operator {
AssignmentOperator::Equal => {}
AssignmentOperator::PlusEqual => {
if let Type::List(expected) = self.identifier.expected_type(context)? {
if let Type::ListOf(expected) = self.identifier.expected_type(context)? {
let actual = self.statement.expected_type(context)?;
if !expected.accepts(&actual) {
@ -155,6 +157,8 @@ impl AbstractTree for Assignment {
.set_value(self.identifier.clone(), new_value.clone())?;
}
log::info!("RUN assignment: {} = {}", self.identifier, new_value);
context.set_value(self.identifier.clone(), new_value)?;
Ok(Value::none())

View File

@ -1,4 +1,4 @@
use std::process;
use std::process::{self, Stdio};
use serde::{Deserialize, Serialize};
use tree_sitter::Node as SyntaxNode;
@ -53,18 +53,19 @@ impl AbstractTree for Command {
}
fn validate(&self, _source: &str, _context: &Context) -> Result<(), ValidationError> {
todo!()
Ok(())
}
fn run(&self, _source: &str, _context: &Context) -> Result<Value, RuntimeError> {
let output = process::Command::new(&self.command_text)
.args(&self.command_arguments)
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()?
.wait_with_output()?
.stdout;
let string = String::from_utf8(output)?;
Ok(Value::String(string))
Ok(Value::String(String::from_utf8(output)?))
}
}

View File

@ -9,11 +9,11 @@ use crate::{
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct EnumDefinition {
identifier: Identifier,
variants: Vec<(Identifier, Option<Type>)>,
variants: Vec<(Identifier, Vec<Type>)>,
}
impl EnumDefinition {
pub fn new(identifier: Identifier, variants: Vec<(Identifier, Option<Type>)>) -> Self {
pub fn new(identifier: Identifier, variants: Vec<(Identifier, Vec<Type>)>) -> Self {
Self {
identifier,
variants,
@ -27,6 +27,10 @@ impl EnumDefinition {
pub fn identifier(&self) -> &Identifier {
&self.identifier
}
pub fn variants(&self) -> &Vec<(Identifier, Vec<Type>)> {
&self.variants
}
}
impl AbstractTree for EnumDefinition {
@ -37,21 +41,25 @@ impl AbstractTree for EnumDefinition {
let identifier = Identifier::from_syntax(identifier_node, source, context)?;
let mut variants = Vec::new();
let mut current_identifier = None;
let mut current_identifier: Option<Identifier> = None;
let mut types = Vec::new();
for index in 3..node.child_count() - 1 {
let child = node.child(index).unwrap();
if child.kind() == "identifier" {
if let Some(identifier) = &current_identifier {
variants.push((identifier.clone(), types));
}
current_identifier = Some(Identifier::from_syntax(child, source, context)?);
types = Vec::new();
}
if let Some(identifier) = &current_identifier {
if child.kind() == "type" {
let r#type = Type::from_syntax(child, source, context)?;
if child.kind() == "type" {
let r#type = Type::from_syntax(child, source, context)?;
variants.push((identifier.clone(), Some(r#type)));
}
types.push(r#type);
}
}
@ -65,13 +73,14 @@ impl AbstractTree for EnumDefinition {
Ok(Type::None)
}
fn validate(&self, _source: &str, _context: &Context) -> Result<(), ValidationError> {
fn validate(&self, _source: &str, context: &Context) -> Result<(), ValidationError> {
context.set_definition(self.identifier.clone(), TypeDefinition::Enum(self.clone()))?;
self.identifier.validate(_source, context)?;
Ok(())
}
fn run(&self, _source: &str, context: &Context) -> Result<Value, RuntimeError> {
context.set_definition(self.identifier.clone(), TypeDefinition::Enum(self.clone()))?;
fn run(&self, _source: &str, _context: &Context) -> Result<Value, RuntimeError> {
Ok(Value::none())
}
}

View File

@ -66,16 +66,22 @@ impl AbstractTree for For {
self.collection.validate(_source, context)?;
let collection_type = self.collection.expected_type(context)?;
let item_type = if let Type::List(item_type) = collection_type {
item_type.as_ref().clone()
} else if let Type::Range = collection_type {
Type::Integer
} else {
return Err(ValidationError::TypeCheck {
expected: Type::Collection,
actual: collection_type,
position: self.source_position,
});
let item_type = match collection_type {
Type::Any => Type::Any,
Type::Collection => Type::Any,
Type::List => Type::Any,
Type::ListOf(_) => todo!(),
Type::ListExact(_) => todo!(),
Type::Map(_) => todo!(),
Type::String => todo!(),
Type::Range => todo!(),
_ => {
return Err(ValidationError::TypeCheck {
expected: Type::Collection,
actual: collection_type,
position: self.source_position,
});
}
};
let key = self.item_id.clone();
@ -94,12 +100,14 @@ impl AbstractTree for For {
if let Value::Range(range) = expression_run {
if self.is_async {
range.into_par_iter().try_for_each(|integer| {
self.context.add_allowance(key)?;
self.context
.set_value(key.clone(), Value::Integer(integer))?;
self.block.run(source, &self.context).map(|_value| ())
})?;
} else {
for i in range {
self.context.add_allowance(key)?;
self.context.set_value(key.clone(), Value::Integer(i))?;
self.block.run(source, &self.context)?;
}
@ -111,11 +119,13 @@ impl AbstractTree for For {
if let Value::List(list) = &expression_run {
if self.is_async {
list.items()?.par_iter().try_for_each(|value| {
self.context.add_allowance(key)?;
self.context.set_value(key.clone(), value.clone())?;
self.block.run(source, &self.context).map(|_value| ())
})?;
} else {
for value in list.items()?.iter() {
self.context.add_allowance(key)?;
self.context.set_value(key.clone(), value.clone())?;
self.block.run(source, &self.context)?;
}

View File

@ -35,7 +35,7 @@ impl AbstractTree for Index {
fn expected_type(&self, context: &Context) -> Result<Type, ValidationError> {
match self.collection.expected_type(context)? {
Type::List(item_type) => Ok(*item_type.clone()),
Type::ListOf(item_type) => Ok(*item_type.clone()),
Type::Map(map_types_option) => {
if let (Some(map_type), IndexExpression::Identifier(identifier)) =
(map_types_option, &self.index)

View File

@ -57,13 +57,17 @@ impl AbstractTree for IndexExpression {
}
}
fn validate(&self, _source: &str, _context: &Context) -> Result<(), ValidationError> {
fn validate(&self, _source: &str, context: &Context) -> Result<(), ValidationError> {
match self {
IndexExpression::Value(value_node) => value_node.validate(_source, _context),
IndexExpression::Identifier(identifier) => identifier.validate(_source, _context),
IndexExpression::Index(index) => index.validate(_source, _context),
IndexExpression::Value(value_node) => value_node.validate(_source, context),
IndexExpression::Identifier(identifier) => {
context.add_allowance(identifier)?;
Ok(())
}
IndexExpression::Index(index) => index.validate(_source, context),
IndexExpression::FunctionCall(function_call) => {
function_call.validate(_source, _context)
function_call.validate(_source, context)
}
}
}

View File

@ -45,6 +45,8 @@ impl AbstractTree for Logic {
}
fn validate(&self, _source: &str, _context: &Context) -> Result<(), ValidationError> {
log::info!("VALIDATE logic expression");
self.left.validate(_source, _context)?;
self.right.validate(_source, _context)
}
@ -52,6 +54,9 @@ impl AbstractTree for Logic {
fn run(&self, source: &str, context: &Context) -> Result<Value, RuntimeError> {
let left = self.left.run(source, context)?;
let right = self.right.run(source, context)?;
log::info!("RUN logic expression: {left} {} {right}", self.operator);
let result = match self.operator {
LogicOperator::Equal => {
if let (Ok(left_num), Ok(right_num)) = (left.as_number(), right.as_number()) {

View File

@ -1,3 +1,5 @@
use std::fmt::{self, Display, Formatter};
use serde::{Deserialize, Serialize};
use crate::{
@ -74,3 +76,18 @@ impl Format for LogicOperator {
}
}
}
impl Display for LogicOperator {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
LogicOperator::Equal => write!(f, "="),
LogicOperator::NotEqual => write!(f, "!="),
LogicOperator::And => write!(f, "&&"),
LogicOperator::Or => write!(f, "||"),
LogicOperator::Greater => write!(f, ">"),
LogicOperator::Less => write!(f, "<"),
LogicOperator::GreaterOrEqual => write!(f, ">="),
LogicOperator::LessOrEqual => write!(f, "<="),
}
}
}

View File

@ -20,6 +20,8 @@ impl AbstractTree for MathOperator {
_source: &str,
_context: &Context,
) -> Result<Self, SyntaxError> {
SyntaxError::expect_syntax_node("math_operator", node)?;
let operator_node = node.child(0).unwrap();
let operator = match operator_node.kind() {
"+" => MathOperator::Add,

View File

@ -19,7 +19,7 @@ pub enum Type {
Collection,
Custom {
name: Identifier,
argument: Option<Box<Type>>,
arguments: Vec<Type>,
},
Float,
Function {
@ -27,20 +27,19 @@ pub enum Type {
return_type: Box<Type>,
},
Integer,
List(Box<Type>),
List,
ListOf(Box<Type>),
ListExact(Vec<Type>),
Map(Option<BTreeMap<Identifier, Type>>),
None,
Number,
String,
Range,
None,
}
impl Type {
pub fn custom(name: Identifier, argument: Option<Type>) -> Self {
Type::Custom {
name,
argument: argument.map(|r#type| Box::new(r#type)),
}
pub fn custom(name: Identifier, arguments: Vec<Type>) -> Self {
Type::Custom { name, arguments }
}
pub fn option(inner_type: Option<Type>) -> Self {
@ -48,7 +47,7 @@ impl Type {
}
pub fn list(item_type: Type) -> Self {
Type::List(Box::new(item_type))
Type::ListOf(Box::new(item_type))
}
pub fn function(parameter_types: Vec<Type>, return_type: Type) -> Self {
@ -70,14 +69,19 @@ impl Type {
| (_, Type::Any)
| (Type::Boolean, Type::Boolean)
| (Type::Collection, Type::Collection)
| (Type::Collection, Type::List(_))
| (Type::List(_), Type::Collection)
| (Type::Collection, Type::String)
| (Type::Collection, Type::List)
| (Type::List, Type::Collection)
| (Type::Collection, Type::ListExact(_))
| (Type::ListExact(_), Type::Collection)
| (Type::Collection, Type::ListOf(_))
| (Type::ListOf(_), Type::Collection)
| (Type::Collection, Type::Map(_))
| (Type::Map(_), Type::Collection)
| (Type::Collection, Type::String)
| (Type::String, Type::Collection)
| (Type::Float, Type::Float)
| (Type::Integer, Type::Integer)
| (Type::List, Type::List)
| (Type::Map(None), Type::Map(None))
| (Type::Number, Type::Number)
| (Type::Number, Type::Integer)
@ -90,22 +94,29 @@ impl Type {
(
Type::Custom {
name: left_name,
argument: left_argument,
arguments: left_arguments,
},
Type::Custom {
name: right_name,
argument: right_argument,
arguments: right_arguments,
},
) => {
if left_name != right_name {
false
} else {
left_argument == right_argument
}
}
(Type::List(self_item_type), Type::List(other_item_type)) => {
) => left_name == right_name && left_arguments == right_arguments,
(Type::ListOf(self_item_type), Type::ListOf(other_item_type)) => {
self_item_type.accepts(&other_item_type)
}
(Type::ListExact(self_types), Type::ListExact(other_types)) => {
for (left, right) in self_types.iter().zip(other_types.iter()) {
if !left.accepts(right) {
return false;
}
}
true
}
(Type::ListExact(exact_types), Type::ListOf(of_type))
| (Type::ListOf(of_type), Type::ListExact(exact_types)) => {
exact_types.iter().all(|r#type| r#type == of_type.as_ref())
}
(
Type::Function {
parameter_types: self_parameter_types,
@ -137,7 +148,7 @@ impl Type {
}
pub fn is_list(&self) -> bool {
matches!(self, Type::List(_))
matches!(self, Type::ListOf(_))
}
pub fn is_map(&self) -> bool {
@ -158,13 +169,19 @@ impl AbstractTree for Type {
let r#type = match type_node.kind() {
"identifier" => {
let name = Identifier::from_syntax(type_node, _source, context)?;
let argument = if let Some(child) = node.child(2) {
Some(Type::from_syntax(child, _source, context)?)
} else {
None
};
let mut arguments = Vec::new();
Type::custom(name, argument)
for index in 2..node.child_count() - 1 {
let child = node.child(index).unwrap();
if child.is_named() {
let r#type = Type::from_syntax(child, _source, context)?;
arguments.push(r#type);
}
}
Type::custom(name, arguments)
}
"{" => {
let mut type_map = BTreeMap::new();
@ -191,7 +208,16 @@ impl AbstractTree for Type {
let item_type_node = node.child(1).unwrap();
let item_type = Type::from_syntax(item_type_node, _source, context)?;
Type::List(Box::new(item_type))
Type::ListOf(Box::new(item_type))
}
"list" => {
let item_type_node = node.child(1);
if let Some(child) = item_type_node {
Type::ListOf(Box::new(Type::from_syntax(child, _source, context)?))
} else {
Type::List
}
}
"any" => Type::Any,
"bool" => Type::Boolean,
@ -230,7 +256,8 @@ impl AbstractTree for Type {
"str" => Type::String,
_ => {
return Err(SyntaxError::UnexpectedSyntaxNode {
expected: "any, bool, float, int, num, str, custom type, (, [ or {".to_string(),
expected: "any, bool, float, int, num, str, list, map, custom type, (, [ or {"
.to_string(),
actual: type_node.kind().to_string(),
position: node.range().into(),
})
@ -261,7 +288,7 @@ impl Format for Type {
Type::Collection => output.push_str("collection"),
Type::Custom {
name: _,
argument: _,
arguments: _,
} => todo!(),
Type::Float => output.push_str("float"),
Type::Function {
@ -282,11 +309,13 @@ impl Format for Type {
return_type.format(output, indent_level);
}
Type::Integer => output.push_str("int"),
Type::List(item_type) => {
Type::List => todo!(),
Type::ListOf(item_type) => {
output.push('[');
item_type.format(output, indent_level);
output.push(']');
}
Type::ListExact(_) => todo!(),
Type::Map(_) => {
output.push_str("map");
}
@ -304,9 +333,19 @@ impl Display for Type {
Type::Any => write!(f, "any"),
Type::Boolean => write!(f, "bool"),
Type::Collection => write!(f, "collection"),
Type::Custom { name, argument } => {
if let Some(argument) = argument {
write!(f, "{name}<{argument}>")
Type::Custom { name, arguments } => {
if !arguments.is_empty() {
write!(f, "<")?;
for (index, r#type) in arguments.into_iter().enumerate() {
if index == arguments.len() - 1 {
write!(f, "{}", r#type)?;
} else {
write!(f, "{}, ", r#type)?;
}
}
write!(f, ">")
} else {
write!(f, "{name}")
}
@ -330,7 +369,21 @@ impl Display for Type {
write!(f, " -> {return_type}")
}
Type::Integer => write!(f, "int"),
Type::List(item_type) => write!(f, "[{item_type}]"),
Type::List => write!(f, "list"),
Type::ListOf(item_type) => write!(f, "[{item_type}]"),
Type::ListExact(types) => {
write!(f, "[")?;
for (index, r#type) in types.into_iter().enumerate() {
if index == types.len() - 1 {
write!(f, "{}", r#type)?;
} else {
write!(f, "{}, ", r#type)?;
}
}
write!(f, "]")
}
Type::Map(_) => write!(f, "map"),
Type::Number => write!(f, "num"),
Type::None => write!(f, "none"),

View File

@ -124,43 +124,47 @@ impl AbstractTree for ValueNode {
ValueNode::Integer(_) => Type::Integer,
ValueNode::String(_) => Type::String,
ValueNode::List(expressions) => {
let mut previous_type = None;
let mut item_types = Vec::new();
for expression in expressions {
let expression_type = expression.expected_type(context)?;
if let Some(previous) = previous_type {
if expression_type != previous {
return Ok(Type::List(Box::new(Type::Any)));
}
}
previous_type = Some(expression_type);
item_types.push(expression_type);
}
if let Some(previous) = previous_type {
Type::List(Box::new(previous))
} else {
Type::List(Box::new(Type::Any))
}
Type::ListExact(item_types)
}
ValueNode::Map(map_node) => map_node.expected_type(context)?,
ValueNode::Struct { name, .. } => {
Type::Custom { name: name.clone(), argument: None }
Type::custom(name.clone(), Vec::with_capacity(0))
}
ValueNode::Range(_) => Type::Range,
ValueNode::Enum { name, variant: _, expression } => {
if let Some(expression) = expression {
Type::Custom {
name: name.clone(),
argument: Some(Box::new(expression.expected_type(context)?))
ValueNode::Enum { name, variant, expression: _ } => {
let types: Vec<Type> = if let Some(type_definition) = context.get_definition(name)? {
if let TypeDefinition::Enum(enum_definition) = type_definition {
let types = enum_definition.variants().into_iter().find_map(|(identifier, types)| {
if identifier == variant {
Some(types.clone())
} else {
None
}
});
if let Some(types) = types {
types
} else {
return Err(ValidationError::VariableIdentifierNotFound(variant.clone()));
}
} else {
return Err(ValidationError::ExpectedEnumDefintion { actual: type_definition.clone() });
}
} else {
Type::Custom{
name: name.clone(),
argument: None,
}
}
return Err(ValidationError::VariableIdentifierNotFound(name.clone()));
};
Type::custom(name.clone(), types.clone())
},
};
@ -175,6 +179,13 @@ impl AbstractTree for ValueNode {
}
}
ValueNode::Map(map_node) => map_node.validate(_source, context)?,
ValueNode::Enum { name, expression, .. } => {
name.validate(_source, context)?;
if let Some(expression) = expression {
expression.validate(_source, context)?;
}
}
_ => {},
}
@ -340,7 +351,7 @@ impl Ord for ValueNode {
}
impl PartialOrd for ValueNode {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

View File

@ -32,15 +32,21 @@ impl AbstractTree for While {
}
fn validate(&self, _source: &str, context: &Context) -> Result<(), ValidationError> {
log::info!("VALIDATE while loop");
self.expression.validate(_source, context)?;
self.block.validate(_source, context)
}
fn run(&self, source: &str, context: &Context) -> Result<Value, RuntimeError> {
log::info!("RUN while loop start");
while self.expression.run(source, context)?.as_boolean()? {
self.block.run(source, context)?;
}
log::info!("RUN while loop end");
Ok(Value::none())
}
}

View File

@ -1,4 +1,4 @@
use std::fs::read_to_string;
use std::{fs::File, io::Read};
use enum_iterator::{all, Sequence};
use serde::{Deserialize, Serialize};
@ -46,7 +46,11 @@ impl Callable for Fs {
RuntimeError::expect_argument_amount(self.name(), 1, arguments.len())?;
let path = arguments.first().unwrap().as_string()?;
let file_content = read_to_string(path.as_str())?;
let mut file = File::open(path)?;
let file_size = file.metadata()?.len() as usize;
let mut file_content = String::with_capacity(file_size);
file.read_to_string(&mut file_content)?;
Ok(Value::string(file_content))
}

View File

@ -73,13 +73,7 @@ impl Callable for BuiltInFunction {
fn r#type(&self) -> Type {
match self {
BuiltInFunction::AssertEqual => Type::function(
vec![Type::Any, Type::Any],
Type::Custom {
name: Identifier::new("Result"),
argument: None,
},
),
BuiltInFunction::AssertEqual => Type::function(vec![Type::Any, Type::Any], Type::None),
BuiltInFunction::Fs(fs_function) => fs_function.r#type(),
BuiltInFunction::Json(json_function) => json_function.r#type(),
BuiltInFunction::Length => Type::function(vec![Type::Collection], Type::Integer),

View File

@ -33,8 +33,8 @@ impl BuiltInTypeDefinition {
let definition = TypeDefinition::Enum(EnumDefinition::new(
Identifier::new(self.name()),
vec![
(Identifier::new("Some"), Some(Type::Any)),
(Identifier::new("None"), None),
(Identifier::new("Some"), vec![Type::Any]),
(Identifier::new("None"), Vec::with_capacity(0)),
],
));
@ -44,8 +44,8 @@ impl BuiltInTypeDefinition {
let definition = TypeDefinition::Enum(EnumDefinition::new(
Identifier::new(self.name()),
vec![
(Identifier::new("Ok"), Some(Type::Any)),
(Identifier::new("Err"), Some(Type::Any)),
(Identifier::new("Ok"), vec![Type::Any]),
(Identifier::new("Error"), vec![Type::Any]),
],
));

View File

@ -19,15 +19,9 @@ impl BuiltInType {
match self {
BuiltInType::Option(content_type) => OPTION.get_or_init(|| {
if let Some(content_type) = content_type {
Type::Custom {
name: Identifier::new("Option"),
argument: Some(Box::new(content_type.clone())),
}
Type::custom(Identifier::new("Option"), vec![content_type.clone()])
} else {
Type::Custom {
name: Identifier::new("Option"),
argument: None,
}
Type::custom(Identifier::new("Option"), Vec::with_capacity(0))
}
}),
}

View File

@ -264,13 +264,11 @@ impl Context {
/// Set a value to a key.
pub fn set_value(&self, key: Identifier, value: Value) -> Result<(), RwLockError> {
log::info!("Setting value: {key} = {value}");
let mut map = self.inner.write()?;
let old_data = map.remove(&key);
if let Some((_, old_counter)) = old_data {
map.insert(key, (ValueData::Value(value), old_counter.clone()));
map.insert(key, (ValueData::Value(value), old_counter));
} else {
map.insert(key, (ValueData::Value(value), UsageCounter::new()));
}
@ -283,8 +281,6 @@ impl Context {
/// This allows the interpreter to check a value's type before the value
/// actually exists by predicting what the abstract tree will produce.
pub fn set_type(&self, key: Identifier, r#type: Type) -> Result<(), RwLockError> {
log::info!("Setting type: {key} <{}>", r#type);
self.inner
.write()?
.insert(key, (ValueData::TypeHint(r#type), UsageCounter::new()));

View File

@ -16,7 +16,7 @@ use tree_sitter::LanguageError;
use std::fmt::{self, Formatter};
#[derive(PartialEq)]
#[derive(Debug, PartialEq)]
pub enum Error {
Syntax(SyntaxError),
@ -95,12 +95,6 @@ impl From<LanguageError> for Error {
impl std::error::Error for Error {}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use Error::*;

View File

@ -1,5 +1,6 @@
use std::fmt::{self, Display, Formatter};
use colored::Colorize;
use lyneate::Report;
use serde::{Deserialize, Serialize};
use tree_sitter::Node as SyntaxNode;
@ -12,6 +13,8 @@ use super::rw_lock_error::RwLockError;
pub enum SyntaxError {
/// Invalid user input.
InvalidSource {
expected: String,
actual: String,
position: SourcePosition,
},
@ -27,25 +30,25 @@ pub enum SyntaxError {
impl SyntaxError {
pub fn create_report(&self, source: &str) -> String {
let messages = match self {
SyntaxError::InvalidSource { position } => {
SyntaxError::InvalidSource { position, .. } => self
.to_string()
.split_inclusive(".")
.map(|message_part| {
(
position.start_byte..position.end_byte,
message_part.to_string(),
(255, 200, 100),
)
})
.collect(),
SyntaxError::RwLock(_) => todo!(),
SyntaxError::UnexpectedSyntaxNode { position, .. } => {
vec![(
position.start_byte..position.end_byte,
format!(
"Invalid syntax from ({}, {}) to ({}, {}).",
position.start_row,
position.start_column,
position.end_row,
position.end_column,
),
self.to_string(),
(255, 200, 100),
)]
}
SyntaxError::RwLock(_) => todo!(),
SyntaxError::UnexpectedSyntaxNode {
expected: _,
actual: _,
position: _,
} => todo!(),
};
Report::new_byte_spanned(source, messages).display_str()
@ -58,6 +61,8 @@ impl SyntaxError {
Ok(())
} else if actual.is_error() {
Err(SyntaxError::InvalidSource {
expected: expected.to_owned(),
actual: actual.kind().to_string(),
position: SourcePosition::from(actual.range()),
})
} else {
@ -79,16 +84,43 @@ impl From<RwLockError> for SyntaxError {
impl Display for SyntaxError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
SyntaxError::InvalidSource { position } => write!(f, "Invalid syntax at {position:?}."),
SyntaxError::InvalidSource {
expected,
actual,
position,
} => {
let actual = if actual == "ERROR" {
"unrecognized characters"
} else {
actual
};
write!(
f,
"Invalid syntax from ({}, {}) to ({}, {}). Exected {} but found {}.",
position.start_row,
position.start_column,
position.end_row,
position.end_column,
expected.bold().green(),
actual.bold().red(),
)
}
SyntaxError::RwLock(_) => todo!(),
SyntaxError::UnexpectedSyntaxNode {
expected,
actual,
position,
} => write!(
f,
"Unexpected syntax node. Expected {expected} but got {actual} at {position:?}."
),
} => {
write!(
f,
"Interpreter Error. Tried to parse {actual} as {expected} from ({}, {}) to ({}, {}).",
position.start_row,
position.start_column,
position.end_row,
position.end_column,
)
}
}
}
}

View File

@ -1,5 +1,6 @@
use std::fmt::{self, Display, Formatter};
use colored::Colorize;
use lyneate::Report;
use serde::{Deserialize, Serialize};
@ -223,10 +224,17 @@ impl ValidationError {
position,
} => vec![(
position.start_byte..position.end_byte,
format!("Type {actual} is incompatible with {expected}."),
format!(
"Type {} is incompatible with {}.",
actual.to_string().bold().red(),
expected.to_string().bold().green()
),
(200, 200, 200),
)],
ValidationError::TypeCheckExpectedFunction { actual: _, position: _ } => todo!(),
ValidationError::TypeCheckExpectedFunction {
actual: _,
position: _,
} => todo!(),
ValidationError::VariableIdentifierNotFound(_) => todo!(),
ValidationError::TypeDefinitionNotFound(_) => todo!(),
ValidationError::ExpectedEnumDefintion { actual: _ } => todo!(),

View File

@ -74,25 +74,15 @@ impl Value {
pub fn r#type(&self) -> Result<Type, RwLockError> {
let r#type = match self {
Value::List(list) => {
let mut previous_type = None;
let mut item_types = Vec::new();
for value in list.items()?.iter() {
let value_type = value.r#type();
let r#type = value.r#type()?;
if let Some(previous) = &previous_type {
if &value_type != previous {
return Ok(Type::List(Box::new(Type::Any)));
}
}
previous_type = Some(value_type);
item_types.push(r#type);
}
if let Some(previous) = previous_type {
Type::List(Box::new(previous?))
} else {
Type::List(Box::new(Type::Any))
}
Type::ListExact(item_types)
}
Value::Map(map) => {
if map.inner().is_empty() {
@ -114,7 +104,18 @@ impl Value {
Value::Boolean(_) => Type::Boolean,
Value::Range(_) => todo!(),
Value::Struct(_) => todo!(),
Value::Enum(_) => todo!(),
Value::Enum(enum_instance) => {
let arguments = if let Some(value) = enum_instance.value() {
vec![value.r#type()?]
} else {
Vec::with_capacity(0)
};
Type::Custom {
name: enum_instance.name().clone(),
arguments,
}
}
};
Ok(r#type)

View File

@ -24,7 +24,7 @@ fn string_as_list_error() {
interpret("'foobar' as [float]"),
Err(Error::Validation(ValidationError::ConversionImpossible {
initial_type: Type::String,
target_type: Type::List(Box::new(Type::Float))
target_type: Type::ListOf(Box::new(Type::Float))
}))
)
}
@ -39,14 +39,14 @@ fn conversion_runtime_error() {
interpret(&format!("json:parse('{JSON}') as [map]")),
Err(Error::Runtime(RuntimeError::ConversionImpossible {
from: json_value.r#type().unwrap(),
to: Type::List(Box::new(Type::Map(None))),
to: Type::ListOf(Box::new(Type::Map(None))),
position: SourcePosition {
start_byte: 0,
end_byte: 0,
start_row: 0,
end_byte: 33,
start_row: 1,
start_column: 0,
end_row: 0,
end_column: 0
end_row: 1,
end_column: 33,
}
}))
)

View File

@ -1,5 +1,27 @@
use dust_lang::*;
#[test]
fn override_built_ins() {
assert_eq!(
interpret(
"
enum Option {
Some<str>
None
}
my_option <Option> = Option::Some('foo')
my_option
"
),
Ok(Value::Enum(EnumInstance::new(
Identifier::new("Option"),
Identifier::new("Some"),
Some(Value::String("foo".to_string())),
)))
);
}
#[test]
fn option() {
assert_eq!(
@ -13,14 +35,12 @@ fn option() {
assert_eq!(
interpret(
"
opt <Option<int>> = Option::Some(1);
opt
Option::Some(1)
"
),
Ok(Value::Enum(EnumInstance::new(
Identifier::new("Option"),
Identifier::new("None"),
Identifier::new("Some"),
Some(Value::Integer(1)),
)))
);
@ -39,8 +59,7 @@ fn result() {
assert_eq!(
interpret(
"
result <Result<int, str>> = Result::Error('uh-oh!')
result
Result::Error('uh-oh!')
"
),
Ok(Value::Enum(EnumInstance::new(

View File

@ -1,4 +1,4 @@
use dust_lang::*;
use dust_lang::{error::RuntimeError, *};
#[test]
fn args() {
@ -23,10 +23,9 @@ fn assert_equal() {
);
assert_eq!(
interpret("assert_equal(true, false)"),
Ok(Value::Enum(EnumInstance::new(
Identifier::new("Result"),
Identifier::new("Error"),
Some(Value::none()),
)))
Err(Error::Runtime(RuntimeError::AssertEqualFailed {
left: true.into(),
right: false.into()
}))
);
}

View File

@ -1,20 +1,14 @@
use dust_lang::{interpret, Value};
use std::fs::{remove_file, write};
#[test]
fn simple_command() {
assert_eq!(interpret("^echo hi"), Ok(Value::String("".to_string())))
assert_eq!(interpret("^echo hi"), Ok(Value::String("hi\n".to_string())))
}
#[test]
fn assign_command_output() {
write("target/test.txt", "123").unwrap();
assert_eq!(
interpret("x = ^cat target/test.txt; x"),
Ok(Value::String("123".to_string()))
interpret("x = ^ls; length(str:lines(x))"),
Ok(Value::Integer(11))
);
remove_file("target/test.txt").unwrap();
}

View File

@ -33,7 +33,7 @@ fn nested_enum() {
}
enum Foobar {
Foo,
Bar(Fizzbuzz),
Bar<Fizzbuzz>,
}
Foobar::Bar(Fizzbuzz::Fizz)
@ -53,3 +53,36 @@ fn nested_enum() {
)))
);
}
#[test]
fn enum_with_argument() {
env_logger::builder().is_test(true).try_init().unwrap();
let result = interpret(
"
enum FooBar<T> {
Foo<T>
Bar
}
enum FizzBuzz {
Fizz
Buzz
}
FooBar::Bar(FizzBuzz::Fizz)
",
);
assert_eq!(
result,
Ok(Value::Enum(EnumInstance::new(
Identifier::new("FooBar"),
Identifier::new("Bar"),
Some(Value::Enum(EnumInstance::new(
Identifier::new("FizzBuzz"),
Identifier::new("Fizz"),
Some(Value::none())
)))
)))
);
}

View File

@ -101,7 +101,7 @@ fn function_context_captures_structure_definitions() {
}
bob = () <User> {
new User {
User::{
name = 'bob'
}
}

View File

@ -10,7 +10,7 @@ Simple As
(statement
(statement_kind
(expression
(as
(as_node
(expression
(value
(integer)))
@ -28,7 +28,7 @@ foo as (int) -> int
(statement
(statement_kind
(expression
(as
(as_node
(expression
(identifier))
(type
@ -49,7 +49,7 @@ for i in foobar as [string] {}
(for
(identifier)
(expression
(as
(as_node
(expression
(identifier))
(type

View File

@ -59,33 +59,6 @@ Command Sequence with Arguments
--------------------------------------------------------------------------------
(root
(statement
(statement_kind
(expression
(command
(command_text)
(command_argument)
(command_argument)
(command_argument)
(command_argument)))))
(statement
(statement_kind
(expression
(command
(command_text)
(command_argument)
(command_argument))))))
================================================================================
Command Sequence with Arguments
================================================================================
^cargo run -- -c "output('hi there')"
^ls --long -a
--------------------------------------------------------------------------------
(root
(statement
(statement_kind
@ -108,7 +81,7 @@ Command Sequence with Arguments
Command Assignment
================================================================================
ls_output = ^ls --long -a;
ls_output = ^ls;
cat_output = ^cat Cargo.toml;
--------------------------------------------------------------------------------
@ -123,9 +96,7 @@ cat_output = ^cat Cargo.toml;
(statement_kind
(expression
(command
(command_text)
(command_argument)
(command_argument))))))))
(command_text))))))))
(statement
(statement_kind
(assignment
@ -146,32 +117,6 @@ ls_output = ^ls --long -a; ls_output
--------------------------------------------------------------------------------
(root
(statement
(statement_kind
(assignment
(identifier)
(assignment_operator)
(statement
(statement_kind
(expression
(command
(command_text)
(command_argument)
(command_argument))))))))
(statement
(statement_kind
(expression
(identifier)))))
================================================================================
Command with Semicolon
================================================================================
ls_output = ^ls --long -a; ls_output
--------------------------------------------------------------------------------
(root
(statement
(statement_kind

View File

@ -19,15 +19,12 @@ enum Foobar {
(identifier))))))
================================================================================
Nested Enum
Enum with Arguments
================================================================================
enum Foobar {
Foo(str),
Bar(enum BazBuff {
Baz,
Buff,
})
enum Foobar<T, U> {
Foo<T>,
Bar<U>,
}
--------------------------------------------------------------------------------
@ -38,20 +35,62 @@ enum Foobar {
(type_definition
(enum_definition
(identifier)
(type_arguments
(type
(identifier))
(type
(identifier)))
(identifier)
(type)
(type_arguments
(type
(identifier)))
(identifier)
(type_definition
(enum_definition
(identifier)
(identifier)
(type_arguments
(type
(identifier))))))))
================================================================================
Complex Enum
================================================================================
enum Foobar<T> {
Foo<Foo>,
Bar<int, float>,
Baz<Option<T>>,
}
--------------------------------------------------------------------------------
(root
(statement
(statement_kind
(type_definition
(enum_definition
(identifier)
(type_arguments
(type
(identifier)))
(identifier)
(type_arguments
(type
(identifier)))
(identifier)
(type_arguments
(type)
(type))
(identifier)
(type_arguments
(type
(identifier)
(type_arguments
(type
(identifier))))))))))
================================================================================
Simple Enum Instance
================================================================================
Foobar::Foo
FooBar::Foo
--------------------------------------------------------------------------------

View File

@ -28,3 +28,69 @@ x <{ y <int> }> = { y = 2 }
(expression
(value
(integer))))))))))))))
================================================================================
List Of
================================================================================
x <list<str>> = ['foo', 'bar', 'baz']
--------------------------------------------------------------------------------
(root
(statement
(statement_kind
(assignment
(identifier)
(type_specification
(type
(type)))
(assignment_operator)
(statement
(statement_kind
(expression
(value
(list
(expression
(value
(string)))
(expression
(value
(string)))
(expression
(value
(string))))))))))))
================================================================================
Exact List
================================================================================
foo <[int, float, int]> = [1, 2.0, 3]
--------------------------------------------------------------------------------
(root
(statement
(statement_kind
(assignment
(identifier)
(type_specification
(type
(type)
(type)
(type)))
(assignment_operator)
(statement
(statement_kind
(expression
(value
(list
(expression
(value
(integer)))
(expression
(value
(float)))
(expression
(value
(integer))))))))))))

View File

@ -31,6 +31,7 @@ module.exports = grammar({
$.for,
$.if_else,
$.index_assignment,
$.loop_node,
$.match,
$.pipe,
$.while,
@ -51,7 +52,7 @@ module.exports = grammar({
_expression_kind: $ =>
prec.right(
choice(
$.as,
$.as_node,
$.function_call,
$.identifier,
$.index,
@ -72,7 +73,7 @@ module.exports = grammar({
),
),
as: $ =>
as_node: $ =>
seq($.expression, 'as', $.type),
pipe: $ =>
@ -101,7 +102,7 @@ module.exports = grammar({
),
),
command_text: $ => /\S+/,
command_text: $ => /[^\s;]+/,
command_argument: $ =>
choice(
@ -327,6 +328,12 @@ module.exports = grammar({
),
),
loop_node: $ =>
seq(
'loop',
$.block,
),
while: $ =>
seq(
'while',
@ -354,7 +361,21 @@ module.exports = grammar({
'collection',
'float',
'int',
'list',
'map',
'none',
'num',
'str',
// Custom type
$.identifier,
// Custom type with arguments
seq(
$.identifier,
$.type_arguments
),
// Map with exact fields
seq(
'{',
repeat1(
@ -365,22 +386,23 @@ module.exports = grammar({
),
'}',
),
'none',
'num',
'str',
$.identifier,
// List of
seq('list', '<', $.type, '>'),
// Exact list
seq(
$.identifier,
'<',
repeat1(
'[',
repeat(
seq(
$.type,
optional(','),
),
),
'>',
']',
),
seq('[', $.type, ']'),
// Function
seq(
'(',
repeat(
@ -447,27 +469,31 @@ module.exports = grammar({
$.struct_definition,
),
type_arguments: $ =>
seq(
'<',
repeat1(
seq(
$.type,
optional(','),
),
),
'>',
),
enum_definition: $ =>
prec.right(
seq(
'enum',
$.identifier,
optional($.type_arguments),
repeat(
seq(
'{',
repeat1(
seq(
$.identifier,
optional(
seq(
'(',
choice(
$.type,
$.type_definition,
),
')',
),
),
optional($.type_arguments),
optional(','),
),
),

View File

@ -1,20 +1,33 @@
(expression) @expression
(statement) @statement
[
(expression)
(function_expression)
(index_expression)
] @expression
(value) @value
(identifier) @variable
(value) @value
(string) @string
[
(integer)
(float)
] @number
(function) @function
[
(command)
(function)
] @function
(range) @range
(boolean) @boolean
(list) @list
(map) @map
["," ":" ";"] @punctuation.delimiter
(struct_definition) @struct
(enum_definition) @enum
(block) @block
["," ";"] @punctuation.delimiter
[
"["
@ -27,30 +40,32 @@
")"
] @punctuation.bracket
[
(type)
(type_specification)
] @type
(type) @type
(assignment_operator) @operator.assignment
(logic_operator) @operator.logic
(math_operator) @operator.math
[
"async"
"else"
"else if"
"false"
"for"
"if"
"in"
"match"
"self"
"true"
"while"
"->"
"=>"
] @keyword
(built_in_function) @function.builtin
"as" @keyword
"async" @keyword
"break" @keyword
"else" @keyword
"else if" @keyword
"enum" @keyword
"false" @keyword
"for" @keyword
"if" @keyword
"in" @keyword
"match" @keyword
"return" @keyword
"struct" @keyword
"true" @keyword
"while" @keyword
"->" @keyword
":" @keyword
"::" @keyword
"^" @keyword
"loop" @keyword
(function_call) @function.call

View File

@ -93,6 +93,10 @@
"type": "SYMBOL",
"name": "index_assignment"
},
{
"type": "SYMBOL",
"name": "loop_node"
},
{
"type": "SYMBOL",
"name": "match"
@ -146,7 +150,7 @@
"members": [
{
"type": "SYMBOL",
"name": "as"
"name": "as_node"
},
{
"type": "SYMBOL",
@ -207,7 +211,7 @@
}
}
},
"as": {
"as_node": {
"type": "SEQ",
"members": [
{
@ -293,7 +297,7 @@
},
"command_text": {
"type": "PATTERN",
"value": "\\S+"
"value": "[^\\s;]+"
},
"command_argument": {
"type": "CHOICE",
@ -978,6 +982,19 @@
]
}
},
"loop_node": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "loop"
},
{
"type": "SYMBOL",
"name": "block"
}
]
},
"while": {
"type": "SEQ",
"members": [
@ -1072,10 +1089,43 @@
"type": "STRING",
"value": "int"
},
{
"type": "STRING",
"value": "list"
},
{
"type": "STRING",
"value": "map"
},
{
"type": "STRING",
"value": "none"
},
{
"type": "STRING",
"value": "num"
},
{
"type": "STRING",
"value": "str"
},
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "type_arguments"
}
]
},
{
"type": "SEQ",
"members": [
@ -1105,35 +1155,36 @@
}
]
},
{
"type": "STRING",
"value": "none"
},
{
"type": "STRING",
"value": "num"
},
{
"type": "STRING",
"value": "str"
},
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
"type": "STRING",
"value": "list"
},
{
"type": "STRING",
"value": "<"
},
{
"type": "REPEAT1",
"type": "SYMBOL",
"name": "type"
},
{
"type": "STRING",
"value": ">"
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "["
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
@ -1156,23 +1207,6 @@
]
}
},
{
"type": "STRING",
"value": ">"
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "["
},
{
"type": "SYMBOL",
"name": "type"
},
{
"type": "STRING",
"value": "]"
@ -1386,6 +1420,43 @@
}
]
},
"type_arguments": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "<"
},
{
"type": "REPEAT1",
"content": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "type"
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "BLANK"
}
]
}
]
}
},
{
"type": "STRING",
"value": ">"
}
]
},
"enum_definition": {
"type": "PREC_RIGHT",
"value": 0,
@ -1400,6 +1471,18 @@
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "type_arguments"
},
{
"type": "BLANK"
}
]
},
{
"type": "REPEAT",
"content": {
@ -1422,30 +1505,8 @@
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "("
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "type"
},
{
"type": "SYMBOL",
"name": "type_definition"
}
]
},
{
"type": "STRING",
"value": ")"
}
]
"type": "SYMBOL",
"name": "type_arguments"
},
{
"type": "BLANK"

View File

@ -1,6 +1,6 @@
[
{
"type": "as",
"type": "as_node",
"named": true,
"fields": {},
"children": {
@ -141,11 +141,7 @@
"named": true
},
{
"type": "type",
"named": true
},
{
"type": "type_definition",
"type": "type_arguments",
"named": true
}
]
@ -194,7 +190,7 @@
"required": true,
"types": [
{
"type": "as",
"type": "as_node",
"named": true
},
{
@ -475,6 +471,21 @@
"named": true,
"fields": {}
},
{
"type": "loop_node",
"named": true,
"fields": {},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "block",
"named": true
}
]
}
},
{
"type": "map",
"named": true,
@ -649,6 +660,10 @@
"type": "index_assignment",
"named": true
},
{
"type": "loop_node",
"named": true
},
{
"type": "match",
"named": true
@ -731,6 +746,10 @@
"type": "type",
"named": true
},
{
"type": "type_arguments",
"named": true
},
{
"type": "type_specification",
"named": true
@ -738,6 +757,21 @@
]
}
},
{
"type": "type_arguments",
"named": true,
"fields": {},
"children": {
"multiple": true,
"required": true,
"types": [
{
"type": "type",
"named": true
}
]
}
},
{
"type": "type_definition",
"named": true,
@ -1030,6 +1064,14 @@
"type": "integer",
"named": true
},
{
"type": "list",
"named": false
},
{
"type": "loop",
"named": false
},
{
"type": "map",
"named": false

File diff suppressed because it is too large Load Diff