diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0bfc6f2..4a7eba5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug Report about: Report a bug to help us improve! title: '' -labels: Bug, Needs Labels +labels: C-Bug, S-Needs-Triage assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index fa0286c..cc6e731 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature Request about: Propose a new feature! title: '' -labels: Enchancement, Needs Labels +labels: C-Enhancement, S-Needs-Triage assignees: '' --- diff --git a/.github/actions/install-linux-deps/action.yml b/.github/actions/install-linux-deps/action.yml new file mode 100644 index 0000000..16c6d1d --- /dev/null +++ b/.github/actions/install-linux-deps/action.yml @@ -0,0 +1,43 @@ +name: Install Linux dependencies +description: Installs the dependencies necessary to build Bevy on Linux. +inputs: + alsa: + description: Install alsa (libasound2-dev) + required: false + default: "true" + udev: + description: Install udev (libudev-dev) + required: false + default: "true" + wayland: + description: Install Wayland (libwayland-dev) + required: false + default: "true" + xkb: + description: Install xkb (libxkbcommon-dev) + required: false + default: "false" + x264: + description: Install x264 (libx264-dev) + required: false + default: "false" + fontconfig: + description: Install fontconfig (libfontconfig1-dev) + required: false + default: "false" +runs: + using: composite + steps: + - name: Install Linux dependencies + shell: bash + if: ${{ runner.os == 'linux' }} + run: > + sudo apt-get update + + sudo apt-get install --no-install-recommends + ${{ fromJSON(inputs.alsa) && 'libasound2-dev' || '' }} + ${{ fromJSON(inputs.udev) && 'libudev-dev' || '' }} + ${{ fromJSON(inputs.wayland) && 'libwayland-dev' || '' }} + ${{ fromJSON(inputs.xkb) && 'libxkbcommon-dev' || '' }} + ${{ fromJSON(inputs.x264) && 'libx264-164 libx264-dev' || '' }} + ${{ fromJSON(inputs.fontconfig) && 'libfontconfig1-dev' || '' }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fd6b9c..96b8cde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,9 @@ name: CI on: push: branches: - - master + - master pull_request: - branches: - - master + env: CARGO_TERM_COLOR: always @@ -15,12 +14,23 @@ jobs: name: Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout sources + uses: actions/checkout@v4 + - name: Cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} - name: Install Rust uses: dtolnay/rust-toolchain@stable - - name: Install alsa and udev - run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev + - name: Install Dependencies if: runner.os == 'linux' + run: sudo apt-get update; sudo apt-get install g++ pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 libwayland-dev libxkbcommon-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev - name: Run cargo check run: cargo check @@ -32,12 +42,23 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: - - uses: actions/checkout@v4 + - name: Checkout sources + uses: actions/checkout@v4 + - name: Cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Install alsa and udev - run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev + uses: dtolnay/rust-toolchain@nightly + - name: Install Dependencies if: runner.os == 'linux' + run: sudo apt-get update; sudo apt-get install g++ pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 libwayland-dev libxkbcommon-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev - name: Run tests run: cargo test @@ -45,30 +66,35 @@ jobs: name: Lints runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout sources + uses: actions/checkout@v4 + - name: Cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} - name: Install Rust uses: dtolnay/rust-toolchain@stable - - name: Install alsa and udev - run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev + - name: Install Dependencies + if: runner.os == 'linux' + run: sudo apt-get update; sudo apt-get install g++ pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 libwayland-dev libxkbcommon-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev - name: Run cargo fmt run: cargo fmt --all -- --check - - name: Run cargo clippy # TODO: Mark warnings as errors once all warnings are gone # run: cargo clippy -- -D warnings + - name: Run cargo clippy run: cargo clippy - typos: + spelling: + name: Spell Check with Typos runs-on: ubuntu-latest - timeout-minutes: 30 steps: - - uses: actions/checkout@v4 - - name: Check for typos - uses: crate-ci/typos@v1.18.2 - - name: Typos info - if: failure() - run: | - echo 'To fix typos, please run `typos -w`' - echo 'To check for a diff, run `typos`' - echo 'You can find typos here: https://crates.io/crates/typos' - echo 'if you use VSCode, you can also install `Typos Spell Checker' - echo 'You can find the extension here: https://marketplace.visualstudio.com/items?itemName=tekumara.typos-vscode' + - name: Checkout Actions Repository + uses: actions/checkout@v5 + - name: Spell Check Repo + uses: crate-ci/typos@v1.44.0 \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..908d510 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,90 @@ +name: Deploy Docs +# Based on https://github.com/bevyengine/bevy/blob/main/.github/workflows/docs.yml + +on: + push: + branches: + - master + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + RUSTDOCFLAGS: --html-in-header header.html + # If nightly is breaking CI, modify this variable to target a specific nightly version. + NIGHTLY_TOOLCHAIN: nightly + +# Sets the permissions to allow deploying to Github pages. +permissions: + contents: read + pages: write + id-token: write + +# Only allow one deployment to run at a time, however it will not cancel in-progress runs. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + # Only run this job when on the main bevy_dev_console repository. Without this, it would also run on forks + # where developers work on the main branch but have not enabled Github Pages. + if: ${{ github.repository == 'doonv/bevy_dev_console' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} + + - name: Install Linux dependencies + uses: ./.github/actions/install-linux-deps + with: + wayland: true + xkb: true + x264: true + fontconfig: true + + # This does the following: + # - Adds a meta tag that forces Google not to index any page on the site. + - name: Pre-docs-build + run: | + echo "" > header.html + + - name: Build docs + env: + # needs to be in sync with [package.metadata.docs.rs] + RUSTFLAGS: --cfg docsrs_dep + RUSTDOCFLAGS: -Zunstable-options --cfg=docsrs --generate-link-to-definition --generate-macro-expansion --html-after-content docs-rs/trait-tags.html + run: | + cargo doc \ + -Zunstable-options \ + -Zrustdoc-scrape-examples \ + -Zrustdoc-map \ + --all-features \ + --no-deps \ + --document-private-items + + # This adds the following: + # - A top level redirect to the bevy_dev_console crate documentation + # - A robots.txt file to forbid any crawling of the site (to defer to the docs.rs site on search engines). + - name: Finalize documentation + run: | + echo "" > target/doc/index.html + echo $'User-Agent: *\nDisallow: /' > target/doc/robots.txt + rm target/doc/.lock + + - name: Upload site artifact + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 + with: + path: target/doc + + - name: Deploy to Github Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 88f3db6..4d3be4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "bevy_dev_console" -version = "0.1.0" -edition = "2021" -exclude = ["assets/", "docs/", ".github/"] +version = "0.1.0-dev" +edition = "2024" +exclude = ["assets/", "doc/", ".github/"] keywords = ["bevy", "console", "development", "source"] license = "MIT OR Apache-2.0" +rust-version = "1.89.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] @@ -19,24 +20,64 @@ builtin-parser-completions = [ "dep:fuzzy-matcher", ] -builtin-parser = ["dep:logos"] +builtin-parser = [ + "dep:logos", + "dep:variadics_please", + "dep:kinded", + "dep:thiserror", + "dep:smallvec", + "dep:pastey", +] [dependencies] -# This crate by itself doesn't use any bevy features, but `bevy_egui` (dep) uses "bevy_asset". -bevy = { version = "0.14.0", default-features = false, features = [] } -bevy_egui = "0.28.0" -chrono = "0.4.31" -tracing-log = "0.2.0" -tracing-subscriber = "0.3.18" -web-time = "1.0.0" +bevy = { version = "0.18.1", default-features = false, features = [ + "bevy_color", + "bevy_log", +] } +bevy_egui = "0.39.1" +chrono = "0.4.44" +ansitok = "0.3.0" +anstyle = "1.0.14" # builtin-parser features -logos = { version = "0.14.0", optional = true } +pastey = { version = "*", optional = true } +logos = { version = "0.16.0", optional = true } fuzzy-matcher = { version = "0.3.7", optional = true } +variadics_please = { version = "2.0.0", optional = true } +kinded = { version = "0.5.0", optional = true } +thiserror = { version = "2.0.18", optional = true } +smallvec = { version = "1", optional = true } [dev-dependencies] -bevy = "0.14.0" +bevy = { version = "0.18.1", default-features = false, features = ["2d"] } +nu-ansi-term = "0.50.3" + +[package.metadata.docs.rs] +rustc-args = ["--cfg", "docsrs_dep"] +rustdoc-args = [ + "--cfg", + "docsrs", + "--generate-link-to-definition", + "--generate-macro-expansion", + "--html-after-content", + "docs-rs/trait-tags.html", + +] + + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } +# missing_docs = "warn" + +[lints.clippy] +type_complexity = "allow" +must_use_candidate = "warn" +too_many_arguments = "allow" +str_to_string = "warn" + -[lints] -clippy.useless_format = "allow" -rust.missing_docs = "warn" +# Required to make `cargo doc` scrape examples, +# otherwise it will skip them as they require dev depenendencies +[[example]] +name = "basic" +doc-scrape-examples = true diff --git a/README.md b/README.md index 13596ec..b96f4d5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # bevy_dev_console -`bevy_dev_console` is a Source-inspired developer console plugin for the [Bevy Game Engine](https://github.com/bevyengine/bevy). +`bevy_dev_console` is a experimental developer console plugin for the [Bevy Game Engine](https://github.com/bevyengine/bevy). ![Image of the developer console](doc/console.png) -> [!WARNING] -> +> [!WARNING] > `bevy_dev_console` is currently in its early development stages. Expect breaking changes in the near future (especially when using the built-in command parser). For this reason its only available as a git package at the moment. ## Features @@ -17,9 +16,9 @@ - Variables - Uses a simplified version of ownership and borrowing - Standard library (Doesn't have much at the moment) - - [Custom native functions](https://github.com/doonv/bevy_dev_console/blob/master/examples/custom_functions.rs) (`World` access included!) + - [Custom native functions](https://github.com/doonv/bevy_dev_console/blob/master/examples/custom_functions.rs) that work exactly like regular Rust functions (`World` access included!) - [Many types](https://github.com/doonv/bevy_dev_console/wiki/Built%E2%80%90in-Parser#types) - - Resource viewing and modification + - Resource viewing and modification with Reflection - Enums - Structs - ~~Entity queries~~ [*Coming Soon...*](https://github.com/doonv/bevy_dev_console/issues/3) (Syntax suggestions would be appreciated!) @@ -42,30 +41,25 @@ 3. Add the plugins. ```rust,no_run - use bevy::{prelude::*, log::LogPlugin}; + use bevy::prelude::*; use bevy_dev_console::prelude::*; App::new() .add_plugins(( - // Add the log plugin with the custom log layer - DefaultPlugins.set(LogPlugin { - custom_layer: custom_log_layer, - ..default() - }), - // Add the dev console plugin itself. + DefaultPlugins.set(console_log_plugin()), // Don't forget to set the LogPlugin DevConsolePlugin, )) .run(); ``` -4. That should be it! You can now press the `` ` `` / `~` key on your keyboard and it should open the console! +4. That should be it! You can now press the \` / ~ key on your keyboard and it should open the console! -## Togglable Features +## Toggleable Features -**(default)** `builtin-parser` includes the default parser. Disabling this allows you to remove the built-in parser and replace it with your own (or you could do nothing and make the console into a log reader). +`builtin-parser` **(default)** - Includes the default parser. Disabling this allows you to remove the built-in parser and replace it with your own (or you could do nothing and make the console into a log reader). ## Bevy Compatibility | bevy | bevy_dev_console | | ------ | ---------------- | -| 0.14.* | git (master) | +| 0.18.* | git (master) | diff --git a/docs-rs/README.md b/docs-rs/README.md new file mode 100644 index 0000000..44be0ac --- /dev/null +++ b/docs-rs/README.md @@ -0,0 +1,33 @@ +# Docs.rs Extensions + +This directory includes some templates and styling to extend and modify [rustdoc]'s output +for Bevy's documentation on [docs.rs]. Currently this consists of tags indicating core +`bevy_ecs` traits. + +## 3rd Party Crates + +To use in your own crate, first copy this folder into your project, +then add the following to your Cargo.toml: + +```toml +[package.metadata.docs.rs] +rustc-args = ["--cfg", "docsrs_dep"] +rustdoc-args = [ + "--cfg", "docsrs_dep", + "--html-after-content", "docs-rs/trait-tags.html", +] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } +``` + +## Local Testing + +Build the documentation with the extension enabled like this: + +```bash +RUSTDOCFLAGS="--html-after-content docs-rs/trait-tags.html --cfg docsrs_dep" RUSTFLAGS="--cfg docsrs_dep" cargo doc --no-deps --package +``` + +[rustdoc]: https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html +[docs.rs]: https://docs.rs diff --git a/docs-rs/trait-tags.html b/docs-rs/trait-tags.html new file mode 100644 index 0000000..e34614e --- /dev/null +++ b/docs-rs/trait-tags.html @@ -0,0 +1,177 @@ + + + \ No newline at end of file diff --git a/examples/basic.rs b/examples/basic.rs index 444bc0c..0dc29f3 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,6 +1,6 @@ //! A simple example showing how to setup the developer console plugin. -use bevy::log::LogPlugin; +use bevy::log::{DEFAULT_FILTER, LogPlugin}; use bevy::prelude::*; use bevy_dev_console::prelude::*; @@ -9,10 +9,9 @@ fn main() { .add_plugins(( // Add the log plugin with the custom log layer DefaultPlugins.set(LogPlugin { - custom_layer: custom_log_layer, - // Add a filter to the log plugin that shows all log levels from this example - filter: format!("wgpu=error,naga=warn,{}=trace", module_path!()), - ..default() + // Change the filter of the log plugin that shows all log levels from this example + filter: format!("{DEFAULT_FILTER},{}=trace", module_path!()), + ..console_log_plugin() }), // Add the dev console plugin itself. DevConsolePlugin, @@ -21,9 +20,11 @@ fn main() { .run(); } -fn test() { +fn test(mut commands: Commands) { + commands.spawn(Camera2d); + trace!("tracing"); - debug!("solving issues..."); + debug!("solving {}...", nu_ansi_term::Color::Red.paint("issues")); info!("hello :)"); warn!("spooky warning"); error!("scary error"); diff --git a/examples/custom_functions.rs b/examples/custom_functions.rs index c785862..b7fe1d3 100644 --- a/examples/custom_functions.rs +++ b/examples/custom_functions.rs @@ -1,11 +1,13 @@ //! An example showing how to create custom functions -use bevy::log::LogPlugin; +use std::time; + use bevy::prelude::*; -use bevy_dev_console::builtin_parser::{Environment, EvalError, Number, Spanned, StrongRef, Value}; +use bevy_dev_console::builtin_parser::{ + Diagnostic, Environment, EvalError, Number, Spanned, Value, +}; use bevy_dev_console::prelude::*; use bevy_dev_console::register; -use web_time as time; // Declare the functions we want to create: @@ -24,54 +26,57 @@ fn add(num1: f64, num2: f64) -> f64 { num1 + num2 } +/// A "Generic" function that works with any [`Number`] type. +fn add_generic( + num1: Spanned, + num2: Spanned, +) -> Result> { + num1 + num2 // Adding two Spanned Numbers automatically returns the correct diagnostic for you. +} + /// Function with any value + span -fn print_debug_info(value: Spanned) { - info!( - "Location in command: {:?}, Value: {:?}", - value.span, value.value - ) +fn print_debug_info(Spanned { span, value }: Spanned) { + info!("Location of command: {span:?}, Value: {value:?}"); } #[derive(Resource)] struct MyCounter(u32); /// Function with [`World`] -fn increment_global_counter(world: &mut World) -> u32 { - world.resource_scope(|_, mut counter: Mut| { - counter.0 += 1; +fn add_to_global_counter(num: u32, world: &mut World) -> u32 { + let mut counter = world.resource_mut::(); + + counter.0 += num; - counter.0 - }) + counter.0 } -// Function with reference (Syntax subject to change soon) -fn increment_number(number: Spanned>) -> Result<(), EvalError> { - let span = number.span; - let mut reference = number.value.borrow_mut(); - if let Value::Number(number) = &mut *reference { - *number = Number::add(*number, Number::Integer(1), span).unwrap(); - Ok(()) - } else { - Err(EvalError::Custom { - text: "Oh nooo".into(), - span, - }) - } +// Function with reference +fn toggle_bool(value: &mut bool) { + *value = !*value; } -// For more examples take a look at the standard library. +// Variable argument function +fn count_args(args: Vec) -> usize { + args.len() +} + +// For more examples take a look at the [standard library](https://github.com/doonv/bevy_dev_console/blob/master/src/builtin_parser/runner/stdlib.rs). // Register our functions by creating and inserting our own environment fn custom_environment() -> Environment { let mut environment = Environment::default(); - // The register macro allows us to easily add functions to the environment. + // The register macro allows us to easily add functions to + // the environment without needing to specify the name twice. register!(&mut environment => { fn time_since_epoch; fn add; + fn add_generic; fn print_debug_info; - fn increment_global_counter; - fn increment_number; + fn add_to_global_counter; + fn toggle_bool; + fn count_args; }); environment @@ -82,12 +87,11 @@ fn main() { .insert_resource(MyCounter(0)) // Insert our new environment .insert_non_send_resource(custom_environment()) - .add_plugins(( - DefaultPlugins.set(LogPlugin { - custom_layer: custom_log_layer, - ..default() - }), - DevConsolePlugin, - )) + .add_plugins((DefaultPlugins.set(console_log_plugin()), DevConsolePlugin)) + .add_systems(Startup, spawn_camera) .run(); } + +fn spawn_camera(mut commands: Commands) { + commands.spawn(Camera2d); +} diff --git a/examples/resource.rs b/examples/resource.rs index 0f91f78..3494c39 100644 --- a/examples/resource.rs +++ b/examples/resource.rs @@ -1,8 +1,9 @@ //! Example of modifying resources via the console via reflection. //! +//! To use, start by typing `MyStruct` into the dev console. Then try modifying it! +//! //! **Warning:** This is very experimental, might not work. -use bevy::log::LogPlugin; use bevy::prelude::*; use bevy_dev_console::prelude::*; @@ -34,24 +35,21 @@ struct SubStruct { fn main() { App::new() - .register_type::() .init_resource::() .insert_resource(MyStruct { number: 5.6, - string: "hi there :)".to_string(), + string: "hi there :)".to_owned(), struct_in_struct: SubStruct { boolean: false, - enume: MyEnum::Tupleo("nooo".to_string(), 5.), + enume: MyEnum::Tupleo("nooo".to_owned(), 5.), }, tuple: (-5, 255), }) - .register_type::() - .add_plugins(( - DefaultPlugins.set(LogPlugin { - custom_layer: custom_log_layer, - ..default() - }), - DevConsolePlugin, - )) + .add_plugins((DefaultPlugins.set(console_log_plugin()), DevConsolePlugin)) + .add_systems(Startup, spawn_camera) .run(); } + +fn spawn_camera(mut commands: Commands) { + commands.spawn(Camera2d); +} diff --git a/rustfmt.toml b/rustfmt.toml index c1578aa..2726184 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,5 @@ imports_granularity = "Module" +use_field_init_shorthand = true +newline_style = "Unix" +normalize_comments = true +style_edition = "2024" diff --git a/src/builtin_parser.rs b/src/builtin_parser.rs index 7b0ba86..259ee27 100644 --- a/src/builtin_parser.rs +++ b/src/builtin_parser.rs @@ -1,14 +1,28 @@ //! [`bevy_dev_console`](crate)'s built-in command parser. //! -//! Currently the built-in command parser is in very early development. +//! The built-in parser provides an experimental mini-language that can evaluate expressions, call commands, and modify resources. +//! //! It's purpose is to provide a simple, yet powerful method of modifying //! the game world via commands. +//! +//! Take a look at the [docs] for how to use it. + +use std::fmt::Display; use bevy::prelude::*; use logos::Span; +use smallvec::SmallVec; -use crate::builtin_parser::runner::ExecutionError; -use crate::command::{CommandHints, CommandParser, DefaultCommandParser}; +use crate::builtin_parser::parser::ParseError; +use crate::command::{ + COMMAND_MESSAGE_NAME, COMMAND_RESULT_NAME, CommandParser, DefaultCommandParser, + format_command_with_hints, +}; + +/// Prefix for log messages that show a previous command. +const COMMAND_MESSAGE_PREFIX: &str = "$ "; +/// Prefix for log messages that show the result of a command. +const COMMAND_RESULT_PREFIX: &str = "> "; #[cfg(feature = "builtin-parser-completions")] use crate::command::CompletionSuggestion; @@ -20,33 +34,148 @@ pub(crate) mod number; pub(crate) mod parser; pub(crate) mod runner; +#[doc = include_str!("./builtin_parser/docs.md")] +#[cfg(doc)] +pub mod docs {} + +/// A macro to test example usages of the builtin parser. +// #[cfg(doctest)] // This doesn't work right now, see https://github.com/rust-lang/rust/issues/67295 +#[doc(hidden)] +#[macro_export] +macro_rules! test_builtin_parser { + ( + $( { + $dollar:tt $($expr:expr)+ + $(; $($result:tt)+)? + } + )+ + ) => {{ + use bevy::prelude::World; + use bevy_dev_console::builtin_parser::*; + use bevy::prelude::AppTypeRegistry; + + fn strip_ansi(ansi: &str) -> String { + ansitok::parse_ansi(ansi) + .filter_map(|element| { + if let ansitok::ElementKind::Text = element.kind() { + Some(&ansi[element.range()]) + } else { + None + } + }) + .collect() + } + let mut world = World::new(); + let mut environment = Environment::default(); + let registry = AppTypeRegistry::default(); + + $( + let cmd = stringify!($($expr)+); + let result = run( + cmd, + &mut world, + &mut environment, + ®istry, + ); + $( + bevy_dev_console::test_builtin_parser!( + (result, cmd); + $($result)+ + ); + )? + )+ + }}; + ( + ($got:ident, $cmd:ident); + $( > $($expected:tt)+)? + ) => { + $( assert_eq!(strip_ansi(&$got.expect($cmd).unwrap()), stringify!($($expected)+)); )? + }; + ( + ($got:ident, $cmd:ident); + err $expected:literal + ) => { + assert_eq!(strip_ansi(&$got.unwrap_err().to_string()), $expected.trim().trim_start_matches("ERROR ")); + }; +} + pub use number::*; +pub use runner::Value; pub use runner::environment::Environment; pub use runner::error::EvalError; pub use runner::unique_rc::*; -pub use runner::Value; -/// Additional traits for span. +const fn color(color: anstyle::AnsiColor) -> anstyle::Style { + anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(color))) +} + +pub(crate) const DARK: anstyle::Style = color(anstyle::AnsiColor::BrightBlack); +pub(crate) const MEMBER: anstyle::Style = color(anstyle::AnsiColor::Red); +pub(crate) const VALUE: anstyle::Style = color(anstyle::AnsiColor::Yellow); +pub(crate) const TYPE: anstyle::Style = color(anstyle::AnsiColor::BrightYellow); +pub(crate) const STRING: anstyle::Style = color(anstyle::AnsiColor::Green); +pub(crate) const FUNCTION: anstyle::Style = color(anstyle::AnsiColor::Blue); +pub(crate) const KEYWORD: anstyle::Style = color(anstyle::AnsiColor::Magenta); +pub(crate) const VARIANT: anstyle::Style = color(anstyle::AnsiColor::Cyan); + +/// Additional methods for [`Span`]. pub trait SpanExtension { /// Wrap this value with a [`Spanned`]. #[must_use] fn wrap(self, value: T) -> Spanned; + /// Combine two [`Span`]s into one. #[must_use] - fn join(self, span: Self) -> Self; + fn join(&self, span: &Self) -> Self; + + /// Wrap an error in a [`Diagnostic`] with this [`Span`]. + fn diagnose(self, error: E) -> Diagnostic; + + /// Adds the left and right values of the provided [`Span`] to this [`Span`]. + #[must_use] + fn add(self, range: Span) -> Self; } impl SpanExtension for Span { #[inline] fn wrap(self, value: T) -> Spanned { Spanned { span: self, value } } + #[inline] - fn join(self, span: Self) -> Self { + fn join(&self, span: &Self) -> Self { + debug_assert!(self.start <= span.end); + self.start..span.end } + + #[inline] + fn diagnose(self, error: E) -> Diagnostic { + Diagnostic::single(self, error) + } + + fn add(self, range: Span) -> Self { + Span { + start: self.start + range.start, + end: self.end + range.end, + } + } } -/// Wrapper around `T` that stores a [Span] (A location in the source code) +pub trait ErrorExtension { + fn diagnosed(self, span: Span) -> Result>; +} +impl> ErrorExtension for Result { + fn diagnosed(self, span: Span) -> Result> { + self.map_err(|e| span.diagnose(e.into())) + } +} +impl> ErrorExtension for Result { + fn diagnosed(self, span: Span) -> Result> { + self.map_err(|e| span.diagnose(e.into())) + } +} + +/// Wrapper around `T` that stores a [Span]. #[derive(Debug, Clone)] pub struct Spanned { /// The location of `T` in the source/command. @@ -66,6 +195,12 @@ impl Spanned { } } +impl Display for Spanned { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } +} + impl Default for DefaultCommandParser { fn default() -> Self { Self(Box::new(BuiltinCommandParser)) @@ -81,41 +216,42 @@ impl CommandParser for BuiltinCommandParser { fn parse(&self, command: &str, world: &mut World) { let mut tokens = lexer::TokenStream::new(command); - let environment = world.non_send_resource::(); - let ast = parser::parse(&mut tokens, environment); - - dbg!(&ast); - - match ast { - Ok(ast) => match runner::run(ast, world) { - Ok(()) => (), - Err(error) => { - if let ExecutionError::Eval(eval_error) = &error { - world - .resource_mut::() - .push(eval_error.hints()); + // Can't `resource_scope` the environment because it's a non send resource. + let mut environment = world.remove_non_send_resource::().unwrap(); + let ast = parser::parse(&mut tokens, &environment); + world.resource_scope(|world, registry: Mut| { + match ast { + Ok(ast) => match runner::eval(ast, world, &mut environment, ®istry) { + Ok(value) => { + info!(name: COMMAND_MESSAGE_NAME, "{DARK}{COMMAND_MESSAGE_PREFIX}{DARK:#}{command}"); + if let Some(value) = value { + info!(name: COMMAND_RESULT_NAME, "{DARK}{COMMAND_RESULT_PREFIX}{DARK:#}{value}"); + } + } + Err(error) => { + let highlighted = format_command_with_hints(command, &error.spans); + info!(name: COMMAND_MESSAGE_NAME, "{DARK}{COMMAND_MESSAGE_PREFIX}{DARK:#}{highlighted}"); + error!("{error}"); } - error!("{error}") + }, + Err(err) => { + let highlighted = format_command_with_hints(command, &err.spans); + info!(name: COMMAND_MESSAGE_NAME, "{DARK}{COMMAND_MESSAGE_PREFIX}{DARK:#}{highlighted}"); + error!("{err}"); } - }, - Err(err) => { - world.resource_mut::().push([err.hint()]); - - error!("{err}") } - } + }); #[cfg(feature = "builtin-parser-completions")] { - *world.resource_mut() = - completions::store_in_cache(world.non_send_resource::()); + *world.resource_mut() = completions::store_in_cache(&environment); } + world.insert_non_send_resource(environment); } #[cfg(feature = "builtin-parser-completions")] fn completion(&self, command: &str, world: &World) -> Vec { - use fuzzy_matcher::FuzzyMatcher; - use crate::builtin_parser::completions::EnvironmentCache; + use fuzzy_matcher::FuzzyMatcher; let matcher = fuzzy_matcher::skim::SkimMatcherV2::default(); let environment_cache = world.resource::(); @@ -124,9 +260,19 @@ impl CommandParser for BuiltinCommandParser { .function_names .iter() .chain(environment_cache.variable_names.iter()) - .map(|name| (matcher.fuzzy_indices(name, command), name.clone())) + .cloned() + .chain( + world + .resource::() + .read() + .iter() + .filter(|&v| world.components().get_resource_id(v.type_id()).is_some()) + .map(|v| v.type_info().type_path_table().short_path().to_owned()), + ) + .map(|name| (matcher.fuzzy_indices(&name, command), name)) .filter_map(|(fuzzy, name)| fuzzy.map(|v| (v, name))) .collect(); + names.sort_by_key(|((score, _), _)| std::cmp::Reverse(*score)); names.truncate(crate::ui::MAX_COMPLETION_SUGGESTIONS); @@ -139,3 +285,63 @@ impl CommandParser for BuiltinCommandParser { .collect() } } + +#[must_use] +#[derive(thiserror::Error, Debug)] +pub struct Diagnostic { + pub spans: SmallVec<[Span; 1]>, + #[source] + pub error: Box, +} +impl Diagnostic { + pub fn empty(error: E) -> Self { + Self { + spans: SmallVec::new(), + error: Box::new(error), + } + } + pub fn single(span: Span, error: E) -> Self { + Self { + spans: SmallVec::from_buf([span]), + error: Box::new(error), + } + } +} + +impl Display for Diagnostic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.error, f) + } +} +impl From> for Diagnostic { + fn from(Spanned { span, value }: Spanned) -> Self { + Self::single(span, value) + } +} +impl> From for Diagnostic { + fn from(value: T) -> Self { + Diagnostic::empty(value.into()) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum RunError { + #[error(transparent)] + Parse(Diagnostic), + #[error(transparent)] + Eval(Diagnostic), +} + +pub fn run( + command: &str, + world: &mut World, + environment: &mut Environment, + registry: &AppTypeRegistry, +) -> Result, RunError> { + let mut tokens = lexer::TokenStream::new(command); + + let ast = parser::parse(&mut tokens, environment).map_err(RunError::Parse)?; + let value = runner::eval(ast, world, environment, registry).map_err(RunError::Eval)?; + + Ok(value) +} diff --git a/src/builtin_parser/completions.rs b/src/builtin_parser/completions.rs index b861b2c..0d57918 100644 --- a/src/builtin_parser/completions.rs +++ b/src/builtin_parser/completions.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; -use super::runner::environment::Variable; use super::Environment; +use super::runner::environment::Variable; /// Stores the names of variables and functions for fast async access. #[derive(Resource)] @@ -11,11 +11,9 @@ pub struct EnvironmentCache { } impl FromWorld for EnvironmentCache { fn from_world(world: &mut World) -> Self { - if let Some(environment) = world.get_non_send_resource::() { - store_in_cache(environment) - } else { - Self::empty() - } + world + .get_non_send_resource::() + .map_or_else(Self::empty, store_in_cache) } } impl EnvironmentCache { diff --git a/src/builtin_parser/docs.md b/src/builtin_parser/docs.md new file mode 100644 index 0000000..5b8b312 --- /dev/null +++ b/src/builtin_parser/docs.md @@ -0,0 +1,201 @@ + +The built-in parser provides a mini-language that can evaluate expressions, call commands, and modify resources. + +
+Note + +This guide assumes you already have used Rust before and that you know how it works. +
+ +## Basic variable assignments and math + +For simplicity, the `let` keyword is not required for creating variables. + +You can do most basic operations: + +- Arithmetic: Addition/Subtraction/Multiplication/Division/Modulus +- Bitwise operations: AND/XOR/OR/NOT +- ~~Comparisons~~ (Coming soon-ish) + +```rust +# bevy_dev_console::test_builtin_parser!({ +$ x = 5 + 4 +# ; +> 9 (integer) +# }{ +$ x - 2 +# ; +> 7 (integer) +# }{ +$ x = x * -3 +# ; +> -27 (integer) +# }{ +$ !true +# ; +> false +# }{ +$ !5u8 +# ; +> 250 (u8) +# }{ +$ 84 ^ 32 +# ; +> 116 (integer) +# }) +``` + +## Ownership and Borrowing + +The built-in parser uses the a similar memory model to Rust's. + +This means assigning a variable's value to another causes the original variable to get moved. + +```rust +# bevy_dev_console::test_builtin_parser!({ +$ x = "Hello World!" +# ; +> "Hello World!" +# }{ +$ y = x +# ; +> "Hello World!" +# }{ +$ x +# ; err " +ERROR variable `x` was moved +# " }); +``` + +For simplicity, references are always mutable, and you can have multiple of them. + +You can deference references to modify the value they are references. + +```rust +# bevy_dev_console::test_builtin_parser!({ +$ x = "Hi" +# ; +> "Hi" +# }{ +$ y = &x +# ; +> "Hi" +# }{ +$ z = &x +# ; +> "Hi" +# }{ +$ *z = "Bye" +# ; +> "Bye" +# }{ +$ x +# ; +> "Bye" +# }{ +$ y +# ; +> "Bye" +# }); +``` + +You can also use dereferencing to clone a variable's value. + +```rust +# bevy_dev_console::test_builtin_parser!({ +$ x = "Hmmm" +# }{ +$ y = *x +# }{ +$ z = x +# }); +``` + +### Copy types + +Certain types are `Copy`, which, like in Rust, means they are automatically copied instead of moved. +This means they never can be moved. + +The copy types in the builtin parser are: `()`, all number types, and `bool`. + +```rust +# bevy_dev_console::test_builtin_parser!({ +$ x = 12 +# ; +> 12 (integer) +# }{ +$ drop x +# }{ +$ x +# ; +> 12 (integer) +# }); +``` + +## Types + +Every value can have one of these types: + +- **None**: The same as `()` in Rust. +- **Number**: Any type of Rust number (You can specify a type like this: `5usize`, `2.4f32`, `8f64`), along with these 2 additional types: + - **Integer**: A generic integer of an unknown size that can get downcasted to the appropriate size when used with an integer of a known size. + - **Float**: A generic float of an unknown size that can get downcasted to the appropriate size when used with a float of a known size. +- **Boolean**: A value that can either be `true` or `false`. +- **String**: An array of characters, the same as the [`String`] type in Rust. +- **Reference**: A mutable reference to another value. +- **Object**: A dynamic object, equivalent to [`HashMap`](std::collections::HashMap) in Rust. +- **StructObject**: A dynamic object, but with a name. Mostly used for enum assignments. +- **Tuple**: A dynamic tuple. +- **StructTuple**: A dynamic tuple, but with a name. Mostly used for enum assignments. +- **Resource**: A reference to a resource that can be modified. + +## Functions + +You can call functions by the name, followed by space separated arguments. + +```rust +# bevy_dev_console::test_builtin_parser!({ +$ print "Hello World!" +# }); stringify!( // TODO maybe test log output one day? +INFO "Hello World!" +# ); +``` + +Custom native functions can be created and added, see the [`custom_functions` example](https://github.com/doonv/bevy_dev_console/blob/master/examples/custom_functions.rs) for an example. + +## Resource Modification + +You can modify [`Resource`]s that implement the [`Reflect`] trait by their name. See the [`resources` example](https://github.com/doonv/bevy_dev_console/blob/master/examples/resources.rs) for some resources to play around with. + +
+Note + +Error handling for this is not finished, expect panics. +
+ +```rust,ignore +MyStructResource.number1 = 100 +MyEnumResource = Option2 +MyEnumResource = StructVariant { a: 5.2, b: "Hi!" } +MyEnumResource = TupleVariant(false, 5u64) +``` + +## Entity Queries and Modification + +[*Coming soon...*](https://github.com/doonv/bevy_dev_console/issues/3) + +## Loops + +[*Coming soon...*](https://github.com/doonv/bevy_dev_console/issues/8) + +## Functions/Closures + +[*Coming soon...*](https://github.com/doonv/bevy_dev_console/issues/12) + +## Boolean comparisons + +*Coming soon...* + +## The end, for now + +`bevy_dev_console` is still in active development, and more features will be added soon. diff --git a/src/builtin_parser/lexer.rs b/src/builtin_parser/lexer.rs index f15311c..20ec2e7 100644 --- a/src/builtin_parser/lexer.rs +++ b/src/builtin_parser/lexer.rs @@ -36,6 +36,11 @@ pub enum Token { #[token("%")] Modulo, + #[token("!")] + Not, + #[token("^")] + Xor, + #[token(".", priority = 10)] Dot, #[token("&")] @@ -47,6 +52,8 @@ pub enum Token { For, #[token("while")] While, + #[token("if")] + If, #[token("in")] In, @@ -57,6 +64,8 @@ pub enum Token { SemiColon, #[token(",")] Comma, + #[token("|")] + Pipe, #[token("true")] True, @@ -69,24 +78,10 @@ pub enum Token { #[regex("[a-zA-Z_][a-zA-Z0-9_]*")] Identifier, - #[regex(r#"[0-9]+"#)] + #[regex(r#"[0-9]+[A-Za-z0-9_]*"#)] IntegerNumber, - #[regex(r#"[0-9]+\.[0-9]*"#)] + #[regex(r#"[0-9]+\.[0-9]*[A-Za-z0-9_]*"#)] FloatNumber, - - #[token("i8")] - #[token("i16")] - #[token("i32")] - #[token("i64")] - #[token("isize")] - #[token("u8")] - #[token("u16")] - #[token("u32")] - #[token("u64")] - #[token("usize")] - #[token("f32")] - #[token("f64")] - NumberType, } /// A wrapper for the lexer which provides token peeking and other helper functions @@ -100,12 +95,16 @@ pub struct TokenStream<'a> { impl<'a> TokenStream<'a> { /// Creates a new [`TokenStream`] from `src`. + #[must_use] pub fn new(src: &'a str) -> Self { let mut lexer = Token::lexer(src); - let current_slice = lexer.slice(); - let current_span = lexer.span(); let next = lexer.next(); + let (current_span, current_slice) = if let Some(Err(FailedToLexCharacter)) = next { + (0..1, &src[0..1]) + } else { + (lexer.span(), lexer.slice()) + }; Self { lexer, @@ -125,6 +124,12 @@ impl<'a> TokenStream<'a> { val } + /// Returns advances the iterator and discards the [`Token`] + pub fn skip_one(&mut self) -> &mut Self { + self.next(); + self + } + // pub fn next_pe(&mut self) -> Result { // let token = self.next(); @@ -154,6 +159,24 @@ impl<'a> TokenStream<'a> { self.current_span.clone() } + /// Advances the stream until a certain [`Token`] is reached and returns the entire span between now and that [`Token`]. + #[must_use] + pub fn span_until(&mut self, token: Token) -> Span { + let start = self.current_span.start; + loop { + match self.next() { + Some(Ok(t)) if t == token => break, + Some(Err(_)) | None => break, + Some(Ok(_)) => {} + } + } + + Span { + start, + end: self.current_span.end, + } + } + /// Get a [`str`] slice of the current [`Token`]. #[inline] #[must_use] diff --git a/src/builtin_parser/number.rs b/src/builtin_parser/number.rs index bb57ea1..b93a90b 100644 --- a/src/builtin_parser/number.rs +++ b/src/builtin_parser/number.rs @@ -1,225 +1,962 @@ -use std::fmt::Display; +#![allow(missing_docs, non_camel_case_types)] + +use std::fmt::{Debug, Display}; use std::ops::*; use bevy::reflect::Reflect; +use kinded::Kinded; use logos::Span; -use super::{EvalError, SpanExtension, Spanned}; +use crate::builtin_parser::parser::BinaryOperator; +use crate::builtin_parser::{Diagnostic, ErrorExtension, VALUE}; -/// An enum that contains any type of number. -/// -/// The [`Integer`](Number::Integer) and [`Float`](Number::Float) types -/// are generic types that then get downcasted when they first interact -/// with a concrete type. (i.e. calling a function, etc) -#[derive(Debug, Clone, Copy)] -#[allow(missing_docs, non_camel_case_types)] -pub enum Number { - /// Generic integer that can get downcasted. - Integer(i128), - /// Generic float that can get downcasted to a [`f64`] and [`f32`] - Float(f64), +use super::runner::error::EvalError; +use super::{SpanExtension, Spanned}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Kinded)] +#[kinded(skip_derive(Display))] +pub enum UnsignedInteger { u8(u8), u16(u16), u32(u32), u64(u64), usize(usize), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Kinded)] +#[kinded(skip_derive(Display))] +pub enum SignedInteger { i8(i8), i16(i16), i32(i32), i64(i64), isize(isize), + /// Generic integer that can get downcasted. + Unspecified(i128), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Integer { + Unsigned(UnsignedInteger), + Signed(SignedInteger), +} + +#[derive(Debug, Clone, Copy, PartialEq, Kinded)] +#[kinded(skip_derive(Display))] +pub enum Float { f32(f32), f64(f64), + /// Generic float that can get downcasted to a [`f64`] and [`f32`] + Unspecified(f64), } -impl Number { +/// An enum that contains any type of number. +/// +/// The [`Integer`](SignedInteger::Unspecified) and [`Float`](Float::Float) types +/// are generic types that then get downcasted when they first interact +/// with a concrete type. (i.e. calling a function, etc) +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Number { + Integer(Integer), + Float(Float), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IntegerKind { + Unsigned(UnsignedIntegerKind), + Signed(SignedIntegerKind), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NumberKind { + Integer(IntegerKind), + Float(FloatKind), +} + +impl kinded::Kind for IntegerKind { + fn all() -> &'static [Self] { + &[] + } +} + +impl kinded::Kind for NumberKind { + fn all() -> &'static [Self] { + &[] + } +} + +impl Kinded for Integer { + type Kind = IntegerKind; + + fn kind(&self) -> Self::Kind { + match self { + Integer::Unsigned(v) => IntegerKind::Unsigned(v.kind()), + Integer::Signed(v) => IntegerKind::Signed(v.kind()), + } + } +} + +impl Kinded for Number { + type Kind = NumberKind; + + fn kind(&self) -> Self::Kind { + match self { + Number::Integer(v) => NumberKind::Integer(v.kind()), + Number::Float(v) => NumberKind::Float(v.kind()), + } + } +} + +impl Integer { /// Converts this into a [`Box`](Reflect). - pub fn reflect(self, span: Span, ty: &str) -> Result, EvalError> { + pub fn reflect(self, span: Span, ty: &str) -> Result, Diagnostic> { match self { - Number::u8(number) => Ok(Box::new(number)), - Number::u16(number) => Ok(Box::new(number)), - Number::u32(number) => Ok(Box::new(number)), - Number::u64(number) => Ok(Box::new(number)), - Number::usize(number) => Ok(Box::new(number)), - Number::i8(number) => Ok(Box::new(number)), - Number::i16(number) => Ok(Box::new(number)), - Number::i32(number) => Ok(Box::new(number)), - Number::i64(number) => Ok(Box::new(number)), - Number::isize(number) => Ok(Box::new(number)), - Number::f32(number) => Ok(Box::new(number)), - Number::f64(number) => Ok(Box::new(number)), - Number::Integer(number) => match ty { - "u8" => Ok(Box::new(number as u8)), - "u16" => Ok(Box::new(number as u16)), - "u32" => Ok(Box::new(number as u32)), - "u64" => Ok(Box::new(number as u64)), - "usize" => Ok(Box::new(number as usize)), - "i8" => Ok(Box::new(number as i8)), - "i16" => Ok(Box::new(number as i16)), - "i32" => Ok(Box::new(number as i32)), - "i64" => Ok(Box::new(number as i64)), - "isize" => Ok(Box::new(number as isize)), - ty => Err(EvalError::IncompatibleReflectTypes { - expected: "integer".to_string(), - actual: ty.to_string(), - span, - }), + Integer::Unsigned(number) => match number { + UnsignedInteger::u8(number) => Ok(Box::new(number)), + UnsignedInteger::u16(number) => Ok(Box::new(number)), + UnsignedInteger::u32(number) => Ok(Box::new(number)), + UnsignedInteger::u64(number) => Ok(Box::new(number)), + UnsignedInteger::usize(number) => Ok(Box::new(number)), }, - Number::Float(number) => match ty { - "f32" => Ok(Box::new(number as f32)), - "f64" => Ok(Box::new(number)), - ty => Err(EvalError::IncompatibleReflectTypes { - expected: "float".to_string(), - actual: ty.to_string(), - span, - }), + Integer::Signed(number) => match number { + SignedInteger::i8(number) => Ok(Box::new(number)), + SignedInteger::i16(number) => Ok(Box::new(number)), + SignedInteger::i32(number) => Ok(Box::new(number)), + SignedInteger::i64(number) => Ok(Box::new(number)), + SignedInteger::isize(number) => Ok(Box::new(number)), + SignedInteger::Unspecified(number) => match ty { + "u8" => Ok(Box::new(number as u8)), + "u16" => Ok(Box::new(number as u16)), + "u32" => Ok(Box::new(number as u32)), + "u64" => Ok(Box::new(number as u64)), + "usize" => Ok(Box::new(number as usize)), + "i8" => Ok(Box::new(number as i8)), + "i16" => Ok(Box::new(number as i16)), + "i32" => Ok(Box::new(number as i32)), + "i64" => Ok(Box::new(number as i64)), + "isize" => Ok(Box::new(number as isize)), + ty => Err(span.diagnose(EvalError::IncompatibleReflectTypes { + expected: "integer".to_owned(), + actual: ty.to_owned(), + })), + }, }, } } +} - /// Returns the kind of [`Number`] as a [string slice](str). - /// You may want to use [`natural_kind`](Self::natural_kind) +impl Number { + /// Converts this into a [`Box`](Reflect). + pub fn reflect(self, span: Span, ty: &str) -> Result, Diagnostic> { + match self { + Number::Integer(integer) => integer.reflect(span, ty), + Number::Float(number) => match number { + Float::f32(number) => Ok(Box::new(number)), + Float::f64(number) => Ok(Box::new(number)), + Float::Unspecified(number) => match ty { + "f32" => Ok(Box::new(number as f32)), + "f64" => Ok(Box::new(number)), + ty => Err(span.diagnose(EvalError::IncompatibleReflectTypes { + expected: "float".to_owned(), + actual: ty.to_owned(), + })), + }, + }, + } + } +} + +impl Display for UnsignedInteger { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UnsignedInteger::u8(number) => write!(f, "{VALUE}{number}{VALUE:#} (u8)"), + UnsignedInteger::u16(number) => write!(f, "{VALUE}{number}{VALUE:#} (u16)"), + UnsignedInteger::u32(number) => write!(f, "{VALUE}{number}{VALUE:#} (u32)"), + UnsignedInteger::u64(number) => write!(f, "{VALUE}{number}{VALUE:#} (u64)"), + UnsignedInteger::usize(number) => write!(f, "{VALUE}{number}{VALUE:#} (usize)"), + } + } +} + +impl Display for SignedInteger { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SignedInteger::i8(number) => write!(f, "{VALUE}{number}{VALUE:#} (i8)"), + SignedInteger::i16(number) => write!(f, "{VALUE}{number}{VALUE:#} (i16)"), + SignedInteger::i32(number) => write!(f, "{VALUE}{number}{VALUE:#} (i32)"), + SignedInteger::i64(number) => write!(f, "{VALUE}{number}{VALUE:#} (i64)"), + SignedInteger::isize(number) => write!(f, "{VALUE}{number}{VALUE:#} (isize)"), + SignedInteger::Unspecified(number) => { + write!(f, "{VALUE}{number}{VALUE:#} (integer)") + } + } + } +} + +impl Display for Float { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Float::f32(number) => write!(f, "{VALUE}{number}{VALUE:#} (f32)"), + Float::f64(number) => write!(f, "{VALUE}{number}{VALUE:#} (f64)"), + Float::Unspecified(number) => write!(f, "{VALUE}{number}{VALUE:#} (float)"), + } + } +} + +impl Display for Integer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Integer::Unsigned(number) => Display::fmt(number, f), + Integer::Signed(number) => Display::fmt(number, f), + } + } +} + +impl Display for Number { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Number::Integer(number) => Display::fmt(number, f), + Number::Float(number) => Display::fmt(number, f), + } + } +} + +macro_rules! impl_kind_methods { + ($kind:ident, $($variant:ident => ($str:expr, $natural:expr)),*$(,)?) => { + impl $kind { + /// Converts this kind into a [`&'static str`](str) + /// You may want to use [`as_natural`](Self::as_natural) + /// instead for more natural sounding error messages + #[must_use] + pub const fn as_str(&self) -> &'static str { + match self { + $(Self::$variant => $str),* + } + } + + /// Returns the kind of value as a [string slice](str) with an `a` or `an` prepended to it. + /// Used for more natural sounding error messages. + #[must_use] + pub const fn as_natural(&self) -> &'static str { + match self { + $(Self::$variant => $natural),* + } + } + } + impl Display for $kind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + f.write_str(self.as_natural()) + } else { + f.write_str(self.as_str()) + } + } + } + }; +} + +impl_kind_methods!(UnsignedIntegerKind, + u8 => ("u8", "a u8"), + u16 => ("u16", "a u16"), + u32 => ("u32", "a u32"), + u64 => ("u64", "a u64"), + usize => ("usize", "a usize"), +); + +impl_kind_methods!(SignedIntegerKind, + i8 => ("i8", "a i8"), + i16 => ("i16", "a i16"), + i32 => ("i32", "a i32"), + i64 => ("i64", "a i64"), + isize => ("isize", "an isize"), + Unspecified => ("integer", "an integer"), +); + +impl_kind_methods!(FloatKind, + f32 => ("f32", "a f32"), + f64 => ("f64", "a f64"), + Unspecified => ("float", "a float"), +); + +impl IntegerKind { + /// Converts this [`IntegerKind`] into a [`&'static str`](str) + /// You may want to use [`as_natural`](Self::as_natural) /// instead for more natural sounding error messages - pub const fn kind(&self) -> &'static str { + #[must_use] + pub const fn as_str(&self) -> &'static str { match self { - Number::Float(_) => "float", - Number::Integer(_) => "integer", - Number::u8(_) => "u8", - Number::u16(_) => "u16", - Number::u32(_) => "u32", - Number::u64(_) => "u64", - Number::usize(_) => "usize", - Number::i8(_) => "i8", - Number::i16(_) => "i16", - Number::i32(_) => "i32", - Number::i64(_) => "i64", - Number::isize(_) => "usize", - Number::f32(_) => "f32", - Number::f64(_) => "f64", + Self::Unsigned(kind) => kind.as_str(), + Self::Signed(kind) => kind.as_str(), } } - /// Returns the kind of [`Number`] as a [string slice](str) with an `a` or `an` prepended to it. + /// Returns the kind of [`Integer`] as a [string slice](str) with an `a` or `an` prepended to it. /// Used for more natural sounding error messages. - pub const fn natural_kind(&self) -> &'static str { + #[must_use] + pub const fn as_natural(&self) -> &'static str { match self { - Number::Float(_) => "a float", - Number::Integer(_) => "an integer", - Number::u8(_) => "a u8", - Number::u16(_) => "a u16", - Number::u32(_) => "a u32", - Number::u64(_) => "a u64", - Number::usize(_) => "a usize", - Number::i8(_) => "a i8", - Number::i16(_) => "a i16", - Number::i32(_) => "a i32", - Number::i64(_) => "a i64", - Number::isize(_) => "a usize", - Number::f32(_) => "a f32", - Number::f64(_) => "a f64", + Self::Unsigned(kind) => kind.as_natural(), + Self::Signed(kind) => kind.as_natural(), + } + } +} +impl Display for IntegerKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + f.write_str(self.as_natural()) + } else { + f.write_str(self.as_str()) } } } -impl Display for Number { +impl NumberKind { + /// Converts this [`NumberKind`] into a [`&'static str`](str) + /// You may want to use [`as_natural`](Self::as_natural) + /// instead for more natural sounding error messages + #[must_use] + pub const fn as_str(&self) -> &'static str { + match self { + Self::Integer(kind) => kind.as_str(), + Self::Float(kind) => kind.as_str(), + } + } + + /// Returns the kind of [`Number`] as a [string slice](str) with an `a` or `an` prepended to it. + /// Used for more natural sounding error messages. + #[must_use] + pub const fn as_natural(&self) -> &'static str { + match self { + Self::Integer(kind) => kind.as_natural(), + Self::Float(kind) => kind.as_natural(), + } + } +} +impl Display for NumberKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + f.write_str(self.as_natural()) + } else { + f.write_str(self.as_str()) + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("cannot apply unary operator `-` to type `{0}`")] +pub struct CannotNegateUnsignedInteger(pub NumberKind); + +#[derive(Debug, thiserror::Error)] +#[error("Incompatible number types; `{left}` and `{right}` are incompatible.")] +pub struct IncompatibleNumberTypes { + pub left: NumberKind, + pub right: NumberKind, +} + +#[derive(Debug, thiserror::Error)] +#[error("cannot {operator} {left} by {right}")] +pub struct InvalidBinaryOperation { + pub left: Number, + pub right: Number, + pub operator: BinaryOperator, +} + +#[derive(Debug, thiserror::Error)] +#[error("integer value `{value}` out of range for type `{ty}`")] +pub struct ValueOutOfRange { + pub value: i128, + pub ty: NumberKind, +} + +#[derive(thiserror::Error, Debug)] +pub enum NumberError { + #[error(transparent)] + InvalidBinaryOperation(#[from] InvalidBinaryOperation), + #[error(transparent)] + ValueOutOfRange(#[from] ValueOutOfRange), + #[error(transparent)] + IncompatibleNumberTypes(#[from] IncompatibleNumberTypes), + #[error(transparent)] + CannotNegateUnsignedInteger(#[from] CannotNegateUnsignedInteger), + #[error( + "cannot apply bitwise operator `{operator}` to type `{operand}`. only integers are supported." + )] + InvalidBitwiseOperation { + operator: BinaryOperator, + operand: NumberKind, + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum IntegerOperationError { + #[error(transparent)] + IncompatibleTypes(#[from] IncompatibleNumberTypes), + #[error(transparent)] + InvalidBinaryOperation(#[from] InvalidBinaryOperation), + #[error(transparent)] + ValueOutOfRange(#[from] ValueOutOfRange), +} + +impl From for NumberError { + fn from(error: IntegerOperationError) -> Self { + match error { + IntegerOperationError::IncompatibleTypes(e) => Self::IncompatibleNumberTypes(e), + IntegerOperationError::InvalidBinaryOperation(e) => Self::InvalidBinaryOperation(e), + IntegerOperationError::ValueOutOfRange(e) => Self::ValueOutOfRange(e), + } + } +} + +macro_rules! impl_unsigned_op { + ($trait:ident, $method:ident, $checked:ident, $operator:ident) => { + impl $trait for UnsignedInteger { + type Output = Result; + fn $method(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::u8(l), Self::u8(r)) => l.$checked(r).map(Self::u8), + (Self::u16(l), Self::u16(r)) => l.$checked(r).map(Self::u16), + (Self::u32(l), Self::u32(r)) => l.$checked(r).map(Self::u32), + (Self::u64(l), Self::u64(r)) => l.$checked(r).map(Self::u64), + (Self::usize(l), Self::usize(r)) => l.$checked(r).map(Self::usize), + _ => { + return Err(IncompatibleNumberTypes { + left: self.kind().into(), + right: rhs.kind().into(), + } + .into()); + } + } + .ok_or_else(|| { + InvalidBinaryOperation { + left: self.into(), + right: rhs.into(), + operator: BinaryOperator::$operator, + } + .into() + }) + } + } + }; +} + +impl_unsigned_op!(Add, add, checked_add, Add); +impl_unsigned_op!(Sub, sub, checked_sub, Sub); +impl_unsigned_op!(Mul, mul, checked_mul, Mul); +impl_unsigned_op!(Div, div, checked_div, Div); +impl_unsigned_op!(Rem, rem, checked_rem, Mod); + +macro_rules! impl_unsigned_bitwise_op { + ($trait:ident, $method:ident) => { + impl $trait for UnsignedInteger { + type Output = Result; + fn $method(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::u8(l), Self::u8(r)) => Ok(Self::u8(l.$method(r))), + (Self::u16(l), Self::u16(r)) => Ok(Self::u16(l.$method(r))), + (Self::u32(l), Self::u32(r)) => Ok(Self::u32(l.$method(r))), + (Self::u64(l), Self::u64(r)) => Ok(Self::u64(l.$method(r))), + (Self::usize(l), Self::usize(r)) => Ok(Self::usize(l.$method(r))), + _ => Err(IncompatibleNumberTypes { + left: self.kind().into(), + right: rhs.kind().into(), + }), + } + } + } + }; +} + +impl_unsigned_bitwise_op!(BitAnd, bitand); +impl_unsigned_bitwise_op!(BitOr, bitor); +impl_unsigned_bitwise_op!(BitXor, bitxor); + +impl Not for UnsignedInteger { + type Output = Self; + fn not(self) -> Self::Output { + match self { + Self::u8(v) => Self::u8(!v), + Self::u16(v) => Self::u16(!v), + Self::u32(v) => Self::u32(!v), + Self::u64(v) => Self::u64(!v), + Self::usize(v) => Self::usize(!v), + } + } +} + +macro_rules! impl_signed_op { + ($trait:ident, $method:ident, $checked:ident, $operator:ident) => { + impl $trait for SignedInteger { + type Output = Result; + fn $method(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::i8(l), Self::i8(r)) => l.$checked(r).map(Self::i8), + (Self::i16(l), Self::i16(r)) => l.$checked(r).map(Self::i16), + (Self::i32(l), Self::i32(r)) => l.$checked(r).map(Self::i32), + (Self::i64(l), Self::i64(r)) => l.$checked(r).map(Self::i64), + (Self::isize(l), Self::isize(r)) => l.$checked(r).map(Self::isize), + (Self::Unspecified(l), Self::Unspecified(r)) => { + l.$checked(r).map(Self::Unspecified) + } + _ => { + return Err(IncompatibleNumberTypes { + left: self.kind().into(), + right: rhs.kind().into(), + } + .into()); + } + } + .ok_or_else(|| { + InvalidBinaryOperation { + left: self.into(), + right: rhs.into(), + operator: BinaryOperator::$operator, + } + .into() + }) + } + } + }; +} + +impl_signed_op!(Add, add, checked_add, Add); +impl_signed_op!(Sub, sub, checked_sub, Sub); +impl_signed_op!(Mul, mul, checked_mul, Mul); +impl_signed_op!(Div, div, checked_div, Div); +impl_signed_op!(Rem, rem, checked_rem, Mod); + +macro_rules! impl_signed_bitwise_op { + ($trait:ident, $method:ident) => { + impl $trait for SignedInteger { + type Output = Result; + fn $method(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::i8(l), Self::i8(r)) => Ok(Self::i8(l.$method(r))), + (Self::i16(l), Self::i16(r)) => Ok(Self::i16(l.$method(r))), + (Self::i32(l), Self::i32(r)) => Ok(Self::i32(l.$method(r))), + (Self::i64(l), Self::i64(r)) => Ok(Self::i64(l.$method(r))), + (Self::isize(l), Self::isize(r)) => Ok(Self::isize(l.$method(r))), + (Self::Unspecified(l), Self::Unspecified(r)) => { + Ok(Self::Unspecified(l.$method(r))) + } + _ => Err(IncompatibleNumberTypes { + left: self.kind().into(), + right: rhs.kind().into(), + }), + } + } + } + }; +} + +impl_signed_bitwise_op!(BitAnd, bitand); +impl_signed_bitwise_op!(BitOr, bitor); +impl_signed_bitwise_op!(BitXor, bitxor); + +impl Not for SignedInteger { + type Output = Self; + fn not(self) -> Self::Output { + match self { + Self::i8(v) => Self::i8(!v), + Self::i16(v) => Self::i16(!v), + Self::i32(v) => Self::i32(!v), + Self::i64(v) => Self::i64(!v), + Self::isize(v) => Self::isize(!v), + Self::Unspecified(v) => Self::Unspecified(!v), + } + } +} + +impl Neg for SignedInteger { + type Output = Self; + fn neg(self) -> Self::Output { + match self { + Self::i8(v) => Self::i8(-v), + Self::i16(v) => Self::i16(-v), + Self::i32(v) => Self::i32(-v), + Self::i64(v) => Self::i64(-v), + Self::isize(v) => Self::isize(-v), + Self::Unspecified(v) => Self::Unspecified(-v), + } + } +} + +macro_rules! impl_integer_op { + ($trait:ident, $method:ident, $operator:ident) => { + impl $trait for Integer { + type Output = Result; + fn $method(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::Signed(SignedInteger::Unspecified(l)), Self::Unsigned(r)) => { + let l_converted = + match r { + UnsignedInteger::u8(_) => u8::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::u8(v))), + UnsignedInteger::u16(_) => u16::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::u16(v))), + UnsignedInteger::u32(_) => u32::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::u32(v))), + UnsignedInteger::u64(_) => u64::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::u64(v))), + UnsignedInteger::usize(_) => usize::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::usize(v))), + } + .map_err(|_| ValueOutOfRange { + value: l, + ty: rhs.kind().into(), + })?; + l_converted.$method(rhs) + } + (Self::Unsigned(l), Self::Signed(SignedInteger::Unspecified(r))) => { + let r_converted = + match l { + UnsignedInteger::u8(_) => u8::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::u8(v))), + UnsignedInteger::u16(_) => u16::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::u16(v))), + UnsignedInteger::u32(_) => u32::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::u32(v))), + UnsignedInteger::u64(_) => u64::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::u64(v))), + UnsignedInteger::usize(_) => usize::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::usize(v))), + } + .map_err(|_| ValueOutOfRange { + value: r, + ty: l.kind().into(), + })?; + self.$method(r_converted) + } + (Self::Signed(SignedInteger::Unspecified(l)), Self::Signed(r)) => match r { + SignedInteger::Unspecified(r_val) => pastey::paste! { + l.[](r_val) + } + .map(|v| Self::Signed(SignedInteger::Unspecified(v))) + .ok_or_else(|| { + InvalidBinaryOperation { + left: self.into(), + right: rhs.into(), + operator: BinaryOperator::$operator, + } + .into() + }), + _ => { + let l_converted = + match r { + SignedInteger::i8(_) => i8::try_from(l) + .map(|v| Integer::Signed(SignedInteger::i8(v))), + SignedInteger::i16(_) => i16::try_from(l) + .map(|v| Integer::Signed(SignedInteger::i16(v))), + SignedInteger::i32(_) => i32::try_from(l) + .map(|v| Integer::Signed(SignedInteger::i32(v))), + SignedInteger::i64(_) => i64::try_from(l) + .map(|v| Integer::Signed(SignedInteger::i64(v))), + SignedInteger::isize(_) => isize::try_from(l) + .map(|v| Integer::Signed(SignedInteger::isize(v))), + SignedInteger::Unspecified(_) => unreachable!(), + } + .map_err(|_| ValueOutOfRange { + value: l, + ty: r.kind().into(), + })?; + l_converted.$method(rhs) + } + }, + (Self::Signed(l), Self::Signed(SignedInteger::Unspecified(r))) => match l { + SignedInteger::Unspecified(_) => unreachable!("Caught by arm above"), + _ => { + let r_converted = + match l { + SignedInteger::i8(_) => i8::try_from(r) + .map(|v| Integer::Signed(SignedInteger::i8(v))), + SignedInteger::i16(_) => i16::try_from(r) + .map(|v| Integer::Signed(SignedInteger::i16(v))), + SignedInteger::i32(_) => i32::try_from(r) + .map(|v| Integer::Signed(SignedInteger::i32(v))), + SignedInteger::i64(_) => i64::try_from(r) + .map(|v| Integer::Signed(SignedInteger::i64(v))), + SignedInteger::isize(_) => isize::try_from(r) + .map(|v| Integer::Signed(SignedInteger::isize(v))), + SignedInteger::Unspecified(_) => unreachable!(), + } + .map_err(|_| ValueOutOfRange { + value: r, + ty: l.kind().into(), + })?; + self.$method(r_converted) + } + }, + (Self::Unsigned(l), Self::Unsigned(r)) => Ok(Self::Unsigned(l.$method(r)?)), + (Self::Signed(l), Self::Signed(r)) => Ok(Self::Signed(l.$method(r)?)), + _ => Err(IncompatibleNumberTypes { + left: self.kind().into(), + right: rhs.kind().into(), + } + .into()), + } + } + } + }; +} + +impl_integer_op!(Add, add, Add); +impl_integer_op!(Sub, sub, Sub); +impl_integer_op!(Mul, mul, Mul); +impl_integer_op!(Div, div, Div); +impl_integer_op!(Rem, rem, Mod); + +macro_rules! impl_integer_bitwise_op { + ($trait:ident, $method:ident) => { + impl $trait for Integer { + type Output = Result; + fn $method(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::Signed(SignedInteger::Unspecified(l)), Self::Unsigned(r)) => { + let l_converted = + match r { + UnsignedInteger::u8(_) => u8::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::u8(v))), + UnsignedInteger::u16(_) => u16::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::u16(v))), + UnsignedInteger::u32(_) => u32::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::u32(v))), + UnsignedInteger::u64(_) => u64::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::u64(v))), + UnsignedInteger::usize(_) => usize::try_from(l) + .map(|v| Integer::Unsigned(UnsignedInteger::usize(v))), + } + .map_err(|_| ValueOutOfRange { + value: l, + ty: rhs.kind().into(), + })?; + l_converted.$method(rhs) + } + (Self::Unsigned(l), Self::Signed(SignedInteger::Unspecified(r))) => { + let r_converted = + match l { + UnsignedInteger::u8(_) => u8::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::u8(v))), + UnsignedInteger::u16(_) => u16::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::u16(v))), + UnsignedInteger::u32(_) => u32::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::u32(v))), + UnsignedInteger::u64(_) => u64::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::u64(v))), + UnsignedInteger::usize(_) => usize::try_from(r) + .map(|v| Integer::Unsigned(UnsignedInteger::usize(v))), + } + .map_err(|_| ValueOutOfRange { + value: r, + ty: l.kind().into(), + })?; + self.$method(r_converted) + } + (Self::Signed(SignedInteger::Unspecified(l)), Self::Signed(r)) => match r { + SignedInteger::Unspecified(r_val) => { + Ok(Self::Signed(SignedInteger::Unspecified(l.$method(r_val)))) + } + _ => { + let l_converted = + match r { + SignedInteger::i8(_) => i8::try_from(l) + .map(|v| Integer::Signed(SignedInteger::i8(v))), + SignedInteger::i16(_) => i16::try_from(l) + .map(|v| Integer::Signed(SignedInteger::i16(v))), + SignedInteger::i32(_) => i32::try_from(l) + .map(|v| Integer::Signed(SignedInteger::i32(v))), + SignedInteger::i64(_) => i64::try_from(l) + .map(|v| Integer::Signed(SignedInteger::i64(v))), + SignedInteger::isize(_) => isize::try_from(l) + .map(|v| Integer::Signed(SignedInteger::isize(v))), + SignedInteger::Unspecified(_) => unreachable!(), + } + .map_err(|_| ValueOutOfRange { + value: l, + ty: r.kind().into(), + })?; + l_converted.$method(rhs) + } + }, + (Self::Signed(l), Self::Signed(SignedInteger::Unspecified(r))) => match l { + SignedInteger::Unspecified(_) => unreachable!("Caught by arm above"), + _ => { + let r_converted = + match l { + SignedInteger::i8(_) => i8::try_from(r) + .map(|v| Integer::Signed(SignedInteger::i8(v))), + SignedInteger::i16(_) => i16::try_from(r) + .map(|v| Integer::Signed(SignedInteger::i16(v))), + SignedInteger::i32(_) => i32::try_from(r) + .map(|v| Integer::Signed(SignedInteger::i32(v))), + SignedInteger::i64(_) => i64::try_from(r) + .map(|v| Integer::Signed(SignedInteger::i64(v))), + SignedInteger::isize(_) => isize::try_from(r) + .map(|v| Integer::Signed(SignedInteger::isize(v))), + SignedInteger::Unspecified(_) => unreachable!(), + } + .map_err(|_| ValueOutOfRange { + value: r, + ty: l.kind().into(), + })?; + self.$method(r_converted) + } + }, + (Self::Unsigned(l), Self::Unsigned(r)) => Ok(Self::Unsigned(l.$method(r)?)), + (Self::Signed(l), Self::Signed(r)) => Ok(Self::Signed(l.$method(r)?)), + _ => Err(IncompatibleNumberTypes { + left: self.kind().into(), + right: rhs.kind().into(), + } + .into()), + } + } + } + }; +} + +impl_integer_bitwise_op!(BitAnd, bitand); +impl_integer_bitwise_op!(BitOr, bitor); +impl_integer_bitwise_op!(BitXor, bitxor); + +impl Not for Integer { + type Output = Self; + fn not(self) -> Self::Output { + match self { + Self::Unsigned(v) => Self::Unsigned(!v), + Self::Signed(v) => Self::Signed(!v), + } + } +} + +impl Neg for Integer { + type Output = Result; + fn neg(self) -> Self::Output { + match self { + Self::Unsigned(_) => Err(CannotNegateUnsignedInteger(self.kind().into())), + Self::Signed(v) => Ok(Self::Signed(-v)), + } + } +} + +macro_rules! impl_float_op { + ($trait:ident, $method:ident) => { + impl $trait for Float { + type Output = Result; + fn $method(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::f32(l), Self::f32(r)) => Ok(Self::f32(l.$method(r))), + (Self::f64(l), Self::f64(r)) => Ok(Self::f64(l.$method(r))), + (Self::Unspecified(l), Self::Unspecified(r)) => { + Ok(Self::Unspecified(l.$method(r))) + } + (Self::Unspecified(l), Self::f32(r)) => Ok(Self::f32((l as f32).$method(r))), + (Self::Unspecified(l), Self::f64(r)) => Ok(Self::f64(l.$method(r))), + (Self::f32(l), Self::Unspecified(r)) => Ok(Self::f32(l.$method(r as f32))), + (Self::f64(l), Self::Unspecified(r)) => Ok(Self::f64(l.$method(r))), + _ => Err(IncompatibleNumberTypes { + left: self.kind().into(), + right: rhs.kind().into(), + }), + } + } + } + }; +} + +impl_float_op!(Add, add); +impl_float_op!(Sub, sub); +impl_float_op!(Mul, mul); +impl_float_op!(Div, div); +impl_float_op!(Rem, rem); + +impl Neg for Float { + type Output = Self; + fn neg(self) -> Self::Output { match self { - Number::Float(number) => write!(f, "{number} (float)"), - Number::Integer(number) => write!(f, "{number} (integer)"), - Number::u8(number) => write!(f, "{number} (u8)"), - Number::u16(number) => write!(f, "{number} (u16)"), - Number::u32(number) => write!(f, "{number} (u32)"), - Number::u64(number) => write!(f, "{number} (u64)"), - Number::usize(number) => write!(f, "{number} (usize)"), - Number::i8(number) => write!(f, "{number} (i8)"), - Number::i16(number) => write!(f, "{number} (i16)"), - Number::i32(number) => write!(f, "{number} (i32)"), - Number::i64(number) => write!(f, "{number} (i64)"), - Number::isize(number) => write!(f, "{number} (isize)"), - Number::f32(number) => write!(f, "{number} (f32)"), - Number::f64(number) => write!(f, "{number} (f64)"), - } - } -} - -macro_rules! impl_op { - ($fn:ident, $op:tt, $checked:ident)=> { - impl Number { - #[doc = concat!("Performs the `", stringify!($op), "` calculation.")] - pub fn $fn(left: Number, right: Number, span: Span) -> Result { - let op_err = || EvalError::InvalidOperation { - left, - right, - operation: stringify!($fn), - span: span.clone(), - }; - match (left, right) { - (Number::u8(left), Number::u8(right)) => Ok(Number::u8(left.$checked(right).ok_or_else(op_err)?)), - (Number::u16(left), Number::u16(right)) => Ok(Number::u16(left.$checked(right).ok_or_else(op_err)?)), - (Number::u32(left), Number::u32(right)) => Ok(Number::u32(left.$checked(right).ok_or_else(op_err)?)), - (Number::u64(left), Number::u64(right)) => Ok(Number::u64(left.$checked(right).ok_or_else(op_err)?)), - (Number::usize(left), Number::usize(right)) => Ok(Number::usize(left.$checked(right).ok_or_else(op_err)?)), - (Number::i8(left), Number::i8(right)) => Ok(Number::i8(left.$checked(right).ok_or_else(op_err)?)), - (Number::i16(left), Number::i16(right)) => Ok(Number::i16(left.$checked(right).ok_or_else(op_err)?)), - (Number::i32(left), Number::i32(right)) => Ok(Number::i32(left.$checked(right).ok_or_else(op_err)?)), - (Number::i64(left), Number::i64(right)) => Ok(Number::i64(left.$checked(right).ok_or_else(op_err)?)), - (Number::isize(left), Number::isize(right)) => Ok(Number::isize(left.$checked(right).ok_or_else(op_err)?)), - (Number::f32(left), Number::f32(right)) => Ok(Number::f32(left $op right)), - (Number::f64(left), Number::f64(right)) => Ok(Number::f64(left $op right)), - - (Number::Integer(left), Number::u8(right)) => Ok(Number::u8((left as u8).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::u16(right)) => Ok(Number::u16((left as u16).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::u32(right)) => Ok(Number::u32((left as u32).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::u64(right)) => Ok(Number::u64((left as u64).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::usize(right)) => Ok(Number::usize((left as usize).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::i8(right)) => Ok(Number::i8((left as i8).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::i16(right)) => Ok(Number::i16((left as i16).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::i32(right)) => Ok(Number::i32((left as i32).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::i64(right)) => Ok(Number::i64((left as i64).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::isize(right)) => Ok(Number::isize((left as isize).$checked(right).ok_or_else(op_err)?)), - (Number::Integer(left), Number::Integer(right)) => Ok(Number::Integer(left.$checked(right).ok_or_else(op_err)?)), - (Number::u8(left), Number::Integer(right)) => Ok(Number::u8(left.$checked(right as u8).ok_or_else(op_err)?)), - (Number::u16(left), Number::Integer(right)) => Ok(Number::u16(left.$checked(right as u16).ok_or_else(op_err)?)), - (Number::u32(left), Number::Integer(right)) => Ok(Number::u32(left.$checked(right as u32).ok_or_else(op_err)?)), - (Number::u64(left), Number::Integer(right)) => Ok(Number::u64(left.$checked(right as u64).ok_or_else(op_err)?)), - (Number::usize(left), Number::Integer(right)) => Ok(Number::usize(left.$checked(right as usize).ok_or_else(op_err)?)), - (Number::i8(left), Number::Integer(right)) => Ok(Number::i8(left.$checked(right as i8).ok_or_else(op_err)?)), - (Number::i16(left), Number::Integer(right)) => Ok(Number::i16(left.$checked(right as i16).ok_or_else(op_err)?)), - (Number::i32(left), Number::Integer(right)) => Ok(Number::i32(left.$checked(right as i32).ok_or_else(op_err)?)), - (Number::i64(left), Number::Integer(right)) => Ok(Number::i64(left.$checked(right as i64).ok_or_else(op_err)?)), - (Number::isize(left), Number::Integer(right)) => Ok(Number::isize(left.$checked(right as isize).ok_or_else(op_err)?)), - - (Number::Float(left), Number::f32(right)) => Ok(Number::f32(left as f32 $op right)), - (Number::Float(left), Number::f64(right)) => Ok(Number::f64(left as f64 $op right)), - (Number::Float(left), Number::Float(right)) => Ok(Number::Float(left $op right)), - (Number::f32(left), Number::Float(right)) => Ok(Number::f32(left $op right as f32)), - (Number::f64(left), Number::Float(right)) => Ok(Number::f64(left $op right as f64)), - _ => Err(EvalError::IncompatibleNumberTypes { - left: left.natural_kind(), - right: right.natural_kind(), - span - }) + Self::f32(v) => Self::f32(-v), + Self::f64(v) => Self::f64(-v), + Self::Unspecified(v) => Self::Unspecified(-v), + } + } +} + +macro_rules! impl_number_op { + ($trait:ident, $method:ident, $operator:ident) => { + impl $trait for Number { + type Output = Result; + fn $method(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::Integer(l), Self::Integer(r)) => Ok(Self::Integer(l.$method(r)?)), + (Self::Float(l), Self::Float(r)) => Ok(Self::Float(l.$method(r)?)), + _ => Err(IncompatibleNumberTypes { + left: self.kind(), + right: rhs.kind(), + } + .into()), } } } }; } -impl_op!(add, +, checked_add); -impl_op!(sub, -, checked_sub); -impl_op!(mul, *, checked_mul); -impl_op!(div, /, checked_div); -impl_op!(rem, %, checked_rem); +impl_number_op!(Add, add, Add); +impl_number_op!(Sub, sub, Sub); +impl_number_op!(Mul, mul, Mul); +impl_number_op!(Div, div, Div); +impl_number_op!(Rem, rem, Mod); + +macro_rules! impl_number_bitwise_op { + ($trait:ident, $method:ident, $operator:ident) => { + impl $trait for Number { + type Output = Result; + fn $method(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::Integer(l), Self::Integer(r)) => Ok(Self::Integer(l.$method(r)?)), + _ => Err(NumberError::InvalidBitwiseOperation { + operator: BinaryOperator::$operator, + operand: self.kind(), + }), + } + } + } + }; +} + +impl_number_bitwise_op!(BitAnd, bitand, And); +impl_number_bitwise_op!(BitOr, bitor, Or); +impl_number_bitwise_op!(BitXor, bitxor, Xor); + +impl Not for Number { + type Output = Result; + fn not(self) -> Self::Output { + match self { + Self::Integer(v) => Ok(Self::Integer(!v)), + _ => Err(NumberError::InvalidBitwiseOperation { + operator: crate::builtin_parser::parser::BinaryOperator::And, // Dummy + operand: self.kind(), + }), + } + } +} + +impl Neg for Number { + type Output = Result; + fn neg(self) -> Self::Output { + match self { + Self::Integer(v) => Ok(Self::Integer( + v.neg().map_err(NumberError::CannotNegateUnsignedInteger)?, + )), + Self::Float(v) => Ok(Self::Float(-v)), + } + } +} macro_rules! impl_op_spanned { ($trait:ident, $method:ident) => { impl $trait for Spanned { - type Output = Result; + type Output = Result>; fn $method(self, rhs: Self) -> Self::Output { - let span = self.span.join(rhs.span); - - Number::$method(self.value, rhs.value, span) + let span = self.span.join(&rhs.span); + (self.value.$method(rhs.value)).diagnosed(span) } } }; @@ -230,60 +967,140 @@ impl_op_spanned!(Sub, sub); impl_op_spanned!(Mul, mul); impl_op_spanned!(Rem, rem); -impl Number { - /// Performs the unary `-` operation. - pub fn neg(self, span: Span) -> Result { - match self { - Number::u8(_) => Err(EvalError::CannotNegateUnsignedInteger(Spanned { - span, - value: self, - })), - Number::u16(_) => Err(EvalError::CannotNegateUnsignedInteger(Spanned { - span, - value: self, - })), - Number::u32(_) => Err(EvalError::CannotNegateUnsignedInteger(Spanned { - span, - value: self, - })), - Number::u64(_) => Err(EvalError::CannotNegateUnsignedInteger(Spanned { - span, - value: self, - })), - Number::usize(_) => Err(EvalError::CannotNegateUnsignedInteger(Spanned { - span, - value: self, - })), - Number::i8(number) => Ok(Number::i8(-number)), - Number::i16(number) => Ok(Number::i16(-number)), - Number::i32(number) => Ok(Number::i32(-number)), - Number::i64(number) => Ok(Number::i64(-number)), - Number::isize(number) => Ok(Number::isize(-number)), - Number::f32(number) => Ok(Number::f32(-number)), - Number::f64(number) => Ok(Number::f64(-number)), - Number::Float(number) => Ok(Number::Float(-number)), - Number::Integer(number) => Ok(Number::Integer(-number)), - } - } -} - -macro_rules! from_primitive { - ($primitive:ident) => { - impl From<$primitive> for Number { - fn from(value: $primitive) -> Self { - Number::$primitive(value) +impl_op_spanned!(BitAnd, bitand); +impl_op_spanned!(BitOr, bitor); +impl_op_spanned!(BitXor, bitxor); + +macro_rules! from_primitive_integer { + ($($primitive:ident => $group:ident, $enum:ident, $variant:ident),+) => { + $( + impl From<$primitive> for Number { + fn from(value: $primitive) -> Self { + Number::Integer(Integer::$group($enum::$variant(value))) + } } - } + impl From<$primitive> for Integer { + fn from(value: $primitive) -> Self { + Integer::$group($enum::$variant(value)) + } + } + impl From<$primitive> for $enum { + fn from(value: $primitive) -> Self { + $enum::$variant(value) + } + } + )+ + }; +} + +from_primitive_integer!( + u8 => Unsigned, UnsignedInteger, u8, + u16 => Unsigned, UnsignedInteger, u16, + u32 => Unsigned, UnsignedInteger, u32, + u64 => Unsigned, UnsignedInteger, u64, + usize => Unsigned, UnsignedInteger, usize, + i8 => Signed, SignedInteger, i8, + i16 => Signed, SignedInteger, i16, + i32 => Signed, SignedInteger, i32, + i64 => Signed, SignedInteger, i64, + isize => Signed, SignedInteger, isize +); + +macro_rules! from_primitive_float { + ($($primitive:ident => $enum:ident, $variant:ident),+) => { + $( + impl From<$primitive> for Number { + fn from(value: $primitive) -> Self { + Number::Float($enum::$variant(value)) + } + } + impl From<$primitive> for Float { + fn from(value: $primitive) -> Self { + Float::$variant(value) + } + } + )+ }; } -from_primitive!(u8); -from_primitive!(u16); -from_primitive!(u32); -from_primitive!(u64); -from_primitive!(i8); -from_primitive!(i16); -from_primitive!(i32); -from_primitive!(i64); -from_primitive!(f32); -from_primitive!(f64); +from_primitive_float!( + f32 => Float, f32, + f64 => Float, f64 +); + +impl From for Number { + fn from(value: Integer) -> Self { + Number::Integer(value) + } +} +impl From for Number { + fn from(value: UnsignedInteger) -> Self { + Number::Integer(Integer::Unsigned(value)) + } +} +impl From for Number { + fn from(value: SignedInteger) -> Self { + Number::Integer(Integer::Signed(value)) + } +} +impl From for Integer { + fn from(value: UnsignedInteger) -> Self { + Integer::Unsigned(value) + } +} +impl From for Integer { + fn from(value: SignedInteger) -> Self { + Integer::Signed(value) + } +} +impl From for Number { + fn from(value: Float) -> Self { + Number::Float(value) + } +} + +impl From for NumberKind { + fn from(kind: IntegerKind) -> Self { + NumberKind::Integer(kind) + } +} +impl From for IntegerKind { + fn from(kind: UnsignedIntegerKind) -> Self { + IntegerKind::Unsigned(kind) + } +} +impl From for IntegerKind { + fn from(kind: SignedIntegerKind) -> Self { + IntegerKind::Signed(kind) + } +} +impl From for NumberKind { + fn from(kind: UnsignedIntegerKind) -> Self { + NumberKind::Integer(IntegerKind::Unsigned(kind)) + } +} +impl From for NumberKind { + fn from(kind: SignedIntegerKind) -> Self { + NumberKind::Integer(IntegerKind::Signed(kind)) + } +} +impl From for NumberKind { + fn from(kind: FloatKind) -> Self { + NumberKind::Float(kind) + } +} + +impl From> for Diagnostic { + fn from(diagnostic: Diagnostic) -> Self { + Diagnostic { + spans: diagnostic.spans, + error: Box::new(EvalError::Number(*diagnostic.error)), + } + } +} + +impl From for EvalError { + fn from(error: CannotNegateUnsignedInteger) -> Self { + EvalError::Number(NumberError::CannotNegateUnsignedInteger(error)) + } +} diff --git a/src/builtin_parser/parser.rs b/src/builtin_parser/parser.rs index a248b40..44efa86 100644 --- a/src/builtin_parser/parser.rs +++ b/src/builtin_parser/parser.rs @@ -1,282 +1,51 @@ //! Generates an abstract syntax tree from a list of tokens. +mod ast; +mod error; +#[cfg(test)] +mod tests; + +pub use ast::*; +pub use error::*; + use logos::Span; use std::collections::HashMap; use std::num::IntErrorKind; -use crate::command::{CommandHint, CommandHintColor}; +use crate::builtin_parser::number::{ + Float, NumberKind, SignedInteger, SignedIntegerKind, UnsignedInteger, UnsignedIntegerKind, +}; use super::lexer::{FailedToLexCharacter, Token, TokenStream}; use super::number::Number; use super::runner::environment::Function; -use super::{Environment, SpanExtension, Spanned}; - -/// An [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree). -/// -/// This type represents a list of expressions, which is what makes up a command. -pub type Ast = Vec>; +use super::{Diagnostic, Environment, SpanExtension, Spanned}; macro_rules! expect { ($tokens:ident, $($token:tt)+) => { match $tokens.next() { Some(Ok($($token)+)) => ($($token)+) , Some(Ok(token)) => { - return Err(ParseError::ExpectedTokenButGot { + return Err($tokens.span().wrap(ParseError::ExpectedTokenButGot { expected: $($token)+, got: token, - span: $tokens.span(), - }) + }).into()) } Some(Err(FailedToLexCharacter)) => { - return Err(ParseError::FailedToLexCharacters($tokens.span().wrap($tokens.slice().to_string()))) + return Err($tokens.span().diagnose(ParseError::FailedToLexCharacters($tokens.slice().to_owned()))) } - None => return Err(ParseError::ExpectedMoreTokens($tokens.span())), + None => return Err($tokens.span().diagnose(ParseError::ExpectedMoreTokens)), } }; } -/// A type that represents an expression. -#[derive(Debug, Clone)] -pub enum Expression { - // Primitives - None, - Boolean(bool), - Number(Number), - Variable(String), - String(String), - Borrow(Box>), - Dereference(Box>), - Object(HashMap>), - StructObject { - name: String, - map: HashMap>, - }, - Tuple(Vec>), - StructTuple { - name: String, - tuple: Vec>, - }, - - // Expressions - BinaryOp { - left: Box>, - operator: Operator, - right: Box>, - }, - UnaryOp(Box>), - Member { - left: Box>, - right: Spanned, - }, - - // Statement-like - VarAssign { - name: Box>, - value: Box>, - }, - Function { - name: String, - arguments: Vec>, - }, - ForLoop { - index_name: String, - loop_count: u64, - block: Ast, - }, -} - -/// A singular element access within a [`Expression::Member`]. -/// -/// Based on `bevy_reflect`'s `Access`. -#[derive(Debug, Clone)] -pub enum Access { - /// A name-based field access on a struct. - Field(String), - /// An index-based access on a tuple. - TupleIndex(usize), - // /// An index-based access on a list. - // ListIndex(usize), -} -pub enum AccessKind { - Field, - TupleIndex, -} -impl Access { - pub const fn kind(&self) -> AccessKind { - match self { - Access::Field(_) => AccessKind::Field, - Access::TupleIndex(_) => AccessKind::TupleIndex, - } - } - /// Returns the kind of [`Access`] as a [string slice](str) with an `a` or `an` prepended to it. - /// Used for more natural sounding error messages. - pub const fn natural_kind(&self) -> &'static str { - self.kind().natural() - } -} - -impl AccessKind { - /// Returns the kind of [`Access`] as a [string slice](str) with an `a` or `an` prepended to it. - /// Used for more natural sounding error messages. - pub const fn natural(&self) -> &'static str { - match self { - AccessKind::Field => "a field", - AccessKind::TupleIndex => "a tuple", - } - } -} - -/// Get the access if its of a certain type, if not, return a [`EvalError`](super::runner::error::EvalError). -/// -/// For examples, take a look at existing uses in the code. -macro_rules! access_unwrap { - ($expected:literal, $($variant:ident($variant_inner:ident))|+ = $val:expr => $block:block) => {{ - let val = $val; - if let $(Access::$variant($variant_inner))|+ = val.value $block else { - use $crate::builtin_parser::parser::AccessKind; - use $crate::builtin_parser::runner::error::EvalError; - - // We have to put this in a `const` first to avoid a - // `temporary value dropped while borrowed` error. - const EXPECTED_ACCESS: &[&str] = &[$(AccessKind::$variant.natural()),+]; - Err(EvalError::IncorrectAccessOperation { - span: val.span, - expected_access: EXPECTED_ACCESS, - expected_type: $expected, - got: val.value, - })? - } - }}; -} -pub(crate) use access_unwrap; - -impl Expression { - pub const fn kind(&self) -> &'static str { - match self { - Expression::None => "nothing", - Expression::Boolean(..) => "a boolean", - Expression::Number(..) => "a number", - Expression::Variable(..) => "a variable name", - Expression::String(..) => "a string", - Expression::Borrow(..) => "a borrow", - Expression::Dereference(..) => "a dereference", - Expression::Object(..) => "an object", - Expression::StructObject { .. } => "a struct object", - Expression::Tuple(..) => "a tuple", - Expression::StructTuple { .. } => "a struct tuple", - - Expression::BinaryOp { .. } => "a binary operation", - Expression::UnaryOp(..) => "a unary operation", - Expression::Member { .. } => "a member expression", - Expression::VarAssign { .. } => "a variable assignment", - Expression::Function { .. } => "a function call", - Expression::ForLoop { .. } => "a for loop", - } - } -} - -#[derive(Debug, Clone)] -pub enum Operator { - Add, - Sub, - Mul, - Div, - Mod, -} - -#[derive(Debug)] -pub enum ParseError { - FailedToLexCharacters(Spanned), - ExpectedMoreTokens(Span), - ExpectedTokenButGot { - expected: Token, - got: Token, - span: Span, - }, - ExpectedEndline(Spanned), - ExpectedLiteral(Spanned), - InvalidSuffixForFloat(Spanned), - NegativeIntOverflow { - span: Span, - number: String, - number_kind: &'static str, - }, - PositiveIntOverflow { - span: Span, - number: String, - number_kind: &'static str, - }, - ExpectedObjectContinuation(Spanned>>), - ExpectedIndexer { - got: Token, - span: Span, - }, - UnsupportedLoop { - ty: &'static str, - span: Span, - }, -} - -impl ParseError { - pub fn span(&self) -> Span { - use ParseError as E; - - match self { - E::FailedToLexCharacters(Spanned { span, value: _ }) => span, - E::ExpectedMoreTokens(span) => span, - E::ExpectedTokenButGot { span, .. } => span, - E::ExpectedEndline(Spanned { span, value: _ }) => span, - E::ExpectedLiteral(Spanned { span, value: _ }) => span, - E::InvalidSuffixForFloat(Spanned { span, value: _ }) => span, - E::PositiveIntOverflow { span, .. } => span, - E::NegativeIntOverflow { span, .. } => span, - E::ExpectedObjectContinuation(Spanned { span, value: _ }) => span, - E::ExpectedIndexer { got: _, span } => span, - E::UnsupportedLoop { ty: _, span } => span, - } - .clone() - } - - pub fn hint(&self) -> CommandHint { - CommandHint { - color: CommandHintColor::Error, - span: self.span(), - description: self.to_string().into(), - } - } -} - -impl std::fmt::Display for ParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use ParseError as E; - - match self { - E::FailedToLexCharacters(Spanned { span: _, value }) => write!(f, "Invalid character(s) \"{value}\" (Did you mean to use a string?)"), - E::ExpectedMoreTokens(_) => write!(f, "Expected more tokens, got nothing."), - E::ExpectedTokenButGot { - expected, - got, - span: _, - } => write!(f, "Expected token {expected:?}, got token {got:?} instead."), - E::ExpectedEndline(_) => write!(f, "Expected a semicolon or endline after a complete statement, but got more tokens than expected."), - E::ExpectedLiteral(Spanned { span: _, value }) => write!(f, "Expected a literal token, got {value:?} which is not a valid literal."), - E::InvalidSuffixForFloat(Spanned { span: _, value: suffix }) => write!(f, r#""{suffix}" is an invalid suffix for a float. The only valid suffixes are "f32" and "f64"."#), - E::NegativeIntOverflow { span: _, number, number_kind } => write!(f, "{number} cannot be represented as a {number_kind} as it is too small."), - E::PositiveIntOverflow { span: _, number, number_kind } => write!(f, "{number} cannot be represented as a {number_kind} as it is too large."), - E::ExpectedObjectContinuation(Spanned { span: _, value: got }) => write!(f, "Expected a continuation to the object declaration (such as a comma or a closing bracket), but got {got:?} instead."), - E::ExpectedIndexer { got, span: _ } => write!(f, "Expected an identifier or integer when accessing member of variable, got {got:?} instead."), - E::UnsupportedLoop { ty, span : _} => write!(f, "{ty} loops are not yet supported. See issue #8.") - } - } -} -impl std::error::Error for ParseError {} - const FLOAT_PARSE_EXPECT_REASON: &str = "Float parsing errors are handled by the lexer, and floats cannot overflow."; -const NUMBER_TYPE_WILDCARD_UNREACHABLE_REASON: &str = - "Lexer guarantees `NumberType`'s slice to be included one of the match arms."; -pub fn parse(tokens: &mut TokenStream, environment: &Environment) -> Result { +pub fn parse( + tokens: &mut TokenStream, + environment: &Environment, +) -> Result> { let mut ast = Vec::new(); while tokens.peek().is_some() { @@ -284,11 +53,17 @@ pub fn parse(tokens: &mut TokenStream, environment: &Environment) -> Result continue, - Some(Ok(token)) => return Err(ParseError::ExpectedEndline(tokens.span().wrap(token))), + Some(Ok(token)) => { + return Err(tokens + .span() + .wrap(ParseError::ExpectedEndline(token)) + .into()); + } Some(Err(FailedToLexCharacter)) => { - return Err(ParseError::FailedToLexCharacters( - tokens.span().wrap(tokens.slice().to_string()), - )) + return Err(tokens + .span() + .wrap(ParseError::FailedToLexCharacters(tokens.slice().to_owned())) + .into()); } None => break, } @@ -300,20 +75,37 @@ pub fn parse(tokens: &mut TokenStream, environment: &Environment) -> Result Result, ParseError> { +) -> Result, Diagnostic> { match tokens.peek() { - Some(Ok(Token::Loop)) => Err(ParseError::UnsupportedLoop { - ty: "infinite", - span: tokens.peek_span(), - }), - Some(Ok(Token::While)) => Err(ParseError::UnsupportedLoop { - ty: "while", - span: tokens.peek_span(), - }), - Some(Ok(Token::For)) => Err(ParseError::UnsupportedLoop { - ty: "for", - span: tokens.peek_span(), - }), + Some(Ok(Token::Loop)) => Err(tokens + .peek_span() + .wrap(ParseError::UnsupportedFeature { + feature: "infinite loops", + issue: 8, + }) + .into()), + Some(Ok(Token::While)) => Err(tokens + .peek_span() + .wrap(ParseError::UnsupportedFeature { + feature: "while loops", + issue: 8, + }) + .into()), + Some(Ok(Token::For)) => Err(tokens + .peek_span() + .wrap(ParseError::UnsupportedFeature { + feature: "for loops", + issue: 8, + }) + .into()), + Some(Ok(Token::If)) => Err(tokens + .skip_one() + .span_until(Token::RightBracket) + .wrap(ParseError::UnsupportedFeature { + feature: "if statements", + issue: 0, + }) + .into()), Some(Ok(_)) => { let expr = parse_additive(tokens, environment)?; @@ -322,37 +114,45 @@ fn parse_expression( _ => Ok(expr), } } - Some(Err(FailedToLexCharacter)) => Err(ParseError::FailedToLexCharacters( - tokens.peek_span().wrap(tokens.slice().to_string()), - )), - None => Err(ParseError::ExpectedMoreTokens(tokens.peek_span())), + Some(Err(FailedToLexCharacter)) => Err(tokens + .peek_span() + .wrap(ParseError::FailedToLexCharacters(tokens.slice().to_owned())) + .into()), + None => Err(tokens + .peek_span() + .wrap(ParseError::ExpectedMoreTokens) + .into()), } } -fn _parse_block(tokens: &mut TokenStream, environment: &Environment) -> Result { +fn _parse_block( + tokens: &mut TokenStream, + environment: &Environment, +) -> Result> { expect!(tokens, Token::LeftBracket); let ast = parse(tokens, environment)?; expect!(tokens, Token::RightBracket); + Ok(ast) } fn parse_additive( tokens: &mut TokenStream, environment: &Environment, -) -> Result, ParseError> { +) -> Result, Diagnostic> { let mut node = parse_multiplicitive(tokens, environment)?; while let Some(Ok(Token::Plus | Token::Minus)) = tokens.peek() { let operator = match tokens.next() { - Some(Ok(Token::Plus)) => Operator::Add, - Some(Ok(Token::Minus)) => Operator::Sub, + Some(Ok(Token::Plus)) => BinaryOperator::Add, + Some(Ok(Token::Minus)) => BinaryOperator::Sub, _ => unreachable!(), }; let right = parse_multiplicitive(tokens, environment)?; node = Spanned { - span: node.span.start..right.span.end, + span: node.span.join(&right.span), value: Expression::BinaryOp { left: Box::new(node), operator, @@ -366,18 +166,18 @@ fn parse_additive( fn parse_multiplicitive( tokens: &mut TokenStream, environment: &Environment, -) -> Result, ParseError> { - let mut node = parse_value(tokens, environment)?; +) -> Result, Diagnostic> { + let mut node = parse_and(tokens, environment)?; while let Some(Ok(Token::Asterisk | Token::Slash | Token::Modulo)) = tokens.peek() { let operator = match tokens.next() { - Some(Ok(Token::Asterisk)) => Operator::Mul, - Some(Ok(Token::Slash)) => Operator::Div, - Some(Ok(Token::Modulo)) => Operator::Mod, + Some(Ok(Token::Asterisk)) => BinaryOperator::Mul, + Some(Ok(Token::Slash)) => BinaryOperator::Div, + Some(Ok(Token::Modulo)) => BinaryOperator::Mod, _ => unreachable!(), }; - let right = parse_value(tokens, environment)?; + let right = parse_and(tokens, environment)?; node = Spanned { span: node.span.start..right.span.end, @@ -391,16 +191,45 @@ fn parse_multiplicitive( Ok(node) } +macro_rules! parse_bitwise { + ($op:ident, $token:ident: $name:ident => $next:ident) => { + fn $name( + tokens: &mut TokenStream, + environment: &Environment, + ) -> Result, Diagnostic> { + let mut node = $next(tokens, environment)?; + + while let Some(Ok(Token::$token)) = tokens.peek() { + tokens.next(); + let right = $next(tokens, environment)?; + + node = Spanned { + span: node.span.start..right.span.end, + value: Expression::BinaryOp { + left: Box::new(node), + operator: BinaryOperator::$op, + right: Box::new(right), + }, + }; + } + + Ok(node) + } + }; +} +parse_bitwise!(And, Ampersand: parse_and => parse_xor); +parse_bitwise!(Xor, Xor: parse_xor => parse_or); +parse_bitwise!(Or, Pipe: parse_or => parse_value); fn parse_value( tokens: &mut TokenStream, environment: &Environment, -) -> Result, ParseError> { +) -> Result, Diagnostic> { /// Parses a literal (value without member expressions) fn parse_literal( tokens: &mut TokenStream, environment: &Environment, - ) -> Result, ParseError> { + ) -> Result, Diagnostic> { match tokens.next() { Some(Ok(Token::LeftParen)) => { let start = tokens.span().start; @@ -437,7 +266,7 @@ fn parse_value( } Some(Ok(Token::Identifier)) => { let start = tokens.span().start; - let name = tokens.slice().to_string(); + let name = tokens.slice().to_owned(); match tokens.peek() { Some(Ok(Token::LeftParen)) => { @@ -475,13 +304,21 @@ fn parse_value( if let Some(Function { argument_count, .. }) = environment.get_function(&name) { - dbg!(argument_count); - let mut arguments = Vec::new(); for _ in 0..(*argument_count) { let expr = parse_expression(tokens, environment)?; arguments.push(expr); } + while !matches!( + tokens.peek(), + Some(Ok(Token::SemiColon + | Token::RightBrace + | Token::RightBracket + | Token::RightParen)) + ) && let Ok(additional) = parse_expression(tokens, environment) + { + arguments.push(additional); + } Ok(Spanned { span: start..tokens.span().end, value: Expression::Function { name, arguments }, @@ -502,12 +339,19 @@ fn parse_value( } Some(Ok(Token::String)) => { let slice = tokens.slice(); - let string = slice[1..slice.len() - 1].to_string(); + let string = slice[1..slice.len() - 1].to_owned(); Ok(tokens.span().wrap(Expression::String(string))) } - Some(Ok(Token::Minus)) => { + Some(Ok(token @ (Token::Minus | Token::Not))) => { let expr = parse_literal(tokens, environment)?; - Ok(tokens.span().wrap(Expression::UnaryOp(Box::new(expr)))) + Ok(tokens.span().wrap(Expression::UnaryOp { + operator: match token { + Token::Minus => UnaryOperator::Minus, + Token::Not => UnaryOperator::Not, + _ => unreachable!(), + }, + operand: Box::new(expr), + })) } Some(Ok(Token::Ampersand)) => { let expr = parse_literal(tokens, environment)?; @@ -522,44 +366,68 @@ fn parse_value( parse_number(tokens).map(|s| s.map(Expression::Number)) } Some(Ok(Token::FloatNumber)) => { - if let Some(Ok(Token::NumberType)) = tokens.peek() { - let number: Number = match tokens.peek_slice() { - "u8" | "u16" | "u32" | "u64" | "usize" | "i8" | "i16" | "i32" | "i64" - | "isize" => Err(ParseError::InvalidSuffixForFloat( - tokens.span().wrap(tokens.slice().to_string()), - ))?, - "f32" => { - Number::f32(tokens.slice().parse().expect(FLOAT_PARSE_EXPECT_REASON)) - } - "f64" => { - Number::f64(tokens.slice().parse().expect(FLOAT_PARSE_EXPECT_REASON)) - } - _ => unreachable!("{NUMBER_TYPE_WILDCARD_UNREACHABLE_REASON}"), - }; - let start_span = tokens.span().end; - - tokens.next(); - - Ok(Spanned { - span: start_span..tokens.span().end, - value: Expression::Number(number), - }) - } else { - let number = Number::Float(tokens.slice().parse().unwrap()); + let (number, suffix) = split_number(tokens); + let number: Number = match suffix { + "u8" | "u16" | "u32" | "u64" | "usize" | "i8" | "i16" | "i32" | "i64" + | "isize" => Err(Diagnostic::single( + tokens.span().add(number.len()..0), + ParseError::IntegerSuffixOnFloat(suffix.to_owned()), + ))?, + "f32" => { + Number::Float(Float::f32(number.parse().expect(FLOAT_PARSE_EXPECT_REASON))) + } + "f64" => { + Number::Float(Float::f64(number.parse().expect(FLOAT_PARSE_EXPECT_REASON))) + } + "" => Number::Float(Float::Unspecified( + number.parse().expect(FLOAT_PARSE_EXPECT_REASON), + )), + _ => { + return Err(tokens + .span() + .add(number.len()..0) + .wrap(ParseError::InvalidSuffixForFloat(suffix.to_owned())) + .into()); + } + }; + let start_span = tokens.span().end; - Ok(Spanned { - span: tokens.span(), - value: Expression::Number(number), - }) - } + Ok(Spanned { + span: start_span..tokens.span().end, + value: Expression::Number(number), + }) } Some(Ok(Token::True)) => Ok(tokens.span().wrap(Expression::Boolean(true))), Some(Ok(Token::False)) => Ok(tokens.span().wrap(Expression::Boolean(false))), - Some(Ok(token)) => Err(ParseError::ExpectedLiteral(tokens.span().wrap(token))), - Some(Err(FailedToLexCharacter)) => Err(ParseError::FailedToLexCharacters( - tokens.span().wrap(tokens.slice().to_string()), - )), - None => Err(ParseError::ExpectedMoreTokens(tokens.span())), + Some(Ok(Token::LeftBrace)) => Err(tokens + .span_until(Token::RightBrace) + .wrap(ParseError::UnsupportedFeature { + feature: "lists and vectors", + issue: 10, + }) + .into()), + Some(Ok(Token::Pipe)) => Err(tokens + .span_until(Token::Pipe) + .wrap(ParseError::UnsupportedFeature { + feature: "closures", + issue: 12, + }) + .into()), + Some(Ok(Token::RightBrace | Token::RightBracket | Token::RightParen)) => Err(tokens + .span() + .wrap(ParseError::MismatchedDelimiter { + delimiter: tokens.slice().chars().next().unwrap(), + }) + .into()), + Some(Ok(token)) => Err(tokens + .span() + .wrap(ParseError::ExpectedLiteral(token)) + .into()), + Some(Err(FailedToLexCharacter)) => Err(tokens + .span() + .wrap(ParseError::FailedToLexCharacters(tokens.slice().to_owned())) + .into()), + None => Err(tokens.span().diagnose(ParseError::ExpectedMoreTokens)), } } @@ -569,7 +437,7 @@ fn parse_value( tokens.next(); // Skip the dot match tokens.next() { Some(Ok(Token::Identifier)) => { - let right = tokens.slice().to_string(); + let right = tokens.slice().to_owned(); expr = Spanned { span: expr.span.start..tokens.span().end, value: Expression::Member { @@ -582,7 +450,7 @@ fn parse_value( let right = tokens.slice().parse().map_err(map_parseint_error( tokens.span(), tokens.slice(), - "usize", + UnsignedIntegerKind::usize.into(), ))?; expr = Spanned { @@ -594,17 +462,18 @@ fn parse_value( }; } Some(Ok(token)) => { - return Err(ParseError::ExpectedIndexer { - got: token, - span: tokens.span(), - }) + return Err(tokens + .span() + .wrap(ParseError::ExpectedIndexer { got: token }) + .into()); } Some(Err(FailedToLexCharacter)) => { - return Err(ParseError::FailedToLexCharacters( - tokens.span().wrap(tokens.slice().to_string()), - )) + return Err(tokens + .span() + .wrap(ParseError::FailedToLexCharacters(tokens.slice().to_owned())) + .into()); } - None => return Err(ParseError::ExpectedMoreTokens(tokens.span())), + None => return Err(tokens.span().diagnose(ParseError::ExpectedMoreTokens)), } } Ok(expr) @@ -613,103 +482,114 @@ fn parse_value( fn map_parseint_error<'s>( span: Span, slice: &'s str, - number_kind: &'static str, -) -> impl FnOnce(std::num::ParseIntError) -> ParseError + 's { - move |error| match error.kind() { - IntErrorKind::PosOverflow => ParseError::PositiveIntOverflow { - span, - number: slice.to_string(), - number_kind, - }, - IntErrorKind::NegOverflow => ParseError::NegativeIntOverflow { - span, - number: slice.to_string(), - number_kind, - }, - IntErrorKind::Empty | IntErrorKind::InvalidDigit | IntErrorKind::Zero => unreachable!( - "Lexer makes sure other errors aren't possible. Create an bevy_dev_console issue!" - ), - _ => unimplemented!(), // Required due to IntErrorKind being #[non_exhaustive] + number_kind: crate::builtin_parser::number::NumberKind, +) -> impl FnOnce(std::num::ParseIntError) -> Diagnostic + 's { + move |error| { + let error = match error.kind() { + IntErrorKind::PosOverflow => ParseError::PositiveIntOverflow { + number: slice.to_owned(), + number_kind, + }, + IntErrorKind::NegOverflow => ParseError::NegativeIntOverflow { + number: slice.to_owned(), + number_kind, + }, + IntErrorKind::Empty | IntErrorKind::InvalidDigit | IntErrorKind::Zero => unreachable!( + "Lexer makes sure other errors aren't possible. Create an bevy_dev_console issue!" + ), + _ => unimplemented!(), // Required due to IntErrorKind being #[non_exhaustive] + }; + span.diagnose(error) } } -fn parse_number(tokens: &mut TokenStream) -> Result, ParseError> { - if let Some(Ok(Token::NumberType)) = tokens.peek() { - let number: Number = match tokens.peek_slice() { - "u8" => Number::u8(tokens.slice().parse().map_err(map_parseint_error( - tokens.span(), - tokens.slice(), - "u8", - ))?), - "u16" => Number::u16(tokens.slice().parse().map_err(map_parseint_error( - tokens.span(), - tokens.slice(), - "u16", - ))?), - "u32" => Number::u32(tokens.slice().parse().map_err(map_parseint_error( - tokens.span(), - tokens.slice(), - "u32", - ))?), - "u64" => Number::u64(tokens.slice().parse().map_err(map_parseint_error( - tokens.span(), - tokens.slice(), - "u64", - ))?), - "usize" => Number::usize(tokens.slice().parse().map_err(map_parseint_error( - tokens.span(), - tokens.slice(), - "usize", - ))?), - "i8" => Number::i8(tokens.slice().parse().map_err(map_parseint_error( - tokens.span(), - tokens.slice(), - "i8", - ))?), - "i16" => Number::i16(tokens.slice().parse().map_err(map_parseint_error( - tokens.span(), - tokens.slice(), - "i16", - ))?), - "i32" => Number::i32(tokens.slice().parse().map_err(map_parseint_error( - tokens.span(), - tokens.slice(), - "i32", - ))?), - "isize" => Number::isize(tokens.slice().parse().map_err(map_parseint_error( - tokens.span(), - tokens.slice(), - "isize", - ))?), - "f32" => Number::f32(tokens.slice().parse().expect(FLOAT_PARSE_EXPECT_REASON)), - "f64" => Number::f64(tokens.slice().parse().expect(FLOAT_PARSE_EXPECT_REASON)), - _ => unreachable!("{}", NUMBER_TYPE_WILDCARD_UNREACHABLE_REASON), - }; - let start_span = tokens.span().end; - tokens.next(); +fn parse_number(tokens: &mut TokenStream) -> Result, Diagnostic> { + let (number, suffix) = split_number(tokens); + let map = |s: NumberKind| map_parseint_error(tokens.span(), tokens.slice(), s); + + let number = match suffix { + "u8" => UnsignedInteger::u8( + number + .parse() + .map_err(map(UnsignedIntegerKind::u8.into()))?, + ) + .into(), + "u16" => UnsignedInteger::u16( + number + .parse() + .map_err(map(UnsignedIntegerKind::u16.into()))?, + ) + .into(), + "u32" => UnsignedInteger::u32( + number + .parse() + .map_err(map(UnsignedIntegerKind::u32.into()))?, + ) + .into(), + "u64" => UnsignedInteger::u64( + number + .parse() + .map_err(map(UnsignedIntegerKind::u64.into()))?, + ) + .into(), + "usize" => UnsignedInteger::usize( + number + .parse() + .map_err(map(UnsignedIntegerKind::usize.into()))?, + ) + .into(), + "i8" => { + SignedInteger::i8(number.parse().map_err(map(SignedIntegerKind::i8.into()))?).into() + } + "i16" => { + SignedInteger::i16(number.parse().map_err(map(SignedIntegerKind::i16.into()))?).into() + } + "i32" => { + SignedInteger::i32(number.parse().map_err(map(SignedIntegerKind::i32.into()))?).into() + } + "isize" => SignedInteger::isize( + number + .parse() + .map_err(map(SignedIntegerKind::isize.into()))?, + ) + .into(), + "f32" => Number::Float(Float::f32(number.parse().expect(FLOAT_PARSE_EXPECT_REASON))), + "f64" => Number::Float(Float::f64(number.parse().expect(FLOAT_PARSE_EXPECT_REASON))), + "" => SignedInteger::Unspecified(number.parse().unwrap()).into(), + _ => { + return Err(tokens + .span() + .add(number.len()..0) + .wrap(ParseError::InvalidSuffixForNumber(suffix.to_owned())) + .into()); + } + }; + Ok(Spanned { + span: tokens.span(), + value: number, + }) +} - Ok(Spanned { - span: start_span..tokens.span().end, - value: number, - }) - } else { - let number = Number::Integer(tokens.slice().parse().unwrap()); - - Ok(Spanned { - span: tokens.span(), - value: number, - }) - } +fn split_number<'s>(tokens: &'s TokenStream<'_>) -> (&'s str, &'s str) { + let s = tokens.slice(); + let i = s + .as_bytes() + .iter() + .position(|b| b.is_ascii_alphabetic() || *b == b'_') + .unwrap_or(s.len()); + + let (number, suffix) = s.split_at(i); + (number, suffix) } fn parse_var_assign( name: Spanned, tokens: &mut TokenStream<'_>, environment: &Environment, -) -> Result, ParseError> { +) -> Result, Diagnostic> { tokens.next(); // We already know that the next token is an equals - let value = parse_additive(tokens, environment)?; + let value = parse_expression(tokens, environment)?; Ok(Spanned { span: name.span.start..value.span.end, @@ -728,11 +608,11 @@ fn parse_var_assign( fn parse_object( tokens: &mut TokenStream, environment: &Environment, -) -> Result>, ParseError> { +) -> Result>, Diagnostic> { let mut map = HashMap::new(); while let Some(Ok(Token::Identifier)) = tokens.peek() { tokens.next(); - let ident = tokens.slice().to_string(); + let ident = tokens.slice().to_owned(); expect!(tokens, Token::Colon); let expr = parse_expression(tokens, environment)?; map.insert(ident, expr); @@ -741,30 +621,11 @@ fn parse_object( Some(Ok(Token::Comma)) => { tokens.next(); } - token => Err(ParseError::ExpectedObjectContinuation( - tokens.span().wrap(token.clone()), - ))?, + token => Err(tokens + .span() + .diagnose(ParseError::ExpectedObjectContinuation(token.clone())))?, } } expect!(tokens, Token::RightBracket); Ok(map) } - -#[cfg(test)] -mod tests { - use super::super::lexer::TokenStream; - use super::super::Environment; - use super::parse; - - #[test] - fn var_assign() { - let mut lexer = TokenStream::new("x = 1 + 2 - 30 + y"); - let environment = Environment::default(); - - let ast = parse(&mut lexer, &environment); - - assert!(ast.is_ok()); - - // TODO: figure out how to assert ast - } -} diff --git a/src/builtin_parser/parser/ast.rs b/src/builtin_parser/parser/ast.rs new file mode 100644 index 0000000..0ba5912 --- /dev/null +++ b/src/builtin_parser/parser/ast.rs @@ -0,0 +1,177 @@ +use kinded::Kinded; +use std::collections::HashMap; +use std::fmt::Display; + +use crate::builtin_parser::Spanned; +use crate::builtin_parser::number::Number; + +/// An [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree). +/// +/// This type represents a list of expressions, which is what makes up a command. +pub type Ast = Vec>; + +/// A type that represents an expression. +#[derive(Debug, Clone, Kinded)] +pub enum Expression { + // Primitives + None, + Boolean(bool), + Number(Number), + Variable(String), + String(String), + Borrow(Box>), + Dereference(Box>), + Object(HashMap>), + StructObject { + name: String, + map: HashMap>, + }, + Tuple(Vec>), + StructTuple { + name: String, + tuple: Vec>, + }, + + // Expressions + BinaryOp { + left: Box>, + operator: BinaryOperator, + right: Box>, + }, + UnaryOp { + operator: UnaryOperator, + operand: Box>, + }, + Member { + left: Box>, + right: Spanned, + }, + + // Statement-like + VarAssign { + name: Box>, + value: Box>, + }, + Function { + name: String, + arguments: Vec>, + }, + ForLoop { + index_name: String, + loop_count: u64, + block: Ast, + }, +} + +impl ExpressionKind { + pub const fn as_natural(&self) -> &'static str { + match self { + ExpressionKind::None => "nothing", + ExpressionKind::Boolean => "a boolean", + ExpressionKind::Number => "a number", + ExpressionKind::Variable => "a variable name", + ExpressionKind::String => "a string", + ExpressionKind::Borrow => "a borrow", + ExpressionKind::Dereference => "a dereference", + ExpressionKind::Object => "an object", + ExpressionKind::StructObject => "a struct object", + ExpressionKind::Tuple => "a tuple", + ExpressionKind::StructTuple => "a struct tuple", + + ExpressionKind::BinaryOp => "a binary operation", + ExpressionKind::UnaryOp => "a unary operation", + ExpressionKind::Member => "a member expression", + ExpressionKind::VarAssign => "a variable assignment", + ExpressionKind::Function => "a function call", + ExpressionKind::ForLoop => "a for loop", + } + } +} + +/// A singular element access within a [`Expression::Member`]. +/// +/// Based on `bevy_reflect`'s `Access`. +#[derive(Debug, Clone, Kinded)] +pub enum Access { + /// A name-based field access on a struct. + Field(String), + /// An index-based access on a tuple. + TupleIndex(usize), + // /// An index-based access on a list. + // ListIndex(usize), +} +impl AccessKind { + /// Returns the kind of [`Access`] as a [string slice](str) with an `a` or `an` prepended to it. + /// Used for more natural sounding error messages. + pub const fn as_natural(self) -> &'static str { + match self { + AccessKind::Field => "a field", + AccessKind::TupleIndex => "a tuple", + } + } +} + +/// Get the access if its of a certain type, if not, return a [`EvalError`](super::runner::error::EvalError). +/// +/// For examples, take a look at existing uses in the code. +macro_rules! access_unwrap { + ($expected:literal, $($variant:ident($variant_inner:ident))|+ = $val:expr => $block:block) => {{ + let Spanned { span, value } = $val; + if let $(Access::$variant($variant_inner))|+ = value $block else { + use $crate::builtin_parser::parser::AccessKind; + use $crate::builtin_parser::runner::error::EvalError; + + Err(span.diagnose( + EvalError::IncorrectAccessOperation { + expected_access: &[$(AccessKind::$variant),+], + expected_type: $expected, + got: value.kind(), + }, + ))? + } + }}; +} +pub(crate) use access_unwrap; + +#[derive(Debug, Clone)] +pub enum BinaryOperator { + Add, + Sub, + Mul, + Div, + Mod, + + And, + Xor, + Or, +} +impl Display for BinaryOperator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BinaryOperator::Add => write!(f, "+"), + BinaryOperator::Sub => write!(f, "-"), + BinaryOperator::Mul => write!(f, "*"), + BinaryOperator::Div => write!(f, "/"), + BinaryOperator::Mod => write!(f, "%"), + BinaryOperator::And => write!(f, "&"), + BinaryOperator::Xor => write!(f, "^"), + BinaryOperator::Or => write!(f, "|"), + } + } +} + +#[derive(Debug, Clone)] +pub enum UnaryOperator { + /// `-x` + Minus, + /// `!true` + Not, +} +impl Display for UnaryOperator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UnaryOperator::Minus => write!(f, "-"), + UnaryOperator::Not => write!(f, "!"), + } + } +} diff --git a/src/builtin_parser/parser/error.rs b/src/builtin_parser/parser/error.rs new file mode 100644 index 0000000..f0559c9 --- /dev/null +++ b/src/builtin_parser/parser/error.rs @@ -0,0 +1,71 @@ +use crate::builtin_parser::lexer::{FailedToLexCharacter, Token}; +use crate::builtin_parser::number::NumberKind; + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error("unknown token character: {0}")] + FailedToLexCharacters(String), + + #[error("expected more tokens, got nothing.")] + ExpectedMoreTokens, + + #[error("expected token {expected:?}, got token {got:?} instead.")] + ExpectedTokenButGot { expected: Token, got: Token }, + + #[error( + "expected a semicolon or endline after a complete statement, but got more tokens than expected." + )] + ExpectedEndline(Token), + + #[error("expected a literal token, got {0:?} which is not a valid literal.")] + ExpectedLiteral(Token), + + #[error( + "invalid suffix `{0}` for number literal. the suffix must be one of the numeric types (`u32`, `isize`, `f32`, etc.)" + )] + InvalidSuffixForNumber(String), + + #[error( + "invalid suffix `{0}` for float literal. the suffix must be one of the float types (`f32`, `f64`)" + )] + InvalidSuffixForFloat(String), + + #[error(r#""{0}" is an invalid suffix for a float. the valid suffixes are "f32" and "f64"."#)] + IntegerSuffixOnFloat(String), + + #[error("{number} cannot be represented as {number_kind:#} as it is too small.")] + NegativeIntOverflow { + number: String, + number_kind: NumberKind, + }, + + #[error("{number} cannot be represented as {number_kind:#} as it is too large.")] + PositiveIntOverflow { + number: String, + number_kind: NumberKind, + }, + + #[error( + "expected a continuation to the object declaration (such as a comma or a closing bracket), but got {0:?} instead." + )] + ExpectedObjectContinuation(Option>), + + #[error( + "expected an identifier or integer when accessing member of variable, got {got:?} instead." + )] + ExpectedIndexer { got: Token }, + + #[error("{feature} are not yet supported. {}", format_issue(*issue))] + UnsupportedFeature { feature: &'static str, issue: u8 }, + + #[error("unexpected closing delimiter: `{delimiter}`")] + MismatchedDelimiter { delimiter: char }, +} + +pub(crate) fn format_issue(issue: u8) -> String { + if issue != 0 { + format!("see bevy_dev_console issue #{issue}") + } else { + String::new() + } +} diff --git a/src/builtin_parser/parser/tests.rs b/src/builtin_parser/parser/tests.rs new file mode 100644 index 0000000..edbb9e1 --- /dev/null +++ b/src/builtin_parser/parser/tests.rs @@ -0,0 +1,93 @@ +use crate::builtin_parser::Spanned; +use crate::builtin_parser::parser::Expression; +use logos::Span; + +use super::super::Environment; +use super::super::lexer::TokenStream; +use crate::builtin_parser::Number; +use crate::builtin_parser::parser::BinaryOperator::*; +use std::assert_matches; + +fn setup(src: &str) -> std::vec::IntoIter> { + let mut lexer = TokenStream::new(src); + let environment = Environment::default(); + + let ast = super::parse(&mut lexer, &environment).unwrap(); + + ast.into_iter() +} + +#[test] +fn var_assign() { + let mut stmts = setup("x = 1 + 2 - 30 + y"); + + assert_matches!( + stmts.next(), + Some(Spanned { + span: Span { start: 0, end: 18 }, + value: + Expression::VarAssign { + name: + box Spanned { + span: Span { start: 0, end: 1 }, + value: Expression::Variable(_), + }, + value: + box Spanned { + span: Span { start: 4, end: 18 }, + value: + Expression::BinaryOp { + left: + box Spanned { + span: Span { start: 4, end: 14 }, + value: + Expression::BinaryOp { + left: + box Spanned { + span: Span { start: 4, end: 9 }, + value: + Expression::BinaryOp { + left: + box Spanned { + span: + Span { + start: 4, + end: 5, + }, + value: + Expression::Number(Number::Integer(crate::builtin_parser::number::Integer::Signed(crate::builtin_parser::number::SignedInteger::Unspecified(1)))), + }, + operator: Add, + right: + box Spanned { + span: + Span { + start: 8, + end: 9, + }, + value: + Expression::Number(Number::Integer(crate::builtin_parser::number::Integer::Signed(crate::builtin_parser::number::SignedInteger::Unspecified(2)))), + }, + }, + }, + operator: Sub, + right: + box Spanned { + span: Span { start: 12, end: 14 }, + value: Expression::Number(Number::Integer(crate::builtin_parser::number::Integer::Signed(crate::builtin_parser::number::SignedInteger::Unspecified(30)))), + }, + }, + }, + operator: Add, + right: + box Spanned { + span: Span { start: 17, end: 18 }, + value: Expression::Variable(_), + }, + }, + }, + }, + }) + ); + assert!(stmts.next().is_none()); +} diff --git a/src/builtin_parser/runner.rs b/src/builtin_parser/runner.rs index 1146605..a7fbd01 100644 --- a/src/builtin_parser/runner.rs +++ b/src/builtin_parser/runner.rs @@ -1,619 +1,117 @@ //! Executes the abstract syntax tree. use environment::Environment; -use std::collections::HashMap; use bevy::prelude::*; -use bevy::reflect::{ - DynamicEnum, DynamicTuple, ReflectMut, TypeInfo, TypeRegistration, VariantInfo, -}; - -use crate::ui::COMMAND_RESULT_NAME; +use bevy::reflect::TypeRegistration; use self::error::EvalError; -use self::member::{eval_member_expression, eval_path, Path}; -use self::reflection::{object_to_dynamic_struct, CreateRegistration, IntoResource}; -use self::unique_rc::UniqueRc; -use super::parser::{Ast, Expression, Operator}; -use super::{Number, SpanExtension, Spanned}; +use super::Spanned; +use super::parser::{Ast, Expression}; pub(super) mod environment; pub(super) mod error; +pub(super) mod eval; +pub(super) mod function; pub(super) mod member; pub(super) mod reflection; pub(super) mod stdlib; pub(super) mod unique_rc; pub(super) mod value; +pub use eval::eval_expression; pub use value::Value; /// Temporary macro that prevents panicking by replacing the [`todo!`] panic with an error message. macro_rules! todo_error { () => { - Err(EvalError::Custom { - text: concat!("todo error invoked at ", file!(), ":", line!(), ":", column!()).into(), - span: 0..0 - })? + return Err($crate::builtin_parser::Diagnostic::empty($crate::builtin_parser::EvalError::Custom( + concat!("todo error invoked at ", file!(), ":", line!(), ":", column!()).into(), + ))) }; ($($arg:tt)+) => { - Err(EvalError::Custom { - text: format!(concat!("todo error invoked at ", file!(), ":", line!(), ":", column!(), " : {}"), format_args!($($arg)+)).into(), - span: 0..0 - })? + return Err($crate::builtin_parser::Diagnostic::empty($crate::builtin_parser::EvalError::Custom( + format!( + concat!( + "todo error invoked at ", + file!(), + ":", + line!(), + ":", + column!(), + ": {}" + ), + format_args!($($arg)+) + ) + .into(), + ), + )) }; } -// This makes `todo_error` accessible to the runners submodules -use todo_error; +pub(crate) use todo_error; /// Container for every value needed by evaluation functions. pub struct EvalParams<'world, 'env, 'reg> { - world: &'world mut World, - environment: &'env mut Environment, - registrations: &'reg [&'reg TypeRegistration], -} - -#[derive(Debug)] -pub enum ExecutionError { - NoEnvironment, - NoTypeRegistry, - Eval(EvalError), -} - -impl std::fmt::Display for ExecutionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::NoEnvironment => write!( - f, - "Environment resource doesn't exist, not executing command." - ), - Self::NoTypeRegistry => write!( - f, - "The AppTypeRegistry doesn't exist, not executing command. " - ), - Self::Eval(run_error) => ::fmt(run_error, f), - } - } -} -impl std::error::Error for ExecutionError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - ExecutionError::Eval(eval) => Some(eval), - _ => None, - } - } -} -impl From for ExecutionError { - fn from(value: EvalError) -> Self { - Self::Eval(value) - } + pub world: &'world mut World, + pub environment: &'env mut Environment, + pub registrations: &'reg [&'reg TypeRegistration], } -pub fn run(ast: Ast, world: &mut World) -> Result<(), ExecutionError> { - // Temporarily remove the [`Environment`] resource to gain - // mutability without needing a mutable reference. - let mut environment = world - .remove_non_send_resource::() - .ok_or(ExecutionError::NoEnvironment)?; - - // Same thing here (this time we are doing it because we are passing a `&mut World` to `eval_expression`) - let Some(registry) = world.remove_resource::() else { - // Make sure to re-insert the resource on failure - world.insert_non_send_resource(environment); - - return Err(ExecutionError::NoTypeRegistry); - }; - - let result = (|| { - let registry_read = registry.read(); - - let registrations: Vec<_> = registry_read - .iter() - .filter(|registration| { - world - .components() - .get_resource_id(registration.type_id()) - .is_some() - }) - .collect(); - - for mut statement in ast { - // Automatically borrow variables - statement.value = match statement.value { +pub fn eval( + ast: Ast, + world: &mut World, + environment: &mut Environment, + registry: &AppTypeRegistry, +) -> Result, crate::builtin_parser::Diagnostic> { + let registry_read = registry.read(); + + let registrations: Vec<_> = registry_read + .iter() + .filter(|registration| { + world + .components() + .get_resource_id(registration.type_id()) + .is_some() + }) + .collect(); + let mut last_value = None; + for mut statement in ast { + fn autoborrow(statement: Spanned) -> Spanned { + let value = match statement.value { Expression::Variable(variable) => Expression::Borrow(Box::new(Spanned { span: statement.span.clone(), value: Expression::Variable(variable), })), + Expression::Member { mut left, right } => { + *left = autoborrow(*left); + Expression::Member { left, right } + } expr => expr, }; - - let span = statement.span.clone(); - let value = eval_expression( - statement, - EvalParams { - world, - environment: &mut environment, - registrations: ®istrations, - }, - )?; - - match value { - Value::None => {} - value => { - let value = value.try_format(span, world, ®istrations)?; - - info!(name: COMMAND_RESULT_NAME, "{}{value}", crate::ui::COMMAND_RESULT_PREFIX); - } - } - } - - Ok(()) - })(); - - // Add back the resources - world.insert_resource(registry); - world.insert_non_send_resource(environment); - - result -} - -fn eval_expression( - expr: Spanned, - EvalParams { - world, - environment, - registrations, - }: EvalParams, -) -> Result { - match expr.value { - Expression::VarAssign { - name, - value: value_expr, - } => match eval_path( - *name, - EvalParams { - world, - environment, - registrations, - }, - )? - .value - { - Path::Variable(variable) => { - let value = eval_expression( - *value_expr, - EvalParams { - world, - environment, - registrations, - }, - )?; - - match variable.upgrade() { - Some(strong) => *strong.borrow_mut() = value, - None => todo_error!("cannot "), - } - - Ok(Value::Reference(variable)) - } - Path::NewVariable(variable) => { - let value = eval_expression( - *value_expr, - EvalParams { - world, - environment, - registrations, - }, - )?; - let rc = UniqueRc::new(value); - let weak = rc.borrow(); - - environment.set(variable, rc); - - Ok(Value::Reference(weak)) - } - Path::Resource(resource) => { - let registration = registrations.create_registration(resource.id); - let mut dyn_reflect = resource.mut_dyn_reflect(world, registration); - - let reflect = dyn_reflect - .reflect_path_mut(resource.path.as_str()) - .unwrap(); - - match reflect.reflect_mut() { - ReflectMut::Enum(dyn_enum) => { - let TypeInfo::Enum(enum_info) = registration.type_info() else { - unreachable!() - }; - let Spanned { span, value } = *value_expr; - match value { - Expression::Variable(name) => { - let variant_info = match enum_info.variant(&name) { - Some(variant_info) => variant_info, - None => { - return Err(EvalError::EnumVariantNotFound(span.wrap(name))) - } - }; - let VariantInfo::Unit(_) = variant_info else { - return todo_error!("{variant_info:?}"); - }; - - let new_enum = DynamicEnum::new(name, ()); - - dyn_enum.apply(&new_enum); - } - Expression::StructObject { name, map } => { - let variant_info = match enum_info.variant(&name) { - Some(variant_info) => variant_info, - None => { - return Err(EvalError::EnumVariantNotFound(span.wrap(name))) - } - }; - let VariantInfo::Struct(variant_info) = variant_info else { - return todo_error!("{variant_info:?}"); - }; - - let map: HashMap<_, _> = map - .into_iter() - .map(|(k, v)| { - let ty = match variant_info.field(&k) { - Some(field) => Ok(field.type_path_table().short_path()), - None => { - Err(EvalError::EnumVariantStructFieldNotFound { - field_name: k.clone(), - variant_name: name.clone(), - span: span.clone(), - }) - } - }?; - - let span = v.span.clone(); - - Ok(( - k, - ( - eval_expression( - v, - EvalParams { - world, - environment, - registrations, - }, - )?, - span, - ty, - ), - )) - }) - .collect::>()?; - - let new_enum = - DynamicEnum::new(name, object_to_dynamic_struct(map)?); - - let mut dyn_reflect = - resource.mut_dyn_reflect(world, registrations); - - let dyn_enum = dyn_reflect - .reflect_path_mut(resource.path.as_str()) - .unwrap(); - - dyn_enum.apply(&new_enum); - } - Expression::StructTuple { name, tuple } => { - let variant_info = match enum_info.variant(&name) { - Some(variant_info) => variant_info, - None => { - return Err(EvalError::EnumVariantNotFound(span.wrap(name))) - } - }; - let VariantInfo::Tuple(variant_info) = variant_info else { - return todo_error!("{variant_info:?}"); - }; - - let tuple = eval_tuple( - tuple, - EvalParams { - world, - environment, - registrations, - }, - )?; - - let mut dynamic_tuple = DynamicTuple::default(); - - for (index, element) in tuple.into_vec().into_iter().enumerate() { - let ty = match variant_info.field_at(index) { - Some(field) => Ok(field.type_path_table().short_path()), - None => Err(EvalError::EnumVariantTupleFieldNotFound { - field_index: index, - variant_name: name.clone(), - span: span.clone(), - }), - }?; - - dynamic_tuple.insert_boxed( - element.value.into_inner().reflect(element.span, ty)?, - ); - } - - let new_enum = DynamicEnum::new(name, dynamic_tuple); - - let mut dyn_reflect = - resource.mut_dyn_reflect(world, registrations); - - let dyn_enum = dyn_reflect - .reflect_path_mut(resource.path.as_str()) - .unwrap(); - - dyn_enum.apply(&new_enum); - } - _ => todo_error!(), - } - } - _ => { - let span = value_expr.span.clone(); - let ty = reflect.reflect_short_type_path().to_owned(); - let value = eval_expression( - *value_expr, - EvalParams { - world, - environment, - registrations, - }, - )?; - let value_reflect = value.reflect(span.clone(), &ty)?; - - let mut dyn_reflect = resource.mut_dyn_reflect(world, registrations); - - let reflect = dyn_reflect - .reflect_path_mut(resource.path.as_str()) - .unwrap(); - - reflect.set(value_reflect).map_err(|value_reflect| { - EvalError::IncompatibleReflectTypes { - span, - expected: reflect.reflect_type_path().to_string(), - actual: value_reflect.reflect_type_path().to_string(), - } - })?; - } - } - - Ok(Value::Resource(resource)) - } - }, - Expression::String(string) => Ok(Value::String(string)), - Expression::Number(number) => Ok(Value::Number(number)), - Expression::Variable(variable) => { - if registrations - .iter() - .any(|v| v.type_info().type_path_table().short_path() == variable) - { - Err(EvalError::CannotMoveOutOfResource(Spanned { - span: expr.span, - value: variable, - })) - } else { - environment.move_var(&variable, expr.span) + Spanned { + span: statement.span, + value, } } - Expression::StructObject { name, map } => { - let hashmap = eval_object( - map, - EvalParams { - world, - environment, - registrations, - }, - )?; - Ok(Value::StructObject { name, map: hashmap }) - } - Expression::Object(map) => { - let hashmap = eval_object( - map, - EvalParams { - world, - environment, - registrations, - }, - )?; - Ok(Value::Object(hashmap)) - } - Expression::Tuple(tuple) => { - let tuple = eval_tuple( - tuple, - EvalParams { - world, - environment, - registrations, - }, - )?; - Ok(Value::Tuple(tuple)) - } - Expression::StructTuple { name, tuple } => { - let tuple = eval_tuple( - tuple, - EvalParams { - world, - environment, - registrations, - }, - )?; - Ok(Value::StructTuple { name, tuple }) - } - - Expression::BinaryOp { - left, - operator, - right, - } => { - let left = eval_expression( - *left, - EvalParams { - world, - environment, - registrations, - }, - )?; - let right = eval_expression( - *right, - EvalParams { - world, - environment, - registrations, - }, - )?; + // Automatically borrow variables to prevent move errors on `x` + statement = autoborrow(statement); - match (left, right) { - (Value::Number(left), Value::Number(right)) => Ok(Value::Number(match operator { - Operator::Add => Number::add(left, right, expr.span)?, - Operator::Sub => Number::sub(left, right, expr.span)?, - Operator::Mul => Number::mul(left, right, expr.span)?, - Operator::Div => Number::div(left, right, expr.span)?, - Operator::Mod => Number::rem(left, right, expr.span)?, - })), - (left, right) => todo_error!("{left:#?}, {right:#?}"), - } - } - Expression::ForLoop { - index_name, - loop_count, - block, - } => todo_error!("for loop {index_name}, {loop_count}, {block:#?}"), - Expression::Member { left, right } => eval_member_expression( - *left, - right, + let span = statement.span.clone(); + let value = eval_expression( + statement, EvalParams { world, environment, - registrations, + registrations: ®istrations, }, - ), - Expression::UnaryOp(sub_expr) => { - let span = sub_expr.span.clone(); - let value = eval_expression( - *sub_expr, - EvalParams { - world, - environment, - registrations, - }, - )?; + )?; - if let Value::Number(number) = value { - Ok(Value::Number(number.neg(span)?)) - } else { - Err(EvalError::ExpectedNumberAfterUnaryOperator(Spanned { - span, - value, - })) - } - } - Expression::Dereference(inner) => { - if let Expression::Variable(variable) = inner.value { - let var = environment.get(&variable, inner.span)?; - match &*var.borrow_inner().borrow() { - Value::Reference(reference) => { - let reference = reference - .upgrade() - .ok_or(EvalError::ReferenceToMovedData(expr.span))?; - let owned = reference.borrow().clone(); - Ok(owned) - } - value => Ok(value.clone()), - } - } else { - Err(EvalError::CannotDereferenceValue( - expr.span.wrap(inner.value.kind()), - )) - } - } - Expression::Borrow(inner) => { - if let Expression::Variable(variable) = inner.value { - if let Some(registration) = registrations - .iter() - .find(|v| v.type_info().type_path_table().short_path() == variable) - { - Ok(Value::Resource(IntoResource::new(registration.type_id()))) - } else { - let rc = environment.get(&variable, inner.span)?; - let weak = rc.borrow(); - - Ok(Value::Reference(weak)) - } - } else { - Err(EvalError::CannotBorrowValue( - expr.span.wrap(inner.value.kind()), - )) - } - } - Expression::None => Ok(Value::None), - Expression::Boolean(bool) => Ok(Value::Boolean(bool)), - Expression::Function { name, arguments } => { - environment.function_scope(&name, move |environment, function| { - (function.body)( - arguments, - EvalParams { - world, - environment, - registrations, - }, - ) - }) + last_value = match value { + Value::None => None, + value => Some(value.try_format(span, world, ®istrations)?), } } -} - -fn eval_object( - map: HashMap>, - EvalParams { - world, - environment, - registrations, - }: EvalParams, -) -> Result>, EvalError> { - let map = map - .into_iter() - .map( - |(key, expr)| -> Result<(String, UniqueRc), EvalError> { - Ok(( - key, - UniqueRc::new(eval_expression( - expr, - EvalParams { - world, - environment, - registrations, - }, - )?), - )) - }, - ) - .collect::>()?; - Ok(map) -} -fn eval_tuple( - tuple: Vec>, - EvalParams { - world, - environment, - registrations, - }: EvalParams, -) -> Result>]>, EvalError> { - tuple - .into_iter() - .map(|expr| { - let span = expr.span.clone(); - let value = UniqueRc::new(eval_expression( - expr, - EvalParams { - world, - environment, - registrations, - }, - )?); - Ok(span.wrap(value)) - }) - .collect::>() + Ok(last_value) } diff --git a/src/builtin_parser/runner/environment.rs b/src/builtin_parser/runner/environment.rs index b30d345..e6eea19 100644 --- a/src/builtin_parser/runner/environment.rs +++ b/src/builtin_parser/runner/environment.rs @@ -1,39 +1,47 @@ -//! Environment and function registration +//! Environment and variable storage use std::collections::HashMap; -use std::fmt::Debug; -use crate::builtin_parser::SpanExtension; +use crate::builtin_parser::runner::EvalParams; +use crate::builtin_parser::{Diagnostic, Spanned}; use bevy::ecs::world::World; use bevy::log::warn; use bevy::reflect::TypeRegistration; -use logos::Span; -use super::super::parser::Expression; -use super::super::Spanned; use super::error::EvalError; +pub use super::function::{Function, IntoFunction}; use super::unique_rc::UniqueRc; -use super::{eval_expression, stdlib, EvalParams, Value}; +use super::{Value, stdlib}; -/// Macro for mass registering functions. +/// Macro for mass registering [`Function`]s to an [`Environment`]. /// +/// ## Usage /// ``` -/// fn a() {} -/// fn b() {} -/// fn c() {} +/// fn my_func() {} +/// +/// # use bevy_dev_console::register; +/// # let mut environment = bevy_dev_console::builtin_parser::Environment::default(); +/// register!(environment => fn my_func); +/// ``` +/// +/// ``` +/// # use bevy::prelude::World; +/// fn pow2(n: i32) -> i32 { n * n } +/// fn add(a: f32, b: f32) -> f32 { a + b } +/// fn toggle_debug(world: &mut World) { /* ... */ } /// /// # use bevy_dev_console::register; /// # let mut environment = bevy_dev_console::builtin_parser::Environment::default(); /// register!(environment => { -/// fn a; -/// fn b; -/// fn c; +/// fn pow2; +/// fn add; +/// fn toggle_debug as "tdbg"; /// }); /// ``` #[macro_export] macro_rules! register { { - $environment:expr => fn $fn_name:ident; + $environment:expr => fn $fn_name:ident } => { $environment .register_fn(stringify!($fn_name), $fn_name) @@ -55,144 +63,6 @@ macro_rules! register { }; } -/// Get around implementation of Result causing stupid errors -pub(super) struct ResultContainer(pub Result); - -impl> From for ResultContainer { - fn from(value: T) -> Self { - ResultContainer(Ok(value.into())) - } -} -impl From> for Result { - fn from(ResultContainer(result): ResultContainer) -> Self { - result - } -} -impl, E> From> for ResultContainer { - fn from(result: Result) -> Self { - ResultContainer(result.map(|v| v.into())) - } -} -/// A parameter in a [`Function`]. -pub trait FunctionParam: Sized { - /// TODO: Add `Self` as default when gets merged - type Item<'world, 'env, 'reg>; - /// Whether this parameter requires a [`Spanned`]. - /// If `false` then `FunctionParam::get`'s `value` will be [`None`], and vice versa. - const USES_VALUE: bool; - - fn get<'world, 'env, 'reg>( - value: Option>, - world: &mut Option<&'world mut World>, - environment: &mut Option<&'env mut Environment>, - registrations: &'reg [&'reg TypeRegistration], - ) -> Result, EvalError>; -} - -pub type FunctionType = dyn FnMut(Vec>, EvalParams) -> Result; -pub struct Function { - pub argument_count: usize, - pub body: Box, -} -impl Debug for Function { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Function") - .field("argument_count", &self.argument_count) - .finish_non_exhaustive() - } -} - -/// Trait that represents a [`Fn`] that can be turned into a [`Function`]. -pub trait IntoFunction { - fn into_function(self) -> Function; -} - -macro_rules! impl_into_function { - ( - $($( - $params:ident - ),+)? - ) => { - #[allow(non_snake_case)] - impl IntoFunction<( $($($params,)+)? )> for F - where - for<'a, 'world, 'env, 'reg> &'a mut F: - FnMut( $($($params),*)? ) -> R + - FnMut( $($(<$params as FunctionParam>::Item<'world, 'env, 'reg>),*)? ) -> R, - R: Into>, - { - fn into_function(mut self) -> Function { - #[allow(unused_variables, unused_mut)] - let body = Box::new(move |args: Vec>, params: EvalParams| { - let EvalParams { - world, - environment, - registrations, - } = params; - let mut args = args.into_iter().map(|expr| { - Ok(Spanned { - span: expr.span.clone(), - value: eval_expression( - expr, - EvalParams { - world, - environment, - registrations, - } - )? - }) - }).collect::, EvalError>>()?.into_iter(); - let world = &mut Some(world); - let environment = &mut Some(environment); - - #[allow(clippy::too_many_arguments)] - fn call_inner>, $($($params),*)?>( - mut f: impl FnMut($($($params),*)?) -> R, - $($($params: $params),*)? - ) -> R { - f($($($params),*)?) - } - call_inner( - &mut self, - $($({ - let arg = if $params::USES_VALUE { - Some(args.next().unwrap()) - } else { - None - }; - - let res = $params::get( - arg, - world, - environment, - registrations - )?; - - res - }),+)? - ) - .into().into() - }); - - let argument_count = $($( - $params::USES_VALUE as usize + - )+)? 0; - - Function { body, argument_count } - } - } - } -} -impl_into_function!(); -impl_into_function!(T1); -impl_into_function!(T1, T2); -impl_into_function!(T1, T2, T3); -impl_into_function!(T1, T2, T3, T4); -impl_into_function!(T1, T2, T3, T4, T5); -impl_into_function!(T1, T2, T3, T4, T5, T6); -impl_into_function!(T1, T2, T3, T4, T5, T6, T7); -impl_into_function!(T1, T2, T3, T4, T5, T6, T7, T8); - /// A variable inside the [`Environment`]. #[derive(Debug)] pub enum Variable { @@ -201,17 +71,14 @@ pub enum Variable { Function(Function), } -/// The environment stores all variables and functions. +/// The environment stores all variables and functions for the builtin parser. pub struct Environment { pub(crate) parent: Option>, pub(crate) variables: HashMap, } impl Default for Environment { fn default() -> Self { - let mut env = Self { - parent: None, - variables: HashMap::new(), - }; + let mut env = Self::empty(); stdlib::register(&mut env); @@ -219,15 +86,61 @@ impl Default for Environment { } } +/// An error that can occur when interacting with a variable in the [`Environment`]. +#[derive(Debug, thiserror::Error)] +pub enum VariableError { + /// The variable was not found. + #[error("Variable `{0}` not found.")] + NotFound(String), + /// The variable was moved and is no longer available. + #[error("variable `{0}` was moved")] + Moved(String), + /// Expected a variable, but found a function. + #[error("expected `{0}` to be a variable, but got a function instead")] + ExpectedVariableGotFunction(String), +} + +/// An error that can occur when running a function in the [`Environment`]. +#[derive(Debug, thiserror::Error)] +pub enum RunFunctionError { + /// The function was not found. + #[error("Function `{0}` not found.")] + NotFound(String), + /// An error occurred while evaluating the function. + #[error(transparent)] + Eval(#[from] Diagnostic), +} + +impl From for Diagnostic { + fn from(value: RunFunctionError) -> Self { + match value { + RunFunctionError::NotFound(name) => { + Diagnostic::empty(VariableError::NotFound(name).into()) + } + RunFunctionError::Eval(diag) => diag, + } + } +} + impl Environment { + /// A completely empty [`Environment`] without any standard library. + #[must_use] + pub fn empty() -> Self { + Self { + parent: None, + variables: HashMap::new(), + } + } + /// Set a variable. pub fn set(&mut self, name: impl Into, value: UniqueRc) { self.variables.insert(name.into(), Variable::Unmoved(value)); } /// Returns a reference to a function if it exists. + #[must_use] pub fn get_function(&self, name: &str) -> Option<&Function> { - let (env, _) = self.resolve(name, 0..0).ok()?; + let env = self.resolve(name)?; match env.variables.get(name) { Some(Variable::Function(function)) => Some(function), @@ -239,16 +152,13 @@ impl Environment { &mut self, name: &str, function: impl FnOnce(&mut Self, &mut Function) -> T, - ) -> T { - let (env, _) = self.resolve_mut(name, 0..0).unwrap(); - - let return_result; + ) -> Option { + let env = self.resolve_mut(name)?; let var = env.variables.get_mut(name); + let return_result; let fn_obj = match var { - Some(Variable::Function(_)) => { - let Variable::Function(mut fn_obj) = - std::mem::replace(var.unwrap(), Variable::Moved) - else { + Some(var @ Variable::Function(_)) => { + let Variable::Function(mut fn_obj) = std::mem::replace(var, Variable::Moved) else { unreachable!() }; @@ -256,25 +166,27 @@ impl Environment { fn_obj } - _ => unreachable!(), + _ => return None, }; let var = env.variables.get_mut(name); let _ = std::mem::replace(var.unwrap(), Variable::Function(fn_obj)); - return_result + Some(return_result) } + /// Returns a reference to a variable. - pub fn get(&self, name: &str, span: Span) -> Result<&UniqueRc, EvalError> { - let (env, span) = self.resolve(name, span)?; + pub fn get_variable(&self, name: &str) -> Result<&UniqueRc, VariableError> { + let Some(var) = self.get(name) else { + return Err(VariableError::NotFound(name.to_owned())); + }; - match env.variables.get(name) { - Some(Variable::Unmoved(value)) => Ok(value), - Some(Variable::Moved) => Err(EvalError::VariableMoved(span.wrap(name.to_string()))), - Some(Variable::Function(_)) => Err(EvalError::ExpectedVariableGotFunction( - span.wrap(name.to_owned()), - )), - None => Err(EvalError::VariableNotFound(span.wrap(name.to_string()))), + match var { + Variable::Unmoved(value) => Ok(value), + Variable::Moved => Err(VariableError::Moved(name.to_owned())), + Variable::Function(_) => { + Err(VariableError::ExpectedVariableGotFunction(name.to_owned())) + } } } @@ -282,15 +194,17 @@ impl Environment { /// /// However it will no longer be able to be used unless it's a [`Value::None`], /// [`Value::Boolean`], or [`Value::Number`] in which case it will be copied. - pub fn move_var(&mut self, name: &str, span: Span) -> Result { - let (env, span) = self.resolve_mut(name, span)?; - - match env.variables.get_mut(name) { - Some(Variable::Moved) => Err(EvalError::VariableMoved(span.wrap(name.to_string()))), - Some(Variable::Function(_)) => Err(EvalError::ExpectedVariableGotFunction( - span.wrap(name.to_owned()), - )), - Some(variable_reference) => { + pub fn move_var(&mut self, name: &str) -> Result { + let Some(var) = self.get_mut(name) else { + return Err(VariableError::NotFound(name.to_owned())); + }; + + match var { + Variable::Moved => Err(VariableError::Moved(name.to_owned())), + Variable::Function(_) => { + Err(VariableError::ExpectedVariableGotFunction(name.to_owned())) + } + variable_reference @ Variable::Unmoved(_) => { let Variable::Unmoved(reference) = variable_reference else { unreachable!() }; @@ -308,39 +222,48 @@ impl Environment { }; Ok(value.into_inner()) } - None => Err(EvalError::VariableNotFound(span.wrap(name.to_string()))), } } - fn resolve(&self, name: &str, span: Span) -> Result<(&Self, Span), EvalError> { - if self.variables.contains_key(name) { - return Ok((self, span)); + fn get(&self, name: &str) -> Option<&Variable> { + if let Some(var) = self.variables.get(name) { + return Some(var); } - match &self.parent { - Some(parent) => parent.resolve(name, span), - None => Err(EvalError::VariableNotFound(span.wrap(name.to_string()))), + self.parent.as_ref()?.get(name) + } + fn get_mut(&mut self, name: &str) -> Option<&mut Variable> { + if let Some(var) = self.variables.get_mut(name) { + return Some(var); } + + self.parent.as_mut()?.get_mut(name) } - fn resolve_mut(&mut self, name: &str, span: Span) -> Result<(&mut Self, Span), EvalError> { + fn resolve(&self, name: &str) -> Option<&Self> { if self.variables.contains_key(name) { - return Ok((self, span)); + return Some(self); } - match &mut self.parent { - Some(parent) => parent.resolve_mut(name, span), - None => Err(EvalError::VariableNotFound(span.wrap(name.to_string()))), + self.parent.as_ref()?.resolve(name) + } + fn resolve_mut(&mut self, name: &str) -> Option<&mut Self> { + if self.variables.contains_key(name) { + return Some(self); } + + self.parent.as_mut()?.resolve_mut(name) } /// Registers a function for use inside the language. /// - /// All parameters must implement [`FunctionParam`]. - /// There is a limit of 8 parameters. + /// All parameters must implement [`FunctionParam`](super::function::FunctionParam). + /// There is a limit of 15 parameters. /// /// The return value of the function must implement [`Into`] /// - /// You should take a look at the [Standard Library](super::stdlib) for examples. + /// You should take a look at the [Standard Library] for examples. + /// + /// [Standard Library](https://github.com/doonv/bevy_dev_console/blob/master/src/builtin_parser/runner/stdlib.rs) pub fn register_fn( &mut self, name: impl Into, @@ -355,10 +278,41 @@ impl Environment { self } + + pub fn run_function( + &mut self, + name: &str, + arguments: Vec>, + world: &mut World, + registrations: &[&TypeRegistration], + ) -> Result { + self.function_scope(name, move |environment, function| { + (function.body)( + arguments, + EvalParams { + world, + environment, + registrations, + }, + ) + }) + .ok_or_else(|| RunFunctionError::NotFound(name.to_owned()))? + .map_err(RunFunctionError::Eval) + } + /// Iterate over all the variables and functions in the current scope of the environment. /// /// Does not include variables and functions from higher scopes. - pub fn iter(&self) -> std::collections::hash_map::Iter { + #[must_use] + pub fn iter(&self) -> std::collections::hash_map::Iter<'_, String, Variable> { self.variables.iter() } } + +impl<'a> IntoIterator for &'a Environment { + type Item = (&'a String, &'a Variable); + type IntoIter = std::collections::hash_map::Iter<'a, String, Variable>; + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} diff --git a/src/builtin_parser/runner/error.rs b/src/builtin_parser/runner/error.rs index 6892de4..6d5a701 100644 --- a/src/builtin_parser/runner/error.rs +++ b/src/builtin_parser/runner/error.rs @@ -1,239 +1,138 @@ use std::borrow::Cow; +use std::fmt; -use logos::Span; +use crate::builtin_parser::NumberError; +use crate::builtin_parser::parser::{AccessKind, ExpressionKind, UnaryOperator}; +use crate::builtin_parser::runner::environment::VariableError; +use crate::builtin_parser::runner::value::ValueKind; +use bevy::reflect::ApplyError; -use crate::builtin_parser::number::Number; -use crate::builtin_parser::parser::Access; -use crate::builtin_parser::Spanned; -use crate::command::{CommandHint, CommandHintColor}; - -use super::Value; - -/// An error occurring during the while executing the [`AST`](Ast) of the command. -#[derive(Debug)] +/// An error occurring during the while evaluating the command. +#[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum EvalError { /// A custom text message. Contains very little contextual information, try to find an existing error instead. - Custom { - /// The text of the message - text: Cow<'static, str>, - span: Span, - }, - InvalidOperation { - left: Number, - right: Number, - operation: &'static str, - span: Span, - }, - VariableNotFound(Spanned), - ExpectedNumberAfterUnaryOperator(Spanned), - CannotIndexValue(Spanned), - ReferenceToMovedData(Span), - VariableMoved(Spanned), - CannotDereferenceValue(Spanned<&'static str>), - CannotBorrowValue(Spanned<&'static str>), - IncompatibleReflectTypes { - expected: String, - actual: String, - span: Span, - }, - EnumVariantNotFound(Spanned), - CannotMoveOutOfResource(Spanned), - CannotNegateUnsignedInteger(Spanned), - IncompatibleNumberTypes { - left: &'static str, - right: &'static str, - span: Span, + #[error("{0}")] + Custom(Cow<'static, str>), + + #[error(transparent)] + Variable(#[from] VariableError), + + #[error( + "cannot apply unary operator `{operator}` to type `{operand}`. the supported types are: {}", + FancyJoin(accepted) + )] + InvalidUnaryOperation { + operator: UnaryOperator, + operand: ValueKind, + accepted: &'static [ValueKind], }, - IncompatibleFunctionParameter { - expected: &'static str, - actual: &'static str, - span: Span, + + #[error("Cannot index `{0}` with a member expression.")] + CannotIndexValue(ValueKind), + + #[error("Cannot access reference to moved data.")] + ReferenceToMovedData, + + #[error("Cannot dereference {0}.")] + CannotDereferenceValue(ValueKind), + + #[error("Cannot dereference {0}.")] + CannotDereferenceValueExpr(ExpressionKind), + + #[error("Cannot borrow {0}. Only variables can be borrowed.")] + CannotBorrowValue(ExpressionKind), + + #[error("Cannot set incompatible reflect types. Expected `{expected}`, got `{actual}`")] + IncompatibleReflectTypes { expected: String, actual: String }, + + #[error("Enum variant `{0}` was not found.")] + EnumVariantNotFound(String), + + #[error("cannot move out of resource `{0}`, try borrowing it instead")] + CannotMoveOutOfResource(String), + + #[error(transparent)] + Number(#[from] NumberError), + + #[error("mismatched function parameter type. expected {expected:#} but got {actual:#}")] + IncorrectFunctionParameterType { + expected: ValueKind, + actual: ValueKind, }, + + #[error("Field `{field_name}` doesn't exist on struct variant `{variant_name}`")] EnumVariantStructFieldNotFound { field_name: String, variant_name: String, - span: Span, }, - ExpectedVariableGotFunction(Spanned), - CannotReflectReference(Span), - CannotReflectResource(Span), + + #[error("cannot reflect a reference. Try dereferencing it instead")] + CannotReflectReference, + + #[error("cannot reflecting resources is not possible at the moment")] + CannotReflectResource, + + #[error("field `{field_index}` doesn't exist on tuple variant `{variant_name}`")] EnumVariantTupleFieldNotFound { - span: Span, field_index: usize, variant_name: String, }, + + #[error( + "expected {got} access to access {expected_type} but got {:#}", + format_expected_access(expected_access) + )] IncorrectAccessOperation { - span: Span, - expected_access: &'static [&'static str], + expected_access: &'static [AccessKind], expected_type: &'static str, - got: Access, + got: AccessKind, }, - FieldNotFoundInStruct(Spanned), + + #[error("field {0} not found in struct")] + FieldNotFoundInStruct(String), + + #[error("field {field_index} is out of bounds for tuple of size {tuple_size}")] FieldNotFoundInTuple { - span: Span, field_index: usize, tuple_size: usize, }, + + #[error("error while applying value (todo make this error better): {0}")] + ApplyError(#[from] ApplyError), } -impl EvalError { - /// Get all the locations of the error in the source. - pub fn spans(&self) -> Vec { - use EvalError as E; - - match self { - E::Custom { span, .. } => vec![span.clone()], - E::VariableNotFound(Spanned { span, .. }) => vec![span.clone()], - E::ExpectedNumberAfterUnaryOperator(Spanned { span, .. }) => vec![span.clone()], - E::CannotIndexValue(Spanned { span, .. }) => vec![span.clone()], - E::FieldNotFoundInStruct(Spanned { span, value: _ }) => vec![span.clone()], - E::CannotDereferenceValue(Spanned { span, .. }) => vec![span.clone()], - E::ReferenceToMovedData(span) => vec![span.clone()], - E::VariableMoved(Spanned { span, .. }) => vec![span.clone()], - E::CannotBorrowValue(Spanned { span, .. }) => vec![span.clone()], - E::IncompatibleReflectTypes { span, .. } => vec![span.clone()], - E::EnumVariantNotFound(Spanned { span, .. }) => vec![span.clone()], - E::EnumVariantStructFieldNotFound { span, .. } => vec![span.clone()], - E::EnumVariantTupleFieldNotFound { span, .. } => vec![span.clone()], - E::CannotMoveOutOfResource(Spanned { span, .. }) => vec![span.clone()], - E::CannotNegateUnsignedInteger(Spanned { span, .. }) => vec![span.clone()], - E::IncompatibleNumberTypes { span, .. } => vec![span.clone()], - E::IncompatibleFunctionParameter { span, .. } => vec![span.clone()], - E::ExpectedVariableGotFunction(Spanned { span, .. }) => vec![span.clone()], - E::CannotReflectReference(span) => vec![span.clone()], - E::CannotReflectResource(span) => vec![span.clone()], - E::InvalidOperation { span, .. } => vec![span.clone()], - E::IncorrectAccessOperation { span, .. } => vec![span.clone()], - E::FieldNotFoundInTuple { span, .. } => vec![span.clone()], - } - } - /// Returns all the hints for this error. - pub fn hints(&self) -> Vec { - self.spans() - .into_iter() - .map(|span| CommandHint::new(span, CommandHintColor::Error, self.to_string())) - .collect() - } +fn format_expected_access(expected_access: &[AccessKind]) -> String { + expected_access + .iter() + .map(|kind| kind.as_natural()) + .collect::>() + .join(" and ") } -impl std::fmt::Display for EvalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use EvalError as E; +struct FancyJoin<'a, T: fmt::Display>(&'a [T]); - match self { - E::Custom { text, .. } => f.write_str(text), - E::VariableNotFound(Spanned { value, .. }) => { - write!(f, "Variable `{value}` not found.") - } - E::ExpectedNumberAfterUnaryOperator(Spanned { value, .. }) => write!( - f, - "Expected a number after unary operator (-) but got {} instead.", - value.natural_kind() - ), - E::CannotIndexValue(Spanned { span: _, value }) => { - write!(f, "Cannot index {} with a member expression.", value.kind()) - } - E::ReferenceToMovedData(_) => write!(f, "Cannot access reference to moved data."), - E::VariableMoved(Spanned { value, .. }) => { - write!(f, "Variable `{value}` was moved.") - } - E::CannotDereferenceValue(Spanned { value: kind, .. }) => { - write!(f, "Cannot dereference {kind}.") - } - E::CannotBorrowValue(Spanned { value: kind, .. }) => { - write!(f, "Cannot borrow {kind}. Only variables can be borrowed.") +impl<'a, T: fmt::Display> fmt::Display for FancyJoin<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, item) in self.0.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; } - E::IncompatibleReflectTypes { - expected, actual, .. - } => write!( - f, - "Cannot set incompatible reflect types. Expected `{expected}`, got `{actual}`" - ), - E::EnumVariantNotFound(Spanned { value: name, .. }) => { - write!(f, "Enum variant `{name}` was not found.") + if i == self.0.len() - 1 { + f.write_str("and ")?; } - E::EnumVariantStructFieldNotFound { - field_name, - variant_name, - .. - } => write!( - f, - "Field `{field_name}` doesn't exist on struct variant `{variant_name}`." - ), - E::EnumVariantTupleFieldNotFound { - field_index, - variant_name, - .. - } => write!( - f, - "Field `{field_index}` doesn't exist on tuple variant `{variant_name}`." - ), - E::CannotMoveOutOfResource(Spanned { value, .. }) => write!( - f, - "Cannot move out of resource `{value}`, try borrowing it instead." - ), - E::CannotNegateUnsignedInteger(Spanned { value, .. }) => write!( - f, - "Unsigned integers cannot be negated. (Type: {})", - value.natural_kind() - ), - E::IncompatibleNumberTypes { left, right, .. } => write!( - f, - "Incompatible number types; `{left}` and `{right}` are incompatible." - ), - E::IncompatibleFunctionParameter { - expected, actual, .. - } => write!( - f, - "Mismatched function parameter type. Expected {expected} but got {actual}" - ), - E::ExpectedVariableGotFunction(Spanned { value, .. }) => write!( - f, - "Expected `{value}` to be a variable, but got a function instead." - ), - E::CannotReflectReference(_) => { - write!( - f, - "Cannot reflect a reference. Try dereferencing it instead." - ) + if f.alternate() { + write!(f, "{:#}", item)?; + } else { + write!(f, "{}", item)?; } - E::CannotReflectResource(_) => { - write!( - f, - "Cannot reflecting resources is not possible at the moment." - ) - } - E::InvalidOperation { - left, - right, - operation, - span: _, - } => write!(f, "Invalid operation: Cannot {operation} {left} by {right}"), - E::IncorrectAccessOperation { - expected_access, - expected_type, - got, - span: _, - } => write!( - f, - "Expected {} access to access {expected_type} but got {}", - expected_access.join(" and "), - got.natural_kind() - ), - E::FieldNotFoundInStruct(Spanned { span: _, value }) => { - write!(f, "Field {value} not found in struct") - } - E::FieldNotFoundInTuple { - field_index, - tuple_size, - span: _, - } => write!( - f, - "Field {field_index} is out of bounds for tuple of size {tuple_size}" - ), } + Ok(()) } } -impl std::error::Error for EvalError {} +impl From<&'static str> for EvalError { + fn from(value: &'static str) -> Self { + Self::Custom(value.into()) + } +} diff --git a/src/builtin_parser/runner/eval.rs b/src/builtin_parser/runner/eval.rs new file mode 100644 index 0000000..08ca262 --- /dev/null +++ b/src/builtin_parser/runner/eval.rs @@ -0,0 +1,564 @@ +//! Evaluation of expressions + +use std::collections::HashMap; + +use bevy::prelude::*; +use bevy::reflect::{DynamicEnum, DynamicTuple, ReflectMut, TypeInfo, VariantInfo}; +use kinded::Kinded; + +use crate::builtin_parser::parser::{BinaryOperator, Expression, UnaryOperator}; +use crate::builtin_parser::runner::value::ValueKind; +use crate::builtin_parser::{Diagnostic, ErrorExtension, SpanExtension, Spanned, StrongRef}; + +use super::EvalParams; +use super::environment::{RunFunctionError, VariableError}; +use super::error::EvalError; +use super::member::{Path, eval_member_expression, eval_path}; +use super::reflection::{CreateRegistration, IntoResource, object_to_dynamic_struct}; +use super::unique_rc::UniqueRc; +use super::value::Value; + +pub fn eval_expression( + expr: Spanned, + EvalParams { + world, + environment, + registrations, + }: EvalParams, +) -> Result> { + match expr.value { + Expression::VarAssign { + name, + value: value_expr, + } => match eval_path( + *name, + EvalParams { + world, + environment, + registrations, + }, + )? + .value + { + Path::Variable(variable) => { + let value = eval_expression( + *value_expr, + EvalParams { + world, + environment, + registrations, + }, + )?; + + match variable.upgrade() { + Some(strong) => *strong.borrow_mut() = value, + None => { + return Err(expr + .span + .wrap(EvalError::Custom("cannot assign to moved variable".into())) + .into()); + } + } + + Ok(Value::Reference(variable)) + } + Path::NewVariable(variable) => { + let value = eval_expression( + *value_expr, + EvalParams { + world, + environment, + registrations, + }, + )?; + let rc = UniqueRc::new(value); + let weak = rc.borrow(); + + environment.set(variable, rc); + + Ok(Value::Reference(weak)) + } + Path::Resource(resource) => { + let registration = registrations.create_registration(resource.id); + let mut dyn_reflect = resource.mut_dyn_reflect(world, registration); + + let reflect = dyn_reflect + .reflect_path_mut(resource.path.as_str()) + .unwrap(); + + #[expect(clippy::single_match_else, reason = "more should be added later")] + match reflect.reflect_mut() { + ReflectMut::Enum(dyn_enum) => { + let TypeInfo::Enum(enum_info) = registration.type_info() else { + unreachable!() + }; + let Spanned { span, value } = *value_expr; + match value { + Expression::Variable(name) => { + let variant_info = match enum_info.variant(&name) { + Some(variant_info) => variant_info, + None => { + return Err(span + .wrap(EvalError::EnumVariantNotFound(name)) + .into()); + } + }; + let VariantInfo::Unit(_) = variant_info else { + return Err(span + .wrap(EvalError::Custom( + format!("Enum variant {name} is not a unit variant") + .into(), + )) + .into()); + }; + + let new_enum = DynamicEnum::new(name, ()); + + dyn_enum.apply(&new_enum); + } + Expression::StructObject { name, map } => { + let variant_info = match enum_info.variant(&name) { + Some(variant_info) => variant_info, + None => { + return Err(span + .wrap(EvalError::EnumVariantNotFound(name)) + .into()); + } + }; + let VariantInfo::Struct(variant_info) = variant_info else { + return Err(span + .wrap(EvalError::Custom( + format!("Enum variant {name} is not a struct variant") + .into(), + )) + .into()); + }; + + let map: HashMap<_, _> = map + .into_iter() + .map(|(k, v)| { + let ty = match variant_info.field(&k) { + Some(field) => Ok(field.type_path_table().short_path()), + None => Err(span.clone().diagnose( + EvalError::EnumVariantStructFieldNotFound { + field_name: k.clone(), + variant_name: name.clone(), + }, + )), + }?; + + let span = v.span.clone(); + + Ok(( + k, + ( + eval_expression( + v, + EvalParams { + world, + environment, + registrations, + }, + )?, + span, + ty, + ), + )) + }) + .collect::>>()?; + + let new_enum = + DynamicEnum::new(name, object_to_dynamic_struct(map)?); + + let mut dyn_reflect = + resource.mut_dyn_reflect(world, registrations); + + let dyn_enum = dyn_reflect + .reflect_path_mut(resource.path.as_str()) + .unwrap(); + + dyn_enum.apply(&new_enum); + } + Expression::StructTuple { name, tuple } => { + let variant_info = match enum_info.variant(&name) { + Some(variant_info) => variant_info, + None => { + return Err(span + .wrap(EvalError::EnumVariantNotFound(name)) + .into()); + } + }; + let VariantInfo::Tuple(variant_info) = variant_info else { + return Err(span + .wrap(EvalError::Custom( + format!("Enum variant {name} is not a tuple variant") + .into(), + )) + .into()); + }; + + let tuple = eval_tuple( + tuple, + EvalParams { + world, + environment, + registrations, + }, + )?; + + let mut dynamic_tuple = DynamicTuple::default(); + + for (index, element) in tuple.into_vec().into_iter().enumerate() { + let ty = match variant_info.field_at(index) { + Some(field) => Ok(field.type_path_table().short_path()), + None => Err(span.clone().diagnose( + EvalError::EnumVariantTupleFieldNotFound { + field_index: index, + variant_name: name.clone(), + }, + )), + }?; + + dynamic_tuple.insert_boxed( + element + .value + .into_inner() + .reflect(element.span, ty)? + .into_partial_reflect(), + ); + } + + let new_enum = DynamicEnum::new(name, dynamic_tuple); + + let mut dyn_reflect = + resource.mut_dyn_reflect(world, registrations); + + let dyn_enum = dyn_reflect + .reflect_path_mut(resource.path.as_str()) + .unwrap(); + + dyn_enum.apply(&new_enum); + } + _ => { + return Err(span + .wrap(EvalError::Custom( + "Unsupported enum variant assignment".into(), + )) + .into()); + } + } + } + _ => { + let span = value_expr.span.clone(); + let ty = reflect.reflect_short_type_path().to_owned(); + let value = eval_expression( + *value_expr, + EvalParams { + world, + environment, + registrations, + }, + )?; + let value_reflect = value.reflect(span.clone(), &ty)?; + + let mut dyn_reflect = resource.mut_dyn_reflect(world, registrations); + + let reflect = dyn_reflect + .reflect_path_mut(resource.path.as_str()) + .unwrap(); + + reflect + .try_apply(value_reflect.as_partial_reflect()) + .map_err(|apply_error| { + span.diagnose(EvalError::ApplyError(apply_error)) + })?; + } + } + + Ok(Value::Resource(resource)) + } + }, + Expression::String(string) => Ok(Value::String(string)), + Expression::Number(number) => Ok(Value::Number(number)), + Expression::Variable(variable) => { + if registrations + .iter() + .any(|v| v.type_info().type_path_table().short_path() == variable) + { + Err(expr + .span + .wrap(EvalError::CannotMoveOutOfResource(variable)) + .into()) + } else { + environment.move_var(&variable).diagnosed(expr.span) + } + } + Expression::StructObject { name, map } => { + let hashmap = eval_object( + map, + EvalParams { + world, + environment, + registrations, + }, + )?; + Ok(Value::StructObject { name, map: hashmap }) + } + Expression::Object(map) => { + let hashmap = eval_object( + map, + EvalParams { + world, + environment, + registrations, + }, + )?; + Ok(Value::Object(hashmap)) + } + Expression::Tuple(tuple) => { + let tuple = eval_tuple( + tuple, + EvalParams { + world, + environment, + registrations, + }, + )?; + Ok(Value::Tuple(tuple)) + } + Expression::StructTuple { name, tuple } => { + let tuple = eval_tuple( + tuple, + EvalParams { + world, + environment, + registrations, + }, + )?; + Ok(Value::StructTuple { name, tuple }) + } + Expression::BinaryOp { + left, + operator, + right, + } => { + let left = eval_expression( + *left, + EvalParams { + world, + environment, + registrations, + }, + )?; + let right = eval_expression( + *right, + EvalParams { + world, + environment, + registrations, + }, + )?; + + match (left, right) { + (Value::Number(left), Value::Number(right)) => Ok(Value::Number(match operator { + BinaryOperator::Add => (left + right).diagnosed(expr.span)?, + BinaryOperator::Sub => (left - right).diagnosed(expr.span)?, + BinaryOperator::Mul => (left * right).diagnosed(expr.span)?, + BinaryOperator::Div => (left / right).diagnosed(expr.span)?, + BinaryOperator::Mod => (left % right).diagnosed(expr.span)?, + BinaryOperator::And => (left & right).diagnosed(expr.span)?, + BinaryOperator::Xor => (left ^ right).diagnosed(expr.span)?, + BinaryOperator::Or => (left | right).diagnosed(expr.span)?, + })), + (left, right) => Err(expr + .span + .wrap(EvalError::Custom( + format!("Unsupported binary operation between {left:?} and {right:?}") + .into(), + )) + .into()), + } + } + Expression::ForLoop { + index_name, + loop_count, + block, + } => Err(expr + .span + .wrap(EvalError::Custom( + format!( + "For loops are not yet implemented: {index_name}, {loop_count}, {block:#?}" + ) + .into(), + )) + .into()), + Expression::Member { left, right } => eval_member_expression( + *left, + right, + EvalParams { + world, + environment, + registrations, + }, + ), + Expression::UnaryOp { operator, operand } => { + let value = eval_expression( + *operand, + EvalParams { + world, + environment, + registrations, + }, + )?; + match operator { + UnaryOperator::Minus => { + if let Value::Number(number) = value { + Ok(Value::Number((-number).diagnosed(expr.span)?)) + } else { + Err(expr.span.diagnose(EvalError::InvalidUnaryOperation { + operator, + operand: value.kind(), + accepted: &[ValueKind::AnyNumber], + })) + } + } + UnaryOperator::Not => match value { + Value::Boolean(boolean) => Ok(Value::Boolean(!boolean)), + Value::Number(number) => Ok(Value::Number((!number).diagnosed(expr.span)?)), + _ => Err(expr + .span + .wrap(EvalError::InvalidUnaryOperation { + operator, + operand: value.kind(), + accepted: &[ValueKind::Boolean, ValueKind::AnyInteger], + }) + .into()), + }, + } + } + Expression::Dereference(inner) => { + if let Expression::Variable(variable) = inner.value { + let var = environment.get_variable(&variable).diagnosed(inner.span)?; + match &*var.borrow_inner().borrow() { + Value::Reference(reference) => { + let reference: StrongRef = reference + .upgrade() + .ok_or_else(|| expr.span.wrap(EvalError::ReferenceToMovedData))?; + let owned = reference.borrow().clone(); + Ok(owned) + } + value => Ok(value.clone()), + } + } else { + Err(expr + .span + .wrap(EvalError::CannotDereferenceValueExpr(inner.value.kind())) + .into()) + } + } + Expression::Borrow(inner) => { + if let Expression::Variable(variable) = inner.value { + if let Some(registration) = registrations + .iter() + .find(|v| v.type_info().type_path_table().short_path() == variable) + { + Ok(Value::Resource(IntoResource::new(registration.type_id()))) + } else { + let rc = environment.get_variable(&variable).diagnosed(inner.span)?; + let weak = rc.borrow(); + + Ok(Value::Reference(weak)) + } + } else { + Err(expr + .span + .wrap(EvalError::CannotBorrowValue(inner.value.kind())) + .into()) + } + } + Expression::None => Ok(Value::None), + Expression::Boolean(bool) => Ok(Value::Boolean(bool)), + Expression::Function { name, arguments } => { + let args = arguments + .into_iter() + .map(|expr| { + Ok(Spanned { + span: expr.span.clone(), + value: eval_expression( + expr, + EvalParams { + world, + environment, + registrations, + }, + )?, + }) + }) + .collect::, Diagnostic>>()?; + + environment + .run_function(&name, args, world, registrations) + .map_err(|e| match e { + RunFunctionError::NotFound(name) => { + expr.span.diagnose(VariableError::NotFound(name).into()) + } + RunFunctionError::Eval(diag) => diag, + }) + } + } +} + +pub fn eval_object( + map: HashMap>, + EvalParams { + world, + environment, + registrations, + }: EvalParams, +) -> Result>, Diagnostic> { + let map = map + .into_iter() + .map( + |(key, expr)| -> Result<(String, UniqueRc), Diagnostic> { + Ok(( + key, + UniqueRc::new(eval_expression( + expr, + EvalParams { + world, + environment, + registrations, + }, + )?), + )) + }, + ) + .collect::>()?; + + Ok(map) +} + +pub fn eval_tuple( + tuple: Vec>, + EvalParams { + world, + environment, + registrations, + }: EvalParams, +) -> Result>]>, Diagnostic> { + tuple + .into_iter() + .map(|expr| { + let span = expr.span.clone(); + let value = UniqueRc::new(eval_expression( + expr, + EvalParams { + world, + environment, + registrations, + }, + )?); + Ok(span.wrap(value)) + }) + .collect::>() +} diff --git a/src/builtin_parser/runner/function.rs b/src/builtin_parser/runner/function.rs new file mode 100644 index 0000000..b25a019 --- /dev/null +++ b/src/builtin_parser/runner/function.rs @@ -0,0 +1,220 @@ +//! [`Function`] registration and handling + +use std::fmt::Debug; + +use bevy::ecs::world::World; +use bevy::reflect::TypeRegistration; +use smallvec::SmallVec; +use variadics_please::all_tuples; + +use super::super::{Diagnostic, Spanned}; +use super::environment::Environment; +use super::error::EvalError; +use super::{EvalParams, Value}; + +/// Get around implementation of [`Result`] causing stupid errors +pub(super) struct ResultContainer(pub Result); + +/// Trait for types that can be returned from a registered [`Function`]. +pub(super) trait FunctionReturn { + fn into_result_container(self) -> ResultContainer>; +} + +impl> FunctionReturn for T { + fn into_result_container(self) -> ResultContainer> { + ResultContainer(Ok(self.into())) + } +} + +impl, E: Into>> FunctionReturn for Result { + fn into_result_container(self) -> ResultContainer> { + ResultContainer(self.map(Into::into).map_err(Into::into)) + } +} + +impl From> for Result { + fn from(ResultContainer(result): ResultContainer) -> Self { + result + } +} + +/// A parameter in a [`Function`]. +/// +/// This trait uses a three-step process to allow for parameters like [`&Value`] to be possible. +/// +/// 1. **[`get`](Self::get)**: Gets the parameter data. +/// 2. **[`borrow`](Self::borrow)**: Creates a [`Ref`]/[`RefMut`] of that data, or just transfers the data over. +/// 3. **[`as_arg`](Self::as_arg)**: [`Ref`] is converted into `&T`, or just transfers the data over again. +/// +/// [`&Value`]: Value +/// [`Ref`]: std::cell::Ref +/// [`RefMut`]: std::cell::RefMut +/// [`Ref`]: std::cell::Ref +/// [`Item`]: Self::Item +pub trait FunctionParam: Sized { + /// The data of the parameter. + type State<'world, 'env, 'reg>; + + /// A temporary guard (like a [`Ref`](std::cell::Ref) that must live on the stack + /// while the function is being executed. Or just the same as [`State`](Self::State). + type Guard<'val, 'world, 'env, 'reg>; + + /// The final type passed as an argument to the function. + /// + /// Should always be `Self`, but [associated type defaults are unstable](https://github.com/rust-lang/rust/issues/29661). + type Item<'val, 'world, 'env, 'reg>; // = Self + + /// Decides how long [`get`]' `value` parameter is. See the documentation for [`get`] for details. + /// + /// [`get`]: FunctionParam::get + const PARAMETER_TYPE: ParamType; + + /// Step 1: Initialize the parameter state. + /// + /// Depending on [`PARAMETER_TYPE`](Self::PARAMETER_TYPE) `value` will have different lengths. + /// - [`ParamType::Parameter`] - 0 elements. This parameter is derived from `world`, `environment`, or `registrations`. + /// - [`ParamType::Argument`] - 1 element. This parameter is a function parameter. + /// - [`ParamType::VarArg`] - N elements. Where N is the amount of arguments passed to the function. + fn get<'world, 'env, 'reg>( + value: SmallVec<[Spanned; 1]>, + world: &mut Option<&'world mut World>, + environment: &mut Option<&'env mut Environment>, + registrations: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic>; + + /// Step 2: Create a [`Guard`](Self::Guard) from the [`State`] if needed, otherwise just transfer over the [`State`]. + /// + /// [`State`]: Self::State + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg>; + + /// Step 3: Produce `Self`. + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic>; +} +pub type FunctionType = + dyn FnMut(Vec>, EvalParams) -> Result>; + +pub enum ParamType { + Parameter, + Argument, + VarArg, +} + +/// A builtin-parser `Function` is a function whose every parameter is a [`FunctionParam`], +/// and whose return value is a [`FunctionReturn`]. +pub struct Function { + /// The minimum amount of arguments this function requires. + pub argument_count: usize, + pub body: Box, +} +impl Debug for Function { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Function") + .field("argument_count", &self.argument_count) + .finish_non_exhaustive() + } +} + +/// Trait that represents a [`Fn`] that can be turned into a parser [`Function`]. +pub trait IntoFunction { + fn into_function(self) -> Function; +} + +impl IntoFunction<()> for Function { + fn into_function(self) -> Function { + self + } +} + +macro_rules! impl_into_function { + ( + $(#[$meta:meta])* $($( + $params:ident + ),+)? + ) => { + $(#[$meta])* + #[allow(non_snake_case, reason = "param types (like T0, T1, T2...) are used as variable names")] + impl IntoFunction<( $($($params,)+)? )> for F + where + for<'val, 'world, 'env, 'reg> &'val mut F: + FnMut( $($($params),*)? ) -> R + + FnMut( $($(<$params as FunctionParam>::Item<'val, 'world, 'env, 'reg>),*)? ) -> R, + R: FunctionReturn, + { + #[track_caller] + fn into_function(mut self) -> Function { + #[allow(unused_variables, unused_mut)] + let body = Box::new(move |args: Vec>, params: EvalParams| { + let EvalParams { + world, + environment, + registrations, + } = params; + let mut args = args.into_iter(); + let world = &mut Some(world); + let environment = &mut Some(environment); + + $( + $( + let arg = match $params::PARAMETER_TYPE { + ParamType::Parameter => SmallVec::new(), + ParamType::Argument => SmallVec::from_buf([args.next().unwrap()]), + ParamType::VarArg => SmallVec::from_vec(args.by_ref().collect()), + }; + + let mut $params = $params::get( + arg, + world, + environment, + registrations + )?; + )+ + )? + + $( + $( + let mut $params = $params::borrow(&mut $params); + )+ + )? + + #[allow(clippy::too_many_arguments)] + fn call_inner( + mut f: impl FnMut($($($params),*)?) -> R, + $($($params: $params),*)? + ) -> R { + f($($($params),*)?) + } + + call_inner( + &mut self, + $($( + $params::as_arg(&mut $params)? + ),+)? + ) + .into_result_container().into() + }); + + let argument_count = $($( + match $params::PARAMETER_TYPE { + ParamType::Parameter => 0, + ParamType::Argument => 1, + ParamType::VarArg => 0, + } + + )+)? 0; + + Function { body, argument_count } + } + } + } +} + +all_tuples!( + #[doc(fake_variadic)] + impl_into_function, + 0, + 15, + T +); diff --git a/src/builtin_parser/runner/member.rs b/src/builtin_parser/runner/member.rs index 4043f57..00950d9 100644 --- a/src/builtin_parser/runner/member.rs +++ b/src/builtin_parser/runner/member.rs @@ -1,10 +1,14 @@ //! Evaluation for member expressions and paths -use crate::builtin_parser::parser::{access_unwrap, Access, Expression}; -use crate::builtin_parser::{EvalError, SpanExtension, Spanned, WeakRef}; +use kinded::Kinded; +use crate::builtin_parser::parser::{Access, Expression, access_unwrap}; +use crate::builtin_parser::runner::todo_error; +use crate::builtin_parser::{Diagnostic, EvalError, SpanExtension, Spanned, WeakRef}; + +use super::environment::VariableError; use super::reflection::IntoResource; -use super::{eval_expression, todo_error, EvalParams, Value}; +use super::{EvalParams, Value, eval_expression}; /// Evaluate a member expression. /// @@ -33,7 +37,7 @@ pub fn eval_member_expression( environment, registrations, }: EvalParams, -) -> Result { +) -> Result> { let left_span = left.span.clone(); let span = left.span.start..right.span.end; let left = eval_expression( @@ -48,13 +52,15 @@ pub fn eval_member_expression( match left { Value::Reference(reference) => { let Some(strong) = reference.upgrade() else { - return Err(EvalError::ReferenceToMovedData(left_span)); + return Err(left_span.diagnose(EvalError::ReferenceToMovedData)); }; let reference = strong.borrow(); match &&*reference { Value::Object(map) | Value::StructObject { map, .. } => { access_unwrap!("an object reference", Field(field) = right => { - let value = map.get(&field).ok_or(EvalError::FieldNotFoundInStruct(span.wrap(field)))?; + let value = map.get(&field).ok_or_else(|| { + span.diagnose(EvalError::FieldNotFoundInStruct(field)) + })?; Ok(Value::Reference(value.borrow())) }) @@ -62,10 +68,11 @@ pub fn eval_member_expression( Value::Tuple(tuple) | Value::StructTuple { tuple, .. } => { access_unwrap!("a tuple reference", TupleIndex(index) = right => { let Spanned { span: _, value } = - tuple.get(index).ok_or(EvalError::FieldNotFoundInTuple { - span, - field_index: index, - tuple_size: tuple.len(), + tuple.get(index).ok_or_else(|| { + span.diagnose(EvalError::FieldNotFoundInTuple { + field_index: index, + tuple_size: tuple.len(), + }) })?; Ok(Value::Reference(value.borrow())) @@ -81,30 +88,31 @@ pub fn eval_member_expression( Ok(Value::Resource(resource)) }) } - var => Err(EvalError::CannotIndexValue(left_span.wrap((*var).clone()))), + var => Err(left_span.diagnose(EvalError::CannotIndexValue(var.kind()))), } } Value::Object(mut map) | Value::StructObject { mut map, .. } => { access_unwrap!("an object", Field(field) = right => { - let value = map - .remove(&field) - .ok_or(EvalError::FieldNotFoundInStruct(span.wrap(field)))?; + let value = map.remove(&field).ok_or_else(|| { + span.diagnose(EvalError::FieldNotFoundInStruct(field)) + })?; Ok(value.into_inner()) }) } Value::Tuple(tuple) | Value::StructTuple { tuple, .. } => { - access_unwrap!("a tuple reference", TupleIndex(field_index) = right => { + access_unwrap!("a tuple", TupleIndex(field_index) = right => { let tuple_size = tuple.len(); let Spanned { span: _, value } = tuple .into_vec() .into_iter() .nth(field_index) - .ok_or(EvalError::FieldNotFoundInTuple { - span, - field_index, - tuple_size, + .ok_or_else(|| { + span.diagnose(EvalError::FieldNotFoundInTuple { + field_index, + tuple_size, + }) })?; Ok(value.into_inner()) @@ -118,7 +126,7 @@ pub fn eval_member_expression( Ok(Value::Resource(resource)) }) } - _ => Err(EvalError::CannotIndexValue(left_span.wrap(left))), + _ => Err(left_span.diagnose(EvalError::CannotIndexValue(left.kind()))), } } @@ -147,7 +155,7 @@ pub fn eval_path( environment, registrations, }: EvalParams, -) -> Result, EvalError> { +) -> Result, Diagnostic> { match expr.value { Expression::Variable(variable) => { if let Some(registration) = registrations @@ -158,16 +166,17 @@ pub fn eval_path( span: expr.span, value: Path::Resource(IntoResource::new(registration.type_id())), }) - } else if let Ok(variable) = environment.get(&variable, expr.span.clone()) { - Ok(Spanned { - span: expr.span, - value: Path::Variable(variable.borrow()), - }) } else { - Ok(Spanned { - span: expr.span, - value: Path::NewVariable(variable), - }) + match environment.get_variable(&variable) { + Ok(variable) => Ok(Spanned { + span: expr.span, + value: Path::Variable(variable.borrow()), + }), + _ => Ok(Spanned { + span: expr.span, + value: Path::NewVariable(variable), + }), + } } } Expression::Member { left, right } => { @@ -197,7 +206,7 @@ pub fn eval_path( let weak = match object.get(&field) { Some(rc) => rc.borrow(), None => { - return Err(EvalError::FieldNotFoundInStruct(span.wrap(field))) + return Err(span.diagnose(EvalError::FieldNotFoundInStruct(field))) } }; @@ -210,11 +219,10 @@ pub fn eval_path( let weak = match tuple.get(index) { Some(Spanned { value: rc, span: _ }) => rc.borrow(), None => { - return Err(EvalError::FieldNotFoundInTuple { - span, + return Err(span.wrap(EvalError::FieldNotFoundInTuple { field_index: index, tuple_size: tuple.len(), - }) + }).into()) } }; @@ -231,7 +239,9 @@ pub fn eval_path( Ok(left.span.wrap(Path::Resource(resource))) }) } - Path::NewVariable(name) => Err(EvalError::VariableNotFound(left.span.wrap(name))), + Path::NewVariable(name) => Err(left + .span + .diagnose(EvalError::from(VariableError::NotFound(name)))), } } Expression::Dereference(inner) => { @@ -247,15 +257,15 @@ pub fn eval_path( Path::Variable(value) => { let strong = value .upgrade() - .ok_or(EvalError::ReferenceToMovedData(path.span))?; + .ok_or_else(|| path.span.wrap(EvalError::ReferenceToMovedData))?; let borrow = strong.borrow(); - if let Value::Reference(ref reference) = &*borrow { + if let Value::Reference(reference) = &*borrow { Ok(expr.span.wrap(Path::Variable(reference.clone()))) } else { - Err(EvalError::CannotDereferenceValue( - expr.span.wrap(borrow.natural_kind()), - )) + Err(expr + .span + .diagnose(EvalError::CannotDereferenceValue(borrow.kind()))) } } Path::NewVariable(_) => todo_error!(), diff --git a/src/builtin_parser/runner/reflection.rs b/src/builtin_parser/runner/reflection.rs index 2114f5d..3951d7b 100644 --- a/src/builtin_parser/runner/reflection.rs +++ b/src/builtin_parser/runner/reflection.rs @@ -5,7 +5,7 @@ use bevy::prelude::*; use bevy::reflect::{DynamicStruct, ReflectFromPtr, TypeRegistration}; use logos::Span; -use crate::builtin_parser::EvalError; +use crate::builtin_parser::{Diagnostic, EvalError}; use super::Value; @@ -29,9 +29,8 @@ impl IntoResource { registration: impl CreateRegistration, ) -> &'a dyn Reflect { let registration = registration.create_registration(self.id); - let ref_dyn_reflect = ref_dyn_reflect(world, registration).unwrap(); - ref_dyn_reflect + ref_dyn_reflect(world, registration).unwrap() } pub fn mut_dyn_reflect<'a>( &self, @@ -39,15 +38,14 @@ impl IntoResource { registration: impl CreateRegistration, ) -> Mut<'a, dyn Reflect> { let registration = registration.create_registration(self.id); - let ref_dyn_reflect = mut_dyn_reflect(world, registration).unwrap(); - ref_dyn_reflect + mut_dyn_reflect(world, registration).unwrap() } } pub fn object_to_dynamic_struct( hashmap: HashMap, -) -> Result { +) -> Result> { let mut dynamic_struct = DynamicStruct::default(); for (key, (value, span, reflect)) in hashmap { @@ -61,15 +59,21 @@ pub fn mut_dyn_reflect<'a>( world: &'a mut World, registration: &TypeRegistration, ) -> Option> { - let Some(component_id) = world.components().get_resource_id(registration.type_id()) else { + if let Some(reflect_resource) = registration.data::() { + return reflect_resource.reflect_mut(world).ok(); + } + + // Fallback if #[reflect(Resource)] is missing but the resource exists and is reflected. + let component_id = world.components().get_resource_id(registration.type_id())?; + let resource = world.get_resource_mut_by_id(component_id)?; + let reflect_from_ptr = registration.data::().or_else(|| { error!( - "Couldn't get the component id of the {} resource.", + "The {} type is not reflected (missing #[derive(Reflect)])", registration.type_info().type_path() ); - return None; - }; - let resource = world.get_resource_mut_by_id(component_id).unwrap(); - let reflect_from_ptr = registration.data::().unwrap(); + None + })?; + // SAFETY: from the context it is known that `ReflectFromPtr` was made for the type of the `MutUntyped` let val: Mut = resource.map_unchanged(|ptr| unsafe { reflect_from_ptr.as_reflect_mut(ptr) }); @@ -80,16 +84,22 @@ pub fn ref_dyn_reflect<'a>( world: &'a World, registration: &TypeRegistration, ) -> Option<&'a dyn Reflect> { - let Some(component_id) = world.components().get_resource_id(registration.type_id()) else { + if let Some(reflect_resource) = registration.data::() { + return reflect_resource.reflect(world).ok(); + } + + // Fallback if #[reflect(Resource)] is missing but the resource exists and is reflected. + let component_id = world.components().get_resource_id(registration.type_id())?; + let resource = world.get_resource_by_id(component_id)?; + let reflect_from_ptr = registration.data::().or_else(|| { error!( - "Couldn't get the component id of the {} resource.", + "The {} type is not reflected (missing #[derive(Reflect)])", registration.type_info().type_path() ); - return None; - }; - let resource = world.get_resource_by_id(component_id).unwrap(); - let reflect_from_ptr = registration.data::().unwrap(); - // SAFETY: from the context it is known that `ReflectFromPtr` was made for the type of the `MutUntyped` + None + })?; + + // SAFETY: from the context it is known that `ReflectFromPtr` was made for the type of the `Ptr` let val: &dyn Reflect = unsafe { reflect_from_ptr.as_reflect(resource) }; Some(val) } diff --git a/src/builtin_parser/runner/stdlib.rs b/src/builtin_parser/runner/stdlib.rs index 938df3b..5eb3cee 100644 --- a/src/builtin_parser/runner/stdlib.rs +++ b/src/builtin_parser/runner/stdlib.rs @@ -1,9 +1,16 @@ +//! A "standard library" for the builtin parser, this is a collection of functions that could +//! be useful for whatever you need to do with the builtin parser. + use crate::builtin_parser::runner::environment::Variable; +use crate::builtin_parser::runner::function::Function; +use crate::builtin_parser::{Diagnostic, SpanExtension}; use crate::register; use bevy::ecs::world::World; use bevy::log::info; use bevy::reflect::TypeRegistration; +use kinded::Kinded; use std::cell::Ref; +use std::fmt::Write; use std::ops::Range; mod math; @@ -12,17 +19,19 @@ use super::error::EvalError; use super::{Environment, Spanned, Value}; fn print( - value: Spanned, + values: Vec>, world: &mut World, registrations: &[&TypeRegistration], -) -> Result<(), EvalError> { - match value.value { - Value::String(string) => info!("{string}"), - _ => { - let string = value.value.try_format(value.span, world, registrations)?; - info!("{string}"); - } +) -> Result<(), Diagnostic> { + let mut output = String::new(); + for Spanned { span, value } in values { + let string = match value { + Value::String(string) => string, + _ => value.try_format(span, world, registrations)?, + }; + write!(output, "{string} ").unwrap(); } + info!("{output}"); Ok(()) } @@ -30,14 +39,17 @@ fn dbg(any: Value) { info!("Value::{any:?}"); } -fn ref_depth(Spanned { span, value }: Spanned) -> Result { - fn ref_depth_reference(value: Ref, span: Range) -> Result { +fn ref_depth(Spanned { span, value }: Spanned) -> Result> { + fn ref_depth_reference( + value: Ref, + span: Range, + ) -> Result> { Ok(match &*value { Value::Reference(reference) => { ref_depth_reference( reference .upgrade() - .ok_or(EvalError::ReferenceToMovedData(span.clone()))? + .ok_or_else(|| span.clone().diagnose(EvalError::ReferenceToMovedData))? .borrow(), span, )? + 1 @@ -51,7 +63,7 @@ fn ref_depth(Spanned { span, value }: Spanned) -> Result String { - value.kind().to_string() + value.kind().as_str().to_owned() } /// Disposes of a [`Value`]. fn drop(_: Value) {} -pub fn register(environment: &mut Environment) { +fn alias(from: String, to: String, environment: &mut Environment) -> Result<(), &'static str> { + if environment.get_function(&from).is_none() { + Err("Function doesn't exist")?; + } + + environment.register_fn( + to, + move |arguments: Vec>, + environment: &mut Environment, + world: &mut World, + registrations: &[&TypeRegistration]| { + environment.run_function(&from, arguments, world, registrations) + }, + ); + Ok(()) +} + +fn help(environment: &Environment) { + struct Help<'e>(&'e Environment); + impl<'e> std::fmt::Display for Help<'e> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (name, variable) in self.0 { + if let &Variable::Function(Function { argument_count, .. }) = variable { + write!( + f, + "\n {name} - {argument_count} arg{} - TODO", + if argument_count == 1 { "" } else { "s" } + )?; + } + } + Ok(()) + } + } + info!( + "TODO: Add help text for functions and function signatures {}", + Help(environment) + ); +} + +pub(super) fn register(environment: &mut Environment) { math::register(environment); register!(environment => { @@ -87,5 +138,7 @@ pub fn register(environment: &mut Environment) { fn drop; fn print_env; fn typeof_value as "typeof"; + fn alias; + fn help; }); } diff --git a/src/builtin_parser/runner/stdlib/math.rs b/src/builtin_parser/runner/stdlib/math.rs index a9cafa9..1d0e43d 100644 --- a/src/builtin_parser/runner/stdlib/math.rs +++ b/src/builtin_parser/runner/stdlib/math.rs @@ -1,17 +1,13 @@ -use crate::builtin_parser::{Environment, EvalError, Number, Spanned}; +use crate::builtin_parser::{Environment, Float}; use crate::register; macro_rules! float_calc_op { ($fn:ident, $name:expr) => { - fn $fn(number: Spanned) -> Result { - match number.value { - Number::Float(number) => Ok(Number::Float(number.$fn())), - Number::f32(number) => Ok(Number::f32(number.$fn())), - Number::f64(number) => Ok(Number::f64(number.$fn())), - _ => Err(EvalError::Custom { - text: concat!("Cannot calculate the ", $name, " of a non-float value").into(), - span: number.span, - }), + fn $fn(number: Float) -> Float { + match number { + Float::f32(number) => Float::f32(number.$fn()), + Float::f64(number) => Float::f64(number.$fn()), + Float::Unspecified(number) => Float::Unspecified(number.$fn()), } } }; diff --git a/src/builtin_parser/runner/unique_rc.rs b/src/builtin_parser/runner/unique_rc.rs index f4e5c29..1257122 100644 --- a/src/builtin_parser/runner/unique_rc.rs +++ b/src/builtin_parser/runner/unique_rc.rs @@ -8,7 +8,7 @@ use std::rc::{Rc, Weak}; /// This represents an [`Rc`] that is known to be uniquely owned -- that is, have exactly one strong /// reference. /// -/// **TODO:** This is actually going to be a standard library feature. Use [`alloc::rc::UniqueRc`] when it is stabilized. +/// **TODO:** This is actually going to be a standard library feature. Use [`std::rc::UniqueRc`] when it is stabilized. #[derive(Debug)] pub struct UniqueRc(Rc>); impl UniqueRc { @@ -25,6 +25,7 @@ impl UniqueRc { &self.0 } /// Create a new weak pointer to this [`UniqueRc`]. + #[must_use] pub fn borrow(&self) -> WeakRef { WeakRef::new(self) } @@ -35,6 +36,11 @@ impl UniqueRc { UniqueRc(Rc::new(RefCell::new(value))) } /// Get the inner value (`T`) of this [`UniqueRc`]. + /// + /// # Panics + /// + /// Panics if there is more than one strong pointer to this [`UniqueRc`]. + #[must_use] pub fn into_inner(self) -> T { Rc::try_unwrap(self.0) .unwrap_or_else(|rc| { @@ -46,7 +52,7 @@ impl UniqueRc { .into_inner() } } -impl Clone for UniqueRc { +impl Clone for UniqueRc { fn clone(&self) -> Self { let t = self.borrow_inner().clone().into_inner(); @@ -54,13 +60,13 @@ impl Clone for UniqueRc { } } -impl Deref for UniqueRc { +impl Deref for UniqueRc { type Target = RefCell; fn deref(&self) -> &Self::Target { self.0.as_ref() } } -impl DerefMut for UniqueRc { +impl DerefMut for UniqueRc { fn deref_mut(&mut self) -> &mut Self::Target { Rc::get_mut(&mut self.0).unwrap() } @@ -87,6 +93,7 @@ impl WeakRef { } } /// Converts this [`WeakRef`] into a [`StrongRef`] (may be unsafe, see [`StrongRef`]'s documentation). + #[must_use] pub fn upgrade(&self) -> Option> { Some(StrongRef(self.reference.upgrade()?)) } @@ -114,11 +121,13 @@ impl WeakRef { pub struct StrongRef(Rc>); impl StrongRef { /// Immutably borrows the wrapped value. - pub fn borrow(&self) -> Ref { + #[must_use] + pub fn borrow(&'_ self) -> Ref<'_, T> { self.0.borrow() } /// Mutably borrows the wrapped value. - pub fn borrow_mut(&self) -> RefMut { + #[must_use] + pub fn borrow_mut(&'_ self) -> RefMut<'_, T> { self.0.borrow_mut() } } @@ -128,7 +137,7 @@ mod tests { use super::*; #[test] - #[should_panic] + #[should_panic = "There are 2 strong pointers to a UniqueRc!"] fn strong_ref_panic() { let rc = UniqueRc::new(0); diff --git a/src/builtin_parser/runner/value.rs b/src/builtin_parser/runner/value.rs index 711ed3a..fefcf9e 100644 --- a/src/builtin_parser/runner/value.rs +++ b/src/builtin_parser/runner/value.rs @@ -1,22 +1,22 @@ +//! Runtime values + use std::collections::HashMap; -use std::fmt::Debug; -use crate::builtin_parser::number::Number; -use crate::builtin_parser::{Environment, StrongRef, UniqueRc}; +use bevy::reflect::{DynamicStruct, DynamicTuple, PartialReflect}; +use logos::Span; + +use crate::builtin_parser::number::{Float, Integer, Number, SignedInteger, UnsignedInteger}; +use crate::builtin_parser::{Diagnostic, SpanExtension, Spanned, UniqueRc}; -use super::super::Spanned; -use super::environment::FunctionParam; use super::error::EvalError; -use super::reflection::{CreateRegistration, IntoResource}; +use super::reflection::IntoResource; use super::unique_rc::WeakRef; -use bevy::ecs::world::World; -use bevy::reflect::{ - DynamicStruct, DynamicTuple, GetPath, Reflect, ReflectRef, TypeInfo, TypeRegistration, - VariantInfo, VariantType, -}; +pub mod format; +pub mod function_param; +pub mod kind; -use logos::Span; +pub use kind::ValueKind; /// A runtime value #[derive(Debug, Clone)] @@ -37,6 +37,9 @@ pub enum Value { /// the owner of the value have a strong reference, while every /// other value has a weak reference. This causes /// [`Rc::try_unwrap`] to succeed every time) + /// + /// [`Rc::try_unwrap`]: std::rc::Rc::try_unwrap + /// [`Rc>`]: std::rc::Rc Reference(WeakRef), /// A dynamic [`HashMap`]. Object(HashMap>), @@ -61,16 +64,22 @@ pub enum Value { } impl Value { - /// Converts this value into a [`Box`]. + /// Converts this value into a [`Box`]. /// /// `ty` is used for type inference. - pub fn reflect(self, span: Span, ty: &str) -> Result, EvalError> { + pub fn reflect( + self, + span: Span, + ty: &str, + ) -> Result, Diagnostic> { match self { Value::None => Ok(Box::new(())), - Value::Number(number) => number.reflect(span, ty), + Value::Number(number) => number + .reflect(span, ty) + .map(PartialReflect::into_partial_reflect), Value::Boolean(boolean) => Ok(Box::new(boolean)), Value::String(string) => Ok(Box::new(string)), - Value::Reference(_reference) => Err(EvalError::CannotReflectReference(span)), + Value::Reference(_reference) => Err(span.diagnose(EvalError::CannotReflectReference)), Value::Object(object) | Value::StructObject { map: object, .. } => { let mut dyn_struct = DynamicStruct::default(); @@ -89,333 +98,13 @@ impl Value { Ok(Box::new(dyn_tuple)) } - Value::Resource(_) => Err(EvalError::CannotReflectResource(span)), - } - } - - /// Attempts to format this [`Value`]. - /// - /// Returns an error if the [`Value`] is a reference to moved data. - pub fn try_format( - &self, - span: Span, - world: &World, - registrations: &[&TypeRegistration], - ) -> Result { - const TAB: &str = " "; - match self { - Value::None => Ok(format!("()")), - Value::Number(number) => Ok(format!("{number}")), - Value::Boolean(bool) => Ok(format!("{bool}")), - Value::String(string) => Ok(format!("\"{string}\"")), - Value::Reference(reference) => { - if let Some(rc) = reference.upgrade() { - Ok(rc.borrow().try_format(span, world, registrations)?) - } else { - Err(EvalError::ReferenceToMovedData(span)) - } - } - Value::Object(map) => { - let mut string = String::new(); - string.push('{'); - for (key, value) in map { - string += &format!( - "\n{TAB}{key}: {},", - value.borrow_inner().borrow().try_format( - span.clone(), - world, - registrations - )? - ); - } - if !map.is_empty() { - string.push('\n'); - } - string.push('}'); - Ok(string) - } - Value::StructObject { name, map } => { - let mut string = String::new(); - string += &format!("{name} {{"); - for (key, value) in map { - string += &format!( - "\n{TAB}{key}: {},", - value.borrow_inner().borrow().try_format( - span.clone(), - world, - registrations - )? - ); - } - if !map.is_empty() { - string.push('\n'); - } - string.push('}'); - Ok(string) - } - Value::Tuple(tuple) => { - let mut string = String::new(); - string.push('('); - for element in tuple.iter() { - string += &format!( - "\n{TAB}{},", - element.value.borrow_inner().borrow().try_format( - span.clone(), - world, - registrations - )? - ); - } - if !tuple.is_empty() { - string.push('\n'); - } - string.push(')'); - Ok(string) - } - Value::StructTuple { name, tuple } => { - let mut string = String::new(); - string.push_str(name); - string.push('('); - for element in tuple.iter() { - string += &format!( - "\n{TAB}{},", - element.value.borrow_inner().borrow().try_format( - span.clone(), - world, - registrations - )? - ); - } - if !tuple.is_empty() { - string.push('\n'); - } - string.push(')'); - Ok(string) - } - Value::Resource(resource) => Ok(fancy_debug_print(resource, world, registrations)), - } - } - - /// Returns the kind of [`Value`] as a [string slice](str). - /// You may want to use [`natural_kind`](Self::natural_kind) - /// instead for more natural sounding error messages - pub const fn kind(&self) -> &'static str { - match self { - Value::None => "none", - Value::Number(number) => number.kind(), - Value::Boolean(..) => "boolean", - Value::String(..) => "string", - Value::Reference(..) => "reference", - Value::Object(..) => "object", - Value::StructObject { .. } => "struct object", - Value::Tuple(..) => "tuple", - Value::StructTuple { .. } => "struct tuple", - Value::Resource(..) => "resource", - } - } - - /// Returns the kind of [`Value`] as a [string slice](str) with an `a` or `an` prepended to it. - /// Used for more natural sounding error messages. - pub const fn natural_kind(&self) -> &'static str { - match self { - Value::None => "nothing", - Value::Number(number) => number.natural_kind(), - Value::Boolean(..) => "a boolean", - Value::String(..) => "a string", - Value::Reference(..) => "a reference", - Value::Object(..) => "a object", - Value::StructObject { .. } => "a struct object", - Value::Tuple(..) => "a tuple", - Value::StructTuple { .. } => "a struct tuple", - Value::Resource(..) => "a resource", + Value::Resource(_) => Err(span.diagnose(EvalError::CannotReflectResource)), } } } -/// A massive function that takes in a type registration and the world and then -/// does all the hard work of printing out the type nicely. -fn fancy_debug_print( - resource: &IntoResource, - world: &World, - registrations: &[&TypeRegistration], -) -> String { - const TAB: &str = " "; - let registration = registrations.create_registration(resource.id); - let dyn_reflect = resource.ref_dyn_reflect(world, registration); - - let reflect = dyn_reflect.reflect_path(resource.path.as_str()).unwrap(); - - fn debug_subprint(reflect: &dyn Reflect, indentation: usize) -> String { - let mut f = String::new(); - let reflect_ref = reflect.reflect_ref(); - let indentation_string = TAB.repeat(indentation); - match reflect_ref { - ReflectRef::Struct(struct_info) => { - f += "{\n"; - for i in 0..struct_info.field_len() { - let field = struct_info.field_at(i).unwrap(); - let field_name = struct_info.name_at(i).unwrap(); - - let field_value = debug_subprint(field, indentation + 1); - f += &format!( - "{indentation_string}{TAB}{field_name}: {} = {field_value},\n", - field.reflect_short_type_path(), - ); - } - f += &indentation_string; - f += "}"; - } - ReflectRef::TupleStruct(_) => todo!(), - ReflectRef::Tuple(tuple_info) => { - f += "(\n"; - for field in tuple_info.iter_fields() { - let field_value = debug_subprint(field, indentation + 1); - f += &format!("{indentation_string}{TAB}{field_value},\n",); - } - f += &indentation_string; - f += ")"; - } - ReflectRef::List(_) => todo!(), - ReflectRef::Array(_) => todo!(), - ReflectRef::Map(_) => todo!(), - ReflectRef::Enum(variant) => { - // Print out the enum types - f += variant.variant_name(); - - match variant.variant_type() { - VariantType::Struct => { - f += " {\n"; - for field in variant.iter_fields() { - f += &format!( - "{indentation_string}{TAB}{}: {} = {},\n", - field.name().unwrap(), - field.value().reflect_short_type_path(), - debug_subprint(field.value(), indentation + 1) - ); - } - f += &indentation_string; - f += "}"; - } - VariantType::Tuple => { - f += "(\n"; - for field in variant.iter_fields() { - f += &format!( - "{indentation_string}{TAB}{} = {},\n", - field.value().reflect_short_type_path(), - debug_subprint(field.value(), indentation + 1) - ); - } - f += &indentation_string; - f += ")"; - } - VariantType::Unit => {} - } - } - ReflectRef::Value(_) => { - f += &format!("{reflect:?}"); - } - } - - f - } - - let mut f = String::new(); - let reflect_ref = reflect.reflect_ref(); - match reflect_ref { - ReflectRef::Struct(struct_info) => { - f += &format!("struct {} {{\n", struct_info.reflect_short_type_path()); - for i in 0..struct_info.field_len() { - let field = struct_info.field_at(i).unwrap(); - let field_name = struct_info.name_at(i).unwrap(); - - let field_value = debug_subprint(field, 1); - f += &format!( - "{TAB}{}: {} = {},\n", - field_name, - field.reflect_short_type_path(), - field_value - ); - } - f += "}"; - } - ReflectRef::TupleStruct(_) => todo!(), - ReflectRef::Tuple(_) => todo!(), - ReflectRef::List(_) => todo!(), - ReflectRef::Array(_) => todo!(), - ReflectRef::Map(_) => todo!(), - ReflectRef::Enum(set_variant_info) => { - // Print out the enum types - f += &format!("enum {} {{\n", set_variant_info.reflect_short_type_path()); - let TypeInfo::Enum(enum_info) = registration.type_info() else { - unreachable!() - }; - for variant in enum_info.iter() { - f += "\t"; - f += variant.name(); - match variant { - VariantInfo::Struct(variant) => { - f += " {\n"; - for field in variant.iter() { - f += &format!( - "{TAB}{TAB}{}: {},\n", - field.name(), - field.type_path_table().short_path() - ); - } - f += TAB; - f += "}"; - } - VariantInfo::Tuple(variant) => { - f += "("; - let mut iter = variant.iter(); - if let Some(first) = iter.next() { - f += &format!("{}", first.type_path_table().short_path()); - for field in iter { - f += &format!(", {}", field.type_path_table().short_path()); - } - } - f += ")"; - } - VariantInfo::Unit(_) => {} - } - f += ",\n"; - } - // Print out the current value - f += "} = "; - f += set_variant_info.variant_name(); - match set_variant_info.variant_type() { - VariantType::Struct => { - f += " {\n"; - for field in set_variant_info.iter_fields() { - f += &format!("{TAB}{}: {:?},\n", field.name().unwrap(), field.value()); - } - f += "}"; - } - VariantType::Tuple => { - f += "(\n"; - for field in set_variant_info.iter_fields() { - f += &format!("{TAB}{:?},\n", field.value()); - } - f += ")"; - } - VariantType::Unit => {} - } - } - ReflectRef::Value(value) => { - f += &format!("{value:?}"); - } - } - f -} - -impl From<()> for Value { - fn from((): ()) -> Self { - Value::None - } -} - macro_rules! from_t { - (impl $type:ty: $var:ident => $expr:expr) => { + (impl $type:ty: $var:pat => $expr:expr) => { impl From<$type> for Value { fn from($var: $type) -> Self { $expr @@ -423,19 +112,28 @@ macro_rules! from_t { } }; } -macro_rules! from_number { - ($($number:ident),*$(,)?) => { - $( - from_t!(impl $number: number => Value::Number(Number::$number(number))); - )* - }; -} -from_number!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64); +from_t!(impl (): () => Value::None); +from_t!(impl u8: n => Value::Number(n.into())); +from_t!(impl u16: n => Value::Number(n.into())); +from_t!(impl u32: n => Value::Number(n.into())); +from_t!(impl u64: n => Value::Number(n.into())); +from_t!(impl usize: n => Value::Number(n.into())); +from_t!(impl i8: n => Value::Number(n.into())); +from_t!(impl i16: n => Value::Number(n.into())); +from_t!(impl i32: n => Value::Number(n.into())); +from_t!(impl i64: n => Value::Number(n.into())); +from_t!(impl isize: n => Value::Number(n.into())); +from_t!(impl f32: n => Value::Number(n.into())); +from_t!(impl f64: n => Value::Number(n.into())); from_t!(impl String: string => Value::String(string)); from_t!(impl bool: bool => Value::Boolean(bool)); from_t!(impl Number: number => Value::Number(number)); +from_t!(impl Float: float => Value::Number(Number::Float(float))); +from_t!(impl Integer: integer => Value::Number(Number::Integer(integer))); +from_t!(impl UnsignedInteger: uint => Value::Number(Number::Integer(Integer::Unsigned(uint)))); +from_t!(impl SignedInteger: sint => Value::Number(Number::Integer(Integer::Signed(sint)))); from_t!(impl HashMap>: hashmap => Value::Object(hashmap)); from_t!(impl HashMap: hashmap => Value::Object( hashmap @@ -443,188 +141,3 @@ from_t!(impl HashMap: hashmap => Value::Object( .map(|(k, v)| (k, UniqueRc::new(v))) .collect(), )); - -impl FunctionParam for Spanned { - type Item<'world, 'env, 'reg> = Self; - const USES_VALUE: bool = true; - - fn get<'world, 'env, 'reg>( - value: Option>, - _: &mut Option<&'world mut World>, - _: &mut Option<&'env mut Environment>, - _: &'reg [&'reg TypeRegistration], - ) -> Result, EvalError> { - Ok(value.unwrap()) - } -} -impl, Error = EvalError>> FunctionParam for Spanned { - type Item<'world, 'env, 'reg> = Self; - const USES_VALUE: bool = true; - - fn get<'world, 'env, 'reg>( - value: Option>, - _: &mut Option<&'world mut World>, - _: &mut Option<&'env mut Environment>, - _: &'reg [&'reg TypeRegistration], - ) -> Result, EvalError> { - let value = value.unwrap(); - Ok(Spanned { - span: value.span.clone(), - value: T::try_from(value)?, - }) - } -} -impl FunctionParam for Value { - type Item<'world, 'env, 'reg> = Self; - const USES_VALUE: bool = true; - - fn get<'world, 'env, 'reg>( - value: Option>, - _: &mut Option<&'world mut World>, - _: &mut Option<&'env mut Environment>, - _: &'reg [&'reg TypeRegistration], - ) -> Result, EvalError> { - Ok(value.unwrap().value) - } -} - -macro_rules! impl_function_param_for_value { - (impl $type:ty: $value_pattern:pat => $return:expr) => { - impl FunctionParam for $type { - type Item<'world, 'env, 'reg> = Self; - const USES_VALUE: bool = true; - - fn get<'world, 'env, 'reg>( - value: Option>, - _: &mut Option<&'world mut World>, - _: &mut Option<&'env mut Environment>, - _: &'reg [&'reg TypeRegistration], - ) -> Result, EvalError> { - let value = value.unwrap(); - if let $value_pattern = value.value { - Ok($return) - } else { - Err(EvalError::IncompatibleFunctionParameter { - expected: stringify!($type), - actual: value.value.natural_kind(), - span: value.span, - }) - } - } - } - impl TryFrom> for $type { - type Error = EvalError; - - fn try_from(value: Spanned) -> Result { - if let $value_pattern = value.value { - Ok($return) - } else { - todo!() - } - } - } - }; -} -macro_rules! impl_function_param_for_numbers { - ($generic:ident ($($number:ident),*$(,)?)) => { - $( - impl FunctionParam for $number { - type Item<'world, 'env, 'reg> = Self; - const USES_VALUE: bool = true; - - fn get<'world, 'env, 'reg>( - value: Option>, - _: &mut Option<&'world mut World>, - _: &mut Option<&'env mut Environment>, - _: &'reg [&'reg TypeRegistration], - ) -> Result, EvalError> { - let value = value.unwrap(); - match value.value { - Value::Number(Number::$number(value)) => Ok(value), - Value::Number(Number::$generic(value)) => Ok(value as $number), - _ => Err(EvalError::IncompatibleFunctionParameter { - expected: concat!("a ", stringify!($number)), - actual: value.value.natural_kind(), - span: value.span, - }) - } - } - } - impl TryFrom> for $number { - type Error = EvalError; - - fn try_from(value: Spanned) -> Result { - match value.value { - Value::Number(Number::$number(value)) => Ok(value), - Value::Number(Number::$generic(value)) => Ok(value as $number), - _ => Err(EvalError::IncompatibleFunctionParameter { - expected: concat!("a ", stringify!($number)), - actual: value.value.natural_kind(), - span: value.span - }) - } - } - } - )* - }; -} - -impl_function_param_for_numbers!(Float(f32, f64)); -impl_function_param_for_numbers!(Integer(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize)); - -impl_function_param_for_value!(impl bool: Value::Boolean(boolean) => boolean); -impl_function_param_for_value!(impl Number: Value::Number(number) => number); -impl_function_param_for_value!(impl String: Value::String(string) => string); -// impl_function_param_for_value!(impl HashMap>: Value::Object(object) => object); -impl_function_param_for_value!(impl HashMap: Value::Object(object) => { - object.into_iter().map(|(k, v)| (k, v.into_inner())).collect() -}); -impl_function_param_for_value!(impl StrongRef: Value::Reference(reference) => reference.upgrade().unwrap()); - -impl FunctionParam for &mut World { - type Item<'world, 'env, 'reg> = &'world mut World; - const USES_VALUE: bool = false; - - fn get<'world, 'env, 'reg>( - _: Option>, - world: &mut Option<&'world mut World>, - _: &mut Option<&'env mut Environment>, - _: &'reg [&'reg TypeRegistration], - ) -> Result, EvalError> { - let Some(world) = world.take() else { - // make this unreachable by checking the function when it gets registered - todo!("world borrowed twice"); - }; - - Ok(world) - } -} - -// This probably isn't a good idea. But eh who cares, more power to the user. -impl FunctionParam for &mut Environment { - type Item<'world, 'env, 'reg> = &'env mut Environment; - const USES_VALUE: bool = false; - - fn get<'world, 'env, 'reg>( - _: Option>, - _: &mut Option<&'world mut World>, - environment: &mut Option<&'env mut Environment>, - _: &'reg [&'reg TypeRegistration], - ) -> Result, EvalError> { - Ok(environment.take().unwrap()) - } -} - -impl FunctionParam for &[&TypeRegistration] { - type Item<'world, 'env, 'reg> = &'reg [&'reg TypeRegistration]; - const USES_VALUE: bool = false; - - fn get<'world, 'env, 'reg>( - _: Option>, - _: &mut Option<&'world mut World>, - _: &mut Option<&'env mut Environment>, - registrations: &'reg [&'reg TypeRegistration], - ) -> Result, EvalError> { - Ok(registrations) - } -} diff --git a/src/builtin_parser/runner/value/format.rs b/src/builtin_parser/runner/value/format.rs new file mode 100644 index 0000000..f210240 --- /dev/null +++ b/src/builtin_parser/runner/value/format.rs @@ -0,0 +1,346 @@ +//! Formatting logic for values + +use std::any::TypeId; +use std::fmt::Write; + +use bevy::ecs::world::World; +use bevy::reflect::{ + Enum, EnumInfo, GetPath, PartialReflect, ReflectRef, Struct, TypeInfo, TypeRegistration, + VariantInfo, VariantType, +}; + +use crate::builtin_parser::{ + Diagnostic, EvalError, KEYWORD, MEMBER, STRING, SpanExtension, TYPE, VALUE, VARIANT, +}; + +use super::super::reflection::CreateRegistration; +use super::Value; + +macro_rules! w { + ($f:expr, $($arg:tt)*) => { + { let _ = write!($f.buffer, $($arg)*); } + }; +} + +impl Value { + pub fn try_format( + &self, + span: logos::Span, + world: &World, + registrations: &[&TypeRegistration], + ) -> Result> { + let mut formatter = Formatter { + buffer: String::new(), + indentation: 0, + world, + registrations, + span, + }; + formatter.format_value(self)?; + Ok(formatter.buffer) + } +} + +struct Formatter<'a> { + buffer: String, + indentation: usize, + world: &'a World, + registrations: &'a [&'a TypeRegistration], + span: logos::Span, +} + +impl Formatter<'_> { + const TAB: &'static str = " "; + + fn indent(&mut self) { + for _ in 0..self.indentation { + self.buffer.push_str(Self::TAB); + } + } + + fn format_container( + &mut self, + delims: (&str, &str), + items: I, + mut f: F, + ) -> Result<(), Diagnostic> + where + I: IntoIterator, + F: FnMut(&mut Self, I::Item) -> Result<(), Diagnostic>, + { + let mut iter = items.into_iter().peekable(); + let (open, close) = delims; + + if iter.peek().is_none() { + w!(self, "{open}{close}"); + return Ok(()); + } + + w!(self, "{open}"); + self.indentation += 1; + for item in iter { + w!(self, "\n"); + self.indent(); + f(self, item)?; + w!(self, ","); + } + self.indentation -= 1; + w!(self, "\n"); + self.indent(); + w!(self, "{close}"); + Ok(()) + } + + fn format_value(&mut self, value: &Value) -> Result<(), Diagnostic> { + match value { + Value::None => w!(self, "{KEYWORD}(){KEYWORD:#}"), + Value::Number(number) => w!(self, "{number}"), + Value::Boolean(bool) => w!(self, "{VALUE}{bool}{VALUE:#}"), + Value::String(string) => w!(self, "{STRING}\"{string}\"{STRING:#}"), + Value::Reference(reference) => match reference.upgrade() { + Some(rc) => self.format_value(&rc.borrow())?, + _ => return Err(self.span.clone().diagnose(EvalError::ReferenceToMovedData)), + }, + Value::Object(map) => { + self.format_container(("{", "}"), map.iter(), |f, (key, value)| { + w!(f, "{MEMBER}{key}{MEMBER:#}: "); + f.format_value(&value.borrow_inner().borrow()) + })? + } + Value::StructObject { name, map } => { + w!(self, "{TYPE}{name}{TYPE:#} "); + self.format_container(("{", "}"), map.iter(), |f, (key, value)| { + w!(f, "{MEMBER}{key}{MEMBER:#}: "); + f.format_value(&value.borrow_inner().borrow()) + })?; + } + Value::Tuple(tuple) => { + self.format_container(("(", ")"), tuple.iter(), |f, element| { + f.format_value(&element.value.borrow_inner().borrow()) + })?; + } + Value::StructTuple { name, tuple } => { + w!(self, "{TYPE}{name}{TYPE:#}"); + self.format_container(("(", ")"), tuple.iter(), |f, element| { + f.format_value(&element.value.borrow_inner().borrow()) + })?; + } + Value::Resource(resource) => self.format_resource(resource)?, + } + Ok(()) + } + + fn format_resource( + &mut self, + resource: &crate::builtin_parser::runner::reflection::IntoResource, + ) -> Result<(), Diagnostic> { + let registration = self.registrations.create_registration(resource.id); + let dyn_reflect = resource.ref_dyn_reflect(self.world, registration); + let reflect = dyn_reflect.reflect_path(resource.path.as_str()).unwrap(); + let reflect_ref = reflect.reflect_ref(); + + match reflect_ref { + ReflectRef::Struct(s) => self.format_resource_struct(s), + ReflectRef::Enum(v) => { + let TypeInfo::Enum(enum_info) = registration.type_info() else { + unreachable!("{:?}", registration.type_info()) + }; + self.format_resource_enum(enum_info, v) + } + _ => self.format_reflect(reflect), + } + } + + fn format_resource_struct(&mut self, s: &dyn Struct) -> Result<(), Diagnostic> { + w!( + self, + "{KEYWORD}struct{KEYWORD:#} {TYPE}{}{TYPE:#} ", + s.reflect_short_type_path() + ); + self.format_struct_fields(s) + } + + fn format_resource_enum( + &mut self, + enum_info: &EnumInfo, + v: &dyn Enum, + ) -> Result<(), Diagnostic> { + w!( + self, + "{KEYWORD}enum{KEYWORD:#} {TYPE}{}{TYPE:#} ", + v.reflect_short_type_path() + ); + self.format_enum_variant_definitions(enum_info)?; + + w!(self, " = "); + self.format_resource_enum_variant(v) + } + + fn format_enum_variant_definitions( + &mut self, + enum_info: &EnumInfo, + ) -> Result<(), Diagnostic> { + self.format_container(("{", "}"), enum_info.iter(), |f, variant| { + w!(f, "{VARIANT}{}{VARIANT:#}", variant.name()); + match variant { + VariantInfo::Struct(v) => { + w!(f, " "); + f.format_container(("{", "}"), v.iter(), |f2, field| { + w!(f2, "{MEMBER}{}{MEMBER:#}: ", field.name()); + f2.write_type_path(field.type_path_table().short_path()); + Ok(()) + })?; + } + VariantInfo::Tuple(v) => { + f.format_container(("(", ")"), v.iter(), |f2, field| { + f2.write_type_path(field.type_path_table().short_path()); + Ok(()) + })?; + } + VariantInfo::Unit(_) => {} + } + Ok(()) + }) + } + + fn format_resource_enum_variant(&mut self, v: &dyn Enum) -> Result<(), Diagnostic> { + w!(self, "{VARIANT}{}{VARIANT:#}", v.variant_name()); + match v.variant_type() { + VariantType::Struct => { + w!(self, " "); + self.format_container(("{", "}"), v.iter_fields(), |f, field| { + w!(f, "{MEMBER}{}{MEMBER:#}: ", field.name().unwrap()); + f.format_reflect(field.value()) + })?; + } + VariantType::Tuple => { + self.format_container(("(", ")"), v.iter_fields(), |f, field| { + f.format_reflect(field.value()) + })?; + } + VariantType::Unit => {} + } + Ok(()) + } + + fn format_reflect( + &mut self, + reflect: &dyn PartialReflect, + ) -> Result<(), Diagnostic> { + match reflect.reflect_ref() { + ReflectRef::Struct(s) => self.format_struct_fields(s), + ReflectRef::TupleStruct(s) => { + self.format_container(("(", ")"), s.iter_fields(), |f, field| { + f.format_reflect(field) + }) + } + ReflectRef::Tuple(s) => { + self.format_container(("(", ")"), s.iter_fields(), |f, field| { + f.format_reflect(field) + }) + } + ReflectRef::List(l) => { + self.format_container(("[", "]"), l.iter(), |f, item| f.format_reflect(item)) + } + ReflectRef::Array(a) => { + self.format_container(("[", "]"), a.iter(), |f, item| f.format_reflect(item)) + } + ReflectRef::Map(m) => self.format_container(("{", "}"), m.iter(), |f, (k, v)| { + f.format_reflect(k)?; + w!(f, ": "); + f.format_reflect(v) + }), + ReflectRef::Set(s) => { + self.format_container(("[", "]"), s.iter(), |f, item| f.format_reflect(item)) + } + ReflectRef::Enum(v) => self.format_reflect_enum_variant(v), + ReflectRef::Opaque(reflect) => { + if let Some(id) = reflect + .get_represented_type_info() + .map(|info| info.type_id()) + { + if id == TypeId::of::() { + w!(self, "{STRING}{reflect:?}{STRING:#}"); + } else if id == TypeId::of::() || is_number_id(id) { + w!(self, "{VALUE}{reflect:?}{VALUE:#}"); + } else { + w!(self, "{TYPE}{reflect:?}{TYPE:#}"); + } + } else { + w!(self, "{TYPE}{reflect:?}{TYPE:#}"); + } + Ok(()) + } + } + } + + fn write_type_path(&mut self, type_path: &str) { + let mut ident_start: Option = None; + + for (i, c) in type_path.char_indices() { + if c.is_alphanumeric() || c == '_' { + ident_start.get_or_insert(i); + } else { + if let Some(start) = ident_start.take() { + w!(self, "{TYPE}{}{TYPE:#}", &type_path[start..i]); + } + self.buffer.push(c); + } + } + + if let Some(start) = ident_start { + w!(self, "{TYPE}{}{TYPE:#}", &type_path[start..]); + } + } + + fn format_struct_fields(&mut self, s: &dyn Struct) -> Result<(), Diagnostic> { + self.format_container(("{", "}"), 0..s.field_len(), |f, i| { + let field = s.field_at(i).unwrap(); + w!(f, "{MEMBER}{}{MEMBER:#}: ", s.name_at(i).unwrap()); + f.write_type_path(field.reflect_short_type_path()); + w!(f, " = "); + f.format_reflect(field) + }) + } + + fn format_reflect_enum_variant(&mut self, v: &dyn Enum) -> Result<(), Diagnostic> { + w!(self, "{VARIANT}{}{VARIANT:#}", v.variant_name()); + match v.variant_type() { + VariantType::Struct => { + w!(self, " "); + self.format_container(("{", "}"), v.iter_fields(), |f, field| { + w!(f, "{MEMBER}{}{MEMBER:#}: ", field.name().unwrap()); + f.write_type_path(field.value().reflect_short_type_path()); + w!(f, " = "); + f.format_reflect(field.value()) + })?; + } + VariantType::Tuple => { + self.format_container(("(", ")"), v.iter_fields(), |f, field| { + f.write_type_path(field.value().reflect_short_type_path()); + w!(f, " = "); + f.format_reflect(field.value()) + })?; + } + VariantType::Unit => {} + } + Ok(()) + } +} + +fn is_number_id(id: TypeId) -> bool { + id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() + || id == TypeId::of::() +} diff --git a/src/builtin_parser/runner/value/function_param.rs b/src/builtin_parser/runner/value/function_param.rs new file mode 100644 index 0000000..c7402a8 --- /dev/null +++ b/src/builtin_parser/runner/value/function_param.rs @@ -0,0 +1,661 @@ +//! Implementations for [`FunctionParam`]. + +use std::cell::{Ref, RefMut}; + +use bevy::ecs::world::World; +use bevy::reflect::TypeRegistration; +use logos::Span; +use smallvec::SmallVec; + +use kinded::Kinded; + +use crate::builtin_parser::number::{ + Float, FloatKind, Integer, IntegerKind, Number, NumberKind, SignedInteger, SignedIntegerKind, + UnsignedInteger, UnsignedIntegerKind, +}; +use crate::builtin_parser::runner::function::ParamType; +use crate::builtin_parser::{Diagnostic, Environment, SpanExtension, Spanned, StrongRef}; + +use super::super::error::EvalError; +use super::super::function::FunctionParam; +use super::Value; +use super::kind::ValueKind; + +macro_rules! arg { + (impl$({ $($generics:tt)* })? $ty:ty: $value:ident => $expr:expr ) => { + impl$(<$($generics)*>)? FunctionParam for $ty { + type State<'world, 'env, 'reg> = Option; + type Guard<'val, 'world, 'env, 'reg> = Option; + type Item<'val, 'world, 'env, 'reg> = Self; + const PARAMETER_TYPE: ParamType = ParamType::Argument; + + fn get<'world, 'env, 'reg>( + mut value: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let $value = value.pop().unwrap(); + Ok(Some($expr)) + } + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + state.take() + } + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + Ok(guard.take().unwrap()) + } + } + }; +} +arg!(impl Spanned: value => value); +arg!(impl{T: TryFrom, Error = Diagnostic>} Spanned: value => Spanned { + span: value.span.clone(), + value: T::try_from(value)?, +}); +arg!(impl Value: value => value.value); + +macro_rules! impl_function_param_for_value { + (impl $type:ty: $value_kind:ident($value_name:ident) => $return:expr) => { + impl_function_param_for_value!(impl $type: $value_kind($value_name) {$value_kind} => $return); + }; + (impl ref $type:ty: $value_kind:ident($value_name:ident) => $return:expr) => { + impl_function_param_for_value!(impl ref $type: $value_kind($value_name) {$value_kind} => $return); + }; + (impl $type:ty: $value_kind:ident($value_name:ident) {$kind:ident} => $return:expr) => { + impl FunctionParam for $type { + type State<'world, 'env, 'reg> = Option; + type Guard<'val, 'world, 'env, 'reg> = Option; + type Item<'val, 'world, 'env, 'reg> = Self; + const PARAMETER_TYPE: ParamType = ParamType::Argument; + + fn get<'world, 'env, 'reg>( + mut value: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Spanned { span, value } = value.pop().unwrap(); + if let Value::$value_kind($value_name) = value { + Ok(Some($return)) + } else { + Err(span.wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::$kind, + actual: value.kind(), + }).into()) + } + } + fn borrow<'val, 'world, 'env, 'reg>(state: &'val mut Self::State<'world, 'env, 'reg>) -> Self::Guard<'val, 'world, 'env, 'reg> { state.take() } + fn as_arg<'val, 'world, 'env, 'reg>(guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>) -> Result, Diagnostic> { Ok(guard.take().unwrap()) } + } + impl TryFrom> for $type { + type Error = Diagnostic; + fn try_from(Spanned { span, value }: Spanned) -> Result { + if let Value::$value_kind($value_name) = value { + Ok($return) + } else { + Err(span.wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::$kind, + actual: value.kind(), + }).into()) + } + } + } + }; + (impl ref $type:ty: $value_kind:ident($value_name:ident) {$kind:ident} => $return:expr) => { + impl_function_param_for_value!(impl $type: $value_kind($value_name) {$kind} => $return); + + impl FunctionParam for &mut $type { + type State<'world, 'env, 'reg> = (StrongRef, Span); + type Guard<'val, 'world, 'env, 'reg> = (RefMut<'val, Value>, &'val Span); + type Item<'val, 'world, 'env, 'reg> = &'val mut $type; + const PARAMETER_TYPE: ParamType = ParamType::Argument; + + fn get<'world, 'env, 'reg>( + mut value: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Spanned { span, value } = value.pop().unwrap(); + + if let Value::Reference(reference) = value { + reference + .upgrade() + .ok_or_else(|| span.clone().diagnose(EvalError::ReferenceToMovedData)) + .map(|r| (r, span)) + } else { + Err(span.wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::Reference, + actual: value.kind(), + }).into()) + } + } + + fn borrow<'val, 'world, 'env, 'reg>( + (state, span): &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + (state.borrow_mut(), span) + } + + fn as_arg<'val, 'world, 'env, 'reg>( + (guard, span): &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + let reference = &mut **guard; + if let Value::$value_kind(value_ref) = reference { + Ok(value_ref) + } else { + Err(span.clone().wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::$kind, + actual: reference.kind(), + }).into()) + } + } + } + impl FunctionParam for &$type { + type State<'world, 'env, 'reg> = (StrongRef, Span); + type Guard<'val, 'world, 'env, 'reg> = (Ref<'val, Value>, &'val Span); + type Item<'val, 'world, 'env, 'reg> = &'val $type; + const PARAMETER_TYPE: ParamType = ParamType::Argument; + + fn get<'world, 'env, 'reg>( + mut value: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Spanned { span, value } = value.pop().unwrap(); + + if let Value::Reference(reference) = value { + reference + .upgrade() + .ok_or_else(|| span.clone().diagnose(EvalError::ReferenceToMovedData)) + .map(|r| (r, span)) + } else { + Err(span.wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::Reference, + actual: value.kind(), + }).into()) + } + } + + fn borrow<'val, 'world, 'env, 'reg>( + (state, span): &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + (state.borrow(), span) + } + + fn as_arg<'val, 'world, 'env, 'reg>( + (guard, span): &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + let reference = &**guard; + if let Value::$value_kind(value_ref) = reference { + Ok(value_ref) + } else { + Err(span.clone().wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::$kind, + actual: reference.kind(), + }).into()) + } + } + } + }; +} + +impl_function_param_for_value!(impl ref bool: Boolean(boolean) => boolean); +impl_function_param_for_value!(impl ref Number: Number(number) {AnyNumber} => number); +impl_function_param_for_value!(impl ref String: String(string) => string); + +impl_function_param_for_value!(impl std::collections::HashMap: Object(object) => { + object.into_iter().map(|(k, v)| (k, v.into_inner())).collect() +}); +// impl_function_param_for_value!(impl StrongRef: Reference(reference) => reference.upgrade().unwrap()); + +macro_rules! impl_function_param_for_group { + ($type:ty, $pattern:pat, $var:ident, $kind:ident) => { + impl FunctionParam for $type { + type State<'world, 'env, 'reg> = Option; + type Guard<'val, 'world, 'env, 'reg> = Option; + type Item<'val, 'world, 'env, 'reg> = Self; + const PARAMETER_TYPE: ParamType = ParamType::Argument; + + fn get<'world, 'env, 'reg>( + mut value: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Spanned { span, value } = value.pop().unwrap(); + if let Value::Number($pattern) = value { + Ok(Some($var)) + } else { + Err(span + .wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::$kind, + actual: value.kind(), + }) + .into()) + } + } + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + state.take() + } + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + Ok(guard.take().unwrap()) + } + } + impl TryFrom> for $type { + type Error = Diagnostic; + fn try_from(Spanned { span, value }: Spanned) -> Result { + if let Value::Number($pattern) = value { + Ok($var) + } else { + Err(span + .wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::$kind, + actual: value.kind(), + }) + .into()) + } + } + } + }; +} + +impl_function_param_for_group!(Integer, Number::Integer(v), v, AnyInteger); +impl_function_param_for_group!( + UnsignedInteger, + Number::Integer(Integer::Unsigned(v)), + v, + AnyUnsignedInteger +); +impl_function_param_for_group!( + SignedInteger, + Number::Integer(Integer::Signed(v)), + v, + AnySignedInteger +); +impl_function_param_for_group!(Float, Number::Float(v), v, AnyFloat); + +macro_rules! impl_function_param_for_numbers { + ($group_variant:ident, $variant:ident, $group:ident, $group_kind:ident, $generic:ident ($($number:ident),*$(,)?)) => { + $( + impl FunctionParam for $number { + type State<'world, 'env, 'reg> = Option; + type Guard<'val, 'world, 'env, 'reg> = Option; + type Item<'val, 'world, 'env, 'reg> = Self; + const PARAMETER_TYPE: ParamType = ParamType::Argument; + + fn get<'world, 'env, 'reg>( + mut value: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Spanned { span, value } = value.pop().unwrap(); + #[allow(unreachable_patterns)] + match value { + Value::Number(Number::Integer(Integer::$group_variant($group::$number(value)))) => Ok(Some(value)), + Value::Number(Number::Integer(Integer::Signed(SignedInteger::Unspecified(value)))) if stringify!($group_variant) != "Signed" || stringify!($generic) != "Integer" => Ok(Some(value as $number)), + Value::Number(Number::Integer(Integer::$group_variant($group::$generic(value)))) => Ok(Some(value as $number)), + _ => Err(span.wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::Number(NumberKind::Integer(IntegerKind::$group_variant($group_kind::$number))), + actual: value.kind(), + }).into()) + } + } + fn borrow<'val, 'world, 'env, 'reg>(state: &'val mut Self::State<'world, 'env, 'reg>) -> Self::Guard<'val, 'world, 'env, 'reg> { state.take() } + fn as_arg<'val, 'world, 'env, 'reg>(guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>) -> Result, Diagnostic> { Ok(guard.take().unwrap()) } + } + impl TryFrom> for $number { + type Error = Diagnostic; + fn try_from(Spanned {span, value}: Spanned) -> Result { + #[allow(unreachable_patterns)] + match value { + Value::Number(Number::Integer(Integer::$group_variant($group::$number(value)))) => Ok(value), + Value::Number(Number::Integer(Integer::Signed(SignedInteger::Unspecified(value)))) if stringify!($group_variant) != "Signed" || stringify!($generic) != "Integer" => Ok(value as $number), + Value::Number(Number::Integer(Integer::$group_variant($group::$generic(value)))) => Ok(value as $number), + _ => Err(span.wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::Number(NumberKind::Integer(IntegerKind::$group_variant($group_kind::$number))), + actual: value.kind(), + }).into()) + } + } + } + )* + }; +} + +impl_function_param_for_numbers!( + Unsigned, + Unsigned, + UnsignedInteger, + UnsignedIntegerKind, + u64(u8, u16, u32, u64, usize) +); +impl_function_param_for_numbers!( + Signed, + Signed, + SignedInteger, + SignedIntegerKind, + Unspecified(i8, i16, i32, i64, isize) +); + +macro_rules! impl_function_param_for_floats { + ($generic:ident ($($number:ident),*$(,)?)) => { + $( + impl FunctionParam for $number { + type State<'world, 'env, 'reg> = Option; + type Guard<'val, 'world, 'env, 'reg> = Option; + type Item<'val, 'world, 'env, 'reg> = Self; + const PARAMETER_TYPE: ParamType = ParamType::Argument; + + fn get<'world, 'env, 'reg>( + mut value: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Spanned { span, value } = value.pop().unwrap(); + #[allow(unreachable_patterns)] + match value { + Value::Number(Number::Float(Float::$number(value))) => Ok(Some(value)), + Value::Number(Number::Float(Float::$generic(value))) => Ok(Some(value as $number)), + _ => Err(span.wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::Number(NumberKind::Float(FloatKind::$number)), + actual: value.kind(), + }).into()) + } + } + fn borrow<'val, 'world, 'env, 'reg>(state: &'val mut Self::State<'world, 'env, 'reg>) -> Self::Guard<'val, 'world, 'env, 'reg> { state.take() } + fn as_arg<'val, 'world, 'env, 'reg>(guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>) -> Result, Diagnostic> { Ok(guard.take().unwrap()) } + } + impl TryFrom> for $number { + type Error = Diagnostic; + fn try_from(Spanned {span, value}: Spanned) -> Result { + #[allow(unreachable_patterns)] + match value { + Value::Number(Number::Float(Float::$number(value))) => Ok(value), + Value::Number(Number::Float(Float::$generic(value))) => Ok(value as $number), + _ => Err(span.wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::Number(NumberKind::Float(FloatKind::$number)), + actual: value.kind(), + }).into()) + } + } + } + )* + }; +} +impl_function_param_for_floats!(Unspecified(f32, f64)); + +impl FunctionParam for &Value { + type State<'world, 'env, 'reg> = StrongRef; + type Guard<'val, 'world, 'env, 'reg> = Ref<'val, Value>; + type Item<'val, 'world, 'env, 'reg> = &'val Value; + const PARAMETER_TYPE: ParamType = ParamType::Argument; + + fn get<'world, 'env, 'reg>( + mut value: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Spanned { span, value } = value.pop().unwrap(); + + if let Value::Reference(reference) = value { + reference + .upgrade() + .ok_or_else(|| span.diagnose(EvalError::ReferenceToMovedData)) + } else { + Err(span + .wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::Reference, + actual: value.kind(), + }) + .into()) + } + } + + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + state.borrow() + } + + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + Ok(&**guard) + } +} + +impl FunctionParam for &mut Value { + type State<'world, 'env, 'reg> = StrongRef; + type Guard<'val, 'world, 'env, 'reg> = RefMut<'val, Value>; + type Item<'val, 'world, 'env, 'reg> = &'val mut Value; + const PARAMETER_TYPE: ParamType = ParamType::Argument; + + fn get<'world, 'env, 'reg>( + mut value: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Spanned { span, value } = value.pop().unwrap(); + + if let Value::Reference(reference) = value { + reference + .upgrade() + .ok_or_else(|| span.diagnose(EvalError::ReferenceToMovedData)) + } else { + Err(span + .wrap(EvalError::IncorrectFunctionParameterType { + expected: ValueKind::Reference, + actual: value.kind(), + }) + .into()) + } + } + + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + state.borrow_mut() + } + + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + Ok(&mut **guard) + } +} + +impl FunctionParam for &mut World { + type State<'world, 'env, 'reg> = Option<&'world mut World>; + type Guard<'val, 'world, 'env, 'reg> = Option<&'val mut World>; + type Item<'val, 'world, 'env, 'reg> = &'val mut World; + const PARAMETER_TYPE: ParamType = ParamType::Parameter; + + fn get<'world, 'env, 'reg>( + _: SmallVec<[Spanned; 1]>, + world: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Some(world) = world.take() else { + return Err(Diagnostic::empty(EvalError::Custom( + "world borrowed twice".into(), + ))); + }; + Ok(Some(world)) + } + + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + state.take() + } + + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + Ok(guard.as_mut().map(|w| &mut **w).unwrap()) + } +} +impl FunctionParam for &World { + type State<'world, 'env, 'reg> = Option<&'world World>; + type Guard<'val, 'world, 'env, 'reg> = Option<&'val World>; + type Item<'val, 'world, 'env, 'reg> = &'val World; + const PARAMETER_TYPE: ParamType = ParamType::Parameter; + + fn get<'world, 'env, 'reg>( + _: SmallVec<[Spanned; 1]>, + world: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + let Some(world) = world.take() else { + return Err(Diagnostic::empty(EvalError::Custom( + "world borrowed twice".into(), + ))); + }; + Ok(Some(world)) + } + + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + state.take() + } + + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + Ok(guard.as_mut().map(|w| &**w).unwrap()) + } +} + +impl FunctionParam for &mut Environment { + type State<'world, 'env, 'reg> = Option<&'env mut Environment>; + type Guard<'val, 'world, 'env, 'reg> = Option<&'val mut Environment>; + type Item<'val, 'world, 'env, 'reg> = &'val mut Environment; + const PARAMETER_TYPE: ParamType = ParamType::Parameter; + + fn get<'world, 'env, 'reg>( + _: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + environment: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + Ok(Some(environment.take().unwrap())) + } + + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + state.take() + } + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + Ok(guard.as_mut().map(|e| &mut **e).unwrap()) + } +} +impl FunctionParam for &Environment { + type State<'world, 'env, 'reg> = Option<&'env Environment>; + type Guard<'val, 'world, 'env, 'reg> = Option<&'val Environment>; + type Item<'val, 'world, 'env, 'reg> = &'val Environment; + const PARAMETER_TYPE: ParamType = ParamType::Parameter; + + fn get<'world, 'env, 'reg>( + _: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + environment: &mut Option<&'env mut Environment>, + _: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + Ok(Some(environment.take().unwrap())) + } + + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + state.take() + } + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + Ok(guard.as_mut().map(|e| &**e).unwrap()) + } +} + +impl FunctionParam for &[&TypeRegistration] { + type State<'world, 'env, 'reg> = Option<&'reg [&'reg TypeRegistration]>; + type Guard<'val, 'world, 'env, 'reg> = Option<&'reg [&'reg TypeRegistration]>; + type Item<'val, 'world, 'env, 'reg> = &'reg [&'reg TypeRegistration]; + const PARAMETER_TYPE: ParamType = ParamType::Parameter; + + fn get<'world, 'env, 'reg>( + _: SmallVec<[Spanned; 1]>, + _: &mut Option<&'world mut World>, + _: &mut Option<&'env mut Environment>, + registrations: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + Ok(Some(registrations)) + } + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + *state + } + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + Ok(guard.unwrap()) + } +} + +impl FunctionParam for Vec { + type State<'world, 'env, 'reg> = Vec>; + type Guard<'val, 'world, 'env, 'reg> = Vec>; + type Item<'val, 'world, 'env, 'reg> = Vec>; + const PARAMETER_TYPE: ParamType = ParamType::VarArg; + + fn get<'world, 'env, 'reg>( + values: SmallVec<[Spanned; 1]>, + world: &mut Option<&'world mut World>, + environment: &mut Option<&'env mut Environment>, + registrations: &'reg [&'reg TypeRegistration], + ) -> Result, Diagnostic> { + values + .into_iter() + .map(|value| { + T::get( + SmallVec::from_buf([value]), + world, + environment, + registrations, + ) + }) + .collect() + } + + fn borrow<'val, 'world, 'env, 'reg>( + state: &'val mut Self::State<'world, 'env, 'reg>, + ) -> Self::Guard<'val, 'world, 'env, 'reg> { + state.iter_mut().map(T::borrow).collect() + } + + fn as_arg<'val, 'world, 'env, 'reg>( + guard: &'val mut Self::Guard<'_, 'world, 'env, 'reg>, + ) -> Result, Diagnostic> { + guard.iter_mut().map(T::as_arg).collect() + } +} diff --git a/src/builtin_parser/runner/value/kind.rs b/src/builtin_parser/runner/value/kind.rs new file mode 100644 index 0000000..21c5b41 --- /dev/null +++ b/src/builtin_parser/runner/value/kind.rs @@ -0,0 +1,106 @@ +//! Value kinds and related traits + +use kinded::{Kind, Kinded}; + +use crate::builtin_parser::number::NumberKind; + +use super::Value; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ValueKind { + None, + Number(NumberKind), + AnyNumber, + AnyInteger, + AnyUnsignedInteger, + AnySignedInteger, + AnyFloat, + Boolean, + String, + Reference, + Object, + StructObject, + Tuple, + StructTuple, + Resource, +} + +impl Kinded for Value { + type Kind = ValueKind; + + fn kind(&self) -> Self::Kind { + match self { + Self::None => ValueKind::None, + Self::Number(number) => ValueKind::Number(number.kind()), + Self::Boolean(..) => ValueKind::Boolean, + Self::String(..) => ValueKind::String, + Self::Reference(..) => ValueKind::Reference, + Self::Object(..) => ValueKind::Object, + Self::StructObject { .. } => ValueKind::StructObject, + Self::Tuple(..) => ValueKind::Tuple, + Self::StructTuple { .. } => ValueKind::StructTuple, + Self::Resource(..) => ValueKind::Resource, + } + } +} + +impl Kind for ValueKind { + fn all() -> &'static [Self] { + unimplemented!() + } +} + +impl ValueKind { + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::None => "none", + Self::Number(number) => number.as_str(), + Self::AnyNumber => "float/integer", + Self::AnyInteger => "integer", + Self::AnyUnsignedInteger => "unsigned integer", + Self::AnySignedInteger => "signed integer", + Self::AnyFloat => "float", + Self::Boolean => "boolean", + Self::String => "string", + Self::Reference => "reference", + Self::Object => "object", + Self::StructObject => "struct object", + Self::Tuple => "tuple", + Self::StructTuple => "struct tuple", + Self::Resource => "resource", + } + } + + /// Returns the kind of [`Value`] as a [string slice](str) with an `a` or `an` prepended to it. + /// Used for more natural sounding error messages. + pub const fn as_natural(self) -> &'static str { + match self { + Self::None => "nothing", + Self::Number(number) => number.as_natural(), + Self::AnyNumber => "any number", + Self::AnyInteger => "any integer", + Self::AnyUnsignedInteger => "any unsigned integer", + Self::AnySignedInteger => "any signed integer", + Self::AnyFloat => "any float", + Self::Boolean => "a boolean", + Self::String => "a string", + Self::Reference => "a reference", + Self::Object => "a object", + Self::StructObject => "a struct object", + Self::Tuple => "a tuple", + Self::StructTuple => "a struct tuple", + Self::Resource => "a resource", + } + } +} + +impl std::fmt::Display for ValueKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + f.write_str(self.as_natural()) + } else { + f.write_str(self.as_str()) + } + } +} diff --git a/src/command.rs b/src/command.rs index 8ab6fa4..2277fb0 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,22 +1,52 @@ //! Command execution functionality. -use std::borrow::Cow; +use bevy::prelude::*; use std::ops::Range; -use bevy::ecs::world::Command; -use bevy::prelude::*; +/// Identifier for log messages that show a previous command. +pub const COMMAND_MESSAGE_NAME: &str = "console_command"; +/// Identifier for log messages that show the result of a command. +pub const COMMAND_RESULT_NAME: &str = "console_result"; + +/// Formats a command with ANSI highlights for errors. +#[must_use] +pub fn format_command_with_hints(command: &str, spans: &[Range]) -> String { + let mut result = String::new(); + let mut last_end = 0; + + let mut sorted_spans = spans.to_vec(); + sorted_spans.sort_by_key(|s| s.start); + + for span in sorted_spans { + if span.start > last_end { + result.push_str(&command[last_end..span.start]); + } + + const RED_UNDERLINE: anstyle::Style = anstyle::Style::new() + .effects(anstyle::Effects::CURLY_UNDERLINE) + .underline_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))); + + let highlighted = format!("{RED_UNDERLINE}{}{RED_UNDERLINE:#}", &command[span.clone()]); + result.push_str(&highlighted); + last_end = span.end; + } + if last_end < command.len() { + result.push_str(&command[last_end..]); + } + result +} /// The command parser currently being used by the dev console. #[derive(Resource)] pub struct DefaultCommandParser(pub Box); impl DefaultCommandParser { - /// Shortcut method for calling `parser.0.parse(command, world)`. + /// Shortcut method for calling [`parser.0.parse(command, world)`](CommandParser::parse). #[inline] pub fn parse(&self, command: &str, world: &mut World) { - self.0.parse(command, world) + self.0.parse(command, world); } - /// Shortcut method for calling `parser.0.completion(command, world)`. + /// Shortcut method for calling [`parser.0.completion(command, world)`](CommandParser::completion). #[inline] #[must_use] #[cfg(feature = "completions")] @@ -35,86 +65,6 @@ impl From> for DefaultCommandParser { } } -/// A hint displayed to the user when they make a mistake. -#[derive(Debug, Clone)] -pub struct CommandHint { - /// The color of the hint. - pub color: CommandHintColor, - /// The location of the hint in the command. - pub span: Range, - /// Additional information about the hint when hovered over. - /// (Doesn't do anything atm) - pub description: Cow<'static, str>, -} -impl CommandHint { - /// Creates a new [`CommandHint`]. - pub fn new( - span: Range, - color: CommandHintColor, - description: impl Into>, - ) -> Self { - Self { - color, - span, - description: description.into(), - } - } -} - -/// The color of a [`CommandHint`], may either be a standard color or a [`Custom`](CommandHintColor::Custom) [`Color`]. -#[derive(Debug, Clone)] -pub enum CommandHintColor { - /// An error marks bad code that cannot be recovered from. - /// - /// Usually colored red. - Error, - /// A warning marks code that could cause problems in the future. - /// - /// Usually colored yellow. - Warning, - /// A hint marks code that is questionable, but is otherwise fine. - /// - /// Usually colored blue. - Hint, - /// This marks code that could be improved. - /// - /// Usually colored green. - Help, - /// A custom color of your choice! This is usually not recommended as - /// you're much better off using the standard colors. - Custom(Color), -} - -/// A resource where hints (errors/warnings/etc) are stored -/// to be displayed in the developer console. -#[derive(Resource, Debug, Default, Deref)] -pub struct CommandHints { - #[deref] - hints: Vec>, - hint_added: bool, -} -impl CommandHints { - /// Push a list of hints. This should be done once per command call. - pub fn push(&mut self, hints: impl Into>) { - if self.hint_added { - warn!( - "Hints were added twice! Hint 1: {:?}, Hint 2: {:?}", - self.hints.last(), - hints.into() - ) - } else { - self.hint_added = true; - self.hints.push(hints.into()); - } - } - pub(crate) fn reset_hint_added(&mut self) { - if !self.hint_added { - self.push([]); - } - self.hint_added = false; - } -} - /// The trait that all [`CommandParser`]s implement. /// You can take a look at the [builtin parser](crate::builtin_parser) for an advanced example. /// @@ -122,7 +72,7 @@ impl CommandHints { /// # use bevy::ecs::world::World; /// # use bevy_dev_console::command::CommandParser; /// # use bevy::log::info; -/// # use bevy_dev_console::ui::COMMAND_RESULT_NAME; +/// # use bevy_dev_console::command::COMMAND_RESULT_NAME; /// /// pub struct MyCustomParser; /// impl CommandParser for MyCustomParser { @@ -138,7 +88,6 @@ pub trait CommandParser: Send + Sync + 'static { /// This method is called by the console when a command is ran. fn parse(&self, command: &str, world: &mut World); /// This method is called by the console when the command is changed. - #[inline] #[must_use] #[cfg(feature = "completions")] fn completion(&self, keyword: &str, world: &World) -> Vec { @@ -159,27 +108,18 @@ pub struct CompletionSuggestion { pub(crate) struct ExecuteCommand(pub String); impl Command for ExecuteCommand { fn apply(self, world: &mut World) { - if let Some(parser) = world.remove_resource::() { - parser.parse(&self.0, world); - world.insert_resource(parser); - } else { - error!("Default command parser doesn't exist, cannot execute command."); + match world.remove_resource::() { + Some(parser) => { + parser.parse(&self.0, world); + world.insert_resource(parser); + } + _ => { + error!("Default command parser doesn't exist, cannot execute command."); + } } } } #[derive(Resource, Default, Deref, DerefMut)] #[cfg(feature = "completions")] -pub struct AutoCompletions(pub(crate) Vec); -#[cfg(feature = "completions")] -pub(crate) struct UpdateAutoComplete(pub String); -#[cfg(feature = "completions")] -impl Command for UpdateAutoComplete { - fn apply(self, world: &mut World) { - if let Some(parser) = world.remove_resource::() { - let completions = parser.completion(&self.0, world); - world.resource_mut::().0 = completions; - world.insert_resource(parser); - } - } -} +pub(crate) struct AutoCompletions(pub(crate) Vec); diff --git a/src/config.rs b/src/config.rs index 0b03ace..c0a5391 100644 --- a/src/config.rs +++ b/src/config.rs @@ -55,11 +55,25 @@ pub struct ConsoleTheme { /// /// Designates very low priority, often extremely verbose, information. pub trace: Color, + /// The color of keywords. + pub keyword: Color, + /// The color of strings. + pub string: Color, + /// The color of numbers and booleans. + pub value: Color, + /// The color of functions. + pub function: Color, + /// The color of type names. + pub type_name: Color, + /// The color of members. + pub member: Color, + /// The color of enum variants. + pub variant: Color, } -/// Helper trait that allows conversion between [`bevy::Color`](Color) and [`egui::Color32`]. +/// Helper trait that allows conversion between [`bevy::Color`](Color) and [`egui::Color32`](bevy_egui::egui::Color32). pub trait ToColor32 { - /// Convert this [`bevy::Color`](Color) to a [`egui::Color32`]. + /// Convert this [`bevy::Color`](Color) to a [`egui::Color32`](Color32). fn to_color32(&self) -> Color32; } impl ToColor32 for Color { @@ -71,6 +85,11 @@ impl ToColor32 for Color { alpha, } = self.to_srgba(); + #[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "0-1 float maps perfectly" + )] Color32::from_rgba_unmultiplied( (red * 255.0) as u8, (green * 255.0) as u8, @@ -80,15 +99,19 @@ impl ToColor32 for Color { } } -macro_rules! define_text_format_method { - ($name:ident, $color:ident) => { - #[doc = concat!("Returns a [`TextFormat`] colored with [`Self::", stringify!($color), "`]")] - pub fn $name(&self) -> TextFormat { - TextFormat { - color: self.$color.to_color32(), - ..self.format_text() - } - } +macro_rules! define_text_format_methods { + ($($color:ident),+ $(,)?) => { + $( + pastey::paste!( + #[doc = concat!("Returns a [`TextFormat`] colored with [`Self::", stringify!($color), "`]")] + pub fn [](&self) -> TextFormat { + TextFormat { + color: self.$color.to_color32(), + ..self.format_text() + } + } + ); + )+ }; } @@ -98,12 +121,20 @@ impl ConsoleTheme { font: FontId::monospace(14.0), dark: Color::srgb(0.42, 0.44, 0.48), text_color: Color::srgb(0.67, 0.7, 0.75), - error: Color::srgb(0.91, 0.46, 0.5), + error: Color::srgb(0.88, 0.33, 0.38), warning: Color::srgb(0.82, 0.56, 0.32), info: Color::srgb(0.55, 0.76, 0.4), debug: Color::srgb(0.29, 0.65, 0.94), - trace: Color::srgb(0.78, 0.45, 0.89), + trace: Color::srgb(0.76, 0.38, 0.87), + keyword: Color::srgb(0.76, 0.38, 0.87), + string: Color::srgb(0.55, 0.76, 0.4), + value: Color::srgb(0.82, 0.56, 0.32), + function: Color::srgb(0.29, 0.65, 0.94), + type_name: Color::srgb(0.9, 0.75, 0.48), + member: Color::srgb(0.88, 0.42, 0.46), + variant: Color::srgb(0.34, 0.71, 0.76), }; + /// High contrast theme, might help some people. pub const HIGH_CONTRAST: Self = Self { font: FontId::monospace(14.0), @@ -112,11 +143,19 @@ impl ConsoleTheme { error: Color::srgb(1.0, 0.0, 0.0), warning: Color::srgb(1.0, 1.0, 0.0), info: Color::srgb(0.0, 1.0, 0.0), - debug: Color::srgb(0.25, 0.25, 1.0), + debug: Color::srgb(0.5, 0.5, 1.0), trace: Color::srgb(1.0, 0.0, 1.0), + keyword: Color::srgb(1.0, 0.0, 1.0), + string: Color::srgb(0.0, 1.0, 0.0), + value: Color::srgb(1.0, 1.0, 0.0), + function: Color::srgb(0.5, 0.5, 1.0), + type_name: Color::srgb(1.0, 0.8, 0.0), + member: Color::srgb(1.0, 0.0, 0.0), + variant: Color::srgb(0.0, 1.0, 1.0), }; /// Returns a [`Color32`] based on the `level` + #[must_use] pub fn color_level(&self, level: Level) -> Color32 { match level { Level::ERROR => self.error.to_color32(), @@ -128,6 +167,7 @@ impl ConsoleTheme { } /// Returns a [`TextFormat`] with a color based on the [`Level`] and the [`ConsoleTheme`]. + #[must_use] pub fn format_level(&self, level: Level) -> TextFormat { TextFormat { color: self.color_level(level), @@ -136,29 +176,27 @@ impl ConsoleTheme { } /// Returns a [`TextFormat`] with the default font and color. + #[must_use] pub fn format_text(&self) -> TextFormat { TextFormat { font_id: self.font.clone(), color: self.text_color.to_color32(), - ..default() } } /// Returns a [`TextFormat`] with the default font and white color. + #[must_use] pub fn format_bold(&self) -> TextFormat { TextFormat { font_id: self.font.clone(), color: Color32::WHITE, - ..default() } } - define_text_format_method!(format_dark, dark); - define_text_format_method!(format_error, error); - define_text_format_method!(format_warning, warning); - define_text_format_method!(format_info, info); - define_text_format_method!(format_debug, debug); - define_text_format_method!(format_trace, trace); + define_text_format_methods!( + dark, error, warning, info, debug, trace, keyword, string, value, function, type_name, + member + ); } diff --git a/src/lib.rs b/src/lib.rs index 46fb4cb..309e93a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,89 @@ -#![doc = include_str!("../README.md")] +//! `bevy_dev_console` is an experimental developer console plugin for the [Bevy Game Engine](https://bevy.org). +//! +//! ![Image of the developer console](https://raw.githubusercontent.com/doonv/bevy_dev_console/master/doc/console.png) +//! +//!
+//! Warning +//!
+//! bevy_dev_console is currently in its early development stages. Expect breaking changes in the near future +//! (especially when using the built-in command parser). For this reason its only available as a git package at the moment. +//!
+//! +//! ## Features +//! +//! - Log viewing +//! - View all the hidden data from any log message by hovering over it. +//! - Powerful Built-in parser language built specifically for `bevy_dev_console`. Take a look at the [docs](builtin_parser::docs) for how to use it! +//! - Calculations +//! - Variables +//! - Uses a simplified version of ownership and borrowing +//! - Standard library (Doesn't have much at the moment) +//! - - [Custom native functions](https://github.com/doonv/bevy_dev_console/blob/master/examples/custom_functions.rs) are written just like regular rust functions! ([`World`] access included!) +//! - [Many types](https://github.com/doonv/bevy_dev_console/wiki/Built%E2%80%90in-Parser#types) +//! - Resource viewing and modification +//! - Enums +//! - Structs +//! - ~~Entity queries~~ [*Coming Soon...*](https://github.com/doonv/bevy_dev_console/issues/3) (Syntax suggestions would be appreciated!) +//! - ...and more! +//! +//! ## Usage +//! +//! 1. Add the `bevy_dev_console` git package. +//! +//! ```bash +//! cargo add --git https://github.com/doonv/bevy_dev_console.git +//! ``` +//! +//! 2. Import the [`prelude`]. +//! +//! ```rust +//! use bevy_dev_console::prelude::*; +//! ``` +//! +//! 3. Add the plugins. +//! +//! ```rust,no_run +//! use bevy::prelude::*; +//! use bevy_dev_console::prelude::*; +//! +//! App::new() +//! .add_plugins(( +//! DefaultPlugins.set(console_log_plugin()), // Don't forget to set the LogPlugin +//! DevConsolePlugin, +//! )) +//! .run(); +//! ``` +//! +//! 4. That should be it! You can now press the \` / ~ key on your keyboard and it should open the console! +//! +//! ## Toggleable Features +//! +//! `builtin-parser` **(default)** - Includes the default parser. Disabling this allows you to remove the built-in parser and replace it with your own (or you could do nothing and make the console into a log reader). +//! +//! ## Bevy Compatibility +//! +//! | bevy | bevy_dev_console | +//! | ------ | ---------------- | +//! | 0.18.* | git (master) | + +// #![warn(clippy::pedantic)] +// #![warn(clippy::nursery)] +// #![allow(clippy::needless_pass_by_value)] +// #![allow(clippy::too_many_lines)] +// #![allow(clippy::use_self)] +// #![allow(clippy::items_after_statements)] +#![allow(internal_features)] +#![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] +#![cfg_attr(test, feature(box_patterns))] use bevy::prelude::*; -use bevy_egui::EguiPlugin; -use command::CommandHints; +use bevy_egui::prelude::*; use config::ConsoleConfig; use ui::ConsoleUiState; +#[cfg(debug_assertions)] +use crate::logging::LogMessage; + #[cfg(feature = "builtin-parser")] pub mod builtin_parser; pub mod command; @@ -16,38 +94,60 @@ pub mod ui; /// Adds a Developer Console to your Bevy application. /// -/// Requires [custom_log_layer](logging::custom_log_layer). +/// ## Usage +/// +/// ```rust,no_run +/// # use bevy::prelude::*; +/// use bevy_dev_console::prelude::*; +/// +/// App::new() +/// .add_plugins(( +/// DefaultPlugins.set(console_log_plugin()), // Don't forget to set the LogPlugin +/// DevConsolePlugin +/// )) +/// .run(); +/// ``` pub struct DevConsolePlugin; impl Plugin for DevConsolePlugin { fn build(&self, app: &mut App) { if !app.is_plugin_added::() { - app.add_plugins(EguiPlugin); + app.add_plugins(EguiPlugin::default()); } #[cfg(feature = "builtin-parser")] { app.init_non_send_resource::(); app.init_resource::(); + #[cfg(feature = "builtin-parser-completions")] app.init_resource::(); } + #[cfg(feature = "completions")] app.init_resource::(); app.init_resource::() - .init_resource::() .init_resource::() - .register_type::() + .add_systems(Update, ui::read_logs) .add_systems( - Update, - ( - ui::read_logs, - ( - ui::open_close_ui, - ui::render_ui_system.run_if(|s: Res| s.open), - ) - .chain(), - ), + EguiPrimaryContextPass, + ui::render_ui_system.run_if(toggle()), ); } + + fn finish(&self, app: &mut App) { + debug_assert!( + app.world().contains_resource::>(), + "`LogMessage` message not initialized. `DevConsolePlugin` requires `bevy::log::LogPlugin::custom_layer` be \ + set to `bevy_dev_console::logging::console_log_layer`. See `bevy_dev_consoles`'s examples for more info." + ); + } +} + +fn toggle() -> impl FnMut(Res>, Res) -> bool + Clone { + let mut active = false; + move |inputs: Res>, config: Res| { + active ^= inputs.just_pressed(config.open_key); + active + } } diff --git a/src/logging.rs b/src/logging.rs index 1c375ad..d1460f8 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,32 +1,54 @@ -//! Custom [LogPlugin](bevy::log::LogPlugin) functionality. +//! Logging capturing and storage. -use bevy::log::{BoxedLayer, Level}; +use bevy::log::tracing::Subscriber; +use bevy::log::tracing_subscriber::field::Visit; +use bevy::log::tracing_subscriber::{Layer, fmt}; +use bevy::log::{BoxedFmtLayer, BoxedLayer, Level, LogPlugin, tracing_subscriber as subscriber}; use bevy::prelude::*; -use bevy::utils::tracing::Subscriber; use std::sync::mpsc; -use tracing_subscriber::field::Visit; -use tracing_subscriber::Layer; -use web_time::SystemTime; - -/// A function that implements the log reading functionality for the -/// developer console via [`LogPlugin::custom_layer`](bevy::log::LogPlugin::custom_layer). -pub fn custom_log_layer(app: &mut App) -> Option { - Some(Box::new(create_custom_log_layer(app))) + +/// Convince function for setting the [`custom_layer`](LogPlugin::custom_layer) and +/// [`fmt_layer`](LogPlugin::fmt_layer) of a [`LogPlugin`] with [`console_log_layer`] and [`colored_fmt_layer`]. +#[must_use] +pub fn console_log_plugin() -> LogPlugin { + LogPlugin { + custom_layer: console_log_layer, + fmt_layer: colored_fmt_layer, + ..default() + } } -fn create_custom_log_layer(app: &mut App) -> LogCaptureLayer { +/// A [`LogPlugin::custom_layer`] that implements the log reading +/// functionality for the developer console. +#[must_use] +pub fn console_log_layer(app: &mut App) -> Option { let (sender, receiver) = mpsc::channel(); - app.add_event::(); + app.add_message::(); app.insert_non_send_resource(CapturedLogEvents(receiver)); app.add_systems(PostUpdate, transfer_log_events); - LogCaptureLayer { sender } + Some(Box::new(LogCaptureLayer { sender })) +} + +/// A [`LogPlugin::fmt_layer`] that disables [ANSI sanitization](fmt::Layer::with_ansi_sanitization). +/// +/// Allows for colored text at the cost of a minor security vulnerability. The extent of it is that +/// malicious ANSI codes can clear the screen, or embed malicious links, not much more than that. +/// +/// See [tokio-rs/tracing#3378](https://github.com/tokio-rs/tracing/issues/3378) for more info. +#[must_use] +pub fn colored_fmt_layer(_: &mut App) -> Option { + Some(Box::new( + fmt::Layer::default() + .with_ansi_sanitization(false) + .with_writer(std::io::stderr), + )) } -/// A [`tracing`](bevy::utils::tracing) log message event. +/// A [`tracing`](bevy::log::tracing) log message event. /// -/// This event is helpful for creating custom log viewing systems such as consoles and terminals. -#[derive(Event, Debug, Clone)] +/// This is used to transfer the log data from `tracing` to the `bevy_dev_console` UI. +#[derive(Message, Debug, Clone)] pub(crate) struct LogMessage { /// The message contents. pub message: String, @@ -54,19 +76,19 @@ pub(crate) struct LogMessage { pub line: Option, /// The time the log occurred. - pub time: SystemTime, + pub time: chrono::DateTime, } /// Transfers information from the [`CapturedLogEvents`] resource to [`Events`](LogMessage). fn transfer_log_events( receiver: NonSend, - mut log_events: EventWriter, + mut log_events: MessageWriter, ) { - log_events.send_batch(receiver.0.try_iter()); + log_events.write_batch(receiver.0.try_iter()); } /// This struct temporarily stores [`LogMessage`]s before they are -/// written to [`EventWriter`] by [`transfer_log_events`]. +/// written to [`MessageWriter`] by [`transfer_log_events`]. struct CapturedLogEvents(mpsc::Receiver); /// A [`Layer`] that captures log events and saves them to [`CapturedLogEvents`]. @@ -76,8 +98,8 @@ struct LogCaptureLayer { impl Layer for LogCaptureLayer { fn on_event( &self, - event: &bevy::utils::tracing::Event<'_>, - _ctx: tracing_subscriber::layer::Context<'_, S>, + event: &bevy::log::tracing::Event<'_>, + _ctx: subscriber::layer::Context<'_, S>, ) { let mut message = None; event.record(&mut LogEventVisitor(&mut message)); @@ -92,7 +114,7 @@ impl Layer for LogCaptureLayer { module_path: metadata.module_path(), file: metadata.file(), line: metadata.line(), - time: SystemTime::now(), + time: chrono::Utc::now(), }) .expect("CapturedLogEvents resource no longer exists!"); } @@ -104,7 +126,7 @@ struct LogEventVisitor<'a>(&'a mut Option); impl Visit for LogEventVisitor<'_> { fn record_debug( &mut self, - field: &bevy::utils::tracing::field::Field, + field: &bevy::log::tracing::field::Field, value: &dyn std::fmt::Debug, ) { // Only log out messages diff --git a/src/prelude.rs b/src/prelude.rs index 4140925..5e77ae4 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,5 +1,5 @@ //! `use bevy_dev_console::prelude::*` to quickly import the required plugins for [`bevy_dev_console`](crate). -pub use crate::config::ConsoleConfig; -pub use crate::logging::custom_log_layer; pub use crate::DevConsolePlugin; +pub use crate::config::ConsoleConfig; +pub use crate::logging::{colored_fmt_layer, console_log_layer, console_log_plugin}; diff --git a/src/ui.rs b/src/ui.rs index aa859d5..fe5f297 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,76 +4,40 @@ use bevy::prelude::*; use bevy_egui::egui::text::LayoutJob; -use bevy_egui::egui::{Stroke, TextFormat}; -use bevy_egui::*; +use bevy_egui::prelude::*; use chrono::prelude::*; -use web_time::SystemTime; -use crate::command::{CommandHints, ExecuteCommand}; -use crate::config::ToColor32; +use crate::command::{COMMAND_MESSAGE_NAME, COMMAND_RESULT_NAME, ExecuteCommand}; use crate::logging::LogMessage; use crate::prelude::ConsoleConfig; #[cfg(feature = "completions")] use crate::command::AutoCompletions; +use crate::ui::ansi::ansi_to_layout_job; #[cfg(feature = "completions")] mod completions; #[cfg(feature = "completions")] pub use completions::MAX_COMPLETION_SUGGESTIONS; -/// Prefix for log messages that show a previous command. -pub const COMMAND_MESSAGE_PREFIX: &str = "$ "; -/// Prefix for log messages that show the result of a command. -pub const COMMAND_RESULT_PREFIX: &str = "> "; -/// Identifier for log messages that show a previous command. -pub const COMMAND_MESSAGE_NAME: &str = "console_command"; -/// Identifier for log messages that show the result of a command. -pub const COMMAND_RESULT_NAME: &str = "console_result"; +mod ansi; #[derive(Default, Resource)] -pub struct ConsoleUiState { - /// Whether the console is open or not. - pub(crate) open: bool, +pub(crate) struct ConsoleUiState { /// Whether we have set focus this open or not. - pub(crate) text_focus: bool, + pub text_focus: bool, /// A list of all log messages received plus an /// indicator indicating if the message is new. - pub(crate) log: Vec<(LogMessage, bool)>, - /// The command in the text bar. - pub(crate) command: String, + pub log: Vec, + /// The command currently in the text bar. + pub command: String, #[cfg(feature = "completions")] - pub(crate) selected_completion: usize, + pub selected_completion: usize, } -impl ConsoleUiState { - /// Whether the console is currently open or not - pub fn open(&self) -> bool { - self.open - } -} - -fn system_time_to_chrono_utc(t: SystemTime) -> chrono::DateTime { - let dur = t.duration_since(web_time::SystemTime::UNIX_EPOCH).unwrap(); - let (sec, nsec) = (dur.as_secs() as i64, dur.subsec_nanos()); - - chrono::Utc.timestamp_opt(sec, nsec).unwrap() -} - -pub(crate) fn read_logs(mut logs: EventReader, mut state: ResMut) { +pub(crate) fn read_logs(mut logs: MessageReader, mut state: ResMut) { for log_message in logs.read() { - state.log.push((log_message.clone(), true)); - } -} - -pub(crate) fn open_close_ui( - mut state: ResMut, - key: Res>, - config: Res, -) { - if key.just_pressed(config.open_key) { - state.open = !state.open; - state.text_focus = false; + state.log.push(log_message.clone()); } } @@ -82,42 +46,31 @@ pub(crate) fn render_ui_system( mut commands: Commands, mut state: ResMut, key: Res>, - mut hints: ResMut, config: Res, #[cfg(feature = "completions")] completions: Res, ) { egui::Window::new("Developer Console") .collapsible(false) .default_width(900.) - .show(contexts.ctx_mut(), |ui| { - render_ui( - ui, - &mut commands, - &mut state, - &key, - &mut hints, - &config, - &completions, - ) + .show(contexts.ctx_mut().unwrap(), |ui| { + render_ui(ui, &mut commands, &mut state, &key, &config, &completions); }); } /// The function that renders the UI of the developer console. -pub fn render_ui( +pub(crate) fn render_ui( ui: &mut egui::Ui, commands: &mut Commands, state: &mut ConsoleUiState, key: &ButtonInput, - hints: &mut CommandHints, config: &ConsoleConfig, #[cfg(feature = "completions")] completions: &AutoCompletions, ) { fn submit_command(command: &mut String, commands: &mut Commands) { if !command.trim().is_empty() { - info!(name: COMMAND_MESSAGE_NAME, "{COMMAND_MESSAGE_PREFIX}{}", command.trim()); // Get the owned command string by replacing it with an empty string let command = std::mem::take(command); - commands.add(ExecuteCommand(command)); + commands.queue(ExecuteCommand(command)); } } @@ -125,26 +78,24 @@ pub fn render_ui( submit_command(&mut state.command, commands); } - completions::change_selected_completion(ui, state, &completions); + completions::change_selected_completion(ui, state, completions); // A General rule when creating layouts in egui is to place elements which fill remaining space last. // Since immediate mode ui can't predict the final sizes of widgets until they've already been drawn // Thus we create a bottom panel first, where our text edit and submit button resides. egui::TopBottomPanel::bottom("bottom panel") - .frame(egui::Frame::none().outer_margin(egui::Margin { - left: 5.0, - right: 5.0, - top: 5. + 6., - bottom: 5.0, + .frame(egui::Frame::NONE.outer_margin(egui::Margin { + left: 5, + right: 5, + top: 5 + 6, + bottom: 5, })) .show_inside(ui, |ui| { let text_edit_id = egui::Id::new("text_edit"); - //We can use a right to left layout, so we can place the text input last and tell it to fill all remaining space + // We can use a right to left layout, so we can place the text input last and tell it to fill all remaining space ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // ui.button is a shorthand command, a similar command exists for text edits, but this is how to manually construct a widget. - // doing this also allows access to more options of the widget, rather than being stuck with the default the shorthand picks. if ui.button("Submit").clicked() { submit_command(&mut state.command, commands); @@ -169,8 +120,8 @@ pub fn render_ui( state, ui, commands, - &completions, - &config, + completions, + config, ); // Each time we open the console, we want to set focus to the text edit control. @@ -183,37 +134,24 @@ pub fn render_ui( // Now we can fill the remaining minutespace with a scrollarea, which has only the vertical scrollbar enabled and expands to be as big as possible. egui::ScrollArea::new([false, true]) .auto_shrink([false, true]) + .stick_to_bottom(true) .show(ui, |ui| { ui.vertical(|ui| { - let mut command_index = 0; - - for (id, (message, is_new)) in state.log.iter_mut().enumerate() { - add_log(ui, id, message, is_new, hints, &config, &mut command_index); + for (id, message) in state.log.iter_mut().enumerate() { + add_log(ui, id, message, config); } }); }); } -fn add_log( - ui: &mut egui::Ui, - id: usize, - event: &LogMessage, - is_new: &mut bool, - hints: &mut CommandHints, - config: &ConsoleConfig, - command_index: &mut usize, -) { +fn add_log(ui: &mut egui::Ui, id: usize, event: &LogMessage, config: &ConsoleConfig) { ui.push_id(id, |ui| { - let time_utc = system_time_to_chrono_utc(event.time); + let time_utc = event.time; let time: DateTime = time_utc.into(); - let text = format_line(time, config, event, *is_new, command_index, hints); + let text = format_line(time, config, event); let label = ui.label(text); - if *is_new { - label.scroll_to_me(Some(egui::Align::Max)); - *is_new = false; - } label.on_hover_ui(|ui| { let mut text = LayoutJob::default(); text.append("Time: ", 0.0, config.theme.format_text()); @@ -222,22 +160,27 @@ fn add_log( 0.0, config.theme.format_dark(), ); + text.append("\nTime (UTC): ", 0.0, config.theme.format_text()); text.append( &time_utc.to_rfc3339_opts(chrono::SecondsFormat::Micros, true), 0.0, config.theme.format_dark(), ); + text.append("\nName: ", 0.0, config.theme.format_text()); text.append(event.name, 0.0, config.theme.format_dark()); - text.append("\nTarget : ", 0.0, config.theme.format_text()); + + text.append("\nTarget: ", 0.0, config.theme.format_text()); text.append(event.target, 0.0, config.theme.format_dark()); + text.append("\nModule Path: ", 0.0, config.theme.format_text()); if let Some(module_path) = event.module_path { text.append(module_path, 0.0, config.theme.format_dark()); } else { text.append("(Unknown)", 0.0, config.theme.format_dark()); } + text.append("\nFile: ", 0.0, config.theme.format_text()); if let (Some(file), Some(line)) = (event.file, event.line) { text.append(&format!("{file}:{line}"), 0.0, config.theme.format_dark()); @@ -259,9 +202,6 @@ fn format_line( level, .. }: &LogMessage, - new: bool, - command_index: &mut usize, - hints: &mut CommandHints, ) -> LayoutJob { let mut text = LayoutJob::default(); text.append( @@ -270,59 +210,12 @@ fn format_line( config.theme.format_dark(), ); match *name { - COMMAND_MESSAGE_NAME => { - if new { - hints.reset_hint_added(); - } - let hints = &hints[*command_index]; - - *command_index += 1; - - let message_stripped = message - .strip_prefix(COMMAND_MESSAGE_PREFIX) - .unwrap_or(message); - text.append(COMMAND_MESSAGE_PREFIX, 0.0, config.theme.format_dark()); - // TODO: Handle more than just the first element - if let Some(hint) = hints.first() { - text.append( - &message_stripped[..hint.span.start], - 0., - config.theme.format_text(), - ); - text.append( - &message_stripped[hint.span.start..hint.span.end], - 0., - TextFormat { - underline: Stroke::new(1.0, config.theme.error.to_color32()), - ..config.theme.format_text() - }, - ); - text.append( - &message_stripped[hint.span.end..], - 0., - config.theme.format_text(), - ); - return text; - } - text.append(message_stripped, 0.0, config.theme.format_text()); - text - } - COMMAND_RESULT_NAME => { - text.append(COMMAND_RESULT_PREFIX, 0.0, config.theme.format_dark()); - text.append( - message - .strip_prefix(COMMAND_RESULT_PREFIX) - .unwrap_or(message), - 0.0, - config.theme.format_text(), - ); - text - } + COMMAND_MESSAGE_NAME | COMMAND_RESULT_NAME => {} _ => { text.append(level.as_str(), 0.0, config.theme.format_level(*level)); - text.append(&format!(" {message}"), 0.0, config.theme.format_text()); - - text + text.append(" ", 0.0, config.theme.format_text()); } } + ansi_to_layout_job(message, config, &mut text); + text } diff --git a/src/ui/ansi.rs b/src/ui/ansi.rs new file mode 100644 index 0000000..a6237e4 --- /dev/null +++ b/src/ui/ansi.rs @@ -0,0 +1,80 @@ +use ansitok::{AnsiColor, ElementKind, Output, VisualAttribute}; +use bevy::log::error_once; +use bevy_egui::egui::text::LayoutJob; +use bevy_egui::egui::{Color32, Stroke}; + +use crate::config::{ConsoleConfig, ToColor32}; + +pub fn ansi_to_layout_job(input: &str, config: &ConsoleConfig, job: &mut LayoutJob) { + let ConsoleConfig { theme, .. } = config; + // Maintain the current style state + let mut current_format = theme.format_text(); + let color4bit = |c| { + match c { + 31 => theme.member, + 32 => theme.string, + 33 => theme.value, + 34 => theme.function, + 35 => theme.keyword, + 36 => theme.variant, + + 90 => theme.dark, + 93 => theme.type_name, + + _ => todo!("color for 4bit ansi color {c}"), + } + .to_color32() + }; + let ansi_to_color32 = |ansi| match ansi { + AnsiColor::Bit4(c) => color4bit(c), + AnsiColor::Bit8(c) => match c { + 0..=7 => color4bit(30 + c), + _ => todo!(), + }, + AnsiColor::Bit24 { r, g, b } => Color32::from_rgb(r, g, b), + }; + + for element in ansitok::parse_ansi(input) { + let text = &input[element.range()]; + match element.kind() { + ElementKind::Sgr => match ansitok::parse_ansi_sgr(text).next().unwrap() { + Output::Escape(esc) => match esc { + VisualAttribute::Bold => todo!(), + VisualAttribute::Faint => current_format.color = theme.text_color.to_color32(), + VisualAttribute::Italic => current_format.italics = true, + VisualAttribute::FgColor(ansi_color) => { + current_format.color = ansi_to_color32(ansi_color); + if current_format.underline.width > 0.0 { + current_format.underline = Stroke::new(1.0_f32, current_format.color); + } + } + VisualAttribute::BgColor(ansi_color) => { + current_format.background = ansi_to_color32(ansi_color); + } + VisualAttribute::UndrColor(ansi_color) => { + current_format.underline = + Stroke::new(1.0_f32, ansi_to_color32(ansi_color)); + } + VisualAttribute::Underline => { + current_format.underline = Stroke::new(1.0_f32, current_format.color); + } + VisualAttribute::Reset(_) => current_format = theme.format_text(), + _ => error_once!("Unrecognized ansi visual attribute type: {esc:?}"), + }, + Output::Text(text) => match &text[0..2] { + // 4: is not part of the ansi spec but it is used in many terminal emulators, + // we use it for errors in bevy_dev_console + // however, ansitok doesn't support parsing it. + "4:" => { + // egui doesn't support fancy underline styles + current_format.underline = Stroke::new(1.0_f32, current_format.color); + } + _ => unreachable!("found text in sgr: {text}"), + }, + }, + ElementKind::Osc => todo!("implement Osc ansi sequences"), + ElementKind::Text => job.append(text, 0.0, current_format.clone()), + _ => {} // ignore + } + } +} diff --git a/src/ui/completions.rs b/src/ui/completions.rs index 208cc13..1f4200f 100644 --- a/src/ui/completions.rs +++ b/src/ui/completions.rs @@ -1,7 +1,7 @@ -use bevy::ecs::system::Commands; -use bevy_egui::egui; +use bevy::prelude::*; +use bevy_egui::egui::{self, LayerId, Popup, PopupAnchor}; -use crate::command::{AutoCompletions, CompletionSuggestion, UpdateAutoComplete}; +use crate::command::{AutoCompletions, CompletionSuggestion, DefaultCommandParser}; use crate::prelude::ConsoleConfig; use super::ConsoleUiState; @@ -13,15 +13,15 @@ pub fn completions( text_edit: egui::text_edit::TextEditOutput, text_edit_id: egui::Id, state: &mut ConsoleUiState, - ui: &mut egui::Ui, + ui: &egui::Ui, commands: &mut Commands, completions: &AutoCompletions, config: &ConsoleConfig, ) { - let text_edit_complete_id = ui.make_persistent_id("text_edit_complete"); + let mut popup_open = false; if let Some(cursor_range) = text_edit.state.cursor.char_range() { - let [primary, secondary] = cursor_range.sorted(); + let [primary, secondary] = cursor_range.sorted_cursors(); fn non_keyword(character: char) -> bool { !(character.is_alphanumeric() || character == '_') @@ -48,102 +48,105 @@ pub fn completions( // if text_edit.response.changed() { if true { if let Some(cursor_index) = cursor_index { - ui.memory_mut(|mem| { + ui.memory_mut(|_| { if !completions.is_empty() { - mem.open_popup(text_edit_complete_id) + popup_open = true; } }); let before_cursor = &state.command[..=cursor_index]; - let keyword_before = match before_cursor.rfind(non_keyword) { - // If found, return the slice from the end of the non-alphanumeric character to the cursor position - Some(index) => &before_cursor[(index + 1)..], - // If not found, the whole substring is a word - None => before_cursor, - }; - commands.add(UpdateAutoComplete(keyword_before.to_owned())); - } else { - ui.memory_mut(|mem| { - if mem.is_popup_open(text_edit_complete_id) { - mem.close_popup(); - } + let keyword_before = before_cursor + .rfind(non_keyword) + .map_or(before_cursor, |index| &before_cursor[(index + 1)..]) + .to_owned(); + commands.queue(move |world: &mut World| { + world.resource_scope(|world, parser: Mut| { + let completions: Vec = + parser.completion(&keyword_before, world); + world.resource_mut::().0 = completions; + }); }); + } else { + ui.memory_mut(|_| popup_open = false); } } - if let Some(cursor_index) = cursor_index { - if ui.input(|i| i.key_pressed(egui::Key::Tab)) { - // Remove the old text - let before_cursor = &state.command[..=cursor_index]; - let index_before = match before_cursor.rfind(non_keyword) { - Some(index) => index + 1, - None => 0, - }; - let after_cursor = &state.command[cursor_index..]; - match after_cursor.find(non_keyword) { - Some(characters_after) => state - .command - .drain(index_before..cursor_index + characters_after), - None => state.command.drain(index_before..), - }; - // Add the completed text - let completed_text = &completions.0[state.selected_completion].suggestion; - - state.command.insert_str(index_before, completed_text); + if let Some(cursor_index) = cursor_index + && let Some(suggestion) = &completions.0.get(state.selected_completion) + && ui.input(|i| i.key_pressed(egui::Key::Tab)) + { + // Remove the old text + let before_cursor = &state.command[..=cursor_index]; + let index_before = before_cursor + .rfind(non_keyword) + .map_or(0, |index| index + 1); + let after_cursor = &state.command[cursor_index..]; + match after_cursor.find(non_keyword) { + Some(characters_after) => state + .command + .drain(index_before..cursor_index + characters_after), + None => state.command.drain(index_before..), + }; + // Add the completed text + let completed_text = &suggestion.suggestion; + state.command.insert_str(index_before, completed_text); - // Set the cursor position - let mut text_edit_state = text_edit.state; + // Set the cursor position + let mut text_edit_state = text_edit.state; - let mut cursor_range = egui::text::CCursorRange::two(primary, secondary); + let mut cursor_range = egui::text::CCursorRange::two(primary, secondary); - cursor_range.primary.index += - completed_text.len() - (cursor_index - index_before) - 1; - cursor_range.secondary.index += - completed_text.len() - (cursor_index - index_before) - 1; + cursor_range.primary.index += completed_text.len() - (cursor_index - index_before) - 1; + cursor_range.secondary.index += + completed_text.len() - (cursor_index - index_before) - 1; - text_edit_state.cursor.set_char_range(Some(cursor_range)); - egui::TextEdit::store_state(ui.ctx(), text_edit_id, text_edit_state); - } + text_edit_state.cursor.set_char_range(Some(cursor_range)); + egui::TextEdit::store_state(ui.ctx(), text_edit_id, text_edit_state); } } - egui::popup_below_widget( - ui, - text_edit_complete_id, - &text_edit.response, - egui::PopupCloseBehavior::CloseOnClickOutside, - |ui| { - ui.vertical(|ui| { - for ( - i, - CompletionSuggestion { - suggestion, - highlighted_indices, - }, - ) in completions.iter().take(6).enumerate() - { - let mut layout = egui::text::LayoutJob::default(); - for (i, _) in suggestion.char_indices() { - layout.append( - &suggestion[i..=i], - 0.0, - if highlighted_indices.contains(&i) { - config.theme.format_bold() - } else { - config.theme.format_text() - }, - ); - } - let res = ui.label(layout); - if i == state.selected_completion { - res.highlight(); - } + Popup::new( + ui.make_persistent_id("text_edit_complete"), + ui.ctx().clone(), + PopupAnchor::ParentRect(text_edit.response.rect), + LayerId::debug(), + ) + .open(popup_open) + .show(|ui| { + ui.set_width(200.0); + ui.vertical(|ui| { + for ( + i, + CompletionSuggestion { + suggestion, + highlighted_indices, + }, + ) in completions + .iter() + .take(MAX_COMPLETION_SUGGESTIONS) + .enumerate() + { + let mut layout = egui::text::LayoutJob::default(); + for (i, _) in suggestion.char_indices() { + layout.append( + &suggestion[i..=i], + 0.0, + if highlighted_indices.contains(&i) { + config.theme.format_bold() + } else { + config.theme.format_text() + }, + ); } - }) - }, - ); + let res = ui.add(egui::Label::new(layout).wrap_mode(egui::TextWrapMode::Truncate)); + if i == state.selected_completion { + res.highlight(); + } + } + }) + }); } /// Also consumes the up and down arrow keys. pub fn change_selected_completion( - ui: &mut egui::Ui, + ui: &egui::Ui, state: &mut ConsoleUiState, completions: &[CompletionSuggestion], ) { diff --git a/typos.toml b/typos.toml new file mode 100644 index 0000000..ee784ae --- /dev/null +++ b/typos.toml @@ -0,0 +1,2 @@ +[default] +locale = "en-us"