diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..10e6520 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust Toolchain + run: | + rustup toolchain install stable --profile minimal + rustup component add clippy + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Format + run: cargo fmt --check + + - name: Lint + run: cargo clippy -- -D warnings + + - name: Test + run: cargo test + + - name: Doc + run: RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaf3d36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.direnv +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9579744 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nolana" +version = "1.0.0" +edition = "2021" +authors = ["arexon "] +categories = ["parser-implementations", "compilers"] +description = "An extremely fast parser Molang parser." +repository = "https://github.com/arexon/nolana" +license = "MIT" + +[dependencies] +logos = "0.15.0" +miette = "7.2.0" +oxc_allocator = "0.40.1" + +[dev-dependencies] +criterion = "0.5.1" +insta = "1.41.1" +miette = { version = "7.2.0", features = ["fancy"] } + +[[bench]] +name = "parser" +harness = false + +[[bench]] +name = "codegen" +harness = false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c4037e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 arexon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bc4607 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +## 🌺 Nolana + +> Nolana is an extremely fast parser for [Molang](https://bedrock.dev/docs/stable/Molang). + +Project goals, in roughly descending priority: + +- Be fully compliant with Molang +- Optimize for speed while maintaining a user-friendly AST +- Ensure the parser can recover from most syntax errors +- Provide extra tools for code generation (printing), simple semantic analysis, and AST traversal + +## ⚡ Performance + +Run `just bench` to try the benchmarks. + +```norust +test parser ... bench: 593 ns/iter (+/- 5) +test codegen ... bench: 182 ns/iter (+/- 1) +``` + +> CPU: Intel Core i5-12400F + +Nolana achieves this performance by leveraging [logos](https://github.com/maciejhirsz/logos) as its lexer, avoiding unnecessary allocations by using an arena allocator, and ensuring the memory size of each AST node is small. + +## 📝 Example + +```rust +use nolana::{ + allocator::Allocator, + codegen::{Codegen, CodegenOptions}, + parser::{Parser, ParserReturn}, + semantic::SemanticChecker, +}; + +let source_text = "math.cos(q.anim_time * 38) * v.rotation_scale + v.x * v.x * q.life_time"; + +// Create an arena allocator to store the AST nodes. +let allocator = Allocator::default(); + +// Parse the provided Molang source code. +let ParserReturn { + program, + errors, + panicked, +} = Parser::new(&allocator, source_text).parse(); + +// Check for syntax errors. +if !errors.is_empty() { + for error in errors { + let error = error.with_source_code(source_text); + print!("{error:?}"); + } + if panicked { + println!("Encountered an unrecoverable error"); + } + return; +} + +// Pretty print the AST. +println!("AST: {:#?}", program); +``` + +For more examples, check the [examples](./examples) directory. + +## 📖 License + +Nolana is free and open-source software distributed under the [MIT License](./LICENSE). diff --git a/benches/codegen.rs b/benches/codegen.rs new file mode 100644 index 0000000..34ffbcf --- /dev/null +++ b/benches/codegen.rs @@ -0,0 +1,22 @@ +use std::fs; + +use criterion::{criterion_group, criterion_main, Criterion}; +use nolana::{allocator::Allocator, ast::Program, codegen::Codegen, parser::Parser}; + +fn codegen(program: &Program) { + let _ = Codegen::default().build(program); +} + +fn bench_codegen(c: &mut Criterion) { + let allocator = Allocator::default(); + let source_code = fs::read_to_string("benches/sample.molang").unwrap(); + let ret = Parser::new(&allocator, &source_code).parse(); + c.bench_function("codegen", |b| { + b.iter(|| { + codegen(&ret.program); + }); + }); +} + +criterion_group!(parser, bench_codegen); +criterion_main!(parser); diff --git a/benches/parser.rs b/benches/parser.rs new file mode 100644 index 0000000..98ea01f --- /dev/null +++ b/benches/parser.rs @@ -0,0 +1,22 @@ +use std::fs; + +use criterion::{criterion_group, criterion_main, Criterion}; +use nolana::{allocator::Allocator, parser::Parser}; + +fn parse(allocator: &Allocator, source: &str) { + let _ = Parser::new(allocator, source).parse(); +} + +fn bench_parser(c: &mut Criterion) { + let mut allocator = Allocator::default(); + let source_code = fs::read_to_string("benches/sample.molang").unwrap(); + c.bench_function("parser", |b| { + b.iter(|| { + parse(&allocator, &source_code); + allocator.reset(); + }); + }); +} + +criterion_group!(parser, bench_parser); +criterion_main!(parser); diff --git a/benches/sample.molang b/benches/sample.molang new file mode 100644 index 0000000..bf7369f --- /dev/null +++ b/benches/sample.molang @@ -0,0 +1 @@ +variable.hand_bob = query.life_time < 0.01 ? 0.0 : variable.hand_bob + ((query.is_on_ground && query.is_alive ? math.clamp(math.sqrt(math.pow(query.position_delta(0), 2.0) + math.pow(query.position_delta(2), 2.0)), 0.0, 0.1) : 0.0) - variable.hand_bob) * 0.02; diff --git a/examples/full.rs b/examples/full.rs new file mode 100644 index 0000000..c111057 --- /dev/null +++ b/examples/full.rs @@ -0,0 +1,48 @@ +use std::fs; + +use nolana::{ + allocator::Allocator, + codegen::{Codegen, CodegenOptions}, + parser::{Parser, ParserReturn}, + semantic::SemanticChecker, +}; + +fn main() { + let source_text = fs::read_to_string("examples/sample.molang").unwrap(); + + let allocator = Allocator::default(); + + let ParserReturn { + program, + errors, + panicked, + } = Parser::new(&allocator, &source_text).parse(); + + if !errors.is_empty() { + for error in errors { + let error = error.with_source_code(source_text.clone()); + print!("{error:?}"); + } + if panicked { + println!("Encountered an unrecoverable error"); + } + return; + } + + let errors = SemanticChecker::default().check(&program); + if !errors.is_empty() { + for error in errors { + let error = error.with_source_code(source_text.clone()); + print!("{error:?}"); + } + return; + } + + println!("AST: {:#?}", program); + + let output = Codegen::default() + .with_options(CodegenOptions { minify: true }) + .build(&program); + + println!("Printed Molang: {output}"); +} diff --git a/examples/parser.rs b/examples/parser.rs new file mode 100644 index 0000000..c24834c --- /dev/null +++ b/examples/parser.rs @@ -0,0 +1,31 @@ +use std::fs; + +use nolana::{ + allocator::Allocator, + parser::{Parser, ParserReturn}, +}; + +fn main() { + let source_text = fs::read_to_string("examples/sample.molang").unwrap(); + + let allocator = Allocator::default(); + + let ParserReturn { + program, + errors, + panicked, + } = Parser::new(&allocator, &source_text).parse(); + + if !errors.is_empty() { + for error in errors { + let error = error.with_source_code(source_text.clone()); + print!("{error:?}"); + } + if panicked { + println!("Encountered an unrecoverable error"); + } + return; + } + + println!("AST: {:#?}", program); +} diff --git a/examples/sample.molang b/examples/sample.molang new file mode 100644 index 0000000..44db2f6 --- /dev/null +++ b/examples/sample.molang @@ -0,0 +1 @@ +math.cos(q.anim_time * 38) * v.rotation_scale + v.x * v.x * q.life_time diff --git a/examples/stats.rs b/examples/stats.rs new file mode 100644 index 0000000..cfb6b04 --- /dev/null +++ b/examples/stats.rs @@ -0,0 +1,61 @@ +use std::fs; + +use nolana::{ + allocator::Allocator, + ast::{CallExpression, CallKind, Program}, + parser::{Parser, ParserReturn}, + visit::{walk::walk_call_expression, Visit}, +}; + +#[derive(Debug)] +struct MolangStats { + pub math_functions: u32, + pub queries: u32, +} + +impl MolangStats { + pub fn new(program: &Program) -> Self { + let mut stats = Self { + math_functions: 0, + queries: 0, + }; + stats.visit_program(program); + stats + } +} + +impl<'a> Visit<'a> for MolangStats { + fn visit_call_expression(&mut self, it: &CallExpression<'a>) { + match it.kind { + CallKind::Math => self.math_functions += 1, + CallKind::Query => self.queries += 1, + } + walk_call_expression(self, it); + } +} + +fn main() { + let source_text = fs::read_to_string("examples/sample.molang").unwrap(); + + let allocator = Allocator::default(); + + let ParserReturn { + program, + errors, + panicked, + } = Parser::new(&allocator, &source_text).parse(); + + if !errors.is_empty() { + for error in errors { + let error = error.with_source_code(source_text.clone()); + print!("{error:?}"); + } + if panicked { + println!("The encountered errors were unrecoverable"); + } + return; + } + + let molang_stats = MolangStats::new(&program); + println!("{molang_stats:?}"); +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3fde363 --- /dev/null +++ b/flake.lock @@ -0,0 +1,82 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1733749988, + "narHash": "sha256-+5qdtgXceqhK5ZR1YbP1fAUsweBIrhL38726oIEAtDs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bc27f0fde01ce4e1bfec1ab122d72b7380278e68", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733970833, + "narHash": "sha256-sPEKtSaZk2CtfF9cdhtbY93S6qGq+d2PKI1fcoDfDaI=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "f7f4c59ccdf1bec3f1547d27398e9589aa94e3e8", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..82deae8 --- /dev/null +++ b/flake.nix @@ -0,0 +1,40 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + flake-utils.url = "github:numtide/flake-utils"; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + nixpkgs, + flake-utils, + rust-overlay, + ... + }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs { + inherit system; + overlays = [(import rust-overlay)]; + }; + in { + formatter = pkgs.alejandra; + + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + (rust-bin.stable.latest.default.override { + extensions = ["rust-analyzer" "rust-src"]; + }) + cargo-insta + just + gnuplot + release-plz + github-cli + ]; + }; + }); +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..b6b2ff6 --- /dev/null +++ b/justfile @@ -0,0 +1,39 @@ +_default: + just --list --unsorted + +# Make sure you have cargo-binstall installed. +# Install tools required for developing the project. +init: + cargo binstall cargo-insta + +# When ready, run the same tasks as CI +ready: + just fmt + just lint + just test + just doc + +# Format all files +fmt: + cargo fmt + +# Run all the tests with cargo-insta +test: + cargo insta test --review + +# Lint the whole project +lint: + cargo clippy -- --deny warnings + +# Run all benchmarks +bench: + cargo bench --bench parser -- --output-format bencher + cargo bench --bench codegen -- --output-format bencher + +[unix] +doc: + RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items + +[windows] +doc: + $Env:RUSTDOCFLAGS='-D warnings'; cargo doc --no-deps --document-private-items diff --git a/src/ast.rs b/src/ast.rs new file mode 100644 index 0000000..470bf97 --- /dev/null +++ b/src/ast.rs @@ -0,0 +1,490 @@ +use oxc_allocator::{Box, Vec}; + +use crate::{span::Span, token::Kind}; + +/// Untyped AST Node kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AstKind { + Program, + BooleanLiteral, + NumericLiteral, + StringLiteral, + IdentifierReference, + VariableExpression, + VariableMember, + ParenthesizedExpression, + BlockExpression, + BinaryExpression, + UnaryExpression, + TernaryExpression, + ConditionalExpression, + AssignmentExpression, + ResourceExpression, + ArrayAccessExpression, + ArrowAccessExpression, + CallExpression, + LoopExpression, + ForEachExpression, + Break, + Continue, + This, + Return, +} + +/// Represents the root of a Molang expression AST, containing all the top-level +/// information. +#[derive(Debug)] +pub struct Program<'a> { + pub span: Span, + pub source: &'a str, + /// Determines whether the expression is complex or simple. If it contains + /// at least one `;` or `=`, it is considered a complex expression. + pub is_complex: bool, + pub body: Vec<'a, Expression<'a>>, +} + +/// +#[derive(Debug)] +pub enum Expression<'a> { + BooleanLiteral(Box<'a, BooleanLiteral>), + NumericLiteral(Box<'a, NumericLiteral<'a>>), + StringLiteral(Box<'a, StringLiteral<'a>>), + Variable(Box<'a, VariableExpression<'a>>), + Parenthesized(Box<'a, ParenthesizedExpression<'a>>), + Block(Box<'a, BlockExpression<'a>>), + Binary(Box<'a, BinaryExpression<'a>>), + Unary(Box<'a, UnaryExpression<'a>>), + Ternary(Box<'a, TernaryExpression<'a>>), + Conditional(Box<'a, ConditionalExpression<'a>>), + Assignment(Box<'a, AssignmentExpression<'a>>), + Resource(Box<'a, ResourceExpression<'a>>), + ArrayAccess(Box<'a, ArrayAccessExpression<'a>>), + ArrowAccess(Box<'a, ArrowAccessExpression<'a>>), + Call(Box<'a, CallExpression<'a>>), + Loop(Box<'a, LoopExpression<'a>>), + ForEach(Box<'a, ForEachExpression<'a>>), + Break(Box<'a, Break>), + Continue(Box<'a, Continue>), + This(Box<'a, This>), + Return(Box<'a, Return<'a>>), +} + +/// `true` or `false` +#[derive(Debug)] +pub struct BooleanLiteral { + pub span: Span, + pub value: bool, +} + +impl BooleanLiteral { + /// Returns `"true"` or `"false"` depending on this boolean's value. + pub fn as_str(&self) -> &'static str { + if self.value { + "true" + } else { + "false" + } + } +} + +/// `1.23` in `v.a = 1.23;` +#[derive(Debug)] +pub struct NumericLiteral<'a> { + pub span: Span, + pub value: f32, + pub raw: &'a str, +} + +/// +/// +/// `'foo bar'` in `v.a = 'foo bar';` +#[derive(Debug)] +pub struct StringLiteral<'a> { + pub span: Span, + pub value: &'a str, +} + +/// `foo` in `v.foo.bar` +#[derive(Debug)] +pub struct IdentifierReference<'a> { + pub span: Span, + pub name: &'a str, +} + +/// +#[derive(Debug)] +pub struct VariableExpression<'a> { + pub span: Span, + pub lifetime: VariableLifetime, + pub member: VariableMember<'a>, +} + +/// The variable lifetime associated with [`VariableExpression`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum VariableLifetime { + /// `temp` in `temp.foo` + Temporary, + /// `variable` in `variable.foo` + Variable, + /// `context` in `context.foo` + Context, +} + +impl VariableLifetime { + /// String representation of the call kind ("temp", "variable", or "context"). + pub fn as_str(&self) -> &'static str { + match self { + Self::Temporary => "temp", + Self::Variable => "variable", + Self::Context => "context", + } + } +} + +impl From for VariableLifetime { + fn from(kind: Kind) -> Self { + match kind { + Kind::Temporary => Self::Temporary, + Kind::Variable => Self::Variable, + Kind::Context => Self::Context, + _ => unreachable!("Variable Lifetime: {kind:?}"), + } + } +} + +/// +#[derive(Debug)] +pub enum VariableMember<'a> { + /// `foo.bar` in `v.foo.bar` + Object { + span: Span, + object: Box<'a, VariableMember<'a>>, + property: IdentifierReference<'a>, + }, + /// `foo` in `v.foo` + Property { + span: Span, + property: IdentifierReference<'a>, + }, +} + +#[derive(Debug)] +pub enum ParenthesizedExpression<'a> { + /// `(1 + 1)` in `(1 + 1) * 2` + Single { + span: Span, + expression: Expression<'a>, + }, + /// `(v.a = 1;)` in `(v.b = 'B'; v.a = 1;);` + Complex { + span: Span, + expressions: Vec<'a, Expression<'a>>, + }, +} + +/// `{ v.a = 0; }` in `loop(10, { v.a = 0; })` +#[derive(Debug)] +pub struct BlockExpression<'a> { + pub span: Span, + pub expressions: Vec<'a, Expression<'a>>, +} + +/// `1 + 1` in `v.a = 1 + 1;` +#[derive(Debug)] +pub struct BinaryExpression<'a> { + pub span: Span, + pub left: Expression<'a>, + pub operator: BinaryOperator, + pub right: Expression<'a>, +} + +/// Operators used in [`BinaryExpression`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOperator { + /// `==` + Equality, + /// `!=` + Inequality, + /// `<` + LessThan, + /// `<=` + LessEqualThan, + /// `>` + GreaterThan, + /// `>=` + GreaterEqualThan, + /// `+` + Addition, + /// `-` + Subtraction, + /// `*` + Multiplication, + /// `/` + Division, + /// `||` + Or, + /// `&&` + And, + /// `??` + Coalesce, +} + +impl BinaryOperator { + /// The string representation of this operator as it appears in source code. + pub fn as_str(&self) -> &'static str { + match self { + Self::Equality => "==", + Self::Inequality => "!=", + Self::LessThan => "<", + Self::LessEqualThan => "<=", + Self::GreaterThan => ">", + Self::GreaterEqualThan => ">=", + Self::Addition => "+", + Self::Subtraction => "-", + Self::Multiplication => "*", + Self::Division => "/", + Self::Or => "||", + Self::And => "&&", + Self::Coalesce => "??", + } + } +} + +impl From for BinaryOperator { + fn from(token: Kind) -> Self { + match token { + Kind::Eq => Self::Equality, + Kind::NotEq => Self::Inequality, + Kind::Lt => Self::LessThan, + Kind::Gt => Self::GreaterThan, + Kind::LtEq => Self::LessEqualThan, + Kind::GtEq => Self::GreaterEqualThan, + Kind::Or => Self::Or, + Kind::And => Self::And, + Kind::NullCoal => Self::Coalesce, + Kind::Minus => Self::Subtraction, + Kind::Plus => Self::Addition, + Kind::Star => Self::Multiplication, + Kind::Slash => Self::Division, + _ => unreachable!("Binary Operator: {token:?}"), + } + } +} + +/// `-1` in `q.foo(-1)` +#[derive(Debug)] +pub struct UnaryExpression<'a> { + pub span: Span, + pub operator: UnaryOperator, + pub argument: Expression<'a>, +} + +/// Operators used in [`UnaryExpression`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnaryOperator { + /// `-` + Negate, + /// `!` + Not, +} + +impl UnaryOperator { + /// The string representation of this operator as it appears in source code. + pub fn as_str(&self) -> &'static str { + match self { + Self::Negate => "-", + Self::Not => "!", + } + } +} + +impl From for UnaryOperator { + fn from(token: Kind) -> Self { + match token { + Kind::Minus => Self::Negate, + Kind::Not => Self::Not, + _ => unreachable!("Unary Operator: {token:?}"), + } + } +} + +/// +/// +/// `q.foo ? 0 : 1` +#[derive(Debug)] +pub struct TernaryExpression<'a> { + pub span: Span, + pub test: Expression<'a>, + pub consequent: Expression<'a>, + pub alternate: Expression<'a>, +} + +/// +/// +/// `q.foo ? 0` +#[derive(Debug)] +pub struct ConditionalExpression<'a> { + pub span: Span, + pub test: Expression<'a>, + pub consequent: Expression<'a>, +} + +/// `v.a = 0;` +#[derive(Debug)] +pub struct AssignmentExpression<'a> { + pub span: Span, + pub left: VariableExpression<'a>, + pub right: Expression<'a>, +} + +/// +#[derive(Debug)] +pub struct ResourceExpression<'a> { + pub span: Span, + pub section: ResourceSection, + pub name: IdentifierReference<'a>, +} + +/// The resource section in [`ResourceExpression`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceSection { + /// `geometry` in `geometry.foo` + Geometry, + /// `material` in `material.foo` + Material, + /// `texture` in `texture.foo` + Texture, +} + +impl ResourceSection { + /// String representation of the resource section ("geometry", "material", or "texture"). + pub fn as_str(&self) -> &'static str { + match self { + Self::Geometry => "geometry", + Self::Material => "material", + Self::Texture => "texture", + } + } +} + +impl From for ResourceSection { + fn from(kind: Kind) -> Self { + match kind { + Kind::Geometry => Self::Geometry, + Kind::Material => Self::Material, + Kind::Texture => Self::Texture, + _ => unreachable!("Resource Section: {kind:?}"), + } + } +} + +/// +/// +/// `array.foo[0]` +#[derive(Debug)] +pub struct ArrayAccessExpression<'a> { + pub span: Span, + pub name: IdentifierReference<'a>, + pub index: Expression<'a>, +} + +/// +/// +/// `v.foo->q.bar` +#[derive(Debug)] +pub struct ArrowAccessExpression<'a> { + pub span: Span, + pub left: Expression<'a>, + pub right: Expression<'a>, +} + +/// +/// +/// +/// `math.random(1, 2)` or `math.random` +#[derive(Debug)] +pub struct CallExpression<'a> { + pub span: Span, + pub kind: CallKind, + pub callee: IdentifierReference<'a>, + pub arguments: Option>>, +} + +impl CallKind { + /// String representation of the call kind ("math" or "query"). + pub fn as_str(&self) -> &'static str { + match self { + Self::Math => "math", + Self::Query => "query", + } + } +} + +impl From for CallKind { + fn from(kind: Kind) -> Self { + match kind { + Kind::Math => Self::Math, + Kind::Query => Self::Query, + _ => unreachable!("Call Kind: {kind:?}"), + } + } +} + +/// The call kind for [`CallExpression`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallKind { + /// `math` in `math.foo` + Math, + /// `query` in `query.foo` + Query, +} + +/// +/// +/// `loop(10, { v.x = v.x + 1; });` +#[derive(Debug)] +pub struct LoopExpression<'a> { + pub span: Span, + pub count: Expression<'a>, + pub expression: BlockExpression<'a>, +} + +/// +/// +/// `for_each(t.foo, q.baz, { v.x = v.x + 1; });` +#[derive(Debug)] +pub struct ForEachExpression<'a> { + pub span: Span, + pub variable: VariableExpression<'a>, + pub array: Expression<'a>, + pub expression: BlockExpression<'a>, +} + +/// +/// +/// `break` in `loop(10, { v.x = v.x + 1; (v.x > 20) ? break; });` +#[derive(Debug)] +pub struct Break { + pub span: Span, +} + +/// +/// +/// `continue` in `loop(10, { (v.x > 5) ? continue; v.x = v.x + 1; });` +#[derive(Debug)] +pub struct Continue { + pub span: Span, +} + +/// `this` in `q.foo(this)` +#[derive(Debug)] +pub struct This { + pub span: Span, +} + +/// `return` in `v.a = 1; return v.a;` +#[derive(Debug)] +pub struct Return<'a> { + pub span: Span, + pub argument: Expression<'a>, +} diff --git a/src/ast_builder.rs b/src/ast_builder.rs new file mode 100644 index 0000000..d514673 --- /dev/null +++ b/src/ast_builder.rs @@ -0,0 +1,511 @@ +use oxc_allocator::{Allocator, Box, IntoIn, Vec}; + +use crate::{ast::*, span::Span}; + +/// Builder for creating AST nodes. +#[derive(Clone, Copy)] +pub struct AstBuilder<'a> { + /// The arena allocator used to store AST nodes. + pub allocator: &'a Allocator, +} + +impl<'a> AstBuilder<'a> { + #[inline] + pub fn new(allocator: &'a Allocator) -> Self { + Self { allocator } + } + + #[inline] + pub fn alloc(self, value: T) -> Box<'a, T> { + Box::new_in(value, self.allocator) + } + + #[inline] + pub fn vec(self) -> Vec<'a, T> { + Vec::new_in(self.allocator) + } + + #[inline] + pub fn program( + self, + span: Span, + source: &'a str, + is_complex: bool, + body: Vec<'a, Expression<'a>>, + ) -> Program<'a> { + Program { + span, + source, + is_complex, + body, + } + } + + #[inline] + pub fn identifier_reference(self, span: Span, name: S) -> IdentifierReference<'a> + where + S: IntoIn<'a, &'a str>, + { + IdentifierReference { + span, + name: name.into_in(self.allocator), + } + } + + #[inline] + pub fn expression_boolean_literal(self, span: Span, value: bool) -> Expression<'a> { + Expression::BooleanLiteral(self.alloc(self.boolean_literal(span, value))) + } + + #[inline] + pub fn expression_numeric_literal(self, span: Span, value: f32, raw: S) -> Expression<'a> + where + S: IntoIn<'a, &'a str>, + { + Expression::NumericLiteral(self.alloc(self.numeric_literal(span, value, raw))) + } + + #[inline] + pub fn expression_string_literal(self, span: Span, value: S) -> Expression<'a> + where + S: IntoIn<'a, &'a str>, + { + Expression::StringLiteral(self.alloc(self.string_literal(span, value))) + } + + #[inline] + pub fn expression_variable( + self, + span: Span, + lifetime: VariableLifetime, + member: VariableMember<'a>, + ) -> Expression<'a> { + Expression::Variable(self.alloc(self.variable_expression(span, lifetime, member))) + } + + #[inline] + pub fn expression_parenthesized_single( + self, + span: Span, + expression: Expression<'a>, + ) -> Expression<'a> { + Expression::Parenthesized( + self.alloc(self.parenthesized_single_expression(span, expression)), + ) + } + + #[inline] + pub fn expression_parenthesized_complex( + self, + span: Span, + expressions: Vec<'a, Expression<'a>>, + ) -> Expression<'a> { + Expression::Parenthesized( + self.alloc(self.parenthesized_complex_expression(span, expressions)), + ) + } + + #[inline] + pub fn expression_block( + self, + span: Span, + expressions: Vec<'a, Expression<'a>>, + ) -> Expression<'a> { + Expression::Block(self.alloc(self.block_expression(span, expressions))) + } + + #[inline] + pub fn expression_binary( + self, + span: Span, + left: Expression<'a>, + operator: BinaryOperator, + right: Expression<'a>, + ) -> Expression<'a> { + Expression::Binary(self.alloc(self.binary_expression(span, left, operator, right))) + } + + #[inline] + pub fn expression_unary( + self, + span: Span, + operator: UnaryOperator, + argument: Expression<'a>, + ) -> Expression<'a> { + Expression::Unary(self.alloc(self.unary_expression(span, operator, argument))) + } + + #[inline] + pub fn expression_ternary( + self, + span: Span, + test: Expression<'a>, + consequent: Expression<'a>, + alternate: Expression<'a>, + ) -> Expression<'a> { + Expression::Ternary(self.alloc(self.ternary_expression(span, test, consequent, alternate))) + } + + #[inline] + pub fn expression_conditional( + self, + span: Span, + test: Expression<'a>, + consequent: Expression<'a>, + ) -> Expression<'a> { + Expression::Conditional(self.alloc(self.conditional_expression(span, test, consequent))) + } + + #[inline] + pub fn expression_assignment( + self, + span: Span, + left: VariableExpression<'a>, + right: Expression<'a>, + ) -> Expression<'a> { + Expression::Assignment(self.alloc(self.assignment_expression(span, left, right))) + } + + #[inline] + pub fn expression_resource( + self, + span: Span, + section: ResourceSection, + name: IdentifierReference<'a>, + ) -> Expression<'a> { + Expression::Resource(self.alloc(self.resource_expression(span, section, name))) + } + + #[inline] + pub fn expression_array_access( + self, + span: Span, + name: IdentifierReference<'a>, + index: Expression<'a>, + ) -> Expression<'a> { + Expression::ArrayAccess(self.alloc(self.array_access_expression(span, name, index))) + } + + #[inline] + pub fn expression_arrow_access( + self, + span: Span, + left: Expression<'a>, + right: Expression<'a>, + ) -> Expression<'a> { + Expression::ArrowAccess(self.alloc(self.arrow_access_expression(span, left, right))) + } + + #[inline] + pub fn expression_call( + self, + span: Span, + kind: CallKind, + callee: IdentifierReference<'a>, + arguments: Option>>, + ) -> Expression<'a> { + Expression::Call(self.alloc(self.call_expression(span, kind, callee, arguments))) + } + + #[inline] + pub fn expression_loop( + self, + span: Span, + count: Expression<'a>, + expression: BlockExpression<'a>, + ) -> Expression<'a> { + Expression::Loop(self.alloc(self.loop_expression(span, count, expression))) + } + + #[inline] + pub fn expression_for_each( + self, + span: Span, + variable: VariableExpression<'a>, + array: Expression<'a>, + expression: BlockExpression<'a>, + ) -> Expression<'a> { + Expression::ForEach(self.alloc(self.for_each_expression(span, variable, array, expression))) + } + + #[inline] + pub fn expression_break(self, span: Span) -> Expression<'a> { + Expression::Break(self.alloc(self.r#break(span))) + } + + #[inline] + pub fn expression_continue(self, span: Span) -> Expression<'a> { + Expression::Continue(self.alloc(self.r#continue(span))) + } + + #[inline] + pub fn expression_this(self, span: Span) -> Expression<'a> { + Expression::This(self.alloc(self.this(span))) + } + + #[inline] + pub fn expression_return(self, span: Span, argument: Expression<'a>) -> Expression<'a> { + Expression::Return(self.alloc(self.r#return(span, argument))) + } + + #[inline] + pub fn boolean_literal(self, span: Span, value: bool) -> BooleanLiteral { + BooleanLiteral { span, value } + } + + #[inline] + pub fn numeric_literal(self, span: Span, value: f32, raw: S) -> NumericLiteral<'a> + where + S: IntoIn<'a, &'a str>, + { + NumericLiteral { + span, + value, + raw: raw.into_in(self.allocator), + } + } + + #[inline] + pub fn string_literal(self, span: Span, value: S) -> StringLiteral<'a> + where + S: IntoIn<'a, &'a str>, + { + StringLiteral { + span, + value: value.into_in(self.allocator), + } + } + + #[inline] + pub fn variable_expression( + self, + span: Span, + lifetime: VariableLifetime, + member: VariableMember<'a>, + ) -> VariableExpression<'a> { + VariableExpression { + span, + lifetime, + member, + } + } + + #[inline] + pub fn variable_member_object( + self, + span: Span, + object: VariableMember<'a>, + property: IdentifierReference<'a>, + ) -> VariableMember<'a> { + VariableMember::Object { + span, + object: object.into_in(self.allocator), + property, + } + } + + #[inline] + pub fn variable_member_property( + self, + span: Span, + property: IdentifierReference<'a>, + ) -> VariableMember<'a> { + VariableMember::Property { span, property } + } + + #[inline] + pub fn parenthesized_single_expression( + self, + span: Span, + expression: Expression<'a>, + ) -> ParenthesizedExpression<'a> { + ParenthesizedExpression::Single { span, expression } + } + + #[inline] + pub fn parenthesized_complex_expression( + self, + span: Span, + expressions: Vec<'a, Expression<'a>>, + ) -> ParenthesizedExpression<'a> { + ParenthesizedExpression::Complex { span, expressions } + } + + #[inline] + pub fn block_expression( + self, + span: Span, + expressions: Vec<'a, Expression<'a>>, + ) -> BlockExpression<'a> { + BlockExpression { span, expressions } + } + + #[inline] + pub fn binary_expression( + self, + span: Span, + left: Expression<'a>, + operator: BinaryOperator, + right: Expression<'a>, + ) -> BinaryExpression<'a> { + BinaryExpression { + span, + left, + operator, + right, + } + } + + #[inline] + pub fn unary_expression( + self, + span: Span, + operator: UnaryOperator, + argument: Expression<'a>, + ) -> UnaryExpression<'a> { + UnaryExpression { + span, + operator, + argument, + } + } + + #[inline] + pub fn ternary_expression( + self, + span: Span, + test: Expression<'a>, + consequent: Expression<'a>, + alternate: Expression<'a>, + ) -> TernaryExpression<'a> { + TernaryExpression { + span, + test, + consequent, + alternate, + } + } + + #[inline] + pub fn conditional_expression( + self, + span: Span, + test: Expression<'a>, + consequent: Expression<'a>, + ) -> ConditionalExpression<'a> { + ConditionalExpression { + span, + test, + consequent, + } + } + + #[inline] + pub fn assignment_expression( + self, + span: Span, + left: VariableExpression<'a>, + right: Expression<'a>, + ) -> AssignmentExpression<'a> { + AssignmentExpression { span, left, right } + } + + #[inline] + pub fn resource_expression( + self, + span: Span, + section: ResourceSection, + name: IdentifierReference<'a>, + ) -> ResourceExpression<'a> { + ResourceExpression { + span, + section, + name, + } + } + + #[inline] + pub fn array_access_expression( + self, + span: Span, + name: IdentifierReference<'a>, + index: Expression<'a>, + ) -> ArrayAccessExpression<'a> { + ArrayAccessExpression { span, name, index } + } + + pub fn arrow_access_expression( + self, + span: Span, + left: Expression<'a>, + right: Expression<'a>, + ) -> ArrowAccessExpression<'a> { + ArrowAccessExpression { span, left, right } + } + + #[inline] + pub fn call_expression( + self, + span: Span, + kind: CallKind, + callee: IdentifierReference<'a>, + arguments: Option>>, + ) -> CallExpression<'a> { + CallExpression { + span, + kind, + callee, + arguments, + } + } + + #[inline] + pub fn loop_expression( + self, + span: Span, + count: Expression<'a>, + expression: BlockExpression<'a>, + ) -> LoopExpression<'a> { + LoopExpression { + span, + count, + expression, + } + } + + #[inline] + pub fn for_each_expression( + self, + span: Span, + variable: VariableExpression<'a>, + array: Expression<'a>, + expression: BlockExpression<'a>, + ) -> ForEachExpression<'a> { + ForEachExpression { + span, + variable, + array, + expression, + } + } + + #[inline] + pub fn r#break(self, span: Span) -> Break { + Break { span } + } + + #[inline] + pub fn r#continue(self, span: Span) -> Continue { + Continue { span } + } + + #[inline] + pub fn this(self, span: Span) -> This { + This { span } + } + + #[inline] + pub fn r#return(self, span: Span, argument: Expression<'a>) -> Return<'a> { + Return { span, argument } + } +} diff --git a/src/codegen.rs b/src/codegen.rs new file mode 100644 index 0000000..d4cbd5f --- /dev/null +++ b/src/codegen.rs @@ -0,0 +1,378 @@ +use crate::ast::*; + +#[derive(Default)] +pub struct CodegenOptions { + pub minify: bool, +} + +#[derive(Default)] +pub struct Codegen { + code: String, + is_complex: bool, + options: CodegenOptions, +} + +impl Codegen { + pub fn build(mut self, program: &Program) -> String { + self.code.reserve(program.source.len()); + self.is_complex = program.is_complex; + program.gen(&mut self); + self.code + } + + pub fn with_options(mut self, options: CodegenOptions) -> Self { + self.options = options; + self + } + + #[inline] + fn print_str(&mut self, s: &str) { + self.code.push_str(s); + } + + #[inline] + fn print_char(&mut self, ch: char) { + self.code.push(ch); + } + + #[inline] + fn print_space(&mut self) { + if self.options.minify { + return; + } + self.code.push(' '); + } + + #[inline] + fn print_dot(&mut self) { + self.code.push('.'); + } + + #[inline] + fn print_comma(&mut self) { + self.code.push(','); + } + + #[inline] + fn print_colon(&mut self) { + self.code.push(':'); + } + + #[inline] + fn print_semi(&mut self) { + self.code.push(';'); + } + + #[inline] + fn print_list(&mut self, items: &[T]) { + for (index, item) in items.iter().enumerate() { + if index != 0 { + self.print_comma(); + self.print_space(); + } + item.gen(self); + } + } +} + +/// Generate code for an AST. +pub trait Gen { + fn gen(&self, c: &mut Codegen); +} + +impl Gen for Program<'_> { + fn gen(&self, c: &mut Codegen) { + for expr in &self.body { + expr.gen(c); + if c.is_complex { + c.print_semi(); + } + } + } +} + +impl Gen for Expression<'_> { + fn gen(&self, c: &mut Codegen) { + match self { + Self::BooleanLiteral(expr) => expr.gen(c), + Self::NumericLiteral(expr) => expr.gen(c), + Self::StringLiteral(expr) => expr.gen(c), + Self::Variable(expr) => expr.gen(c), + Self::Parenthesized(expr) => expr.gen(c), + Self::Block(expr) => expr.gen(c), + Self::Binary(expr) => expr.gen(c), + Self::Unary(expr) => expr.gen(c), + Self::Ternary(expr) => expr.gen(c), + Self::Conditional(expr) => expr.gen(c), + Self::Assignment(expr) => expr.gen(c), + Self::Resource(expr) => expr.gen(c), + Self::ArrayAccess(expr) => expr.gen(c), + Self::ArrowAccess(expr) => expr.gen(c), + Self::Call(expr) => expr.gen(c), + Self::Loop(expr) => expr.gen(c), + Self::ForEach(expr) => expr.gen(c), + Self::Break(expr) => expr.gen(c), + Self::Continue(expr) => expr.gen(c), + Self::This(expr) => expr.gen(c), + Self::Return(expr) => expr.gen(c), + } + } +} + +impl Gen for IdentifierReference<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_str(self.name); + } +} + +impl Gen for BooleanLiteral { + fn gen(&self, c: &mut Codegen) { + c.print_str(self.as_str()); + } +} + +impl Gen for NumericLiteral<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_str(self.raw); + } +} + +impl Gen for StringLiteral<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_char('\''); + c.print_str(self.value); + c.print_char('\''); + } +} + +impl Gen for VariableExpression<'_> { + fn gen(&self, c: &mut Codegen) { + self.lifetime.gen(c); + c.print_dot(); + self.member.gen(c); + } +} + +impl Gen for VariableLifetime { + fn gen(&self, c: &mut Codegen) { + let lifetime = self.as_str(); + if c.options.minify { + c.print_char(lifetime.chars().nth(0).unwrap()); + } else { + c.print_str(lifetime); + } + } +} + +impl Gen for VariableMember<'_> { + fn gen(&self, c: &mut Codegen) { + match self { + Self::Object { + object, property, .. + } => { + object.gen(c); + c.print_dot(); + property.gen(c); + } + Self::Property { property, .. } => { + property.gen(c); + } + } + } +} + +impl Gen for ParenthesizedExpression<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_char('('); + match self { + Self::Single { expression, .. } => expression.gen(c), + Self::Complex { expressions, .. } => { + for expr in expressions { + expr.gen(c); + c.print_semi(); + } + } + } + c.print_char(')'); + } +} + +impl Gen for BlockExpression<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_char('{'); + for expr in &self.expressions { + expr.gen(c); + c.print_semi(); + } + c.print_char('}'); + } +} + +impl Gen for BinaryExpression<'_> { + fn gen(&self, c: &mut Codegen) { + self.left.gen(c); + c.print_space(); + self.operator.gen(c); + c.print_space(); + self.right.gen(c); + } +} + +impl Gen for BinaryOperator { + fn gen(&self, c: &mut Codegen) { + c.print_str(self.as_str()); + } +} + +impl Gen for UnaryExpression<'_> { + fn gen(&self, c: &mut Codegen) { + self.operator.gen(c); + self.argument.gen(c); + } +} + +impl Gen for UnaryOperator { + fn gen(&self, c: &mut Codegen) { + c.print_str(self.as_str()); + } +} + +impl Gen for TernaryExpression<'_> { + fn gen(&self, c: &mut Codegen) { + self.test.gen(c); + c.print_space(); + c.print_char('?'); + c.print_space(); + self.consequent.gen(c); + c.print_space(); + c.print_colon(); + c.print_space(); + self.alternate.gen(c); + } +} + +impl Gen for ConditionalExpression<'_> { + fn gen(&self, c: &mut Codegen) { + self.test.gen(c); + c.print_space(); + c.print_char('?'); + c.print_space(); + self.consequent.gen(c); + } +} + +impl Gen for AssignmentExpression<'_> { + fn gen(&self, c: &mut Codegen) { + self.left.gen(c); + c.print_space(); + c.print_char('='); + c.print_space(); + self.right.gen(c); + } +} + +impl Gen for ResourceExpression<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_str(self.section.as_str()); + c.print_dot(); + self.name.gen(c); + } +} + +impl Gen for ArrayAccessExpression<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_str("array."); + self.name.gen(c); + c.print_char('['); + self.index.gen(c); + c.print_char(']'); + } +} + +impl Gen for ArrowAccessExpression<'_> { + fn gen(&self, c: &mut Codegen) { + self.left.gen(c); + c.print_str("->"); + self.right.gen(c); + } +} + +impl Gen for CallExpression<'_> { + fn gen(&self, c: &mut Codegen) { + self.kind.gen(c); + c.print_dot(); + self.callee.gen(c); + if let Some(args) = &self.arguments { + c.print_char('('); + c.print_list(args); + c.print_char(')'); + } + } +} + +impl Gen for CallKind { + fn gen(&self, c: &mut Codegen) { + if let Self::Query = self { + let kind = self.as_str(); + if c.options.minify { + c.print_char(kind.chars().nth(0).unwrap()); + } else { + c.print_str(kind); + } + } else { + c.print_str(self.as_str()); + } + } +} + +impl Gen for LoopExpression<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_str("loop"); + c.print_char('('); + self.count.gen(c); + c.print_comma(); + c.print_space(); + self.expression.gen(c); + c.print_char(')'); + } +} + +impl Gen for ForEachExpression<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_str("for_each"); + c.print_char('('); + self.variable.gen(c); + c.print_comma(); + c.print_space(); + self.array.gen(c); + c.print_comma(); + c.print_space(); + self.expression.gen(c); + c.print_char(')'); + } +} + +impl Gen for Break { + fn gen(&self, c: &mut Codegen) { + c.print_str("break"); + } +} + +impl Gen for Continue { + fn gen(&self, c: &mut Codegen) { + c.print_str("continue"); + } +} + +impl Gen for Return<'_> { + fn gen(&self, c: &mut Codegen) { + c.print_str("return "); + self.argument.gen(c); + } +} + +impl Gen for This { + fn gen(&self, c: &mut Codegen) { + c.print_str("this"); + } +} diff --git a/src/diagnostic.rs b/src/diagnostic.rs new file mode 100644 index 0000000..7b13676 --- /dev/null +++ b/src/diagnostic.rs @@ -0,0 +1,199 @@ +use std::{borrow::Cow, fmt, ops}; + +use miette::{Diagnostic as MietteDiagnostic, LabeledSpan, Severity, SourceCode}; + +pub type Error = miette::Error; + +/// Alias for a `Result` with the error type as [`Diagnostic`]. +pub type Result = std::result::Result; + +/// Describes an error or warning that occurred during parsing. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Diagnostic { + // Boxed to make `Diagnostic` 8 bytes so that `Result` is small. + // This is due to rust not supporting return value optimization. + // + inner: Box, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DiagnosticInner { + pub message: Cow<'static, str>, + pub labels: Option>, + pub help: Option>, + pub severity: Severity, +} + +impl Diagnostic { + /// Creates a new error-level [`Diagnostic`]. + pub fn error(message: impl Into>) -> Self { + Self { + inner: Box::new(DiagnosticInner { + message: message.into(), + labels: None, + help: None, + severity: Severity::Error, + }), + } + } + + /// Creates a new warning-level [`Diagnostic`]. + pub fn warning(message: impl Into>) -> Self { + Self { + inner: Box::new(DiagnosticInner { + message: message.into(), + labels: None, + help: None, + severity: Severity::Warning, + }), + } + } + + /// Sets a possible suggestion for a problem to the user. + pub fn with_help(mut self, help: impl Into>) -> Self { + self.inner.help = Some(help.into()); + self + } + + /// Sets a label covering the problematic portion of the source code. + /// + /// Existing labels will be removed. Use [`Diagnostic::add_label`] to append + /// labels instead. + pub fn with_label(mut self, label: impl Into) -> Self { + self.inner.labels = Some(vec![label.into()]); + self + } + + /// Appends a label to this diagnostic without affecting previous ones. + pub fn add_label(mut self, label: impl Into) -> Self { + let mut labels = self.inner.labels.unwrap_or_default(); + labels.push(label.into()); + self.inner.labels = Some(labels); + self + } + + /// Adds a source to this diagnostic and converts it into an [`Error`]. + pub fn with_source_code(self, code: impl SourceCode + 'static) -> Error { + Error::from(self).with_source_code(code) + } +} + +impl fmt::Display for Diagnostic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.message) + } +} + +impl std::error::Error for Diagnostic {} + +impl ops::Deref for Diagnostic { + type Target = DiagnosticInner; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl MietteDiagnostic for Diagnostic { + fn severity(&self) -> Option { + Some(self.severity) + } + + fn help<'a>(&'a self) -> Option> { + self.help + .as_ref() + .map(Box::new) + .map(|help| help as Box) + } + + fn labels(&self) -> Option + '_>> { + self.labels + .as_ref() + .map(|labels| labels.iter().cloned()) + .map(Box::new) + .map(|labels| labels as Box>) + } +} + +pub mod errors { + use crate::span::Span; + + use super::*; + + #[cold] + pub fn invalid_number(span: Span) -> Diagnostic { + Diagnostic::error("Invalid number").with_label(span) + } + + #[cold] + pub fn for_each_wrong_first_arg(span: Span) -> Diagnostic { + Diagnostic::error("`for_each` first argument must be either a `variable.` or a `temp.`") + .with_label(span) + } + + #[cold] + pub fn unexpected_token(span: Span) -> Diagnostic { + Diagnostic::error("Unexpected token").with_label(span) + } + + #[cold] + pub fn expected_token(expected: &str, found: &str, span: Span) -> Diagnostic { + Diagnostic::error(format!("Expected `{expected}` but found `{found}`")) + .with_label(span.label("Here")) + } + + #[cold] + pub fn semi_required(span: Span) -> Diagnostic { + Diagnostic::error( + "Semicolons are required for complex Molang expressions (contain `=` or `;`)", + ) + .with_help("Try inserting a semicolon here") + .with_label(span) + } + + #[cold] + pub fn semi_required_in_block_expression(span: Span) -> Diagnostic { + Diagnostic::error("Expressions inside `{}` must be delimited by `;`") + .with_help("Try inserting a semicolon here") + .with_label(span) + } + + #[cold] + pub fn empty_block_expression(span: Span) -> Diagnostic { + Diagnostic::error("Block expressions must contain at least one expression").with_label(span) + } + + #[cold] + pub fn illegal_string_operators(span: Span) -> Diagnostic { + Diagnostic::error("Strings only support `==` and `!=` operators").with_label(span) + } + + #[cold] + pub fn break_outside_loop(span: Span) -> Diagnostic { + Diagnostic::error("`break` is only supported inside `loop` and `for_each` expressions") + .with_label(span) + } + + #[cold] + pub fn continue_outside_loop(span: Span) -> Diagnostic { + Diagnostic::error("`continue` is only supported inside `loop` and `for_each` expressions") + .with_label(span) + } + + #[cold] + pub fn assigning_context(span: Span) -> Diagnostic { + Diagnostic::error("`context.` variables are read-only") + .with_help("Try assigning to `variable.` or `temp.` instead") + .with_label(span) + } + + #[cold] + pub fn unterminated_string(span: Span) -> Diagnostic { + Diagnostic::error("Unterminated string").with_label(span) + } + + #[cold] + pub fn empty_parenthesized_expression(span: Span) -> Diagnostic { + Diagnostic::error("Empty parenthesized expression").with_label(span) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a686045 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,15 @@ +#![doc = include_str!("../README.md")] + +pub mod ast; +pub mod ast_builder; +pub mod codegen; +pub mod diagnostic; +pub mod parser; +pub mod semantic; +pub mod span; +mod token; +pub mod visit; +pub mod visit_mut; +pub mod allocator { + pub use oxc_allocator::*; +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..5c0972d --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,560 @@ +use logos::{Lexer, Logos}; +use oxc_allocator::Allocator; + +use crate::{ + ast::*, + ast_builder::AstBuilder, + diagnostic::{errors, Diagnostic, Result}, + span::Span, + token::{Kind, Token}, +}; + +/// Return value of [`Parser::parse`] which contains the AST and errors. +/// +/// ## AST +/// +/// [`program`] will always contain structurally valid AST, even if there +/// are syntax errors. However, the AST may be semantically invalid. To ensure it is valid: +/// +/// 1. Check that [`errors`] is empty +/// 2. Analyze the AST semantically with [`SemanticChcker`][`crate::semantic::SemanticChecker`] +/// +/// ## Errors +/// +/// Nolana is able to recover from most syntax errors and continue parsing +/// anyway. When this happens: +/// 1. [`program`] will contain an AST +/// 2. [`errors`] will be non-empty +/// 3. [`panicked`] will be false +/// +/// [`program`]: ParserReturn::program +/// [`errors`]: ParserReturn::errors +/// [`panicked`]: ParserReturn::panicked +#[derive(Debug)] +pub struct ParserReturn<'a> { + pub program: Program<'a>, + pub errors: Vec, + pub panicked: bool, +} + +/// Recursive Descent Parser for [Molang](https://bedrock.dev/docs/stable/Molang). +pub struct Parser<'a> { + lexer: Lexer<'a, Kind>, + source_code: &'a str, + token: Token, + prev_token_end: u32, + /// An expression is considered a [`complex expression`] if there is at + /// least one `=` or `;`. + /// + /// [`complex expression`]: https://bedrock.dev/docs/stable/Molang#Simple%20vs%20Complex%20Expressions + is_complex: bool, + errors: Vec, + /// For building AST nodes inside an arena allocator. + ast: AstBuilder<'a>, +} + +impl<'a> Parser<'a> { + /// Creates a new [`Parser`]. + pub fn new(allocator: &'a Allocator, source_code: &'a str) -> Self { + Self { + lexer: Logos::lexer(source_code), + source_code, + token: Token::default(), + prev_token_end: 0, + is_complex: false, + errors: Vec::new(), + ast: AstBuilder::new(allocator), + } + } + + /// Main entry point. + /// + /// See [`ParserReturn`] for more info. + pub fn parse(mut self) -> ParserReturn<'a> { + self.bump(); // First token. + let (program, panicked) = match self.parse_program() { + Ok(program) => (program, false), + Err(error) => { + self.error(error); + let program = + self.ast + .program(Span::default(), self.source_code, false, self.ast.vec()); + (program, true) + } + }; + ParserReturn { + program, + errors: self.errors, + panicked, + } + } + + fn parse_program(&mut self) -> Result> { + let span = self.start_span(); + let mut exprs = self.ast.vec(); + while !self.at(Kind::Eof) { + if let Some(stmt) = self.parse_expression_delimited_by_semi()? { + exprs.push(stmt); + } + } + Ok(self.ast.program( + self.end_span(span), + self.source_code, + self.is_complex, + exprs, + )) + } + + fn parse_expression_delimited_by_semi(&mut self) -> Result>> { + let expr = match self.current_kind() { + Kind::Semi => None, // We skip expressions that start with `;`. + _ => Some(self.parse_expression(0)?), + }; + if self.eat(Kind::Semi) { + if !self.is_complex { + self.is_complex = true; + } + } else if self.is_complex { + self.error(errors::semi_required(self.current_token().span())); + } + Ok(expr) + } + + fn parse_expression(&mut self, min_bp: u8) -> Result> { + let span = self.start_span(); + let mut lhs = match self.current_kind() { + Kind::True | Kind::False => self.parse_literal_boolean()?, + Kind::Number => self.parse_literal_number()?, + Kind::String => self.parse_literal_string()?, + Kind::Temporary | Kind::Variable | Kind::Context => { + self.parse_variable_expression_rest()? + } + Kind::LeftParen => self.parse_parenthesized_expression()?, + Kind::LeftBrace => self + .parse_block_expression() + .map(|expr| Expression::Block(self.ast.alloc(expr)))?, + Kind::Minus | Kind::Not => self.parse_unary_expression()?, + Kind::Query | Kind::Math => self.parse_call_expression()?, + Kind::Geometry | Kind::Material | Kind::Texture => self.parse_resource_expression()?, + Kind::Array => self.parse_array_access_expression()?, + Kind::Loop => self.parse_loop_expression()?, + Kind::ForEach => self.parse_for_each_expression()?, + Kind::Break => self.parse_break_expression()?, + Kind::Continue => self.parse_continue_expression()?, + Kind::This => self.parse_this_expression()?, + Kind::Return => self.parse_return_expression()?, + Kind::UnterminatedString => { + return Err(errors::unterminated_string(self.end_span(span))) + } + _ => return Err(errors::unexpected_token(self.current_token().span())), + }; + + loop { + let kind = self.current_kind(); + + if kind == Kind::Arrow { + lhs = self.parse_arrow_access_expression(span, lhs)?; + break; + } + + let Some((lbp, rbp)) = kind.binding_power() else { + break; + }; + if lbp < min_bp { + break; + } + + match self.current_kind() { + kind if kind.is_binary_operator() => { + lhs = self.parse_binary_expression(span, lhs, rbp)?; + } + Kind::Conditional => { + lhs = self.parse_ternary_or_conditional_expression(span, lhs)?; + } + _ => break, + } + } + + Ok(lhs) + } + + fn parse_literal_boolean(&mut self) -> Result> { + let span = self.start_span(); + let value = match self.current_kind() { + Kind::True => true, + Kind::False => false, + kind => unreachable!("Boolean Literal: {kind:?}"), + }; + self.bump(); + Ok(self + .ast + .expression_boolean_literal(self.end_span(span), value)) + } + + fn parse_literal_number(&mut self) -> Result> { + let span = self.start_span(); + let raw = self.current_src(); + self.expect(Kind::Number)?; + let value = raw + .parse::() + .map_err(|_| errors::invalid_number(self.end_span(span)))?; + Ok(self + .ast + .expression_numeric_literal(self.end_span(span), value, raw)) + } + + pub fn parse_literal_string(&mut self) -> Result> { + let span = self.start_span(); + let value = self.current_src(); + let value = &value[1..value.len() - 1]; + self.expect(Kind::String)?; + Ok(self + .ast + .expression_string_literal(self.end_span(span), value)) + } + + #[inline(always)] // Hot path + fn parse_identifier_reference(&mut self) -> Result> { + let span = self.start_span(); + let name = self.current_src(); + match self.current_kind() { + Kind::Context | Kind::Variable | Kind::Temporary | Kind::Math | Kind::Query => { + self.bump() + } + _ => self.expect(Kind::Identifier)?, + } + Ok(self.ast.identifier_reference(self.end_span(span), name)) + } + + fn parse_parenthesized_expression(&mut self) -> Result> { + let span = self.start_span(); + self.expect(Kind::LeftParen)?; + if self.at(Kind::RightParen) { + let span = self.start_span(); + self.bump(); + return Err(errors::empty_parenthesized_expression(self.end_span(span))); + } + let expr = self.parse_expression(0)?; + if self.eat(Kind::Semi) { + let mut exprs = self.ast.vec(); + exprs.push(expr); + loop { + if let Some(stmt) = self.parse_expression_delimited_by_semi()? { + exprs.push(stmt); + } + if self.at(Kind::RightParen) { + break; + } + } + self.expect(Kind::RightParen)?; + Ok(self + .ast + .expression_parenthesized_complex(self.end_span(span), exprs)) + } else { + self.expect(Kind::RightParen)?; + Ok(self + .ast + .expression_parenthesized_single(self.end_span(span), expr)) + } + } + + fn parse_block_expression(&mut self) -> Result> { + // This deviates from Molang a little bit. However, because every + // expression inside `{}` must be delimited with a `;`, it is grammatically + // correct to do this early. + if !self.is_complex { + self.is_complex = true; + } + let span = self.start_span(); + self.expect(Kind::LeftBrace)?; + let mut exprs = self.ast.vec(); + while !self.at(Kind::RightBrace) { + exprs.push(self.parse_expression(0)?); + if !self.eat(Kind::Semi) { + self.error(errors::semi_required_in_block_expression( + self.current_token().span(), + )) + } + } + self.expect(Kind::RightBrace)?; + Ok(self.ast.block_expression(self.end_span(span), exprs)) + } + + fn parse_binary_expression( + &mut self, + left_span: Span, + left: Expression<'a>, + rbp: u8, + ) -> Result> { + let operator = self.current_kind().into(); + self.bump(); + let right = self.parse_expression(rbp)?; + Ok(self + .ast + .expression_binary(self.end_span(left_span), left, operator, right)) + } + + fn parse_unary_expression(&mut self) -> Result> { + let span = self.start_span(); + let operator = self.current_kind().into(); + self.bump(); + let argument = self.parse_expression(0)?; + Ok(self + .ast + .expression_unary(self.end_span(span), operator, argument)) + } + + fn parse_ternary_or_conditional_expression( + &mut self, + test_span: Span, + test: Expression<'a>, + ) -> Result> { + self.expect(Kind::Conditional)?; + let consequent = self.parse_expression(0)?; + if self.eat(Kind::Colon) { + let alternate = self.parse_expression(0)?; + Ok(self + .ast + .expression_ternary(self.end_span(test_span), test, consequent, alternate)) + } else { + Ok(self + .ast + .expression_conditional(self.end_span(test_span), test, consequent)) + } + } + + fn parse_variable_expression(&mut self) -> Result> { + let span = self.start_span(); + let lifetime: VariableLifetime = self.current_kind().into(); + self.bump(); + self.expect(Kind::Dot)?; + let property = self.parse_identifier_reference()?; + let mut member = self + .ast + .variable_member_property(self.end_span(span), property); + while self.eat(Kind::Dot) { + let property = self.parse_identifier_reference()?; + member = self + .ast + .variable_member_object(self.end_span(span), member, property); + } + Ok(self + .ast + .variable_expression(self.end_span(span), lifetime, member)) + } + + fn parse_variable_expression_rest(&mut self) -> Result> { + let span = self.start_span(); + let left = self.parse_variable_expression()?; + if self.eat(Kind::Assign) { + if !self.is_complex { + self.is_complex = true; + } + let right = self.parse_expression(0)?; + Ok(self + .ast + .expression_assignment(self.end_span(span), left, right)) + } else { + Ok(Expression::Variable(self.ast.alloc(left))) + } + } + + fn parse_resource_expression(&mut self) -> Result> { + let span = self.start_span(); + let section: ResourceSection = self.current_kind().into(); + self.bump(); + self.expect(Kind::Dot)?; + let name = self.parse_identifier_reference()?; + Ok(self + .ast + .expression_resource(self.end_span(span), section, name)) + } + + fn parse_array_access_expression(&mut self) -> Result> { + let span = self.start_span(); + self.expect(Kind::Array)?; + self.expect(Kind::Dot)?; + let name = self.parse_identifier_reference()?; + self.expect(Kind::LeftBracket)?; + let index = self.parse_expression(0)?; + self.expect(Kind::RightBracket)?; + Ok(self + .ast + .expression_array_access(self.end_span(span), name, index)) + } + + fn parse_arrow_access_expression( + &mut self, + left_span: Span, + left: Expression<'a>, + ) -> Result> { + self.expect(Kind::Arrow)?; + let right = self.parse_expression(0)?; + Ok(self + .ast + .expression_arrow_access(self.end_span(left_span), left, right)) + } + + fn parse_call_expression(&mut self) -> Result> { + let span = self.start_span(); + let kind: CallKind = self.current_kind().into(); + self.bump(); + self.expect(Kind::Dot)?; + let callee = self.parse_identifier_reference()?; + let arguments = if self.eat(Kind::LeftParen) { + let mut arguments = self.ast.vec(); + let mut first = true; + loop { + if self.at(Kind::RightParen) || self.at(Kind::Eof) { + break; + } + if first { + first = false; + } else { + self.expect(Kind::Comma)?; + if self.at(Kind::RightParen) { + break; + } + } + arguments.push(self.parse_expression(0)?); + } + self.expect(Kind::RightParen)?; + Some(arguments) + } else { + None + }; + Ok(self + .ast + .expression_call(self.end_span(span), kind, callee, arguments)) + } + + fn parse_loop_expression(&mut self) -> Result> { + let span = self.start_span(); + self.expect(Kind::Loop)?; + self.expect(Kind::LeftParen)?; + let count = self.parse_expression(0)?; + self.expect(Kind::Comma)?; + let expr = self.parse_block_expression()?; + self.expect(Kind::RightParen)?; + Ok(self.ast.expression_loop(self.end_span(span), count, expr)) + } + + fn parse_for_each_expression(&mut self) -> Result> { + let span = self.start_span(); + self.expect(Kind::ForEach)?; + self.expect(Kind::LeftParen)?; + if !(self.at(Kind::Variable) || self.at(Kind::Temporary)) { + return Err(errors::for_each_wrong_first_arg( + self.current_token().span(), + )); + } + let variable = self.parse_variable_expression()?; + self.expect(Kind::Comma)?; + let array = self.parse_expression(0)?; + self.expect(Kind::Comma)?; + let expr = self.parse_block_expression()?; + self.expect(Kind::RightParen)?; + Ok(self + .ast + .expression_for_each(self.end_span(span), variable, array, expr)) + } + + fn parse_break_expression(&mut self) -> Result> { + let span = self.start_span(); + self.expect(Kind::Break)?; + Ok(self.ast.expression_break(self.end_span(span))) + } + + fn parse_continue_expression(&mut self) -> Result> { + let span = self.start_span(); + self.expect(Kind::Continue)?; + Ok(self.ast.expression_continue(self.end_span(span))) + } + + fn parse_this_expression(&mut self) -> Result> { + let span = self.start_span(); + self.expect(Kind::This)?; + Ok(self.ast.expression_this(self.end_span(span))) + } + + fn parse_return_expression(&mut self) -> Result> { + let span = self.start_span(); + self.expect(Kind::Return)?; + let argument = self.parse_expression(0)?; + Ok(self.ast.expression_return(self.end_span(span), argument)) + } + + #[inline] + fn current_token(&self) -> Token { + self.token + } + + #[inline] + fn current_kind(&self) -> Kind { + self.token.kind + } + + #[inline] + fn current_src(&self) -> &'a str { + self.lexer.slice() + } + + #[inline] + fn start_span(&self) -> Span { + Span::new(self.token.start, 0) + } + + #[inline] + fn end_span(&self, mut span: Span) -> Span { + span.end = self.prev_token_end; + debug_assert!(span.end >= span.start); + span + } + + #[inline] + fn at(&self, kind: Kind) -> bool { + self.current_kind() == kind + } + + #[inline(always)] // Hot path + fn bump(&mut self) { + self.prev_token_end = self.token.end; + let kind = self + .lexer + .next() + .unwrap_or(Ok(Kind::Eof)) + .unwrap_or(Kind::UnterminatedString); + let span = self.lexer.span(); + self.token = Token { + kind, + start: span.start as u32, + end: span.end as u32, + }; + } + + #[inline] + fn eat(&mut self, kind: Kind) -> bool { + if self.at(kind) { + self.bump(); + return true; + } + false + } + + #[inline(always)] // Hot path + fn expect(&mut self, kind: Kind) -> Result<()> { + if !self.eat(kind) { + return Err(self.expected_token(kind)); + } + Ok(()) + } + + fn expected_token(&self, kind: Kind) -> Diagnostic { + let curr_token = self.current_token(); + errors::expected_token(kind.to_str(), curr_token.kind.to_str(), curr_token.span()) + } + + fn error(&mut self, error: Diagnostic) { + self.errors.push(error); + } +} diff --git a/src/semantic.rs b/src/semantic.rs new file mode 100644 index 0000000..6d104e6 --- /dev/null +++ b/src/semantic.rs @@ -0,0 +1,81 @@ +use crate::{ + ast::*, + diagnostic::{errors, Diagnostic}, + visit::{walk::*, Visit}, +}; + +/// Traverses an AST and checks the Molang program for any semantic errors. +#[derive(Default)] +pub struct SemanticChecker { + /// `loop` and `for_each` level. + loop_depth: u32, + errors: Vec, +} + +impl SemanticChecker { + /// Main entry point. + pub fn check(mut self, program: &Program) -> Vec { + self.visit_program(program); + self.errors + } + + const fn in_loop(kind: AstKind) -> bool { + matches!(kind, AstKind::LoopExpression | AstKind::ForEachExpression) + } +} + +impl<'a> Visit<'a> for SemanticChecker { + fn enter_node(&mut self, kind: AstKind) { + if Self::in_loop(kind) { + self.loop_depth += 1; + } + } + + fn leave_node(&mut self, kind: AstKind) { + if Self::in_loop(kind) { + self.loop_depth -= 1; + } + } + + fn visit_block_expression(&mut self, it: &BlockExpression<'a>) { + if it.expressions.is_empty() { + self.errors.push(errors::empty_block_expression(it.span)); + } + walk_block_expression(self, it); + } + + fn visit_binary_expression(&mut self, it: &BinaryExpression<'a>) { + use BinaryOperator::*; + use Expression::*; + match (&it.left, it.operator, &it.right) { + (StringLiteral(_), op, StringLiteral(_)) if !matches!(op, Equality | Inequality) => (), + (left, _, StringLiteral(_)) if !matches!(left, StringLiteral(_)) => (), + (StringLiteral(_), _, right) if !matches!(right, StringLiteral(_)) => (), + _ => { + walk_binary_expression(self, it); + return; + } + } + self.errors.push(errors::illegal_string_operators(it.span)); + walk_binary_expression(self, it); + } + + fn visit_assignment_expression(&mut self, it: &AssignmentExpression<'a>) { + if it.left.lifetime == VariableLifetime::Context { + self.errors.push(errors::assigning_context(it.span)) + } + walk_assignment_expression(self, it); + } + + fn visit_break(&mut self, it: &Break) { + if self.loop_depth == 0 { + self.errors.push(errors::break_outside_loop(it.span)); + } + } + + fn visit_continue(&mut self, it: &Continue) { + if self.loop_depth == 0 { + self.errors.push(errors::continue_outside_loop(it.span)); + } + } +} diff --git a/src/span.rs b/src/span.rs new file mode 100644 index 0000000..92398db --- /dev/null +++ b/src/span.rs @@ -0,0 +1,53 @@ +use std::ops::{Index, IndexMut}; + +use miette::{LabeledSpan, SourceOffset, SourceSpan}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Span { + pub start: u32, + pub end: u32, +} + +impl Span { + pub const fn new(start: u32, end: u32) -> Self { + Self { start, end } + } + + pub fn label(self, label: impl Into) -> LabeledSpan { + LabeledSpan::new_with_span(Some(label.into()), self) + } + + const fn size(&self) -> u32 { + debug_assert!(self.start <= self.end); + self.end - self.start + } +} + +impl Index for str { + type Output = str; + + fn index(&self, index: Span) -> &Self::Output { + &self[index.start as usize..index.end as usize] + } +} + +impl IndexMut for str { + fn index_mut(&mut self, index: Span) -> &mut Self::Output { + &mut self[index.start as usize..index.end as usize] + } +} + +impl From for SourceSpan { + fn from(span: Span) -> Self { + Self::new( + SourceOffset::from(span.start as usize), + span.size() as usize, + ) + } +} + +impl From for LabeledSpan { + fn from(span: Span) -> Self { + Self::underline(span) + } +} diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..bf0ddc7 --- /dev/null +++ b/src/token.rs @@ -0,0 +1,417 @@ +use logos::Logos; + +use crate::span::Span; + +#[derive(Debug, Default, Clone, Copy)] +pub struct Token { + pub kind: Kind, + pub start: u32, + pub end: u32, +} + +impl Token { + pub const fn span(&self) -> Span { + Span::new(self.start, self.end) + } +} + +#[derive(Debug, PartialEq, Clone, Copy, Default, Logos)] +#[logos(skip "[ \t\n\r]+")] +pub enum Kind { + #[default] + Eof, + + #[regex("[a-zA-Z_]+[a-zA-Z0-9_]*")] + Identifier, + + #[regex("'[^']*'")] + String, + + #[regex("'[^']*")] + UnterminatedString, + + // NOTE: The optional 'f' suffix must be removed. + #[regex(r"[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?f?")] + Number, + + #[token("(")] + LeftParen, + + #[token(")")] + RightParen, + + #[token("{")] + LeftBrace, + + #[token("}")] + RightBrace, + + #[token("[")] + LeftBracket, + + #[token("]")] + RightBracket, + + #[token("=")] + Assign, + + #[token("!")] + Not, + + #[token("==")] + Eq, + + #[token("!=")] + NotEq, + + #[token("<")] + Lt, + + #[token(">")] + Gt, + + #[token("<=")] + LtEq, + + #[token(">=")] + GtEq, + + #[token("||")] + Or, + + #[token("&&")] + And, + + #[token("->")] + Arrow, + + #[token(".")] + Dot, + + #[token("?")] + Conditional, + + #[token("??")] + NullCoal, + + #[token(":")] + Colon, + + #[token(";")] + Semi, + + #[token(",")] + Comma, + + #[token("-")] + Minus, + + #[token("+")] + Plus, + + #[token("*")] + Star, + + #[token("/")] + Slash, + + #[token("temp")] + #[token("t", priority = 3)] + Temporary, + + #[token("variable")] + #[token("v", priority = 3)] + Variable, + + #[token("context")] + #[token("c", priority = 3)] + Context, + + #[regex(r"[Mm]ath")] + Math, + + #[regex(r"[Qq]uery")] + #[token("q", priority = 3)] + Query, + + #[regex(r"[Gg]eometry")] + Geometry, + + #[regex(r"[Mm]aterial")] + Material, + + #[regex(r"[Tt]exture")] + Texture, + + #[regex(r"[Aa]rray")] + Array, + + #[token("true")] + True, + + #[token("false")] + False, + + #[token("this")] + This, + + #[token("break")] + Break, + + #[token("continue")] + Continue, + + #[token("for_each")] + ForEach, + + #[token("loop")] + Loop, + + #[token("return")] + Return, +} + +impl Kind { + pub fn is_binary_operator(self) -> bool { + matches!( + self, + Kind::Eq + | Kind::NotEq + | Kind::Lt + | Kind::Gt + | Kind::LtEq + | Kind::GtEq + | Kind::Or + | Kind::And + | Kind::NullCoal + | Kind::Minus + | Kind::Plus + | Kind::Star + | Kind::Slash + ) + } + + /// + pub fn binding_power(self) -> Option<(u8, u8)> { + Some(match self { + Self::Not => (16, 17), + Self::Star | Self::Slash => (14, 15), + Self::Plus | Self::Minus => (12, 13), + Self::Lt | Self::Gt | Self::LtEq | Self::GtEq => (10, 11), + Self::Eq | Self::NotEq => (8, 9), + Self::And => (6, 7), + Self::Or => (4, 5), + Self::Conditional => (3, 4), + Self::NullCoal => (1, 2), + _ => return None, + }) + } + + pub fn to_str(self) -> &'static str { + match self { + Kind::Eof => "EOF", + Kind::Identifier => "Identifier", + Kind::String => "string", + Kind::UnterminatedString => "Unterminated String", + Kind::Number => "number", + Kind::LeftParen => "(", + Kind::RightParen => ")", + Kind::LeftBrace => "{", + Kind::RightBrace => "}", + Kind::LeftBracket => "[", + Kind::RightBracket => "]", + Kind::Assign => "=", + Kind::Not => "!", + Kind::Eq => "==", + Kind::NotEq => "!=", + Kind::Lt => "<", + Kind::Gt => ">", + Kind::LtEq => "<=", + Kind::GtEq => ">=", + Kind::Or => "||", + Kind::And => "&&", + Kind::Arrow => "->", + Kind::Dot => ".", + Kind::Conditional => "?", + Kind::NullCoal => "??", + Kind::Colon => ":", + Kind::Semi => ";", + Kind::Comma => ",", + Kind::Minus => "-", + Kind::Plus => "+", + Kind::Star => "*", + Kind::Slash => "/", + Kind::Temporary => "temp", + Kind::Variable => "variable", + Kind::Context => "context", + Kind::Math => "math", + Kind::Query => "query", + Kind::Geometry => "geometry", + Kind::Material => "material", + Kind::Texture => "texture", + Kind::Array => "array", + Kind::True => "true", + Kind::False => "false", + Kind::This => "this", + Kind::Break => "break", + Kind::Continue => "continue", + Kind::ForEach => "for_each", + Kind::Loop => "loop", + Kind::Return => "return", + } + } +} + +#[cfg(all(test, target_pointer_width = "64"))] +mod size_asserts { + const _: () = assert!(size_of::() == 1); +} + +#[cfg(test)] +mod tests { + use logos::Logos; + + use super::*; + + fn assert_lexer(source: &str, expected: &[(Result, &str)]) { + let tokens: Vec<_> = Kind::lexer(source) + .spanned() + .map(|(token, span)| (token, &source[span])) + .collect(); + assert_eq!(tokens, expected); + } + + #[test] + fn test_identifiers() { + assert_lexer( + "foo_bar.a23", + &[ + (Ok(Kind::Identifier), "foo_bar"), + (Ok(Kind::Dot), "."), + (Ok(Kind::Identifier), "a23"), + ], + ); + } + + #[test] + fn test_strings() { + assert_lexer( + "'abc123-_!' '' ' '", + &[ + (Ok(Kind::String), "'abc123-_!'"), + (Ok(Kind::String), "''"), + (Ok(Kind::String), "' '"), + ], + ); + assert_lexer( + "'unterminated {}", + &[(Ok(Kind::UnterminatedString), "'unterminated {}")], + ); + } + + #[test] + fn test_numbers() { + assert_lexer( + "0 123 123.456 .456 123.456f 0.1 1.23e10 1.23E+10 1.23e-10f 1e5 1.5f 0.0 1.23f", + &[ + (Ok(Kind::Number), "0"), + (Ok(Kind::Number), "123"), + (Ok(Kind::Number), "123.456"), + (Ok(Kind::Number), ".456"), + (Ok(Kind::Number), "123.456f"), + (Ok(Kind::Number), "0.1"), + (Ok(Kind::Number), "1.23e10"), + (Ok(Kind::Number), "1.23E+10"), + (Ok(Kind::Number), "1.23e-10f"), + (Ok(Kind::Number), "1e5"), + (Ok(Kind::Number), "1.5f"), + (Ok(Kind::Number), "0.0"), + (Ok(Kind::Number), "1.23f"), + ], + ); + } + + #[test] + fn test_members() { + assert_lexer( + "temp t variable v context c Math math Query query q Geometry geometry Texture texture Material material Array array", + &[ + (Ok(Kind::Temporary), "temp"), + (Ok(Kind::Temporary), "t"), + (Ok(Kind::Variable), "variable"), + (Ok(Kind::Variable), "v"), + (Ok(Kind::Context), "context"), + (Ok(Kind::Context), "c"), + (Ok(Kind::Math), "Math"), + (Ok(Kind::Math), "math"), + (Ok(Kind::Query), "Query"), + (Ok(Kind::Query), "query"), + (Ok(Kind::Query), "q"), + (Ok(Kind::Geometry), "Geometry"), + (Ok(Kind::Geometry), "geometry"), + (Ok(Kind::Texture), "Texture"), + (Ok(Kind::Texture), "texture"), + (Ok(Kind::Material), "Material"), + (Ok(Kind::Material), "material"), + (Ok(Kind::Array), "Array"), + (Ok(Kind::Array), "array"), + ], + ); + } + + #[test] + fn test_keywords() { + assert_lexer( + "true false break continue for_each loop return", + &[ + (Ok(Kind::True), "true"), + (Ok(Kind::False), "false"), + (Ok(Kind::Break), "break"), + (Ok(Kind::Continue), "continue"), + (Ok(Kind::ForEach), "for_each"), + (Ok(Kind::Loop), "loop"), + (Ok(Kind::Return), "return"), + ], + ); + } + + #[test] + fn test_symbols() { + assert_lexer( + "() {} [] = ! == != <> <= >= || && -> ? ?? : ; , - + * /", + &[ + (Ok(Kind::LeftParen), "("), + (Ok(Kind::RightParen), ")"), + (Ok(Kind::LeftBrace), "{"), + (Ok(Kind::RightBrace), "}"), + (Ok(Kind::LeftBracket), "["), + (Ok(Kind::RightBracket), "]"), + (Ok(Kind::Assign), "="), + (Ok(Kind::Not), "!"), + (Ok(Kind::Eq), "=="), + (Ok(Kind::NotEq), "!="), + (Ok(Kind::Lt), "<"), + (Ok(Kind::Gt), ">"), + (Ok(Kind::LtEq), "<="), + (Ok(Kind::GtEq), ">="), + (Ok(Kind::Or), "||"), + (Ok(Kind::And), "&&"), + (Ok(Kind::Arrow), "->"), + (Ok(Kind::Conditional), "?"), + (Ok(Kind::NullCoal), "??"), + (Ok(Kind::Colon), ":"), + (Ok(Kind::Semi), ";"), + (Ok(Kind::Comma), ","), + (Ok(Kind::Minus), "-"), + (Ok(Kind::Plus), "+"), + (Ok(Kind::Star), "*"), + (Ok(Kind::Slash), "/"), + ], + ); + } + + #[test] + fn test_whitespace() { + assert_lexer("\t\r\n", &[]); + } +} diff --git a/src/visit.rs b/src/visit.rs new file mode 100644 index 0000000..7caaefa --- /dev/null +++ b/src/visit.rs @@ -0,0 +1,424 @@ +use oxc_allocator::Vec; + +use crate::ast::*; + +use walk::*; + +/// Syntax tree traversal. +pub trait Visit<'a>: Sized { + #[inline] + #[allow(unused_variables)] + fn enter_node(&mut self, kind: AstKind) {} + + #[inline] + #[allow(unused_variables)] + fn leave_node(&mut self, kind: AstKind) {} + + #[inline] + fn visit_program(&mut self, it: &Program<'a>) { + walk_program(self, it); + } + + #[inline] + fn visit_expressions(&mut self, it: &Vec<'a, Expression<'a>>) { + walk_expressions(self, it); + } + + #[inline] + fn visit_expression(&mut self, it: &Expression<'a>) { + walk_expression(self, it); + } + + #[inline] + fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) { + walk_identifier_reference(self, it) + } + + #[inline] + fn visit_boolean_literal(&mut self, it: &BooleanLiteral) { + walk_boolean_literal(self, it); + } + + #[inline] + fn visit_numeric_literal(&mut self, it: &NumericLiteral<'a>) { + walk_numeric_literal(self, it); + } + + #[inline] + fn visit_string_literal(&mut self, it: &StringLiteral<'a>) { + walk_string_literal(self, it); + } + + #[inline] + fn visit_variable_expression(&mut self, it: &VariableExpression<'a>) { + walk_variable_expression(self, it); + } + + #[inline] + fn visit_variable_member(&mut self, it: &VariableMember<'a>) { + walk_variable_member(self, it); + } + + #[inline] + fn visit_parenthesized_expression(&mut self, it: &ParenthesizedExpression<'a>) { + walk_parenthesized_expression(self, it); + } + + #[inline] + fn visit_block_expression(&mut self, it: &BlockExpression<'a>) { + walk_block_expression(self, it); + } + + #[inline] + fn visit_binary_expression(&mut self, it: &BinaryExpression<'a>) { + walk_binary_expression(self, it); + } + + #[inline] + fn visit_unary_expression(&mut self, it: &UnaryExpression<'a>) { + walk_unary_expression(self, it); + } + + #[inline] + fn visit_ternary_expression(&mut self, it: &TernaryExpression<'a>) { + walk_ternary_expression(self, it); + } + + #[inline] + fn visit_conditional_expression(&mut self, it: &ConditionalExpression<'a>) { + walk_conditional_expression(self, it); + } + + #[inline] + fn visit_assignment_expression(&mut self, it: &AssignmentExpression<'a>) { + walk_assignment_expression(self, it); + } + + #[inline] + fn visit_resource_expression(&mut self, it: &ResourceExpression<'a>) { + walk_resource_expression(self, it); + } + + #[inline] + fn visit_array_access_expression(&mut self, it: &ArrayAccessExpression<'a>) { + walk_array_access_expression(self, it); + } + + #[inline] + fn visit_arrow_access_expression(&mut self, it: &ArrowAccessExpression<'a>) { + walk_arrow_access_expression(self, it); + } + + #[inline] + fn visit_call_expression(&mut self, it: &CallExpression<'a>) { + walk_call_expression(self, it); + } + + #[inline] + fn visit_loop_expression(&mut self, it: &LoopExpression<'a>) { + walk_loop_expression(self, it); + } + + #[inline] + fn visit_for_each_expression(&mut self, it: &ForEachExpression<'a>) { + walk_for_each_expression(self, it); + } + + #[inline] + fn visit_break(&mut self, it: &Break) { + walk_break(self, it); + } + + #[inline] + fn visit_continue(&mut self, it: &Continue) { + walk_continue(self, it); + } + + #[inline] + fn visit_this(&mut self, it: &This) { + walk_this(self, it); + } + + #[inline] + fn visit_return(&mut self, it: &Return<'a>) { + walk_return(self, it); + } +} + +pub mod walk { + use super::*; + + #[inline] + pub fn walk_program<'a>(visitor: &mut impl Visit<'a>, it: &Program<'a>) { + let kind = AstKind::Program; + visitor.enter_node(kind); + visitor.visit_expressions(&it.body); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_expressions<'a>(visitor: &mut impl Visit<'a>, it: &Vec<'a, Expression<'a>>) { + for expr in it { + visitor.visit_expression(expr); + } + } + + #[inline] + pub fn walk_expression<'a>(visitor: &mut impl Visit<'a>, it: &Expression<'a>) { + match it { + Expression::BooleanLiteral(it) => visitor.visit_boolean_literal(it), + Expression::NumericLiteral(it) => visitor.visit_numeric_literal(it), + Expression::StringLiteral(it) => visitor.visit_string_literal(it), + Expression::Variable(it) => visitor.visit_variable_expression(it), + Expression::Parenthesized(it) => visitor.visit_parenthesized_expression(it), + Expression::Block(it) => visitor.visit_block_expression(it), + Expression::Binary(it) => visitor.visit_binary_expression(it), + Expression::Unary(it) => visitor.visit_unary_expression(it), + Expression::Ternary(it) => visitor.visit_ternary_expression(it), + Expression::Conditional(it) => visitor.visit_conditional_expression(it), + Expression::Assignment(it) => visitor.visit_assignment_expression(it), + Expression::Resource(it) => visitor.visit_resource_expression(it), + Expression::ArrayAccess(it) => visitor.visit_array_access_expression(it), + Expression::ArrowAccess(it) => visitor.visit_arrow_access_expression(it), + Expression::Call(it) => visitor.visit_call_expression(it), + Expression::Loop(it) => visitor.visit_loop_expression(it), + Expression::ForEach(it) => visitor.visit_for_each_expression(it), + Expression::Break(it) => visitor.visit_break(it), + Expression::Continue(it) => visitor.visit_continue(it), + Expression::This(it) => visitor.visit_this(it), + Expression::Return(it) => visitor.visit_return(it), + } + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_identifier_reference<'a>( + visitor: &mut impl Visit<'a>, + it: &IdentifierReference<'a>, + ) { + let kind = AstKind::IdentifierReference; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_boolean_literal<'a>(visitor: &mut impl Visit<'a>, it: &BooleanLiteral) { + let kind = AstKind::BooleanLiteral; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_numeric_literal<'a>(visitor: &mut impl Visit<'a>, it: &NumericLiteral<'a>) { + let kind = AstKind::NumericLiteral; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_string_literal<'a>(visitor: &mut impl Visit<'a>, it: &StringLiteral<'a>) { + let kind = AstKind::StringLiteral; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_variable_expression<'a>(visitor: &mut impl Visit<'a>, it: &VariableExpression<'a>) { + let kind = AstKind::VariableExpression; + visitor.enter_node(kind); + visitor.visit_variable_member(&it.member); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_variable_member<'a>(visitor: &mut impl Visit<'a>, it: &VariableMember<'a>) { + let kind = AstKind::VariableMember; + visitor.enter_node(kind); + match it { + VariableMember::Object { + object, property, .. + } => { + visitor.visit_variable_member(object); + visitor.visit_identifier_reference(property); + } + VariableMember::Property { property, .. } => { + visitor.visit_identifier_reference(property); + } + } + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_parenthesized_expression<'a>( + visitor: &mut impl Visit<'a>, + it: &ParenthesizedExpression<'a>, + ) { + let kind = AstKind::ParenthesizedExpression; + visitor.enter_node(kind); + match it { + ParenthesizedExpression::Single { expression, .. } => { + visitor.visit_expression(expression); + } + ParenthesizedExpression::Complex { expressions, .. } => { + visitor.visit_expressions(expressions); + } + } + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_block_expression<'a>(visitor: &mut impl Visit<'a>, it: &BlockExpression<'a>) { + let kind = AstKind::BlockExpression; + visitor.enter_node(kind); + visitor.visit_expressions(&it.expressions); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_binary_expression<'a>(visitor: &mut impl Visit<'a>, it: &BinaryExpression<'a>) { + let kind = AstKind::BinaryExpression; + visitor.enter_node(kind); + visitor.visit_expression(&it.left); + visitor.visit_expression(&it.right); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_unary_expression<'a>(visitor: &mut impl Visit<'a>, it: &UnaryExpression<'a>) { + let kind = AstKind::UnaryExpression; + visitor.enter_node(kind); + visitor.visit_expression(&it.argument); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_ternary_expression<'a>(visitor: &mut impl Visit<'a>, it: &TernaryExpression<'a>) { + let kind = AstKind::TernaryExpression; + visitor.enter_node(kind); + visitor.visit_expression(&it.test); + visitor.visit_expression(&it.consequent); + visitor.visit_expression(&it.alternate); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_conditional_expression<'a>( + visitor: &mut impl Visit<'a>, + it: &ConditionalExpression<'a>, + ) { + let kind = AstKind::ConditionalExpression; + visitor.enter_node(kind); + visitor.visit_expression(&it.test); + visitor.visit_expression(&it.consequent); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_assignment_expression<'a>( + visitor: &mut impl Visit<'a>, + it: &AssignmentExpression<'a>, + ) { + let kind = AstKind::AssignmentExpression; + visitor.enter_node(kind); + visitor.visit_variable_expression(&it.left); + visitor.visit_expression(&it.right); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_resource_expression<'a>(visitor: &mut impl Visit<'a>, it: &ResourceExpression<'a>) { + let kind = AstKind::ResourceExpression; + visitor.enter_node(kind); + visitor.visit_identifier_reference(&it.name); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_array_access_expression<'a>( + visitor: &mut impl Visit<'a>, + it: &ArrayAccessExpression<'a>, + ) { + let kind = AstKind::ArrayAccessExpression; + visitor.enter_node(kind); + visitor.visit_identifier_reference(&it.name); + visitor.visit_expression(&it.index); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_arrow_access_expression<'a>( + visitor: &mut impl Visit<'a>, + it: &ArrowAccessExpression<'a>, + ) { + let kind = AstKind::ArrowAccessExpression; + visitor.enter_node(kind); + visitor.visit_expression(&it.left); + visitor.visit_expression(&it.right); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_call_expression<'a>(visitor: &mut impl Visit<'a>, it: &CallExpression<'a>) { + let kind = AstKind::CallExpression; + visitor.enter_node(kind); + visitor.visit_identifier_reference(&it.callee); + if let Some(args) = &it.arguments { + visitor.visit_expressions(args); + } + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_loop_expression<'a>(visitor: &mut impl Visit<'a>, it: &LoopExpression<'a>) { + let kind = AstKind::LoopExpression; + visitor.enter_node(kind); + visitor.visit_expression(&it.count); + visitor.visit_block_expression(&it.expression); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_for_each_expression<'a>(visitor: &mut impl Visit<'a>, it: &ForEachExpression<'a>) { + let kind = AstKind::ForEachExpression; + visitor.enter_node(kind); + visitor.visit_variable_expression(&it.variable); + visitor.visit_expression(&it.array); + visitor.visit_block_expression(&it.expression); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_break<'a>(visitor: &mut impl Visit<'a>, it: &Break) { + let kind = AstKind::Break; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_continue<'a>(visitor: &mut impl Visit<'a>, it: &Continue) { + let kind = AstKind::Continue; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_this<'a>(visitor: &mut impl Visit<'a>, it: &This) { + let kind = AstKind::This; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_return<'a>(visitor: &mut impl Visit<'a>, it: &Return<'a>) { + let kind = AstKind::Return; + visitor.enter_node(kind); + visitor.visit_expression(&it.argument); + visitor.leave_node(kind); + } +} diff --git a/src/visit_mut.rs b/src/visit_mut.rs new file mode 100644 index 0000000..c2a7504 --- /dev/null +++ b/src/visit_mut.rs @@ -0,0 +1,445 @@ +use oxc_allocator::Vec; + +use crate::ast::*; + +use walk_mut::*; + +/// Syntax tree traversal. +pub trait VisitMut<'a>: Sized { + #[inline] + #[allow(unused_variables)] + fn enter_node(&mut self, kind: AstKind) {} + + #[inline] + #[allow(unused_variables)] + fn leave_node(&mut self, kind: AstKind) {} + + #[inline] + fn visit_program(&mut self, it: &mut Program<'a>) { + walk_program(self, it); + } + + #[inline] + fn visit_expressions(&mut self, it: &mut Vec<'a, Expression<'a>>) { + walk_expressions(self, it); + } + + #[inline] + fn visit_expression(&mut self, it: &mut Expression<'a>) { + walk_expression(self, it); + } + + #[inline] + fn visit_identifier_reference(&mut self, it: &mut IdentifierReference<'a>) { + walk_identifier_reference(self, it) + } + + #[inline] + fn visit_boolean_literal(&mut self, it: &mut BooleanLiteral) { + walk_boolean_literal(self, it); + } + + #[inline] + fn visit_numeric_literal(&mut self, it: &mut NumericLiteral<'a>) { + walk_numeric_literal(self, it); + } + + #[inline] + fn visit_string_literal(&mut self, it: &mut StringLiteral<'a>) { + walk_string_literal(self, it); + } + + #[inline] + fn visit_variable_expression(&mut self, it: &mut VariableExpression<'a>) { + walk_variable_expression(self, it); + } + + #[inline] + fn visit_variable_member(&mut self, it: &mut VariableMember<'a>) { + walk_variable_member(self, it); + } + + #[inline] + fn visit_parenthesized_expression(&mut self, it: &mut ParenthesizedExpression<'a>) { + walk_parenthesized_expression(self, it); + } + + #[inline] + fn visit_block_expression(&mut self, it: &mut BlockExpression<'a>) { + walk_block_expression(self, it); + } + + #[inline] + fn visit_binary_expression(&mut self, it: &mut BinaryExpression<'a>) { + walk_binary_expression(self, it); + } + + #[inline] + fn visit_unary_expression(&mut self, it: &mut UnaryExpression<'a>) { + walk_unary_expression(self, it); + } + + #[inline] + fn visit_ternary_expression(&mut self, it: &mut TernaryExpression<'a>) { + walk_ternary_expression(self, it); + } + + #[inline] + fn visit_conditional_expression(&mut self, it: &mut ConditionalExpression<'a>) { + walk_conditional_expression(self, it); + } + + #[inline] + fn visit_assignment_expression(&mut self, it: &mut AssignmentExpression<'a>) { + walk_assignment_expression(self, it); + } + + #[inline] + fn visit_resource_expression(&mut self, it: &mut ResourceExpression<'a>) { + walk_resource_expression(self, it); + } + + #[inline] + fn visit_array_access_expression(&mut self, it: &mut ArrayAccessExpression<'a>) { + walk_array_access_expression(self, it); + } + + #[inline] + fn visit_arrow_access_expression(&mut self, it: &mut ArrowAccessExpression<'a>) { + walk_arrow_access_expression(self, it); + } + + #[inline] + fn visit_call_expression(&mut self, it: &mut CallExpression<'a>) { + walk_call_expression(self, it); + } + + #[inline] + fn visit_loop_expression(&mut self, it: &mut LoopExpression<'a>) { + walk_loop_expression(self, it); + } + + #[inline] + fn visit_for_each_expression(&mut self, it: &mut ForEachExpression<'a>) { + walk_for_each_expression(self, it); + } + + #[inline] + fn visit_break(&mut self, it: &mut Break) { + walk_break(self, it); + } + + #[inline] + fn visit_continue(&mut self, it: &mut Continue) { + walk_continue(self, it); + } + + #[inline] + fn visit_this(&mut self, it: &mut This) { + walk_this(self, it); + } + + #[inline] + fn visit_return(&mut self, it: &mut Return<'a>) { + walk_return(self, it); + } +} + +pub mod walk_mut { + use super::*; + + #[inline] + pub fn walk_program<'a>(visitor: &mut impl VisitMut<'a>, it: &mut Program<'a>) { + let kind = AstKind::Program; + visitor.enter_node(kind); + visitor.visit_expressions(&mut it.body); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_expressions<'a>(visitor: &mut impl VisitMut<'a>, it: &mut Vec<'a, Expression<'a>>) { + for expr in it.iter_mut() { + visitor.visit_expression(expr); + } + } + + #[inline] + pub fn walk_expression<'a>(visitor: &mut impl VisitMut<'a>, it: &mut Expression<'a>) { + match it { + Expression::BooleanLiteral(it) => visitor.visit_boolean_literal(it), + Expression::NumericLiteral(it) => visitor.visit_numeric_literal(it), + Expression::StringLiteral(it) => visitor.visit_string_literal(it), + Expression::Variable(it) => visitor.visit_variable_expression(it), + Expression::Parenthesized(it) => visitor.visit_parenthesized_expression(it), + Expression::Block(it) => visitor.visit_block_expression(it), + Expression::Binary(it) => visitor.visit_binary_expression(it), + Expression::Unary(it) => visitor.visit_unary_expression(it), + Expression::Ternary(it) => visitor.visit_ternary_expression(it), + Expression::Conditional(it) => visitor.visit_conditional_expression(it), + Expression::Assignment(it) => visitor.visit_assignment_expression(it), + Expression::Resource(it) => visitor.visit_resource_expression(it), + Expression::ArrayAccess(it) => visitor.visit_array_access_expression(it), + Expression::ArrowAccess(it) => visitor.visit_arrow_access_expression(it), + Expression::Call(it) => visitor.visit_call_expression(it), + Expression::Loop(it) => visitor.visit_loop_expression(it), + Expression::ForEach(it) => visitor.visit_for_each_expression(it), + Expression::Break(it) => visitor.visit_break(it), + Expression::Continue(it) => visitor.visit_continue(it), + Expression::This(it) => visitor.visit_this(it), + Expression::Return(it) => visitor.visit_return(it), + } + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_identifier_reference<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut IdentifierReference<'a>, + ) { + let kind = AstKind::IdentifierReference; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_boolean_literal<'a>(visitor: &mut impl VisitMut<'a>, it: &mut BooleanLiteral) { + let kind = AstKind::BooleanLiteral; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_numeric_literal<'a>(visitor: &mut impl VisitMut<'a>, it: &mut NumericLiteral<'a>) { + let kind = AstKind::NumericLiteral; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_string_literal<'a>(visitor: &mut impl VisitMut<'a>, it: &mut StringLiteral<'a>) { + let kind = AstKind::StringLiteral; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_variable_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut VariableExpression<'a>, + ) { + let kind = AstKind::VariableExpression; + visitor.enter_node(kind); + visitor.visit_variable_member(&mut it.member); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_variable_member<'a>(visitor: &mut impl VisitMut<'a>, it: &mut VariableMember<'a>) { + let kind = AstKind::VariableMember; + visitor.enter_node(kind); + match it { + VariableMember::Object { + object, property, .. + } => { + visitor.visit_variable_member(object); + visitor.visit_identifier_reference(property); + } + VariableMember::Property { property, .. } => { + visitor.visit_identifier_reference(property); + } + } + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_parenthesized_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut ParenthesizedExpression<'a>, + ) { + let kind = AstKind::ParenthesizedExpression; + visitor.enter_node(kind); + match it { + ParenthesizedExpression::Single { expression, .. } => { + visitor.visit_expression(expression); + } + ParenthesizedExpression::Complex { expressions, .. } => { + visitor.visit_expressions(expressions); + } + } + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_block_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut BlockExpression<'a>, + ) { + let kind = AstKind::BlockExpression; + visitor.enter_node(kind); + visitor.visit_expressions(&mut it.expressions); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_binary_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut BinaryExpression<'a>, + ) { + let kind = AstKind::BinaryExpression; + visitor.enter_node(kind); + visitor.visit_expression(&mut it.left); + visitor.visit_expression(&mut it.right); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_unary_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut UnaryExpression<'a>, + ) { + let kind = AstKind::UnaryExpression; + visitor.enter_node(kind); + visitor.visit_expression(&mut it.argument); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_ternary_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut TernaryExpression<'a>, + ) { + let kind = AstKind::TernaryExpression; + visitor.enter_node(kind); + visitor.visit_expression(&mut it.test); + visitor.visit_expression(&mut it.consequent); + visitor.visit_expression(&mut it.alternate); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_conditional_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut ConditionalExpression<'a>, + ) { + let kind = AstKind::ConditionalExpression; + visitor.enter_node(kind); + visitor.visit_expression(&mut it.test); + visitor.visit_expression(&mut it.consequent); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_assignment_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut AssignmentExpression<'a>, + ) { + let kind = AstKind::AssignmentExpression; + visitor.enter_node(kind); + visitor.visit_variable_expression(&mut it.left); + visitor.visit_expression(&mut it.right); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_resource_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut ResourceExpression<'a>, + ) { + let kind = AstKind::ResourceExpression; + visitor.enter_node(kind); + visitor.visit_identifier_reference(&mut it.name); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_array_access_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut ArrayAccessExpression<'a>, + ) { + let kind = AstKind::ArrayAccessExpression; + visitor.enter_node(kind); + visitor.visit_identifier_reference(&mut it.name); + visitor.visit_expression(&mut it.index); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_arrow_access_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut ArrowAccessExpression<'a>, + ) { + let kind = AstKind::ArrowAccessExpression; + visitor.enter_node(kind); + visitor.visit_expression(&mut it.left); + visitor.visit_expression(&mut it.right); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_call_expression<'a>(visitor: &mut impl VisitMut<'a>, it: &mut CallExpression<'a>) { + let kind = AstKind::CallExpression; + visitor.enter_node(kind); + visitor.visit_identifier_reference(&mut it.callee); + if let Some(args) = &mut it.arguments { + visitor.visit_expressions(args); + } + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_loop_expression<'a>(visitor: &mut impl VisitMut<'a>, it: &mut LoopExpression<'a>) { + let kind = AstKind::LoopExpression; + visitor.enter_node(kind); + visitor.visit_expression(&mut it.count); + visitor.visit_block_expression(&mut it.expression); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_for_each_expression<'a>( + visitor: &mut impl VisitMut<'a>, + it: &mut ForEachExpression<'a>, + ) { + let kind = AstKind::ForEachExpression; + visitor.enter_node(kind); + visitor.visit_variable_expression(&mut it.variable); + visitor.visit_expression(&mut it.array); + visitor.visit_block_expression(&mut it.expression); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_break<'a>(visitor: &mut impl VisitMut<'a>, it: &mut Break) { + let kind = AstKind::Break; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_continue<'a>(visitor: &mut impl VisitMut<'a>, it: &mut Continue) { + let kind = AstKind::Continue; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + #[allow(unused_variables)] + pub fn walk_this<'a>(visitor: &mut impl VisitMut<'a>, it: &mut This) { + let kind = AstKind::This; + visitor.enter_node(kind); + visitor.leave_node(kind); + } + + #[inline] + pub fn walk_return<'a>(visitor: &mut impl VisitMut<'a>, it: &mut Return<'a>) { + let kind = AstKind::Return; + visitor.enter_node(kind); + visitor.visit_expression(&mut it.argument); + visitor.leave_node(kind); + } +} diff --git a/tests/codegen.rs b/tests/codegen.rs new file mode 100644 index 0000000..cb81161 --- /dev/null +++ b/tests/codegen.rs @@ -0,0 +1,75 @@ +macro_rules! test_codegen { + ($name:ident, $source:literal, @$result:literal $(,)?) => { + #[test] + fn $name() { + let allocator = nolana::allocator::Allocator::default(); + let ret = nolana::parser::Parser::new(&allocator, $source).parse(); + let out = nolana::codegen::Codegen::default().build(&ret.program); + assert!(ret.errors.is_empty()); + assert!(!ret.panicked); + insta::assert_snapshot!(out, @$result); + } + }; +} + +test_codegen!(boolean, "false; true;", @"false;true;"); +test_codegen!(string, "'foo_bar123.-$#*()'", @"'foo_bar123.-$#*()'"); +test_codegen!( + variable, + "variable.foo; v.foo; temp.foo; t.foo; context.foo; c.foo;", + @"variable.foo;variable.foo;temp.foo;temp.foo;context.foo;context.foo;", +); +test_codegen!( + weird_variable_members, + "variable.v.temp.t.context.c.query.q.math.a.b.c", + @"variable.v.temp.t.context.c.query.q.math.a.b.c", +); + +test_codegen!( + binary_and_unary_operations, + "1 == (((2 != 3) < 4 <= 5 > 6) >= -7 + 8 - 9 * 10 / 11 || 12) && !(13 ?? 14)", + @"1 == (((2 != 3) < 4 <= 5 > 6) >= -7 + 8 - 9 * 10 / 11 || 12) && !(13 ?? 14)", +); + +test_codegen!(conditional, "q.foo ? 1", @"query.foo ? 1"); + +test_codegen!(ternary, "q.foo ? 1 : 0", @"query.foo ? 1 : 0"); + +test_codegen!( + assignment, + "v.cow.location = 16;", + @"variable.cow.location = 16;", +); + +test_codegen!(parenthesis_single, "((((16))))", @"((((16))))"); +test_codegen!(parenthesis_complex, "(1; 2; (3; (4; 5;);););", @"(1;2;(3;(4;5;);););"); + +test_codegen!(block, "{v.a = 0;};", @"{variable.a = 0;};"); + +test_codegen!( + resource, + "geometry.foo; material.foo; texture.foo;", + @"geometry.foo;material.foo;texture.foo;", +); + +test_codegen!(array_access, "array.foo[q.bar]", @"array.foo[query.bar]"); + +test_codegen!(arrow_access, "v.foo->v.bar", @"variable.foo->variable.bar"); + +test_codegen!( + r#loop, + "loop(10, {v.i = v.i + 1;});", + @"loop(10, {variable.i = variable.i + 1;});", +); + +test_codegen!( + for_each, + "for_each(v.a, q.foo, {v.b = v.a + 1;});", + @"for_each(variable.a, query.foo, {variable.b = variable.a + 1;});", +); + +test_codegen!( + keywords, + "return v.a; break; continue; this;", + @"return variable.a;break;continue;this;", +); diff --git a/tests/parser.rs b/tests/parser.rs new file mode 100644 index 0000000..99ad382 --- /dev/null +++ b/tests/parser.rs @@ -0,0 +1,98 @@ +macro_rules! test_parser { + ($name:ident, $source:literal) => { + #[test] + fn $name() { + let allocator = nolana::allocator::Allocator::default(); + let ret = nolana::parser::Parser::new(&allocator, $source).parse(); + insta::with_settings!({ omit_expression => true }, { + insta::assert_debug_snapshot!(ret); + }); + } + }; +} + +test_parser!(boolean_false, "false"); +test_parser!(boolean_true, "true"); + +test_parser!(string, "'foo_bar123.-$#*()'"); +test_parser!(unterminated_string, "'hello wor-"); + +test_parser!(variable_variable, "variable.foo"); +test_parser!(variable_v, "v.foo"); +test_parser!(variable_temp, "temp.foo"); +test_parser!(variable_t, "t.foo"); +test_parser!(variable_context, "context.foo"); +test_parser!(variable_c, "c.foo"); +test_parser!( + weird_variable_members, + "variable.v.temp.t.context.c.query.q.math.a.b.c" +); + +test_parser!(binary_operation, "1 + 2 * 3"); +test_parser!(parenthesized_binary_operation, "(1 + 1) * (1 + 1)"); +test_parser!(parenthesized_binary_operation_alt, "((2 * 3) + 1) / 2"); + +test_parser!(negate_operation, "-(1 + 1)"); +test_parser!(not_operation, "!(1 && 0)"); + +test_parser!(null_operation, "v.a ?? 1.2"); + +test_parser!(ternary_double_left, "q.foo ? v.bar == 13 ? 1 : 2 : 3"); +test_parser!(ternary_double_right, "q.foo ? 1 : v.bar == 13 ? 2 : 3"); + +test_parser!(conditional, "q.foo ? 1"); + +test_parser!( + assignment, + "v.cow.location.x = 204.31; v.cow.location.y = 87; v.cow.location.z = 48.933;" +); + +test_parser!(complex_expression, "0; 0; 0;"); + +test_parser!(complex_parenthesized_expression, "(v.a = 1; v.b = 2;);"); +test_parser!(empty_parenthesized_expression, "()"); +test_parser!(nested_parenthesis, "((((16))))"); + +test_parser!(block, "{1;};"); +test_parser!(block_undelimited, "{1}"); + +test_parser!(unclosed_parenthesis_in_call, "q.a(1"); +test_parser!(unclosed_parenthesis_in_parenthesized_expression, "(1+1"); + +test_parser!(resource_geometry, "geometry.foo"); +test_parser!(resource_material, "material.bar"); +test_parser!(resource_texture, "texture.baz"); + +test_parser!(array_access, "array.foo[q.bar]"); + +test_parser!(arrow_access, "v.foo->v.bar"); + +test_parser!(r#loop, "loop(10, {v.i = v.i + 1;});"); + +test_parser!(for_each, "for_each(v.a, q.foo, {v.b = v.a + 1;});"); +test_parser!( + for_each_wrong_first_arg, + "for_each(1, q.foo, {v.b = v.a + 1;});" +); + +test_parser!(r#return, "return v.a"); + +test_parser!(r#break, "break"); + +test_parser!(r#continue, "continue"); + +test_parser!(this, "this"); + +test_parser!(missing_semi_with_semi, "0; 0"); +test_parser!(missing_semi_with_assignment, "v.a = 0; v.a"); + +test_parser!( + semisemisemisemi, + " + ;;;;;;; ;;;;;;; ;;; ;;; ;; + ;; ;; ;;;; ;;;; ;; + ;;;;;;; ;;;;; ;; ;;;; ;; ;; + ;; ;; ;; ;; ;; ;; + ;;;;;;; ;;;;;;; ;; ;; ;; + " +); diff --git a/tests/semantics.rs b/tests/semantics.rs new file mode 100644 index 0000000..9f21f71 --- /dev/null +++ b/tests/semantics.rs @@ -0,0 +1,30 @@ +macro_rules! test_semantics { + ($name:ident, $source:literal) => { + #[test] + fn $name() { + let allocator = nolana::allocator::Allocator::default(); + let ret = nolana::parser::Parser::new(&allocator, $source).parse(); + let errors = nolana::semantic::SemanticChecker::default().check(&ret.program); + insta::with_settings!({ omit_expression => true }, { + insta::assert_debug_snapshot!(errors); + }); + } + }; +} + +test_semantics!(empty_block_expression, "{}"); +test_semantics!(filled_block_expression, "{1;};"); + +test_semantics!(illegal_string_operation_both, "'foo' + 'bar'"); +test_semantics!(illegal_string_operation_left, "'foo' == 1"); +test_semantics!(illegal_string_operation_right, "1 + 'bar'"); +test_semantics!(unequals_string_operation, "'bar' != 'bar'"); +test_semantics!(equals_string_operation, "'bar' == 'bar'"); + +test_semantics!(assigning_context, "context.foo = 0;"); + +test_semantics!(break_outside_loop, "break;"); +test_semantics!(break_inside_loop, "loop(1, {break;});"); + +test_semantics!(continue_outside_loop, "continue;"); +test_semantics!(continue_inside_loop, "loop(1, {continue;});"); diff --git a/tests/snapshots/parser__array_access.snap b/tests/snapshots/parser__array_access.snap new file mode 100644 index 0000000..9605881 --- /dev/null +++ b/tests/snapshots/parser__array_access.snap @@ -0,0 +1,51 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 16, + }, + source: "array.foo[q.bar]", + is_complex: false, + body: Vec( + [ + ArrayAccess( + ArrayAccessExpression { + span: Span { + start: 0, + end: 16, + }, + name: IdentifierReference { + span: Span { + start: 6, + end: 9, + }, + name: "foo", + }, + index: Call( + CallExpression { + span: Span { + start: 10, + end: 15, + }, + kind: Query, + callee: IdentifierReference { + span: Span { + start: 12, + end: 15, + }, + name: "bar", + }, + arguments: None, + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__arrow_access.snap b/tests/snapshots/parser__arrow_access.snap new file mode 100644 index 0000000..efaf1b2 --- /dev/null +++ b/tests/snapshots/parser__arrow_access.snap @@ -0,0 +1,71 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 12, + }, + source: "v.foo->v.bar", + is_complex: false, + body: Vec( + [ + ArrowAccess( + ArrowAccessExpression { + span: Span { + start: 0, + end: 12, + }, + left: Variable( + VariableExpression { + span: Span { + start: 0, + end: 5, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 0, + end: 5, + }, + property: IdentifierReference { + span: Span { + start: 2, + end: 5, + }, + name: "foo", + }, + }, + }, + ), + right: Variable( + VariableExpression { + span: Span { + start: 7, + end: 12, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 7, + end: 12, + }, + property: IdentifierReference { + span: Span { + start: 9, + end: 12, + }, + name: "bar", + }, + }, + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__assignment.snap b/tests/snapshots/parser__assignment.snap new file mode 100644 index 0000000..cfd0df3 --- /dev/null +++ b/tests/snapshots/parser__assignment.snap @@ -0,0 +1,211 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 76, + }, + source: "v.cow.location.x = 204.31; v.cow.location.y = 87; v.cow.location.z = 48.933;", + is_complex: true, + body: Vec( + [ + Assignment( + AssignmentExpression { + span: Span { + start: 0, + end: 25, + }, + left: VariableExpression { + span: Span { + start: 0, + end: 16, + }, + lifetime: Variable, + member: Object { + span: Span { + start: 0, + end: 16, + }, + object: Object { + span: Span { + start: 0, + end: 14, + }, + object: Property { + span: Span { + start: 0, + end: 5, + }, + property: IdentifierReference { + span: Span { + start: 2, + end: 5, + }, + name: "cow", + }, + }, + property: IdentifierReference { + span: Span { + start: 6, + end: 14, + }, + name: "location", + }, + }, + property: IdentifierReference { + span: Span { + start: 15, + end: 16, + }, + name: "x", + }, + }, + }, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 19, + end: 25, + }, + value: 204.31, + raw: "204.31", + }, + ), + }, + ), + Assignment( + AssignmentExpression { + span: Span { + start: 27, + end: 48, + }, + left: VariableExpression { + span: Span { + start: 27, + end: 43, + }, + lifetime: Variable, + member: Object { + span: Span { + start: 27, + end: 43, + }, + object: Object { + span: Span { + start: 27, + end: 41, + }, + object: Property { + span: Span { + start: 27, + end: 32, + }, + property: IdentifierReference { + span: Span { + start: 29, + end: 32, + }, + name: "cow", + }, + }, + property: IdentifierReference { + span: Span { + start: 33, + end: 41, + }, + name: "location", + }, + }, + property: IdentifierReference { + span: Span { + start: 42, + end: 43, + }, + name: "y", + }, + }, + }, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 46, + end: 48, + }, + value: 87.0, + raw: "87", + }, + ), + }, + ), + Assignment( + AssignmentExpression { + span: Span { + start: 50, + end: 75, + }, + left: VariableExpression { + span: Span { + start: 50, + end: 66, + }, + lifetime: Variable, + member: Object { + span: Span { + start: 50, + end: 66, + }, + object: Object { + span: Span { + start: 50, + end: 64, + }, + object: Property { + span: Span { + start: 50, + end: 55, + }, + property: IdentifierReference { + span: Span { + start: 52, + end: 55, + }, + name: "cow", + }, + }, + property: IdentifierReference { + span: Span { + start: 56, + end: 64, + }, + name: "location", + }, + }, + property: IdentifierReference { + span: Span { + start: 65, + end: 66, + }, + name: "z", + }, + }, + }, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 69, + end: 75, + }, + value: 48.933, + raw: "48.933", + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__binary_operation.snap b/tests/snapshots/parser__binary_operation.snap new file mode 100644 index 0000000..f476f4c --- /dev/null +++ b/tests/snapshots/parser__binary_operation.snap @@ -0,0 +1,67 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 9, + }, + source: "1 + 2 * 3", + is_complex: false, + body: Vec( + [ + Binary( + BinaryExpression { + span: Span { + start: 0, + end: 9, + }, + left: NumericLiteral( + NumericLiteral { + span: Span { + start: 0, + end: 1, + }, + value: 1.0, + raw: "1", + }, + ), + operator: Addition, + right: Binary( + BinaryExpression { + span: Span { + start: 4, + end: 9, + }, + left: NumericLiteral( + NumericLiteral { + span: Span { + start: 4, + end: 5, + }, + value: 2.0, + raw: "2", + }, + ), + operator: Multiplication, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 8, + end: 9, + }, + value: 3.0, + raw: "3", + }, + ), + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__block.snap b/tests/snapshots/parser__block.snap new file mode 100644 index 0000000..70f081f --- /dev/null +++ b/tests/snapshots/parser__block.snap @@ -0,0 +1,41 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 5, + }, + source: "{1;};", + is_complex: true, + body: Vec( + [ + Block( + BlockExpression { + span: Span { + start: 0, + end: 4, + }, + expressions: Vec( + [ + NumericLiteral( + NumericLiteral { + span: Span { + start: 1, + end: 2, + }, + value: 1.0, + raw: "1", + }, + ), + ], + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__block_undelimited.snap b/tests/snapshots/parser__block_undelimited.snap new file mode 100644 index 0000000..521dfbd --- /dev/null +++ b/tests/snapshots/parser__block_undelimited.snap @@ -0,0 +1,88 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 3, + }, + source: "{1}", + is_complex: true, + body: Vec( + [ + Block( + BlockExpression { + span: Span { + start: 0, + end: 3, + }, + expressions: Vec( + [ + NumericLiteral( + NumericLiteral { + span: Span { + start: 1, + end: 2, + }, + value: 1.0, + raw: "1", + }, + ), + ], + ), + }, + ), + ], + ), + }, + errors: [ + Diagnostic { + inner: DiagnosticInner { + message: "Expressions inside `{}` must be delimited by `;`", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 2, + ), + length: 1, + }, + primary: false, + }, + ], + ), + help: Some( + "Try inserting a semicolon here", + ), + severity: Error, + }, + }, + Diagnostic { + inner: DiagnosticInner { + message: "Semicolons are required for complex Molang expressions (contain `=` or `;`)", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 3, + ), + length: 0, + }, + primary: false, + }, + ], + ), + help: Some( + "Try inserting a semicolon here", + ), + severity: Error, + }, + }, + ], + panicked: false, +} diff --git a/tests/snapshots/parser__boolean_false.snap b/tests/snapshots/parser__boolean_false.snap new file mode 100644 index 0000000..4cffe2c --- /dev/null +++ b/tests/snapshots/parser__boolean_false.snap @@ -0,0 +1,28 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 5, + }, + source: "false", + is_complex: false, + body: Vec( + [ + BooleanLiteral( + BooleanLiteral { + span: Span { + start: 0, + end: 5, + }, + value: false, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__boolean_true.snap b/tests/snapshots/parser__boolean_true.snap new file mode 100644 index 0000000..29ec72b --- /dev/null +++ b/tests/snapshots/parser__boolean_true.snap @@ -0,0 +1,28 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 4, + }, + source: "true", + is_complex: false, + body: Vec( + [ + BooleanLiteral( + BooleanLiteral { + span: Span { + start: 0, + end: 4, + }, + value: true, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__break.snap b/tests/snapshots/parser__break.snap new file mode 100644 index 0000000..1d1ab22 --- /dev/null +++ b/tests/snapshots/parser__break.snap @@ -0,0 +1,27 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 5, + }, + source: "break", + is_complex: false, + body: Vec( + [ + Break( + Break { + span: Span { + start: 0, + end: 5, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__complex_expression.snap b/tests/snapshots/parser__complex_expression.snap new file mode 100644 index 0000000..732b07d --- /dev/null +++ b/tests/snapshots/parser__complex_expression.snap @@ -0,0 +1,49 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 8, + }, + source: "0; 0; 0;", + is_complex: true, + body: Vec( + [ + NumericLiteral( + NumericLiteral { + span: Span { + start: 0, + end: 1, + }, + value: 0.0, + raw: "0", + }, + ), + NumericLiteral( + NumericLiteral { + span: Span { + start: 3, + end: 4, + }, + value: 0.0, + raw: "0", + }, + ), + NumericLiteral( + NumericLiteral { + span: Span { + start: 6, + end: 7, + }, + value: 0.0, + raw: "0", + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__complex_parenthesized_expression.snap b/tests/snapshots/parser__complex_parenthesized_expression.snap new file mode 100644 index 0000000..d9b15ac --- /dev/null +++ b/tests/snapshots/parser__complex_parenthesized_expression.snap @@ -0,0 +1,107 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 20, + }, + source: "(v.a = 1; v.b = 2;);", + is_complex: true, + body: Vec( + [ + Parenthesized( + Complex { + span: Span { + start: 0, + end: 19, + }, + expressions: Vec( + [ + Assignment( + AssignmentExpression { + span: Span { + start: 1, + end: 8, + }, + left: VariableExpression { + span: Span { + start: 1, + end: 4, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 1, + end: 4, + }, + property: IdentifierReference { + span: Span { + start: 3, + end: 4, + }, + name: "a", + }, + }, + }, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 7, + end: 8, + }, + value: 1.0, + raw: "1", + }, + ), + }, + ), + Assignment( + AssignmentExpression { + span: Span { + start: 10, + end: 17, + }, + left: VariableExpression { + span: Span { + start: 10, + end: 13, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 10, + end: 13, + }, + property: IdentifierReference { + span: Span { + start: 12, + end: 13, + }, + name: "b", + }, + }, + }, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 16, + end: 17, + }, + value: 2.0, + raw: "2", + }, + ), + }, + ), + ], + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__conditional.snap b/tests/snapshots/parser__conditional.snap new file mode 100644 index 0000000..64e5b9a --- /dev/null +++ b/tests/snapshots/parser__conditional.snap @@ -0,0 +1,54 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 9, + }, + source: "q.foo ? 1", + is_complex: false, + body: Vec( + [ + Conditional( + ConditionalExpression { + span: Span { + start: 0, + end: 9, + }, + test: Call( + CallExpression { + span: Span { + start: 0, + end: 5, + }, + kind: Query, + callee: IdentifierReference { + span: Span { + start: 2, + end: 5, + }, + name: "foo", + }, + arguments: None, + }, + ), + consequent: NumericLiteral( + NumericLiteral { + span: Span { + start: 8, + end: 9, + }, + value: 1.0, + raw: "1", + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__continue.snap b/tests/snapshots/parser__continue.snap new file mode 100644 index 0000000..b54e67e --- /dev/null +++ b/tests/snapshots/parser__continue.snap @@ -0,0 +1,27 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 8, + }, + source: "continue", + is_complex: false, + body: Vec( + [ + Continue( + Continue { + span: Span { + start: 0, + end: 8, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__empty_parenthesized_expression.snap b/tests/snapshots/parser__empty_parenthesized_expression.snap new file mode 100644 index 0000000..1111db0 --- /dev/null +++ b/tests/snapshots/parser__empty_parenthesized_expression.snap @@ -0,0 +1,40 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 0, + }, + source: "()", + is_complex: false, + body: Vec( + [], + ), + }, + errors: [ + Diagnostic { + inner: DiagnosticInner { + message: "Empty parenthesized expression", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 1, + ), + length: 1, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, + ], + panicked: true, +} diff --git a/tests/snapshots/parser__for_each.snap b/tests/snapshots/parser__for_each.snap new file mode 100644 index 0000000..c442a23 --- /dev/null +++ b/tests/snapshots/parser__for_each.snap @@ -0,0 +1,143 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 39, + }, + source: "for_each(v.a, q.foo, {v.b = v.a + 1;});", + is_complex: true, + body: Vec( + [ + ForEach( + ForEachExpression { + span: Span { + start: 0, + end: 38, + }, + variable: VariableExpression { + span: Span { + start: 9, + end: 12, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 9, + end: 12, + }, + property: IdentifierReference { + span: Span { + start: 11, + end: 12, + }, + name: "a", + }, + }, + }, + array: Call( + CallExpression { + span: Span { + start: 14, + end: 19, + }, + kind: Query, + callee: IdentifierReference { + span: Span { + start: 16, + end: 19, + }, + name: "foo", + }, + arguments: None, + }, + ), + expression: BlockExpression { + span: Span { + start: 21, + end: 37, + }, + expressions: Vec( + [ + Assignment( + AssignmentExpression { + span: Span { + start: 22, + end: 35, + }, + left: VariableExpression { + span: Span { + start: 22, + end: 25, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 22, + end: 25, + }, + property: IdentifierReference { + span: Span { + start: 24, + end: 25, + }, + name: "b", + }, + }, + }, + right: Binary( + BinaryExpression { + span: Span { + start: 28, + end: 35, + }, + left: Variable( + VariableExpression { + span: Span { + start: 28, + end: 31, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 28, + end: 31, + }, + property: IdentifierReference { + span: Span { + start: 30, + end: 31, + }, + name: "a", + }, + }, + }, + ), + operator: Addition, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 34, + end: 35, + }, + value: 1.0, + raw: "1", + }, + ), + }, + ), + }, + ), + ], + ), + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__for_each_wrong_first_arg.snap b/tests/snapshots/parser__for_each_wrong_first_arg.snap new file mode 100644 index 0000000..963d629 --- /dev/null +++ b/tests/snapshots/parser__for_each_wrong_first_arg.snap @@ -0,0 +1,40 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 0, + }, + source: "for_each(1, q.foo, {v.b = v.a + 1;});", + is_complex: false, + body: Vec( + [], + ), + }, + errors: [ + Diagnostic { + inner: DiagnosticInner { + message: "`for_each` first argument must be either a `variable.` or a `temp.`", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 9, + ), + length: 1, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, + ], + panicked: true, +} diff --git a/tests/snapshots/parser__loop.snap b/tests/snapshots/parser__loop.snap new file mode 100644 index 0000000..41ce9f6 --- /dev/null +++ b/tests/snapshots/parser__loop.snap @@ -0,0 +1,116 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 27, + }, + source: "loop(10, {v.i = v.i + 1;});", + is_complex: true, + body: Vec( + [ + Loop( + LoopExpression { + span: Span { + start: 0, + end: 26, + }, + count: NumericLiteral( + NumericLiteral { + span: Span { + start: 5, + end: 7, + }, + value: 10.0, + raw: "10", + }, + ), + expression: BlockExpression { + span: Span { + start: 9, + end: 25, + }, + expressions: Vec( + [ + Assignment( + AssignmentExpression { + span: Span { + start: 10, + end: 23, + }, + left: VariableExpression { + span: Span { + start: 10, + end: 13, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 10, + end: 13, + }, + property: IdentifierReference { + span: Span { + start: 12, + end: 13, + }, + name: "i", + }, + }, + }, + right: Binary( + BinaryExpression { + span: Span { + start: 16, + end: 23, + }, + left: Variable( + VariableExpression { + span: Span { + start: 16, + end: 19, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 16, + end: 19, + }, + property: IdentifierReference { + span: Span { + start: 18, + end: 19, + }, + name: "i", + }, + }, + }, + ), + operator: Addition, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 22, + end: 23, + }, + value: 1.0, + raw: "1", + }, + ), + }, + ), + }, + ), + ], + ), + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__missing_semi_with_assignment.snap b/tests/snapshots/parser__missing_semi_with_assignment.snap new file mode 100644 index 0000000..9d25ec5 --- /dev/null +++ b/tests/snapshots/parser__missing_semi_with_assignment.snap @@ -0,0 +1,103 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 12, + }, + source: "v.a = 0; v.a", + is_complex: true, + body: Vec( + [ + Assignment( + AssignmentExpression { + span: Span { + start: 0, + end: 7, + }, + left: VariableExpression { + span: Span { + start: 0, + end: 3, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 0, + end: 3, + }, + property: IdentifierReference { + span: Span { + start: 2, + end: 3, + }, + name: "a", + }, + }, + }, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 6, + end: 7, + }, + value: 0.0, + raw: "0", + }, + ), + }, + ), + Variable( + VariableExpression { + span: Span { + start: 9, + end: 12, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 9, + end: 12, + }, + property: IdentifierReference { + span: Span { + start: 11, + end: 12, + }, + name: "a", + }, + }, + }, + ), + ], + ), + }, + errors: [ + Diagnostic { + inner: DiagnosticInner { + message: "Semicolons are required for complex Molang expressions (contain `=` or `;`)", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 12, + ), + length: 0, + }, + primary: false, + }, + ], + ), + help: Some( + "Try inserting a semicolon here", + ), + severity: Error, + }, + }, + ], + panicked: false, +} diff --git a/tests/snapshots/parser__missing_semi_with_semi.snap b/tests/snapshots/parser__missing_semi_with_semi.snap new file mode 100644 index 0000000..bffaf64 --- /dev/null +++ b/tests/snapshots/parser__missing_semi_with_semi.snap @@ -0,0 +1,63 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 4, + }, + source: "0; 0", + is_complex: true, + body: Vec( + [ + NumericLiteral( + NumericLiteral { + span: Span { + start: 0, + end: 1, + }, + value: 0.0, + raw: "0", + }, + ), + NumericLiteral( + NumericLiteral { + span: Span { + start: 3, + end: 4, + }, + value: 0.0, + raw: "0", + }, + ), + ], + ), + }, + errors: [ + Diagnostic { + inner: DiagnosticInner { + message: "Semicolons are required for complex Molang expressions (contain `=` or `;`)", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 4, + ), + length: 0, + }, + primary: false, + }, + ], + ), + help: Some( + "Try inserting a semicolon here", + ), + severity: Error, + }, + }, + ], + panicked: false, +} diff --git a/tests/snapshots/parser__negate_operation.snap b/tests/snapshots/parser__negate_operation.snap new file mode 100644 index 0000000..2296e3d --- /dev/null +++ b/tests/snapshots/parser__negate_operation.snap @@ -0,0 +1,65 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 8, + }, + source: "-(1 + 1)", + is_complex: false, + body: Vec( + [ + Unary( + UnaryExpression { + span: Span { + start: 0, + end: 8, + }, + operator: Negate, + argument: Parenthesized( + Single { + span: Span { + start: 1, + end: 8, + }, + expression: Binary( + BinaryExpression { + span: Span { + start: 2, + end: 7, + }, + left: NumericLiteral( + NumericLiteral { + span: Span { + start: 2, + end: 3, + }, + value: 1.0, + raw: "1", + }, + ), + operator: Addition, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 6, + end: 7, + }, + value: 1.0, + raw: "1", + }, + ), + }, + ), + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__nested_parenthesis.snap b/tests/snapshots/parser__nested_parenthesis.snap new file mode 100644 index 0000000..fcd03a6 --- /dev/null +++ b/tests/snapshots/parser__nested_parenthesis.snap @@ -0,0 +1,61 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 10, + }, + source: "((((16))))", + is_complex: false, + body: Vec( + [ + Parenthesized( + Single { + span: Span { + start: 0, + end: 10, + }, + expression: Parenthesized( + Single { + span: Span { + start: 1, + end: 9, + }, + expression: Parenthesized( + Single { + span: Span { + start: 2, + end: 8, + }, + expression: Parenthesized( + Single { + span: Span { + start: 3, + end: 7, + }, + expression: NumericLiteral( + NumericLiteral { + span: Span { + start: 4, + end: 6, + }, + value: 16.0, + raw: "16", + }, + ), + }, + ), + }, + ), + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__not_operation.snap b/tests/snapshots/parser__not_operation.snap new file mode 100644 index 0000000..ae63c78 --- /dev/null +++ b/tests/snapshots/parser__not_operation.snap @@ -0,0 +1,65 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 9, + }, + source: "!(1 && 0)", + is_complex: false, + body: Vec( + [ + Unary( + UnaryExpression { + span: Span { + start: 0, + end: 9, + }, + operator: Not, + argument: Parenthesized( + Single { + span: Span { + start: 1, + end: 9, + }, + expression: Binary( + BinaryExpression { + span: Span { + start: 2, + end: 8, + }, + left: NumericLiteral( + NumericLiteral { + span: Span { + start: 2, + end: 3, + }, + value: 1.0, + raw: "1", + }, + ), + operator: And, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 7, + end: 8, + }, + value: 0.0, + raw: "0", + }, + ), + }, + ), + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__null_operation.snap b/tests/snapshots/parser__null_operation.snap new file mode 100644 index 0000000..f923733 --- /dev/null +++ b/tests/snapshots/parser__null_operation.snap @@ -0,0 +1,60 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 10, + }, + source: "v.a ?? 1.2", + is_complex: false, + body: Vec( + [ + Binary( + BinaryExpression { + span: Span { + start: 0, + end: 10, + }, + left: Variable( + VariableExpression { + span: Span { + start: 0, + end: 3, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 0, + end: 3, + }, + property: IdentifierReference { + span: Span { + start: 2, + end: 3, + }, + name: "a", + }, + }, + }, + ), + operator: Coalesce, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 7, + end: 10, + }, + value: 1.2, + raw: "1.2", + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__parenthesized_binary_operation.snap b/tests/snapshots/parser__parenthesized_binary_operation.snap new file mode 100644 index 0000000..298e225 --- /dev/null +++ b/tests/snapshots/parser__parenthesized_binary_operation.snap @@ -0,0 +1,102 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 17, + }, + source: "(1 + 1) * (1 + 1)", + is_complex: false, + body: Vec( + [ + Binary( + BinaryExpression { + span: Span { + start: 0, + end: 17, + }, + left: Parenthesized( + Single { + span: Span { + start: 0, + end: 7, + }, + expression: Binary( + BinaryExpression { + span: Span { + start: 1, + end: 6, + }, + left: NumericLiteral( + NumericLiteral { + span: Span { + start: 1, + end: 2, + }, + value: 1.0, + raw: "1", + }, + ), + operator: Addition, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 5, + end: 6, + }, + value: 1.0, + raw: "1", + }, + ), + }, + ), + }, + ), + operator: Multiplication, + right: Parenthesized( + Single { + span: Span { + start: 10, + end: 17, + }, + expression: Binary( + BinaryExpression { + span: Span { + start: 11, + end: 16, + }, + left: NumericLiteral( + NumericLiteral { + span: Span { + start: 11, + end: 12, + }, + value: 1.0, + raw: "1", + }, + ), + operator: Addition, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 15, + end: 16, + }, + value: 1.0, + raw: "1", + }, + ), + }, + ), + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__parenthesized_binary_operation_alt.snap b/tests/snapshots/parser__parenthesized_binary_operation_alt.snap new file mode 100644 index 0000000..02d3c5f --- /dev/null +++ b/tests/snapshots/parser__parenthesized_binary_operation_alt.snap @@ -0,0 +1,102 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 17, + }, + source: "((2 * 3) + 1) / 2", + is_complex: false, + body: Vec( + [ + Binary( + BinaryExpression { + span: Span { + start: 0, + end: 17, + }, + left: Parenthesized( + Single { + span: Span { + start: 0, + end: 13, + }, + expression: Binary( + BinaryExpression { + span: Span { + start: 1, + end: 12, + }, + left: Parenthesized( + Single { + span: Span { + start: 1, + end: 8, + }, + expression: Binary( + BinaryExpression { + span: Span { + start: 2, + end: 7, + }, + left: NumericLiteral( + NumericLiteral { + span: Span { + start: 2, + end: 3, + }, + value: 2.0, + raw: "2", + }, + ), + operator: Multiplication, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 6, + end: 7, + }, + value: 3.0, + raw: "3", + }, + ), + }, + ), + }, + ), + operator: Addition, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 11, + end: 12, + }, + value: 1.0, + raw: "1", + }, + ), + }, + ), + }, + ), + operator: Division, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 16, + end: 17, + }, + value: 2.0, + raw: "2", + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__resource_geometry.snap b/tests/snapshots/parser__resource_geometry.snap new file mode 100644 index 0000000..48567fa --- /dev/null +++ b/tests/snapshots/parser__resource_geometry.snap @@ -0,0 +1,35 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 12, + }, + source: "geometry.foo", + is_complex: false, + body: Vec( + [ + Resource( + ResourceExpression { + span: Span { + start: 0, + end: 12, + }, + section: Geometry, + name: IdentifierReference { + span: Span { + start: 9, + end: 12, + }, + name: "foo", + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__resource_material.snap b/tests/snapshots/parser__resource_material.snap new file mode 100644 index 0000000..e31f0c9 --- /dev/null +++ b/tests/snapshots/parser__resource_material.snap @@ -0,0 +1,35 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 12, + }, + source: "material.bar", + is_complex: false, + body: Vec( + [ + Resource( + ResourceExpression { + span: Span { + start: 0, + end: 12, + }, + section: Material, + name: IdentifierReference { + span: Span { + start: 9, + end: 12, + }, + name: "bar", + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__resource_texture.snap b/tests/snapshots/parser__resource_texture.snap new file mode 100644 index 0000000..c58dd61 --- /dev/null +++ b/tests/snapshots/parser__resource_texture.snap @@ -0,0 +1,35 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 11, + }, + source: "texture.baz", + is_complex: false, + body: Vec( + [ + Resource( + ResourceExpression { + span: Span { + start: 0, + end: 11, + }, + section: Texture, + name: IdentifierReference { + span: Span { + start: 8, + end: 11, + }, + name: "baz", + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__return.snap b/tests/snapshots/parser__return.snap new file mode 100644 index 0000000..74c631f --- /dev/null +++ b/tests/snapshots/parser__return.snap @@ -0,0 +1,49 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 10, + }, + source: "return v.a", + is_complex: false, + body: Vec( + [ + Return( + Return { + span: Span { + start: 0, + end: 10, + }, + argument: Variable( + VariableExpression { + span: Span { + start: 7, + end: 10, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 7, + end: 10, + }, + property: IdentifierReference { + span: Span { + start: 9, + end: 10, + }, + name: "a", + }, + }, + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__semisemisemisemi.snap b/tests/snapshots/parser__semisemisemisemi.snap new file mode 100644 index 0000000..977c327 --- /dev/null +++ b/tests/snapshots/parser__semisemisemisemi.snap @@ -0,0 +1,18 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 5, + end: 170, + }, + source: "\n ;;;;;;; ;;;;;;; ;;; ;;; ;;\n ;; ;; ;;;; ;;;; ;;\n ;;;;;;; ;;;;; ;; ;;;; ;; ;;\n ;; ;; ;; ;; ;; ;;\n ;;;;;;; ;;;;;;; ;; ;; ;;\n ", + is_complex: true, + body: Vec( + [], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__string.snap b/tests/snapshots/parser__string.snap new file mode 100644 index 0000000..ec8778c --- /dev/null +++ b/tests/snapshots/parser__string.snap @@ -0,0 +1,28 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 19, + }, + source: "'foo_bar123.-$#*()'", + is_complex: false, + body: Vec( + [ + StringLiteral( + StringLiteral { + span: Span { + start: 0, + end: 19, + }, + value: "foo_bar123.-$#*()", + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__ternary_double_left.snap b/tests/snapshots/parser__ternary_double_left.snap new file mode 100644 index 0000000..d7d51f5 --- /dev/null +++ b/tests/snapshots/parser__ternary_double_left.snap @@ -0,0 +1,123 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 31, + }, + source: "q.foo ? v.bar == 13 ? 1 : 2 : 3", + is_complex: false, + body: Vec( + [ + Ternary( + TernaryExpression { + span: Span { + start: 0, + end: 31, + }, + test: Call( + CallExpression { + span: Span { + start: 0, + end: 5, + }, + kind: Query, + callee: IdentifierReference { + span: Span { + start: 2, + end: 5, + }, + name: "foo", + }, + arguments: None, + }, + ), + consequent: Ternary( + TernaryExpression { + span: Span { + start: 8, + end: 27, + }, + test: Binary( + BinaryExpression { + span: Span { + start: 8, + end: 19, + }, + left: Variable( + VariableExpression { + span: Span { + start: 8, + end: 13, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 8, + end: 13, + }, + property: IdentifierReference { + span: Span { + start: 10, + end: 13, + }, + name: "bar", + }, + }, + }, + ), + operator: Equality, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 17, + end: 19, + }, + value: 13.0, + raw: "13", + }, + ), + }, + ), + consequent: NumericLiteral( + NumericLiteral { + span: Span { + start: 22, + end: 23, + }, + value: 1.0, + raw: "1", + }, + ), + alternate: NumericLiteral( + NumericLiteral { + span: Span { + start: 26, + end: 27, + }, + value: 2.0, + raw: "2", + }, + ), + }, + ), + alternate: NumericLiteral( + NumericLiteral { + span: Span { + start: 30, + end: 31, + }, + value: 3.0, + raw: "3", + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__ternary_double_right.snap b/tests/snapshots/parser__ternary_double_right.snap new file mode 100644 index 0000000..1aecc6b --- /dev/null +++ b/tests/snapshots/parser__ternary_double_right.snap @@ -0,0 +1,123 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 31, + }, + source: "q.foo ? 1 : v.bar == 13 ? 2 : 3", + is_complex: false, + body: Vec( + [ + Ternary( + TernaryExpression { + span: Span { + start: 0, + end: 31, + }, + test: Call( + CallExpression { + span: Span { + start: 0, + end: 5, + }, + kind: Query, + callee: IdentifierReference { + span: Span { + start: 2, + end: 5, + }, + name: "foo", + }, + arguments: None, + }, + ), + consequent: NumericLiteral( + NumericLiteral { + span: Span { + start: 8, + end: 9, + }, + value: 1.0, + raw: "1", + }, + ), + alternate: Ternary( + TernaryExpression { + span: Span { + start: 12, + end: 31, + }, + test: Binary( + BinaryExpression { + span: Span { + start: 12, + end: 23, + }, + left: Variable( + VariableExpression { + span: Span { + start: 12, + end: 17, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 12, + end: 17, + }, + property: IdentifierReference { + span: Span { + start: 14, + end: 17, + }, + name: "bar", + }, + }, + }, + ), + operator: Equality, + right: NumericLiteral( + NumericLiteral { + span: Span { + start: 21, + end: 23, + }, + value: 13.0, + raw: "13", + }, + ), + }, + ), + consequent: NumericLiteral( + NumericLiteral { + span: Span { + start: 26, + end: 27, + }, + value: 2.0, + raw: "2", + }, + ), + alternate: NumericLiteral( + NumericLiteral { + span: Span { + start: 30, + end: 31, + }, + value: 3.0, + raw: "3", + }, + ), + }, + ), + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__this.snap b/tests/snapshots/parser__this.snap new file mode 100644 index 0000000..78761b0 --- /dev/null +++ b/tests/snapshots/parser__this.snap @@ -0,0 +1,27 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 4, + }, + source: "this", + is_complex: false, + body: Vec( + [ + This( + This { + span: Span { + start: 0, + end: 4, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__unclosed_parenthesis_in_call.snap b/tests/snapshots/parser__unclosed_parenthesis_in_call.snap new file mode 100644 index 0000000..b342a5f --- /dev/null +++ b/tests/snapshots/parser__unclosed_parenthesis_in_call.snap @@ -0,0 +1,42 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 0, + }, + source: "q.a(1", + is_complex: false, + body: Vec( + [], + ), + }, + errors: [ + Diagnostic { + inner: DiagnosticInner { + message: "Expected `)` but found `EOF`", + labels: Some( + [ + LabeledSpan { + label: Some( + "Here", + ), + span: SourceSpan { + offset: SourceOffset( + 5, + ), + length: 0, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, + ], + panicked: true, +} diff --git a/tests/snapshots/parser__unclosed_parenthesis_in_parenthesized_expression.snap b/tests/snapshots/parser__unclosed_parenthesis_in_parenthesized_expression.snap new file mode 100644 index 0000000..dec34a3 --- /dev/null +++ b/tests/snapshots/parser__unclosed_parenthesis_in_parenthesized_expression.snap @@ -0,0 +1,42 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 0, + }, + source: "(1+1", + is_complex: false, + body: Vec( + [], + ), + }, + errors: [ + Diagnostic { + inner: DiagnosticInner { + message: "Expected `)` but found `EOF`", + labels: Some( + [ + LabeledSpan { + label: Some( + "Here", + ), + span: SourceSpan { + offset: SourceOffset( + 4, + ), + length: 0, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, + ], + panicked: true, +} diff --git a/tests/snapshots/parser__unterminated_string.snap b/tests/snapshots/parser__unterminated_string.snap new file mode 100644 index 0000000..41dbd09 --- /dev/null +++ b/tests/snapshots/parser__unterminated_string.snap @@ -0,0 +1,40 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 0, + }, + source: "'hello wor-", + is_complex: false, + body: Vec( + [], + ), + }, + errors: [ + Diagnostic { + inner: DiagnosticInner { + message: "Unterminated string", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 0, + ), + length: 0, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, + ], + panicked: true, +} diff --git a/tests/snapshots/parser__variable_c.snap b/tests/snapshots/parser__variable_c.snap new file mode 100644 index 0000000..03df596 --- /dev/null +++ b/tests/snapshots/parser__variable_c.snap @@ -0,0 +1,41 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 5, + }, + source: "c.foo", + is_complex: false, + body: Vec( + [ + Variable( + VariableExpression { + span: Span { + start: 0, + end: 5, + }, + lifetime: Context, + member: Property { + span: Span { + start: 0, + end: 5, + }, + property: IdentifierReference { + span: Span { + start: 2, + end: 5, + }, + name: "foo", + }, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__variable_context.snap b/tests/snapshots/parser__variable_context.snap new file mode 100644 index 0000000..7011454 --- /dev/null +++ b/tests/snapshots/parser__variable_context.snap @@ -0,0 +1,41 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 11, + }, + source: "context.foo", + is_complex: false, + body: Vec( + [ + Variable( + VariableExpression { + span: Span { + start: 0, + end: 11, + }, + lifetime: Context, + member: Property { + span: Span { + start: 0, + end: 11, + }, + property: IdentifierReference { + span: Span { + start: 8, + end: 11, + }, + name: "foo", + }, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__variable_t.snap b/tests/snapshots/parser__variable_t.snap new file mode 100644 index 0000000..c8079ab --- /dev/null +++ b/tests/snapshots/parser__variable_t.snap @@ -0,0 +1,41 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 5, + }, + source: "t.foo", + is_complex: false, + body: Vec( + [ + Variable( + VariableExpression { + span: Span { + start: 0, + end: 5, + }, + lifetime: Temporary, + member: Property { + span: Span { + start: 0, + end: 5, + }, + property: IdentifierReference { + span: Span { + start: 2, + end: 5, + }, + name: "foo", + }, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__variable_temp.snap b/tests/snapshots/parser__variable_temp.snap new file mode 100644 index 0000000..3b09d44 --- /dev/null +++ b/tests/snapshots/parser__variable_temp.snap @@ -0,0 +1,41 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 8, + }, + source: "temp.foo", + is_complex: false, + body: Vec( + [ + Variable( + VariableExpression { + span: Span { + start: 0, + end: 8, + }, + lifetime: Temporary, + member: Property { + span: Span { + start: 0, + end: 8, + }, + property: IdentifierReference { + span: Span { + start: 5, + end: 8, + }, + name: "foo", + }, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__variable_v.snap b/tests/snapshots/parser__variable_v.snap new file mode 100644 index 0000000..722d229 --- /dev/null +++ b/tests/snapshots/parser__variable_v.snap @@ -0,0 +1,41 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 5, + }, + source: "v.foo", + is_complex: false, + body: Vec( + [ + Variable( + VariableExpression { + span: Span { + start: 0, + end: 5, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 0, + end: 5, + }, + property: IdentifierReference { + span: Span { + start: 2, + end: 5, + }, + name: "foo", + }, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__variable_variable.snap b/tests/snapshots/parser__variable_variable.snap new file mode 100644 index 0000000..462a330 --- /dev/null +++ b/tests/snapshots/parser__variable_variable.snap @@ -0,0 +1,41 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 12, + }, + source: "variable.foo", + is_complex: false, + body: Vec( + [ + Variable( + VariableExpression { + span: Span { + start: 0, + end: 12, + }, + lifetime: Variable, + member: Property { + span: Span { + start: 0, + end: 12, + }, + property: IdentifierReference { + span: Span { + start: 9, + end: 12, + }, + name: "foo", + }, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/parser__weird_variable_members.snap b/tests/snapshots/parser__weird_variable_members.snap new file mode 100644 index 0000000..898effb --- /dev/null +++ b/tests/snapshots/parser__weird_variable_members.snap @@ -0,0 +1,171 @@ +--- +source: tests/parser.rs +--- +ParserReturn { + program: Program { + span: Span { + start: 0, + end: 46, + }, + source: "variable.v.temp.t.context.c.query.q.math.a.b.c", + is_complex: false, + body: Vec( + [ + Variable( + VariableExpression { + span: Span { + start: 0, + end: 46, + }, + lifetime: Variable, + member: Object { + span: Span { + start: 0, + end: 46, + }, + object: Object { + span: Span { + start: 0, + end: 44, + }, + object: Object { + span: Span { + start: 0, + end: 42, + }, + object: Object { + span: Span { + start: 0, + end: 40, + }, + object: Object { + span: Span { + start: 0, + end: 35, + }, + object: Object { + span: Span { + start: 0, + end: 33, + }, + object: Object { + span: Span { + start: 0, + end: 27, + }, + object: Object { + span: Span { + start: 0, + end: 25, + }, + object: Object { + span: Span { + start: 0, + end: 17, + }, + object: Object { + span: Span { + start: 0, + end: 15, + }, + object: Property { + span: Span { + start: 0, + end: 10, + }, + property: IdentifierReference { + span: Span { + start: 9, + end: 10, + }, + name: "v", + }, + }, + property: IdentifierReference { + span: Span { + start: 11, + end: 15, + }, + name: "temp", + }, + }, + property: IdentifierReference { + span: Span { + start: 16, + end: 17, + }, + name: "t", + }, + }, + property: IdentifierReference { + span: Span { + start: 18, + end: 25, + }, + name: "context", + }, + }, + property: IdentifierReference { + span: Span { + start: 26, + end: 27, + }, + name: "c", + }, + }, + property: IdentifierReference { + span: Span { + start: 28, + end: 33, + }, + name: "query", + }, + }, + property: IdentifierReference { + span: Span { + start: 34, + end: 35, + }, + name: "q", + }, + }, + property: IdentifierReference { + span: Span { + start: 36, + end: 40, + }, + name: "math", + }, + }, + property: IdentifierReference { + span: Span { + start: 41, + end: 42, + }, + name: "a", + }, + }, + property: IdentifierReference { + span: Span { + start: 43, + end: 44, + }, + name: "b", + }, + }, + property: IdentifierReference { + span: Span { + start: 45, + end: 46, + }, + name: "c", + }, + }, + }, + ), + ], + ), + }, + errors: [], + panicked: false, +} diff --git a/tests/snapshots/semantics__assigning_context.snap b/tests/snapshots/semantics__assigning_context.snap new file mode 100644 index 0000000..a202b0b --- /dev/null +++ b/tests/snapshots/semantics__assigning_context.snap @@ -0,0 +1,28 @@ +--- +source: tests/semantics.rs +--- +[ + Diagnostic { + inner: DiagnosticInner { + message: "`context.` variables are read-only", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 0, + ), + length: 15, + }, + primary: false, + }, + ], + ), + help: Some( + "Try assigning to `variable.` or `temp.` instead", + ), + severity: Error, + }, + }, +] diff --git a/tests/snapshots/semantics__break_inside_loop.snap b/tests/snapshots/semantics__break_inside_loop.snap new file mode 100644 index 0000000..52cc4f5 --- /dev/null +++ b/tests/snapshots/semantics__break_inside_loop.snap @@ -0,0 +1,4 @@ +--- +source: tests/semantics.rs +--- +[] diff --git a/tests/snapshots/semantics__break_outside_loop.snap b/tests/snapshots/semantics__break_outside_loop.snap new file mode 100644 index 0000000..8d4f874 --- /dev/null +++ b/tests/snapshots/semantics__break_outside_loop.snap @@ -0,0 +1,26 @@ +--- +source: tests/semantics.rs +--- +[ + Diagnostic { + inner: DiagnosticInner { + message: "`break` is only supported inside `loop` and `for_each` expressions", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 0, + ), + length: 5, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, +] diff --git a/tests/snapshots/semantics__continue_inside_loop.snap b/tests/snapshots/semantics__continue_inside_loop.snap new file mode 100644 index 0000000..52cc4f5 --- /dev/null +++ b/tests/snapshots/semantics__continue_inside_loop.snap @@ -0,0 +1,4 @@ +--- +source: tests/semantics.rs +--- +[] diff --git a/tests/snapshots/semantics__continue_outside_loop.snap b/tests/snapshots/semantics__continue_outside_loop.snap new file mode 100644 index 0000000..c0b8dad --- /dev/null +++ b/tests/snapshots/semantics__continue_outside_loop.snap @@ -0,0 +1,26 @@ +--- +source: tests/semantics.rs +--- +[ + Diagnostic { + inner: DiagnosticInner { + message: "`continue` is only supported inside `loop` and `for_each` expressions", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 0, + ), + length: 8, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, +] diff --git a/tests/snapshots/semantics__empty_block_expression.snap b/tests/snapshots/semantics__empty_block_expression.snap new file mode 100644 index 0000000..e67ef0b --- /dev/null +++ b/tests/snapshots/semantics__empty_block_expression.snap @@ -0,0 +1,26 @@ +--- +source: tests/semantics.rs +--- +[ + Diagnostic { + inner: DiagnosticInner { + message: "Block expressions must contain at least one expression", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 0, + ), + length: 2, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, +] diff --git a/tests/snapshots/semantics__equals_string_operation.snap b/tests/snapshots/semantics__equals_string_operation.snap new file mode 100644 index 0000000..52cc4f5 --- /dev/null +++ b/tests/snapshots/semantics__equals_string_operation.snap @@ -0,0 +1,4 @@ +--- +source: tests/semantics.rs +--- +[] diff --git a/tests/snapshots/semantics__filled_block_expression.snap b/tests/snapshots/semantics__filled_block_expression.snap new file mode 100644 index 0000000..52cc4f5 --- /dev/null +++ b/tests/snapshots/semantics__filled_block_expression.snap @@ -0,0 +1,4 @@ +--- +source: tests/semantics.rs +--- +[] diff --git a/tests/snapshots/semantics__illegal_string_operation_both.snap b/tests/snapshots/semantics__illegal_string_operation_both.snap new file mode 100644 index 0000000..517c9e4 --- /dev/null +++ b/tests/snapshots/semantics__illegal_string_operation_both.snap @@ -0,0 +1,26 @@ +--- +source: tests/semantics.rs +--- +[ + Diagnostic { + inner: DiagnosticInner { + message: "Strings only support `==` and `!=` operators", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 0, + ), + length: 13, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, +] diff --git a/tests/snapshots/semantics__illegal_string_operation_left.snap b/tests/snapshots/semantics__illegal_string_operation_left.snap new file mode 100644 index 0000000..3cee3ce --- /dev/null +++ b/tests/snapshots/semantics__illegal_string_operation_left.snap @@ -0,0 +1,26 @@ +--- +source: tests/semantics.rs +--- +[ + Diagnostic { + inner: DiagnosticInner { + message: "Strings only support `==` and `!=` operators", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 0, + ), + length: 10, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, +] diff --git a/tests/snapshots/semantics__illegal_string_operation_right.snap b/tests/snapshots/semantics__illegal_string_operation_right.snap new file mode 100644 index 0000000..2359e6b --- /dev/null +++ b/tests/snapshots/semantics__illegal_string_operation_right.snap @@ -0,0 +1,26 @@ +--- +source: tests/semantics.rs +--- +[ + Diagnostic { + inner: DiagnosticInner { + message: "Strings only support `==` and `!=` operators", + labels: Some( + [ + LabeledSpan { + label: None, + span: SourceSpan { + offset: SourceOffset( + 0, + ), + length: 9, + }, + primary: false, + }, + ], + ), + help: None, + severity: Error, + }, + }, +] diff --git a/tests/snapshots/semantics__unequals_string_operation.snap b/tests/snapshots/semantics__unequals_string_operation.snap new file mode 100644 index 0000000..52cc4f5 --- /dev/null +++ b/tests/snapshots/semantics__unequals_string_operation.snap @@ -0,0 +1,4 @@ +--- +source: tests/semantics.rs +--- +[]