diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 960d48bd..7740c6a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ permissions: contents: read env: - RUSTFLAGS: -Dwarnings + RUSTFLAGS: -Dwarnings # Deny warnings - i.e. warnings cause build errors jobs: test: @@ -31,6 +31,8 @@ jobs: - name: Enable type layout randomization run: echo RUSTFLAGS=${RUSTFLAGS}\ -Zrandomize-layout >> $GITHUB_ENV if: matrix.rust == 'nightly' + - name: Add TEST_RUST_MODE env variable + run: echo TEST_RUST_MODE=${{matrix.rust}} >> $GITHUB_ENV - run: cargo test - uses: actions/upload-artifact@v4 if: matrix.rust == 'nightly' && always() @@ -39,7 +41,7 @@ jobs: path: Cargo.lock msrv: - name: Fresh install of MSRV across self (1.61) + latest dependencies (1.68) compiles + name: MSRV (1.68) Compiles runs-on: ubuntu-latest timeout-minutes: 45 steps: @@ -61,6 +63,29 @@ jobs: toolchain: stable - run: ./style-check.sh + benches: + name: Benchmarks + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + - run: ./bench_compilation.sh + - run: ./bench.sh + + book: + name: Rust Book Test + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + - run: ./book/test.sh + miri: name: Miri runs-on: ubuntu-latest diff --git a/.github/workflows/publish_book.yml b/.github/workflows/publish_book.yml new file mode 100644 index 00000000..a8416d28 --- /dev/null +++ b/.github/workflows/publish_book.yml @@ -0,0 +1,44 @@ +name: Publish book to Github Pages + +on: + push: + branches: + - main + +permissions: + contents: read # to read the docs and build job + pages: write # to deploy to pages + id-token: write # to verify the deployment originates from an appropriate source + +jobs: + # Build job - https://github.com/actions/upload-pages-artifact + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + # Sets up the repository configuration for github pages to be deployed from an action + - name: Setup Pages + uses: actions/configure-pages@v5 + # Builds the book and outputs to book/book + - name: Build book + run: ./book/build.sh + # Creates a correctly compressed archive from the build HTML file, and outputs it as + # a `github-pages` artifact + - name: Upload static files as artifact + uses: actions/upload-pages-artifact@v3 + with: + path: book/book + + # Deployment job - https://github.com/actions/deploy-pages + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + # Deploys the artifact `github-pages` created by the `upload-pages-artifact` job + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..331078b4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + // I was getting errors such as this when Rust Analyzer starts: + // 'PrimeCaches#5' has overflowed its stack + // This setting apparently avoids this https://github.com/rust-lang/rust-analyzer/issues/19698#issuecomment-2833543017 + "rust-analyzer.cachePriming.enable": false +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 089c7c6b..7815b6b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,118 @@ +# Major Version 1.0 + +## 1.0.0 + +This moves preinterpret to an expression-based language, inspired by Rust, but with some twists to make writing code generation code quicker: +* Token streams as a native feature +* Flexible Javascript-like objects/arrays +* New expressions such as `attempt { .. }` for trying alternatives + +### Variable Expansions + +* `#x` outputs the contents of `x`. +* `#(x.to_group())` outputs the contents of `x` in a transparent group. + +### New Commands + +* Core commands: + * Creating errors: + * `%[].error("Error Message")` to output a compile error at the macro call site + * `%[_].error("Error Message")` to output a compile error at the given line + * `%[$token].error("Error Message")` to output a compile error at the span of the tokens + * `%[].assert(, )` to assert the condition is true, else output a compile error at the macro call site + * `%[_].assert(, )` to assert the condition is true, else output a compile error at the given line + * `%[$token].assert(, )` to assert the condition is true, else output a compile error at the span of the tokens + * `#(x += %[...];)` to add extra tokens to a variable's stream. + * `#(let _ = %[...];)` interprets its arguments but then ignores any outputs. + * `%[...]` can be used to just output its interpreted contents. It's useful to create a stream value inside an expression. + * `%[...].reinterpret_as_run()` is like an `eval` command in scripting languages. It takes a stream, and runs it as a preinterpret expression block content like `run!{ ... }`. Similarly, `%[...].reinterpret_as_stream()` runs it as a stream literal, like `stream!{ ... }`. Each is pure - the reinterpreted code can't read from or write to variables, and can only return values. + * `None.configure_preinterpret(%{ iteration_limit: XXX })` can be used to adjust the iteration limit. +* Expression commands: + * The expression block `#(let x = 123; let y = 1.0; y /= x; y + 1)` which is discussed in more detail below. +* Control flow commands: + * `[!if! { ... }]` and `[!if! { ... } !elif! { ... } !else! { ... }]` + * `[!while! { ... }]` + * `[!for! in [ ... ] { ... }]` + * `[!loop! { ... }]` + * `[!continue!]` + * `[!break!]` +* Token-stream utility commands: + * `.is_empty()` + * `.len()` which for streams gives the number of token trees in the token stream. + * `%group[...]` which wraps the tokens in a transparent group. Can be useful if using token streams as iteration sources, e.g. in `!for!`. + * `.intersperse(, ?)` which inserts the separator between each value from the iterator, and returns a vec. This can then be handled as a vector, embedded in a stream, or mapped with `to_string()` or `to_stream()` as required. + * `.split(, ?)` which can be used to split a stream with a given separating stream. + * `[countries, flags, capitals].zip()` or `%{ countries, flags, capitals }.zip()` which can be used to combine multiple streams together. + +### Expressions + +Expressions can be evaluated with `#(...)` and are also used in the `!if!` and `!while!` loop conditions. + +Expressions behave intuitively as you'd expect from writing regular rust code, except they are executed at compile time. + +The `#(...)` expression block behaves much like a `{ .. }` block in rust. It supports multiple statements ending with `;` and optionally a final statement. + +Statements are either expressions `EXPR` or `let x = EXPR`, `x = EXPR`, `x += EXPR` for some operator such as `+`. + +Assigment: +* `let = ` which supports patterns include ignore (`_`), array destructurings (`[a, b, ..]`), object destructurings `{ a: x, b, .. }`, and stream parsing `%[..]`. + +The following are recognized values: +* Object literals `%{ x: "Hello", y, ["z"]: "World" }` behave similarly to Javascript objects. +* Token stream literals `%[...]` take any token stream, and support embedding `#variables` or `#(<..expressions..>)` inside them. +* Raw token stream literals `%raw[...]` are used to capture raw tokens, and are not interpreted (e.g. `#` has no special meaning). +* Integer literals, with or without a suffix +* Float literals, with or without a suffix +* Boolean literals +* String literals +* Char literals +* Other literals +* Rust [ranges](https://doc.rust-lang.org/reference/expressions/range-expr.html) +* Token streams which are defined as `[...]`. + +The following operators are supported: +* The numeric operators: `+ - * / % & | ^` +* The lazy boolean operators: `|| &&` +* The comparison operators: `== != < > <= >=` +* The shift operators: `>> <<` +* The concatenation operator: `+` can be used to concatenate strings and streams. +* Casting with `as` including to untyped integers/floats with `as int` and `as float` and to a flattened stream with `as stream`. +* () and none-delimited groups for precedence + +The following methods are supported: +* On all values: + * `.clone()` - converts a reference to a mutable value. You will be told in an error if this is needed. + * `.as_mut()` - converts an owned value to a mutable value. You will be told in an error if this is needed. + * `.take()` - takes the value from a mutable reference, and replaces it with `None`. Useful instead of cloning. + * `.debug()` - a debugging aid whilst writing code. Causes a compile error with the content of the value. Roughly equivalent to `%[_].error(x.to_debug_string())` + * `.to_debug_string()` - returns the value's contents as a string for debugging purposes +* On arrays: `len()` and `push()` +* On streams: `len()` + +An expression also supports embedding commands `[!xxx! ...]`, other expression blocks, variables and flattened variables. The value type outputted by a command depends on the command. + +### Transforming + +Transforming performs parsing of a token stream, whilst also outputting a stream. The input stream must be parsed in its entirety. + +Transform streams (or substreams) can be redirected to set variables or append to variables. Commands can also be injected to add to the output. + +Inside a transform stream, the following grammar is supported: + +* `@(...)`, `@(#x = ...)`, `@(#x += ...)` and `@(_ = ...)` - Explicit transform (sub)streams which either output, set, append or discard its output. +* Explicit punctuation, idents, literals and groups. These aren't output by default, except directly inside a `@[EXACT ...]` transformer. + +* Named destructurings: + * `@IDENT` - Consumes and output any ident. + * `@PUNCT` - Consumes and outputs any punctation + * `@LITERAL` - Consumes and outputs any literal + * `@REST` - Consumes the rest of the input, until the end of the stream or content of the current group + * `@[UNTIL x]` - Consumes the rest of the input, until the end of stream OR until token `x`. `x` can be a group like `()` which matches the opening bracket `(`. + * `@[GROUP ...]` - Consumes a none-delimited group. Its arguments are used to transform the group's contents. + * `@[EXACT(%[..])]` - Takes an input stream expression. Expects to consume exactly that stream from the output (ignoring none-groups). It outputs the parsed stream. +* Commands: Their output is appended to the transform's output. Useful patterns include: + * If `@(inner = ...)` then `inner.to_group()` - wraps the output in a transparent group + # Major Version 0.2 ## 0.2.1 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..801e06c4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,71 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Preinterpret is a Rust procedural macro crate that provides macros which form a code generation toolkit that simplifies declarative macro development. It combines functionality from quote, paste, and syn crates to enable: + +- Variable definition and substitution with `#(let var = %[...];)` and `#var` +- Commands for concatenation, case conversion, and token manipulation like `[!ident! ...]`, `[!string! ...]`, `[!ident_snake! ...]` +- Control flow and parsing capabilities + +## Plans and Tasks + +There are various files in the `./plans` folder, the "current vision" is `2025-09-vision.md` and the "todo list" is `TODO.md`. + +## Architecture + +The codebase is organized into several main modules: + +- **src/lib.rs** - Main entry point and public API +- **src/interpretation/** - Core interpreter logic for processing commands and variables + - `interpreter.rs` - Main interpreter implementation + - `commands/` - Built-in command implementations (concat, transforming, control flow, etc.) + - `bindings.rs` - Variable binding management +- **src/expressions/** - Expression evaluation system for advanced features + - `evaluation/` - Expression evaluator with type resolution and frame management +- **src/transformation/** - Token stream transformation utilities +- **src/extensions/** - Helper traits and utilities for working with proc-macro2 tokens + +## Development Commands + +### Testing +- `cargo test` - Run all tests +- `cargo test --release` - Run tests in release mode +- `cargo miri test` - Run tests with Miri for memory safety + +### Code Quality +- `./style-check.sh` - Check formatting and run clippy (equivalent to CI checks) +- `./style-fix.sh` - Auto-fix formatting and clippy issues +- `cargo fmt --check` - Check formatting only +- `cargo clippy --tests` - Run clippy lints + +### Build & Check +- `cargo build` - Build the crate +- `cargo check` - Quick syntax/type check +- `./local-check-msrv.sh` - Test minimum supported Rust version (1.63) + +### Documentation +- `cargo doc --open` - Build and open documentation +- `./book/test.sh` - Build the mdbook documentation + +## Key Implementation Notes + +This is a proc-macro crate (`proc-macro = true` in Cargo.toml) that: + +- Uses syn 2.0 for parsing with full feature set enabled +- Supports minimum Rust version 1.63 (Edition 2021) +- Employs RefCell/Rc patterns for interior mutability in expression evaluation +- Uses trybuild for compile-time testing of macro expansions + +The expression system (newer feature) provides advanced capabilities like mathematical operations and control flow, built on top of the core interpretation framework. + +## Testing Strategy + +Tests are located in the `tests/` directory and use trybuild for testing compilation failures. The CI runs tests on stable, beta, and nightly Rust versions with warnings treated as errors. + +## Commit Strategy + +* Run `style-fix.sh` before committing +* Use the conventional commits pattern for commit message prefixes. diff --git a/Cargo.toml b/Cargo.toml index c45a6856..94855c4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,29 @@ keywords = ["macros", "declarative-macros", "toolkit", "interpreter", "preproces # And whilst there's a development-tools::procedural-macro-helpers, there's no declarative macro category. categories = ["development-tools", "compilers"] # MSRV 1.56.0 is the start of Edition 2021 -# MSRV 1.61.0 was the old MSRV of syn -# MRSV 1.68.0 is the new latest MSRV of syn (as of syn 2.0.107 released on 19th October 2025) - but keeping on MSRV 1.61.0 for now +# MSRV 1.61.0 is the old MSRV of syn +# MRSV 1.63.0 is needed to support RefMut::filter_map +# MRSV 1.68.0 is the new latest MSRV of syn (as of syn 2.0.107 released on 19th October 2025) # If changing this, also update the local-check-msrv.sh script and ci.yml -rust-version = "1.61" +rust-version = "1.68" [lib] proc-macro = true +[[bench]] +name = "basic" +harness = false +required-features = ["benchmark"] + +[features] +benchmark = [] # For internal use only +debug = [] # Non-stable, for internal use only + [dependencies] -proc-macro2 = { version = "1.0" } -syn = { version = "2.0", default-features = false, features = ["parsing"] } +proc-macro2 = { version = "1.0.93" } +syn = { version = "2.0.107", default-features = false, features = ["parsing", "derive", "printing", "clone-impls", "full"] } +quote = { version = "1.0.38", default-features = false } +slotmap = { version = "1.0.7" } + +[dev-dependencies] +trybuild = { version = "1.0.110", features = ["diff"] } diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md new file mode 100644 index 00000000..9e9d2095 --- /dev/null +++ b/KNOWN_ISSUES.md @@ -0,0 +1,7 @@ +# Since Major Version 0.3.0 + +* `[MAX LITERAL]` - In an expression, the maximum negative integer literal is not valid, for example `-128i8`. + * By comparison, in `rustc`, there is special handling for such literals, so that e.g. `-128i8` and `--(-128i8)` are accepted. + * In rust analyzer, before the full pass, it seems to store literals as u128 and perform wrapping arithmetic on them. On a save / full pass, it does appear to reject literals which are out of bounds (e.g. 150i8). + * We could fix this by special casing maximal signed integer literals (e.g. 128 for an i8) which have yet to be involved in a non-unary expression. + diff --git a/README.md b/README.md index 4d35d2ed..4848c412 100644 --- a/README.md +++ b/README.md @@ -15,43 +15,103 @@ If updating this readme, please ensure that the lib.rs rustdoc is also updated: * Run ./style-fix.sh --> -This crate provides the `preinterpret!` macro, a simple pre-processor of the token stream. It can be used inside the output of a declarative macro, or as a mini code generation tool all of its own. +Preinterpret takes the pain out of Rust code generation, providing a new paradigm to replace the clunkiness of declarative macros [[1](https://veykril.github.io/tlborm/decl-macros/patterns/callbacks.html), [2](https://github.com/rust-lang/rust/issues/96184#issue-1207293401), [3](https://veykril.github.io/tlborm/decl-macros/minutiae/metavar-and-expansion.html), [4](https://veykril.github.io/tlborm/decl-macros/patterns/push-down-acc.html)]. At its heart it is a bespoke Rust-like interpreted language, built from the ground up for code generation use cases. -It is a more powerful replacement for [paste](https://crates.io/crates/paste), and also brings functionality typically reserved for procedural macros: [quote](https://crates.io/crates/quote)-like token-stream substitution and some [syn](https://crates.io/crates/syn)-based functionality for operating on tokens and literals. +The [preinterpret](https://crates.io/crates/preinterpret) crate provides: +* The `stream!` macro, which starts in token stream output mode +* The `run!` macro, which starts in interpreter mode, and can `emit` or return a token stream -```rust -preinterpret::preinterpret! { - [!set! #type_name = HelloWorld] +To install, add the following to your `Cargo.toml`: - struct #type_name; +```toml +[dependencies] +preinterpret = "0.2" +``` - #[doc = [!string! "This type is called [`" #type_name "`]"]] - impl #type_name { - fn [!ident_snake! say_ #type_name]() -> &'static str { - [!string! "It's time to say: " [!title! #type_name] "!"] +## Use cases + +### Simple code generation + +Sometimes you just need to generate lots of similar code, and preinterpet can be used directly: + +```rust +trait TupleLength { + fn len(&self) -> usize; +} + +preinterpret::run!{ + for N in 0..=12 { + let type_params = %[]; + for a in ('A'..'Z').into_iter().take(N) { + type_params += a.to_ident() + %[,]; } + emit %[ + impl<#type_params> TupleLength for (#type_params) { + fn len(&self) -> usize { + #N + } + } + ]; } } -assert_eq!(HelloWorld::say_hello_world(), "It's time to say: Hello World!") +assert_eq!(('a', 'b', 'c').len(), 3); ``` -To install, add the following to your `Cargo.toml`: +### Inside procedural macros -```toml -[dependencies] -preinterpret = "0.2" +It can be used to simplify code generation inside procedural macro definitions: +* It replaces [paste](https://crates.io/crates/paste) to allow concatenated creation of idents +* It brings various features previously reserved for procedural macros: [quote](https://crates.io/crates/quote)-like token-stream substitution and [syn](https://crates.io/crates/syn)-based functionality for operating on tokens and literals. + +Notably, using variables to name token stream sections for clarity and reuse can make macro code a lot easier to read. + +```rust +macro_rules! create_my_type { + ( + $(#[$attributes:meta])* + $vis:vis struct $type_name:ident { + $($field_name:ident: $inner_type:ident),* $(,)? + } + ) => {preinterpret::stream! { + #{ + let type_name = %ident[My $type_name]; + } + + $(#[$attributes])* + $vis struct #type_name { + $($field_name: $inner_type,)* + } + + impl #type_name { + $( + fn %ident_snake[my_ $inner_type](&self) -> &$inner_type { + &self.$field_name + } + )* + } + }} +} +create_my_type! { + struct Struct { + field0: String, + field1: u64, + } +} +assert_eq!(MyStruct { field0: "Hello".into(), field1: 21 }.my_string(), "Hello") ``` -This README concerns `preinterpret` v0.2 which offers a simple pre-processor. A much more comprehensive rust-inspired interpreter is coming in v1.0, currently in progress on the `develop` branch. +### As a replacement for procedural macros + +Coming soon... ## User Guide Preinterpret works with its own very simple language, with two pieces of syntax: * **Commands**: `[!command_name! input token stream...]` take an input token stream and output a token stream. There are a number of commands which cover a toolkit of useful functions. -* **Variables**: `[!set! #var_name = token stream...]` defines a variable, and `#var_name` substitutes the variable into another command or the output. +* **Variables**: `#(let var_name = %[token stream...];)` defines a variable, and `#var_name` substitutes the variable into another command or the output. -Commands can be nested intuitively. The input of all commands (except `[!raw! ...]`) are first interpreted before the command itself executes. +Commands can be nested intuitively. In general, the input of commands are first interpreted before the command itself executes. ### Declarative macro example @@ -64,8 +124,10 @@ macro_rules! create_my_type { $vis:vis struct $type_name:ident { $($field_name:ident: $inner_type:ident),* $(,)? } - ) => {preinterpret::preinterpret! { - [!set! #type_name = [!ident! My $type_name]] + ) => {preinterpret::stream! { + #{ + let type_name = %ident[My $type_name]; + } $(#[$attributes])* $vis struct #type_name { @@ -74,7 +136,7 @@ macro_rules! create_my_type { impl #type_name { $( - fn [!ident_snake! my_ $inner_type](&self) -> &$inner_type { + fn %ident_snake[my_ $inner_type](&self) -> &$inner_type { &self.$field_name } )* @@ -109,23 +171,25 @@ Preinterpret commands take token streams as input, and return token streams as o If migrating from [paste](https://crates.io/crates/paste), the main difference is that you need to specify _what kind of concatenated thing you want to create_. Paste tried to work this out magically from context, but sometimes got it wrong. -In other words, you typically want to replace `[< ... >]` with `[!ident! ...]`, and sometimes `[!string! ...]` or `[!literal! ...]`: -* To create type and function names, use `[!ident! My #preinterpret_type_name $macro_type_name]`, `[!ident_camel! ...]`, `[!ident_snake! ...]` or `[!ident_upper_snake! ...]` -* For doc macros or concatenated strings, use `[!string! "My type is: " #type_name]` -* If you're creating literals of some kind by concatenating parts together, use `[!literal! 32 u32]` +In other words, you typically want to replace `[< ... >]` with `%ident[...]`, and sometimes `%string[...]` or `%literal[...]`: +* To create type and function names, use `%ident[My #preinterpret_type_name $macro_type_name]`, `%ident_camel[...]`, `%ident_snake[...]` or `%ident_upper_snake[...]` +* For doc macros or concatenated strings, use `%string["My type is: " #type_name]` +* If you're creating literals of some kind by concatenating parts together, use `%literal[32 u32]` For example: ```rust -preinterpret::preinterpret! { - [!set! #type_name = [!ident! HelloWorld]] +preinterpret::stream! { + #{ + let type_name = %ident[Hello World]; + } struct #type_name; - #[doc = [!string! "This type is called [`" #type_name "`]"]] + #[doc = %string["This type is called [`" #type_name "`]"]] impl #type_name { - fn [!ident_snake! say_ #type_name]() -> &'static str { - [!string! "It's time to say: " [!title! #type_name] "!"] + fn %ident_snake[say_ #type_name]() -> &'static str { + %string["It's time to say: " #(type_name.to_string().to_title_case()) "!"] } } } @@ -136,9 +200,9 @@ assert_eq!(HelloWorld::say_hello_world(), "It's time to say: Hello World!") ### Special commands -* `[!set! #foo = Hello]` followed by `[!set! #foo = #bar(World)]` sets the variable `#foo` to the token stream `Hello` and `#bar` to the token stream `Hello(World)`, and outputs no tokens. Using `#foo` or `#bar` later on will output the current value in the corresponding variable. -* `[!raw! abc #abc [!ident! test]]` outputs its contents as-is, without any interpretation, giving the token stream `abc #abc [!ident! test]`. -* `[!ignore! $foo]` ignores all of its content and outputs no tokens. It is useful to make a declarative macro loop over a meta-variable without outputting it into the resulting stream. +* `#(let foo = %[Hello];)` followed by `#(let foo = %[#bar(World)];)` sets the variable `#foo` to the token stream `Hello` and `#bar` to the token stream `Hello(World)`, and outputs no tokens. Using `#foo` or `#bar` later on will output the current value in the corresponding variable. +* `%raw[abc #abc %[test]]` outputs its contents as-is, without any interpretation, giving the token stream `abc #abc %[test]`. +* `let _ = %raw[$foo]` ignores all content inside `[...]` and outputs no tokens. It is useful to make a declarative macro loop over a meta-variable without outputting it into the resulting stream. ### Concatenate and convert commands @@ -149,33 +213,33 @@ Each of these commands functions in three steps: The following commands output idents: -* `[!ident! X Y "Z"]` outputs the ident `XYZ` -* `[!ident_camel! my hello_world]` outputs `MyHelloWorld` -* `[!ident_snake! my_ HelloWorld]` outputs `my_hello_world` -* `[!ident_upper_snake! my_ const Name]` outputs `MY_CONST_NAME` +* `%[X Y "Z"].to_ident()` outputs the ident `XYZ` +* `%[my hello_world].to_ident_camel()` outputs `MyHelloWorld` +* `%[my_ HelloWorld].to_ident_snake()` outputs `my_hello_world` +* `%[my_ const Name].to_ident_upper_snake()` outputs `MY_CONST_NAME` -The `!literal!` command outputs any kind of literal, for example: +The following commands output any kind of literal, for example: -* `[!literal! 31 u 32]` outputs the integer literal `31u32` -* `[!literal! '"' hello '"']` outputs the string literal `"hello"` +* `%[31 u 32].to_literal()` outputs the integer literal `31u32` +* `%['"' hello '"'].to_literal()` outputs the string literal `"hello"` The following commands output strings, without dropping non-alphanumeric characters: -* `[!string! X Y " " Z (Hello World)]` outputs `"XY Z(HelloWorld)"` -* `[!upper! foo_bar]` outputs `"FOO_BAR"` -* `[!lower! FooBar]` outputs `"foobar"` -* `[!capitalize! fooBar]` outputs `"FooBar"` -* `[!decapitalize! FooBar]` outputs `"fooBar"` +* `%[X Y " " Z (Hello World)].to_string()` outputs `"XY Z(HelloWorld)"` +* `"foo_bar".to_uppercase()` outputs `"FOO_BAR"` +* `"FooBar".to_lowercase()` outputs `"foobar"` +* `"fooBar".capitalize()"` outputs `"FooBar"` +* `"FooBar".decapitalize()` outputs `"fooBar"` The following commands output strings, whilst also dropping non-alphanumeric characters: -* `[!snake! FooBar]` and `[!lower_snake! FooBar]` are equivalent and output `"foo_bar"` -* `[!upper_snake! FooBar]` outputs `"FOO_BAR"` -* `[!camel! foo_bar]` and `[!upper_camel! foo_bar]` are equivalent and output `"FooBar"` -* `[!lower_camel! foo_bar]` outputs `"fooBar"` -* `[!kebab! fooBar]` outputs `"foo-bar"` -* `[!title! fooBar]` outputs `"Foo Bar"` -* `[!insert_spaces! fooBar]` outputs `"foo Bar"` +* `"FooBar".to_lower_snake_case()` outputs `"foo_bar"` +* `"FooBar".to_upper_snake_case()"` outputs `"FOO_BAR"` +* `"foo_bar".to_upper_camel_case()"` outputs `"FooBar"` +* `"foo_bar".to_lower_camel_case()"` outputs `"fooBar"` +* `"fooBar".to_kebab_case()"` outputs `"foo-bar"` +* `"fooBar".to_title_case()"` outputs `"Foo Bar"` +* `"fooBar".insert_spaces()"` outputs `"foo Bar"` > [!NOTE] > @@ -214,10 +278,12 @@ macro_rules! impl_marker_traits { // Arbitrary (non-const) type generics < $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? $( = $deflt:tt)? ),+ > )? - } => {preinterpret::preinterpret!{ - [!set! #impl_generics = $(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?] - [!set! #type_generics = $(< $( $lt ),+ >)?] - [!set! #my_type = $type_name #type_generics] + } => {preinterpret::stream!{ + #{ + let impl_generics = %[$(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?]; + let type_generics = %[$(< $( $lt ),+ >)?]; + let my_type = %[$type_name #type_generics]; + } $( // Output each marker trait for the type @@ -243,7 +309,7 @@ For example: macro_rules! create_struct_and_getters { ( $name:ident { $($field:ident),* $(,)? } - ) => {preinterpret::preinterpret!{ + ) => {preinterpret::stream!{ // Define a struct with the given fields pub struct $name { $( @@ -254,7 +320,7 @@ macro_rules! create_struct_and_getters { impl $name { $( // Define get_X for each field X - pub fn [!ident! get_ $field](&self) -> &str { + pub fn #(%[get_ $field].to_ident())(&self) -> &str { &self.$field } )* @@ -266,44 +332,6 @@ create_struct_and_getters! { } ``` -Variable assignment works intuitively with the `* + ?` expansion operators, allowing basic procedural logic, such as creation of loop counts and indices before [meta-variables](https://github.com/rust-lang/rust/issues/83527) are stabilized. - -For example: -```rust -macro_rules! count_idents { - { - $($item: ident),* - } => {preinterpret::preinterpret!{ - [!set! #current_index = 0usize] - $( - [!ignore! $item] // Loop over the items, but don't output them - [!set! #current_index = #current_index + 1] - )* - [!set! #count = #current_index] - #count - }} -} -``` - -To quickly explain how this works, imagine we evaluate `count_idents!(a, b, c)`. As `count_idents!` is the most outer macro, it runs first, and expands into the following token stream: - -```rust -let count = preinterpret::preinterpret!{ - [!set! #current_index = 0usize] - [!ignore! a] - [!set! #current_index = #current_index + 1] - [!ignore! = b] - [!set! #current_index = #current_index + 1] - [!ignore! = c] - [!set! #current_index = #current_index + 1] - [!set! #count = #current_index] - #count -}; -``` - -Now the `preinterpret!` macro runs, resulting in `#count` equal to the token stream `0usize + 1 + 1 + 1`. -This will be improved in future releases by adding support for mathematical operations on integer literals. - ### Simplicity Using preinterpret partially mitigates some common areas of confusion when writing declarative macros. @@ -347,8 +375,8 @@ Preinterpret is more explicit about types, and doesn't have these issues: macro_rules! impl_new_type { { $vis:vis $my_type:ident($my_inner_type:ty) - } => {preinterpret::preinterpret!{ - #[xyz(as_type = [!string! $my_inner_type])] + } => {preinterpret::stream!{ + #[xyz(as_type = #(%[$my_inner_type].to_string()))] $vis struct $my_type($my_inner_type); }} } diff --git a/bench.sh b/bench.sh new file mode 100755 index 00000000..59c81928 --- /dev/null +++ b/bench.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")" + +# NOTES: +# - The `benchmark` feature is required to be enabled for the benchmarks to run, +# - The total "cost" of using preintepret on a fresh build is the sum of: +# - The compile time for preinterpret itself (and its dependencies, e.g. syn) +# - For each item: +# - Overhead invoking the macro +# - The measured Parsing + Evaluation + Output time in the benchmark +# - So the benchmark is useful, but should be considered alongside the compile time +# of preinterpret itself - see `bench_compilation.sh` for that. + +echo Preparing benchmark builds... +cargo build --bench basic --features benchmark --profile=dev +cargo build --bench basic --features benchmark --profile=release + +# Note - the benchmarks themselves actually run *during* build time +# So at this point (courtesy of the build cache) we already have the benchmark results. +# But we print them in a block to make it easier to copy-paste them + +echo +echo "Executing pre-run benchmark (dev profile)..." +cargo bench --bench basic --features benchmark --profile=dev + +echo +echo "Executing pre-run benchmark (release profile)..." +cargo bench --bench basic --features benchmark --profile=release + +echo +echo "If you want to get fresh results, run 'cargo clean' and re-run this script" \ No newline at end of file diff --git a/bench_compilation.sh b/bench_compilation.sh new file mode 100755 index 00000000..bdef9cd4 --- /dev/null +++ b/bench_compilation.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")" + +# Usage: run_build_with_timings +run_build_with_timings() { + local profile="$1" + + echo + echo ">> Checking compilation timings ($profile profile)" + echo + cargo clean + + cargo build --lib --profile="$profile" --timings > /dev/null + + # Find the most recent timing file (cargo creates timestamped files) + local timing_file + timing_file=$(ls -t target/cargo-timings/cargo-timing-*.html 2>/dev/null | head -1) || true + + if [ -n "$timing_file" ] && [ -f "$timing_file" ]; then + echo + + # Extract total time from the HTML summary table + local total_time + total_time=$(grep -o 'Total time:[^<]*' "$timing_file" | grep -o '[0-9.]*s' | head -1) || true + echo "- Total lib build time | $total_time" + + # Extract preinterpret duration from UNIT_DATA JSON + # Use sed to extract just the duration value after finding the preinterpret entry + local preinterpret_duration + preinterpret_duration=$(sed -n '/"name": "preinterpret"/,/}/p' "$timing_file" | grep -o '"duration": [0-9.]*' | grep -o '[0-9.]*') || true + if [ -n "$preinterpret_duration" ]; then + echo "- preinterpret compile time | ${preinterpret_duration}s" + fi + + # Extract syn duration from UNIT_DATA JSON + local syn_duration + syn_duration=$(sed -n '/"name": "syn"/,/}/p' "$timing_file" | grep -o '"duration": [0-9.]*' | grep -o '[0-9.]*') || true + if [ -n "$syn_duration" ]; then + echo "- syn compile time | ${syn_duration}s" + fi + fi +} + +run_build_with_timings "dev" +run_build_with_timings "release" diff --git a/benches/basic.rs b/benches/basic.rs new file mode 100644 index 00000000..2f83c358 --- /dev/null +++ b/benches/basic.rs @@ -0,0 +1,63 @@ +use preinterpret::benchmark_run; + +macro_rules! benchmark { + ($label:expr, { $($code:tt)* }) => { + let output_str = benchmark_run!($($code)*); + println!(); + println!("{:0>8}", $label); + println!("{:0>8}", output_str); + }; +} + +fn main() { + benchmark!("Trivial Sum", { 1 + 1 + 1 }); + benchmark!("For loop adding up 1000 times", { + let output = 0; + for i in 1..=1000 { + output += i + } + output + }); + benchmark!("For loop concatenating to stream 1000 tokens", { + let output = %[]; + for i in 1..=1000 { + output += %[i]; + } + output + }); + benchmark!("Lots of casts", { + 0 as u32 as untyped_int as u8 as char as string as stream + }); + benchmark!("Simple tuple impls", { + for N in 0..=10 { + let comma_separated_types = %[]; + for name in ('A'..).into_iter().take(N) { + let ident = name.to_ident(); + comma_separated_types += %[#ident,]; + } + emit %[ + impl<#comma_separated_types> MyTrait for (#comma_separated_types) {} + ]; + } + }); + benchmark!("Accessing single elements of a large array", { + let array = []; + for i in 0..1000 { + array.push(i); + } + let sum = 0; + for i in 0..100 { + sum += array[i]; + } + }); + benchmark!("Lazy iterator", { + let last = 0; + for i in 0..100000 { + if i == 5 { + last = i; + break; + } + } + %[].assert_eq(last, 5); + }); +} diff --git a/benches/report.md b/benches/report.md new file mode 100644 index 00000000..eb3b18c3 --- /dev/null +++ b/benches/report.md @@ -0,0 +1,134 @@ +# Internal Benchmarks + +The overall effective performance of a procedural macro crate is a combination of: +* Compile time of the macro code itself +* Invocation overhead of the macro +* Runtime of the macro + +Slightly weirdly, on a development build, the macro code is built and run in development mode, but on a release build, it's built and run in `--release` mode. + +Therefore both dev and release build time and execution time are relevant for the effective feel of the crate in Dev/CI. + +The below builds were done on a fast Macbook. Your CI will vary but will typically run in about double the times. + +## Development Performance + +### Compile time + +Script: `cargo clean && cargo build --timings` + +```text +// October 2025, run on Apple Silicon M2 Pro +=> syn v2.0.106 1.2s +=> preinterpret v0.2.0 2.2s +``` + +### Basic Execution Benchmarks + +Script: `cargo bench --features benchmark --profile=dev` + +```text +// October 2025, run on Apple Silicon M2 Pro +Trivial Sum +- Parsing | 8ns +- Analysis | 1ns +- Evaluation | 4ns +- Output | 0ns + +For loop adding up 1000 times +- Parsing | 24ns +- Analysis | 12ns +- Evaluation | 6123ns +- Output | 0ns + +For loop concatenating to stream 1000 tokens +- Parsing | 29ns +- Analysis | 9ns +- Evaluation | 3402ns +- Output | 163ns + +Lots of casts +- Parsing | 7ns +- Analysis | 1ns +- Evaluation | 4ns +- Output | 0ns + +Simple tuple impls +- Parsing | 54ns +- Analysis | 25ns +- Evaluation | 853ns +- Output | 42ns + +Accessing single elements of a large array +- Parsing | 67ns +- Analysis | 32ns +- Evaluation | 5797ns +- Output | 0ns + +Lazy iterator +- Parsing | 45ns +- Analysis | 21ns +- Evaluation | 41ns +- Output | 0ns +``` + +## Release Performance + +### Compile time + +Script: `cargo clean && cargo build --timings --release` + +```text +// October 2025, run on Apple Silicon M2 Pro +=> syn v2.0.106 1.1s +=> preinterpret v0.2.0 3.3s +``` + +### Basic Execution Benchmarks + +Script: `cargo bench --features benchmark` + +```text +// October 2025, run on Apple Silicon M2 Pro +Trivial Sum +- Parsing | 8ns +- Analysis | 1ns +- Evaluation | 4ns +- Output | 0ns + +For loop adding up 1000 times +- Parsing | 25ns +- Analysis | 12ns +- Evaluation | 5335ns +- Output | 0ns + +For loop concatenating to stream 1000 tokens +- Parsing | 26ns +- Analysis | 8ns +- Evaluation | 2996ns +- Output | 125ns + +Lots of casts +- Parsing | 6ns +- Analysis | 0ns +- Evaluation | 4ns +- Output | 0ns + +Simple tuple impls +- Parsing | 49ns +- Analysis | 22ns +- Evaluation | 765ns +- Output | 33ns + +Accessing single elements of a large array +- Parsing | 46ns +- Analysis | 20ns +- Evaluation | 4137ns +- Output | 0ns + +Lazy iterator +- Parsing | 38ns +- Analysis | 18ns +- Evaluation | 36ns +- Output | 0ns +``` \ No newline at end of file diff --git a/book/.gitignore b/book/.gitignore new file mode 100644 index 00000000..80dde542 --- /dev/null +++ b/book/.gitignore @@ -0,0 +1,2 @@ +book +bin \ No newline at end of file diff --git a/book/book.toml b/book/book.toml new file mode 100644 index 00000000..b93b3826 --- /dev/null +++ b/book/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["David Edey"] +language = "en" +multilingual = false +src = "src" +title = "The Preinterpret Guide" diff --git a/book/build.sh b/book/build.sh new file mode 100755 index 00000000..efc16516 --- /dev/null +++ b/book/build.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e pipefail + +# Ensure we're in the book directory +cd "$(dirname "$0")" + +# Ensure mdbook has been downloaded +/usr/bin/env bash ./download-mdbook.sh + +./bin/mdbook build diff --git a/book/download-mdbook.sh b/book/download-mdbook.sh new file mode 100755 index 00000000..0c81772a --- /dev/null +++ b/book/download-mdbook.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -e pipefail + +# Ensure we're in the book directory +cd "$(dirname "$0")" + +MDBOOK_VER="v0.4.31" + +## Check if the right version of mdbook is already downloaded +if [ -f ./bin/mdbook ]; then + if ./bin/mdbook --version | grep $MDBOOK_VER > /dev/null; then + echo "bin/mdbook @ $MDBOOK_VER is already downloaded" + exit 0 + fi + echo "bin/mdbook is already downloaded but the wrong version, so downloading the correct version" +fi + +# Download and extract the mdbook binary into the bin folder +mkdir -p bin && cd bin + +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + TAR_FILE="mdbook-${MDBOOK_VER}-x86_64-unknown-linux-musl.tar.gz" +else + TAR_FILE="mdbook-${MDBOOK_VER}-x86_64-apple-darwin.tar.gz" +fi + +echo "Downloading $TAR_FILE..." +curl -OL "https://github.com/rust-lang/mdBook/releases/download/${MDBOOK_VER}/${TAR_FILE}" +tar -xf "${TAR_FILE}" +rm "${TAR_FILE}" + +echo "Extracted to bin/mdbook" +cd .. \ No newline at end of file diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md new file mode 100644 index 00000000..db2eded8 --- /dev/null +++ b/book/src/SUMMARY.md @@ -0,0 +1,4 @@ +# Summary + + + diff --git a/book/src/concepts.md b/book/src/concepts.md new file mode 100644 index 00000000..839917fc --- /dev/null +++ b/book/src/concepts.md @@ -0,0 +1,3 @@ +# Concepts + +Coming soon... \ No newline at end of file diff --git a/book/src/concepts/commands.md b/book/src/concepts/commands.md new file mode 100644 index 00000000..61c515e7 --- /dev/null +++ b/book/src/concepts/commands.md @@ -0,0 +1 @@ +# Commands diff --git a/book/src/concepts/expressions.md b/book/src/concepts/expressions.md new file mode 100644 index 00000000..d52b5748 --- /dev/null +++ b/book/src/concepts/expressions.md @@ -0,0 +1 @@ +# Expressions diff --git a/book/src/concepts/parsing.md b/book/src/concepts/parsing.md new file mode 100644 index 00000000..dc018419 --- /dev/null +++ b/book/src/concepts/parsing.md @@ -0,0 +1 @@ +# Parsing diff --git a/book/src/concepts/variables.md b/book/src/concepts/variables.md new file mode 100644 index 00000000..ee1fba42 --- /dev/null +++ b/book/src/concepts/variables.md @@ -0,0 +1 @@ +# Variables diff --git a/book/src/introduction.md b/book/src/introduction.md new file mode 100644 index 00000000..5eca8c1d --- /dev/null +++ b/book/src/introduction.md @@ -0,0 +1,3 @@ +# Introduction + +Book coming soon... \ No newline at end of file diff --git a/book/src/introduction/examples.md b/book/src/introduction/examples.md new file mode 100644 index 00000000..df635b4e --- /dev/null +++ b/book/src/introduction/examples.md @@ -0,0 +1 @@ +# Examples diff --git a/book/src/introduction/motivation.md b/book/src/introduction/motivation.md new file mode 100644 index 00000000..6d769339 --- /dev/null +++ b/book/src/introduction/motivation.md @@ -0,0 +1 @@ +# Motivation diff --git a/book/src/introduction/quick-reference.md b/book/src/introduction/quick-reference.md new file mode 100644 index 00000000..fa95ef17 --- /dev/null +++ b/book/src/introduction/quick-reference.md @@ -0,0 +1 @@ +# Quick Reference diff --git a/book/test.sh b/book/test.sh new file mode 100755 index 00000000..c8d2f8cf --- /dev/null +++ b/book/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e pipefail + +# Ensure we're in the book directory +cd "$(dirname "$0")" + +# Ensure mdbook has been downloaded +/usr/bin/env bash ./download-mdbook.sh + +# TODO +# Replace with mdbook test once it actually works with external libraries +# See issue: https://github.com/rust-lang/mdBook/issues/394#issuecomment-2234216353 +# I'm hopeful this will work once https://github.com/rust-lang/mdBook/pull/2503 is merged +bin/mdbook build diff --git a/book/watch.sh b/book/watch.sh new file mode 100755 index 00000000..2a5561fe --- /dev/null +++ b/book/watch.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e pipefail + +# Ensure we're in the book directory +cd "$(dirname "$0")" + +# Ensure mdbook has been downloaded +/usr/bin/env bash ./download-mdbook.sh + +./bin/mdbook watch --open diff --git a/local-check-msrv.sh b/local-check-msrv.sh index e3bd402c..ac001e6f 100755 --- a/local-check-msrv.sh +++ b/local-check-msrv.sh @@ -2,5 +2,7 @@ set -e -rustup install 1.61 -rm Cargo.lock && rustup run 1.61 cargo check +cd "$(dirname "$0")" + +rustup install 1.68 +rm Cargo.lock && rustup run 1.68 cargo check diff --git a/plans/2025-02-vision-parsers-revisited.md b/plans/2025-02-vision-parsers-revisited.md new file mode 100644 index 00000000..77dbcd6e --- /dev/null +++ b/plans/2025-02-vision-parsers-revisited.md @@ -0,0 +1,197 @@ +# Parsers Revisited - JULY 2025 + +This has been superceded by @./2025-09-vision.md + +```rust +// PROPOSAL (subject to the object proposal above): +// * Various modes... +// * EXPRESSION MODE +// * OPENING SYNTAX: +// * #{ ... } or #'name { ... } with a new lexical variable scope +// * #( ... ) with no new scope (TBC if we need both - probably) +// * CONTEXT: +// * Is *not* stream-based - it consists of expressions rather than a stream +// * It *may* have a contextual input parse stream +// * One or more statements, terminated by ; (except maybe the last, which is returned) +// * Idents mean variables. +// * `let x; let _ = ; ; if {} else {}` +// * Error not to discard a non-None value with `let _ = ` +// * If it has an input parse stream, it's allowed to parse by embedding parsers, e.g. @TOKEN_TREE +// * Only the last statement can (optionally) output, with a value model of: +// * Leaf(Bool | Int | Float | String) +// * Object +// * Stream +// * None +// * (In future we could add breaking from labelled block: https://blog.rust-lang.org/2022/11/03/Rust-1.65.0/#break-from-labeled-blocks) +// * The actual type is only known at evaluation time. +// * #var is equivalent to #(var) +// * OUTPUT STREAM MODE +// * OPENING SYNTAX: ~{ ... } or ~( ... ) or raw stream literal with r~( ... ) +// * SIDENOTES: I'd have liked to use $, but it clashes with $() repetition syntax inside macro rules +// * And % might appear in real code as 1 % (2 + 3) +// * But ~ seems to not be used much, and looks a bit like an s. So good enough? +// * CONTEXT: +// * Can make use of a parent output stream, by concatenating into it. +// * Is stream-based: Anything embedded to it can have their outputs concatenated onto the output stream +// * Does *not* have an input parse stream +// * Can embed [!commands! ...], #() and #{ ... }, but not parsers +// * The output from #([...]) gets appended to the stream; ideally by passing it an optional output stream +// * It has a corresponding pattern: +// * SYNTAX: +// * ~#( .. ) to start in expression mode OR +// * ~@( .. ) ~@XXX or ~@[XXX ...] to start with a parser e.g. ~@REST can be used to parse any stream opaquely OR +// * r~( .. ) to match a raw stream (it doesn't have a mode!) +// * Can be used where patterns can be used, in a let expression or a match arm, e.g. +// * let ~#( let x = @IDENT ) = r~(...) +// * ~#( ... ) => { ... } +// * CONTEXT: +// * It implicitly creates a parse-stream for its contents +// * It would never have an output stream +// * It is the only syntax which can create a parse stream, other than the [!parse! ..] command +// * NAMED PARSER +// * OPENING SYNTAX: @XXX or @[XXX] or @[XXX ] +// * CONTEXT: +// * Is typically *not* stream-based - it may has its own arguments, which are typically a pseudo-object `{ }` +// * It has an input parse stream +// * Every named parser @XXX has a typed output, which is: +// * EITHER its input token stream (for simple matchers, e.g. @IDENT) +// * OR an #output OBJECT with at least two properties: +// * input => all matched characters (a slice reference which can be dropped...) +// (it might only be possible to performantly capture this after the syn fork) +// THIS NOTE MIGHT BE RELEVANT, BUT I'M WRITING THIS AFTER 6 MONTHS LACKING CONTEXT: +// This can capture the original tokens by using `let forked = input.fork()` +// and then `let end_cursor = input.end();` and then consuming `TokenTree`s +// from `forked` until `forked.cursor >= end_cursor` (making use of the +// PartialEq implementation) +// * output_to => A lazy function, used to handle the output when #x is embedded into a stream output... +// likely `input` or an error depending on the case. +// * into_iterator +// * ... other properties, depending on the parsee: +// * e.g. a Rust ITEM might have quite a few (mostly lazy) +// * PARSE-STREAM MODE +// * OPENING SYNTAX: +// * @{ ... } or @'block_name { ... } (as a possibly named block) -- both have variable scoping semantics +// * We also support various repetitions such as: @{ ... }? and @{ ... }+ and @{ ... },+ - discussed in next section in more detail +// * We likely don't want/need an un-scoped parse stream +// * CONTEXT: +// * It is stream-based: Any outputs of items in the stream are converted to an exact parse matcher +// * Does have an input parse stream which it can mutate +// * Expression/command outputs are converted into an exact parse matcher +// * RETURN VALUE: +// * Parse blocks can return a value with `#(return X)` or `#(return 'block_name X)` else return None +// * By contrast, `@[STREAM ...]` works very similarly, but outputs its input stream +// * COMMAND +// * OPENING SYNTAX: [!command! ...] +// * CONTEXT: +// * Takes an optional output stream from its parent (for efficiency, if it returns a stream it can instead concat to that) +// * May or may not be stream-based depending on the command... +// * Does *not* have an input parse stream (I think? Maybe it's needed. TBC) +// * Lots of commands can/should probably become functions +// * A flexible utility for different kinds of functionality +// +// * Repetitions (including optional), e.g. @IDENT,* or @[IDENT ...],* or @{...},* +// * Output their contents in a `Repeated` type, with an `items` property, and an into_iterator implementation? +// * Unscoped parse streams can't be repeated, only parse blocks (because they create a new interpreter frame) +// * If we don't move our cursor forward in a loop iteration, it's an error -- this prevents @{}* or @{x?}* causing an infinite loop +// * We do *not* support backtracking. +// * This avoids most kind of catastrophic backtracking explosions, e.g. (x+x+)+y) +// * Things such as @IDENT+ @IDENT will not match `hello world` - this is OK I think +// * Instead, we suggest people to use a match statement or something +// * There is still an issue of REVERSION - "what happens to external state mutated this iteration repetition when a repetition is not possible?" +// * This appears in lots of places: +// * In ? or * blocks where a greedy parse attempt isn't fatal, but should continue +// * In match arms, considering various stream parsers, some of which might fail after mutating state +// * In nested * blocks, with different levels of reversion +// * But it's only a problem with state lexically captured from a parent block +// * We need some way to handle these cases: +// (A) Immutable structures - We use efficient-ish immutable data structures, e.g. ImmutableList, Copy-on-write leaf types etc +// ... so that we can clone them cheaply (e.g. https://github.com/orium/rpds) or even just some manually written tries/cons-lists +// (B) Use CoW/extensions - But naively it still give O(N^2) if we're reverting occasional array.push(..)es +// We could possibly add in some tweaks to avoid clones in some special cases (e.g. array.push and array.pop which don't touch a prefix at the start of a frame) +// (C) Error on mutate - Any changes to variables outside the scope of a given refutable parser => it's a fatal error +// to parse further until that scope is closed. (this needs to handle nested repetitions). +// (D) Error on revert - at reversion time, we check there have been no changes to variables below that depth in the stack tree +// (say by recording a "lowest_stack_touched: usize"), and panic if so; and tell people to use `return` instead; or move state changes to the end. +// We could even prevent parsing in a conditional block after the reversion. +// [!Brilliant!] We could introduce a @[REQUIRE ] which takes the parent conditional block out of conditional mode and makes it a hard error instead. This could dramatically improve error messages, and allow parsing after mutation :). (Ideally they'd be some way of applying it to a specific conditional block, but I think parent is good enough) +// (E) attempt expression... +// Whenever we have a refutable frame, we split it in two in syntax: we have an explicit binding block where variable bindings from parent frames can't be mutable, but new variables can be defined... and an execution mode which is irrefutable. +// - When the first part is executed, the interpreter locks parent variable +// access from being mutable (and also forks a parse stream if one exists). +// - Each option's LHS is tried in turn. +// - If the LHS succeeds, it executes. +// - Else, if it errors, it goes to the next one. If it's the last one, that error is thrown properly. +attempt { + { let x = @IDENT } => { x }, + { let x = @LITERAL } => { x }, + // { @END } => { ... }, // Where @END throws unless there's an end + {} => { "Expected ident or literal".error(@CURSOR) }, +} +// OPTIONAL, REPEATED semantics are basically special cases of `try_many` +// @[..] is a stream exact-parser which can be entered inside expression mode. +// The stream-literal pattern %[..] starts in this mode, but asserts the end too. +// +// ==> E is probably the clearest and most general, albeit a bit verbose +// +// +// QUESTION: +// - What mode do we start in, in v2? +// => Possibly expression mode, which could convert to an output with ~{ ... } +// +// ========= +// EXAMPLES +// ========= + +// EXAMPLE WITH !parse! COMMAND +#( + let parsed = [!parse! { + input: r~( + impl A for X, impl B for Y + ), + parser: @{ // This @{ .. }, returns a `Repeated` type with an `into_iterator` over its items + impl #(let the_trait = @IDENT) for #(let the_type = @IDENT) + #(return { the_trait, the_type }) + },* + }]; + + // Assuming we are writing into an output stream (e.g. outputting from the macro), + // we can auto-optimize - the last command of the expression block gets to write directly to the stream, + // and by extension, each block of the for-loop gets to write directly to the stream + for { the_trait, the_type } in parsed ~{ + impl #the_trait for #the_type {} + } +) + +// EXAMPLE WITH STREAM-PARSING-PATTERN +#( + let ~#( // Defines a stream pattern, starting in expression mode, and can be used to bind variables + let parsed = @{ + #(let item = {}) + impl #(item.the_trait = @IDENT) for #(item.the_type = @IDENT) + #(return item) + } + ) = r~(...) + + for { the_trait, the_type } in parsed ~{ + impl #the_trait for #the_type {} + } +) + +// Example pre-defined with no arguments: +[!define_parser! @IMPL_ITEM @{ // Can either start as #{ ... } or @{ ... } + impl #(let the_trait = @IDENT) for #(let the_type = @IDENT) + #(return { the_trait, the_type }) +}] +for { the_trait, the_type } in [!parse! { input, parser: @IMPL_ITEM,* }] ~{ + impl #the_trait for #the_type {} +} + +// Example pre-defined 2 with arguments: +[!define_parser! @[IMPL_ITEM /* arguments named-parser or parse-stream */] { + @(impl #(let the_trait = @IDENT) for #(let the_type = @IDENT)); + { the_trait, the_type } +}] +for { the_trait, the_type } in [!parse! { input, parser: @IMPL_ITEM,* }] ~{ + impl #the_trait for #the_type {} +} +``` \ No newline at end of file diff --git a/plans/2025-09-vision.md b/plans/2025-09-vision.md new file mode 100644 index 00000000..c7ef6254 --- /dev/null +++ b/plans/2025-09-vision.md @@ -0,0 +1,157 @@ +# Vision - September 2025 + +Builds on the ideas from Parsers revisited in Feb 2025, but takes them further / to their logical conclusion: + +High level, these changes attempt to: +* Make understanding things simpler, and more intuitive +* Go "expression first", get rid of `!commands!` completely +* Remove magic and/or make things explainable in terms of simpler building blocks + +We have: +* Block-scopes, parsed out at compile time (with a stateful parser). These allow us to: + * Have smarter variable references, e.g. auto-take on the last irrefutable use of a variable + * A scope at run-time can be in "parental-read-only" mode, which allows us to revert it +* `%[ .. ]` stream literals and `%raw[ .. ]` raw stream literals +* `for`, `loop` and `while` can return an array value (unless they break with a value!) + This let's us avoid lambdas like `map` for a bit. +* `attempt { ... }` - a really powerful expression block, looks a little like match. + * It's designed to allow us to try out different possible things, and catches errors (errors are absorbed). + * Takes a comma-separated list of attempt-arms. + * An arm is either (A) `` OR (B) ` => ` + * At the start: the parse stream is forked / checkpointed, if it exists. + * Each arm is run in a new scope + * The parse stream is reset to the checkpoint + * When calculating the LHS, the `parental-read-only` is activated (any attempt to get a mutable reference from a parent binding is disallowed) + * If it succeeds, in (A) the expression is returned, or in (B) the output is executed, in the same frame; but with `parental-read-only` disabled, and its results returned + * If no branch succeeds, the `attempt` expression outputs the final error: + * Typically though we'd expect a user to write an `{} => {}` ignore block or an `{} => { "Expected ...".error(@CURSOR) }` block for a better error. + * This construct can be used for arbitrary parsing, including optional, repeat etc. It can also be used as a match statement effectively. +* A `parse` expression `parse { }`. Parsers look like: + * `@(impl Clone)` exact stream-read mode. Can embed other parsers or parse-enabled expressions `#()` into it. + * This returns an object: if any parsers use `@x=IDENT` syntax it instead returns an object with `#(output.x = @IDENT)` + * `@IDENT` or `@[IDENT]` named parsers (without settings or syntax) + * `@[NAMED_PARSER({ }) ]` + * `@[EXACT(%[stream])]` + * `@[REPEATED({ times: 3 }) ]` e.g. `@[REPEATED ({ times: 3 }) { let x = @IDENT; } => { x }]` +* Optionally, we can introduce `%[ .. ]` stream patterns. + * These would start in `$()`-like mode, although would interpret `@x=IDENT` syntax as creating bindings `let x = @IDENT`. + * They would need to match the whole stream. This could be opted out with a `@REST`. + +* Most expressions lose their spans: + * Except streams, which keep spans on literals/groups. + * If someone wants to keep a value's span, they can keep it in a stream and coerce it; or store it as a tuple of a value with its span `[value, %[value]]` + * Bindings such as `Owned` have a span, which: + * Typically refers to the span of the preinterpret code that created the value/binding + * In some cases (e.g. source literals) it can refer to a source span + * And we can add a `with_span(%[..])` method which overrides the span of the binding (it'll have to return a `OwnedFixedSpan` which has different handling) + * We can have a `bool.assert(message, span)` +* Convert all commands to methods or expressions + * `#(%[#x #y].ident_lower_camel())` + * `preinterpret.settings({..})` (`preinterpret` is available as a variable pre-bound on the root frame) + +## Examples + +```rust +// EXAMPLE 1 +// Fully explanatory with no syntax-sugar +#( + let input = %raw[ + impl A for X, impl B for Y + ]; + let parsed = parse input { + let previous_comma_present = true + loop { // Returns an array of all items + attempt { + {@END} => {break} + {} => { + previous_comma_present.assert("Expected ,", @CURSOR) + @(impl) + let the_trait = @IDENT; + @(for) + let the_type = @IDENT; + attempt { // Parse a trailing comma , + {,} => {} + {} => { previous_comma_present = false } + } + { the_trait, the_type } + } + } + } + } + + for { the_trait, the_type } in parsed { + output.append(%[ + impl #the_trait for #the_type {} + ]) + } +) + +// EXAMPLE WITH @REPEATED: +// > Named parsers have settings in an optional `()` block. +#( + let %[ + let parsed = @[REPEATED ({ separator: %[,] }) + { + @(impl #(let the_trait=IDENT) for #(let the_type=IDENT)) + } => { { the_trait, the_type } } + ] + ] = %raw[...] + + // Assuming we are writing into an output stream (e.g. outputting from the macro), + // we can auto-optimize - the last command of the expression block gets to write directly to the stream, + // and by extension, each block of the for-loop gets to write directly to the stream + for { the_trait, the_type } in parsed { + %[ + impl #the_trait for #the_type {} + ] + } +) + +// EXAMPLE WITH FULL SYNTAX SUGAR: +// > The => {{the_trait, the_type}} part is optional: +// > By default if any variables were bound, it returns all variables bound in the previous scope, as an object +// > Otherwise, it returns the results of all parsers as an array +// > @[COMMA_SEPARATED @(impl @the_trait=IDENT for @the_type=IDENT)] allows trailing commas +// > Or we could allow the below syntax - EITHER: +// > @,*PARSER +// > @PARSE,* +// I think personally I prefer the first syntax, but possible the second is more intuitive coming from declarative macros +#( + let parsed = parse %raw[...] { + @,*(impl @the_trait=IDENT for @the_type=IDENT) + }; + // .. for loop +) +``` + +## Future Examples + +The following is not for v1.0, but shows how user-defined parsers might look: + +```rust +// Example pre-defined with no arguments: +define_parser @IMPL_ITEM { + @(impl @the_trait=IDENT for @the_type=IDENT) // Returns something like { the_trait: %[Clone], the_type: %[u32] } +} +for { the_trait, the_type } in parse input { @,*IMPL_ITEM } { + impl #the_trait for #the_type {} +} + +// Example pre-defined with arguments... Use as: +// @[REPEAT_N ({ times = 3 }) @IDENT] +define_parser @[ + REPEAT_N + // Some schema for settings. Settings are passed _as values_. + ({ + times: int, + }) + %[ + @code:PREINTERPRET_EXPRESSION + ] +] +{ + for _ in 0..settings.times { + code.execute() + } +} +``` \ No newline at end of file diff --git a/plans/2025-11-vision.md b/plans/2025-11-vision.md new file mode 100644 index 00000000..114693d0 --- /dev/null +++ b/plans/2025-11-vision.md @@ -0,0 +1,97 @@ +# Vision - November 2025 + +Builds on `2025-09-vision.md`. + +Trying out new syntax. + +## Examples + +```rust +// EXAMPLE 1 +// Fully explanatory with no syntax-sugar +// Temporarily we can have parse |input| {} +#( + let input = %raw[ + impl A for X, impl B for Y + ]; + let parsed = input.parse(|input| { + let previous_comma_present = true + let output = []; + loop { + attempt { + { + input.end(); + } => { + break output; + } + {} => { + input.assert(previous_comma_present, "Expected ,"); + @input[ + impl + #{ let the_trait = input.ident(); } + for + #{ let the_type = input.ident(); } + ]; + // Parse a trailing comma , + attempt { + { @input[,] } => {} + {} => { previous_comma_present = false } + } + output.push(%{ the_trait, the_type }); + } + } + } + }); + + for %{ the_trait, the_type } in parsed { + emit %[ + impl #the_trait for #the_type {} + ] + } +) + +// EXAMPLE WITH @.repeat(..) USING CLOSURES +#( + let parsed = %raw[...].parse(|input| { + input.repeat( + %{ separator: %[,] }, + |input| { + @input[ + impl #{ let the_trait = input.ident(); } for #{ let the_type = input.ident(); } + ]; + %{ the_trait, the_type } + } + ) + }); +) + +// EXAMPLE WITH PARSER REPEAT BLOCK +#( + let input = %raw[...]; + let parsed = []; + input.parse(|input| { + @input[ + impl #{ let the_trait = input.ident(); } for #{ let the_type = input.ident(); } + #{ parsed.push(%{ the_trait, the_type }); } + ],* + }); + // .. for loop +) + +// OR COMBINE INPUT/OUTPUT +#( + let input = %raw[...]; + input.parse(|input| { + @input[ + @( + impl #{ let the_trait = input.ident(); } for #{ let the_type = input.ident(); } + #{ + emit %[ + impl #the_trait for #the_type {} + ]; + } + ),* + ]; + }); +) +``` diff --git a/plans/2026-01-types-and-forms.md b/plans/2026-01-types-and-forms.md new file mode 100644 index 00000000..f98009de --- /dev/null +++ b/plans/2026-01-types-and-forms.md @@ -0,0 +1,202 @@ +# Reflection on types and forms - January 2026 + +In December 2025 I embarked on a longer than expected journey, to "migrate forms to leaves": + +```rust +Shared(AnyValue::X(X::Y(y))) => AnyValue::X(X::Y(Shared(y))) +``` + +This was motivated by a few thoughts: +* It would be nice for e.g. a "Shared int" to be convertible to/from a "Shared any value" +* It's common knowledge that an `Option(&x)` is more useful than an `&Option(x)` +* The code around defining types and forms was ugly and repetitive and could do with a tidy up +* I wanted to start on methods/functions, for e.g. a `.map(|x| x * x)` but something in my + early investigation of function variable closures, I realized that this leaf migration would + be needed... both in an abstract sense, but also when I started investigating a "referenceable" + abstraction to be shared between variables and closures. + +### Original justification in the context of methods + +- [ ] Improved Shared/Mutable handling - See the `Better handling of value sub-references` section. The key requirement is we need to allow a `Shared` to map back to a `Shared`. Moving the "sharedness" to the leaves permits this. + * A function specifies the bindings of its variables + * If we have `my_len = |x: &array| x.len()` and invoke it as `my_len(a.b)` then + when I invoke it, I need to end up with the variable `x := &a.b` + * This is a problem - if we imagine changing what can be stored in a variable to + the following, then it's clear that we need some way to have a `SharedValue` which + has an outer-enum instead of an inner-enum. + * We also need to think about how things like `IterableValue` works. Perhaps it's like an interface, and so defined via `Box` / `Ref` etc? + +I don't fully understand what the problem I saw was now. I think that if `my_len` takes a `x: Shared` then to call `x.len()` I need to convert it back to a `Shared` in order to resolve `x.len()`? Maybe? I'm not too sure. + +## The problem + +The problem is that this works fine for covariant things like shared references... but not at all for things like assignee / mutable. In particular, consider: + +```rust + fn swap(mut a: AnyValueAssignee, mut b: AnyValueAssignee) -> () { + core::mem::swap(a.0.deref_mut(), b.0.deref_mut()); + } +``` + +This requires the thing that can be replaced to be the whole `AnyValue`, not just some leaf content. + +In a way, the "everything in the leaf" paradigm corresponds to walking around with `let x: &leaf_type`, restricting the place to only support that particular leaf type. + +## The reflection - the type of a place & type annotations + +So far places have only ever had one type - `any`. Whilst values have leaf types, the places themselves can be swapped to take `AnyValue`. This is pretty much how most dynamically typed languages work. + +If we wanted to support type annotations, they _could_ work like this, by constraining the type the place supports at a structural level... + +Or we could save the trouble and always store an `AnyValue` structurally but insert a runtime type assertion. This is a tiny bit less performant at runtime, but keeps the code simpler AND less code = less compile time, which is more important than faster runtime on smaller codebases that only use preinterpet a bit. + +### What would a hypothetical structurally type-restricted binding look like? + +Sidenote: I don't think we should necessarily support this, for reasons mentioned above. But it would allow us to support something like slices, e.g. `Mutable<[ArrayValue]>` + +Consider a structural `x: &mut integer`. Then, in the type system a value would look something like this: + +```rust +AnyValueContent::Integer(Mutable(IntegerValueContent::U8(u8))) +``` + +Essentially what we have is a form where the form wrapper isn't restricted to being at the leaf; rather it can be at any level of the type! + +But - how would this be modelled? i.e. how would we model a value which could be structurally type-restricted to be any type + +Well it would probably want to look something like: Each parent type having a `Content` enum and `Wrapper` enum, which looks the same, except it has an `AsSelf` variant option as well, i.e.: + +```rust +// &mut integer +AnyValueWrapper::Integer(IntegerValueWrapper::AsSelf(Mutable(IntegerValueContent::U8(u8)))) +// &mut u8 +AnyValueWrapper::Integer(IntegerValueWrapper::U8(Mutable(u8))) +// &mut any +AnyValueWrapper::AsSelf(Mutable(AnyValueContent::Integer(IntegerValueContent::U8(u8)))) +``` + +Essentially, outside of the `Form` we have `Wrapper` and inside the form we have `Mutable`. + +But then - how do we actually operate on this?? What operations for we supporty? + +Honestly, it's really hard. Would need to think about what operations can be performed etc. Even defining a leaf mapper was nigh-on impossible. + +Might come back to this at some point. + +## What's next? + +Let's put the "concepts" to-leaf on indefinite pause. It's definitely resulted in a lot of good cleaning up, BUT it's also created a lot of mess. + +Eventually we will need to clean it up: +* Better abstract the `IsArgument` impl +* Finish migrating from `bindings.rs` +* Finish removing `into_any_value()` etc +* Finish clearing up `arguments.rs` and `outputs.rs` + +We might well be left with only `BeOwned` and `BeRef`, `BeMut` - we'll see. + +See the below half-baked todo list for where this got to. + +--- + +But *first* I'd like to implement functions _without_ type annotations. Then if we add them, it would probably be easier for them to be runtime assertions rather than structural. + +## Half-finished to-do lists copied over (TODO: clean these up!) + +### Better handling of value sub-references + +- [x] Migrate from `Owned` having a span to a `Spanned` +- [ ] Implement and roll-out GATs + - [x] Initial shell implementation in `concepts` folder + - [x] Improved error handling in the macro (e.g. required arg after optional; no matching strongly-typed signature) + - [x] Separate Hierarchical and DynCompatible Forms + - [x] Improved macro support + - [x] Add source type name to type macro/s + - [x] Generate value kinds from the macros + - [x] Add (temporary) ability to link to TypeData and resolve methods from there + - [x] Then implement all the macros + - [x] And use that to generate AnyValueLeafKind from the new macros + - [x] Find a way to generate `from_source_name` - ideally efficiently + - [x] Add ability to implement IsIterable + - [x] `CastTarget` simply wraps `TypeKind` + - [x] Create a `Ref` and a `Mut` form + - [x] They won't implement `IsArgumentForm` + - [x] Create mappers and suitably generic `as_ref()` and `as_mut()` methods on `Actual` + - [x] Migrate method resolution to the trait macro properly + - [x] And property resolution `dyn TypeFeatureResolver` + - [x] Replace `impl TypeFeatureResolver for $type_def` + - [x] Remove `temp_type_data: $type_data:ident,` + - [x] Remove `HierarchicalTypeData` + - [ ] Stage 1 of the form migration: + - [x] Add temp blanket impl from `IntoValue` for `IntoValueContent` + - [x] Get rid of `BooleanValue` wrapper + - [x] Get rid of `StreamValue` wrapper + - [x] Get rid of `CharValue` wrapper + - [x] Replace `IntegerValue` with `type IntegerValue = IntegerContent<'static, BeOwned>` + - [x] Replace `FloatValue` with `type FloatValue = FloatContent<'static, BeOwned>` + - [x] Replace `Value` with `type Value = ValueContent<'static, BeOwned>` + - [x] Get rid of the `Actual` wrapper inside content + // --- + - [x] Trial getting rid of `Actual` completely? + - [x] Replace `type FloatValue = FloatValueContent` with `type FloatValue = QqqOwned` / `type FloatValueRef<'a> = QqqRef` / `type FloatValueMut = QqqMut` + - [x] Create new branch + - [x] Resolve issue with `2.3` not resolving into `2f32` any more + - [x] .. same for int... + - [x] Update Value: + - [x] `ValueType` => `AnyType` + - [x] `type AnyValue = QqqOwned` + - [x] `type AnyValueRef = QqqRef` + - [x] `type AnyValueMut = QqqMut` + - [x] ... and move methods + - [x] Remove `OwnedValue` + - [x] Get rid of `Owned` + - [ ] Stage 2 of the form migration: + - [x] Attempt to improve mappers: + - [x] Try to replace `ToRefMapper` etc with a `FormMapper::::map_content(content, |x| -> y)` - 6 methods... `map_content`, `map_content_ref`, `map_content_mut` and `try_x` *3; ... and a `ReduceMapper::::map(content, |x| -> y)`... sadly not possible! The lambda needs to be higher-ordered and work for all `L: IsValueLeaf`. + - [x] Finish mapper improvements + - [x] Migrate `self` + - [x] Consider if we even need `MapperOutput` or just some helper functions + - [x] Delay resolving the type kind into the error case when downcasting + - [x] Create macro to define inline mappers in various forms + - [x] Attempt to see if I can get rid of needing `leaf_to_content` and maybe `content_to_leaf` by exploiting a trick to bind associated types via a sub-trait (e.g. as I did with `IsValueContent::LeafType> + IsLeafValueContent`) + - [x] Pivot the argument resolution to work with either old or new values + - [ ] Remove as much from arguments.rs as possible + - [ ] Fix `todo!("Argument")` + - [ ] Pivot the return resolution to work with either old or new values + - [ ] Remove `ResolveAs` + - [ ] Look at better implementations of `FromArgument` + .. potentially via some new selector trait `IsResolvable` with a resolution strategy of `Hierarchichal` | `Dyn` | `Custom` + This will let us implement a more general resolution logic, and amalgamate argument parsing and downcast_resolve/dyn_resolve + - [ ] Reproduce `CopyOnWrite` + - [ ] Implement `TODO[concepts]: COPY ON WRITE MAPPING` + - [ ] `BeCopyOnWrite::owned()` / `shared_as_..` can go via `AnyLevelCopyOnWrite => into_copy_on_write` + - [ ] Reproduce `Shared` methods etc + - [ ] Reproduce `Mutable` methods etc + - [ ] Reproduce `Assignee` methods etc + - [ ] Change `CopyOnWrite` => `CopyOnWriteAnyValue` similarly with `Shared`, `Mutable` and `Assignee` + - [ ] Migrate `CopyOnWrite`, `Shared`, `Mutable`, `Assignee` + - [ ] `Shared` only works for leaves + - [ ] For specific parents, use e.g. `AnyValueShared` + - [ ] If you need something to apply across all leaves, implement it on some trait + `IsSelfSharedContent` depending/auto-implemented on `IsSelfValueContent
` + - [ ] Same for `Mutable` and `Assignee` + - [ ] Stage 3 + - [ ] Migrate `CopyOnWrite` and relevant interconversions + - [ ] Finish migrating to new Argument/Returned resolution and delete old code + - [ ] Remove `impl_resolvable_argument_for` and `TODO[concepts]: Remove when we get rid of impl_resolvable_argument_for` + - [ ] Complete ownership definitions and inter-conversions, including maybe-erroring inter-conversions (possibly with `Result` which we can map out of): + - [ ] Consider migrating LateBound and interconversions + - [ ] Argument, and `ArgumentOwnership` driven conversions into it + - [ ] Generate test over all value kinds which checks for: + - [ ] Maybe over TypeKinds with https://docs.rs/inventory/latest/inventory/ registered as a dev dependency + - [ ] Check for duplicate TypeKind registrations + - [ ] Source type has no spaces and is lower case, and is invertible + - [ ] Ancestor types agree with type kinds + - [ ] Clear up all `TODO[concepts]` + - [ ] Have variables store a `Referenceable` + - [ ] Separate methods and functions + - [ ] Add ability to add comments to types, methods/functions and operations, and generate docs from them + - [ ] Clean-up: + - [ ] Move indexing and property access (see e.g.`into_indexed`) etc to the type resolution system - this map allow us to remove + things like `AnyLevelCopyOnWrite` and the `TypeMapper` + - [ ] Most matching methods on `AnyValue` would be better as leaf methods \ No newline at end of file diff --git a/plans/TODO.md b/plans/TODO.md new file mode 100644 index 00000000..89162502 --- /dev/null +++ b/plans/TODO.md @@ -0,0 +1,629 @@ +# 1.0 Todo List + +This is the to-do-list for 1.0, revised as-of @./2025-09-vision.md + +## (Interpreted) Stream Literals + +- [x] Introduce `%[..]` and the corresponding pattern +- [x] Introduce `%{}` instead of `{}` and the corresponding pattern +- [x] Introduce `%raw[..]` (we don't need such a pattern, as it'll be equal to `@[EXACT(%raw[...])`, but perhaps we should advise of this) +- [x] Remove `[!raw! ...]` and replace with `#..(%raw[...])` +- [x] Remove `#..` because it's confusing and remove grouping from `#`. Instead have a `group()` method on streams. Search for all `#..` to remove. In future, perhaps `@EXPR` could add a group around it at a parser layer. +- [x] Allow %[], %raw[] directly in token streams, and search for all `#(%group` and `#(%raw` to amend. +- [x] Remove `[!stream! ...]` and replace with `%[...]` +- [x] Remove `[!set!]` and replace with `#(let x = %[ ... ])` +- [x] Remove `[!ignore!]` and replace with `#(let _ = %[ ... ])` +- [x] Add `%group[]` and remove `as group` and `[!group! ..]` +- [x] Remove `[!let!]` and replace with `#(let %[..] = %[..])` +- [x] Remove `StopCondition` +- [x] Fix the to_debug_string to add `%raw[..]` around punct groups including `#` or `%` +- [x] Fix grammar-peeking of none-groups so that e.g. `[!reinterpret! %group[#]var_name]` works +- [x] `4usize.to_string()` should return `4` but `debug_string` should return 4usize +- [x] Simplify the EXACT parser +- [x] Migrate `ErrorCommand` +- [x] Migrate `ReinterpretCommand` +- [x] Fix naming of `.string()` etc to `.to_string()`, `.to_stream()`, `.to_group()`, `.to_debug_string()` +- [x] Migrate the `Concat & Type Convert Commands` and `Concat & String Convert Commands`, and decide on method names for e.g. `string()` + * `.to_literal()`, `.concat()`, `.to_ident()`, `.to_ident_camel()`, `.to_ident_snake()`, `.to_ident_upper_snake()` + * String: `.to_lowercase()` <- maybe shouldn't concat, others can: `.to_snake_case()`, `.capitalize()` etc +- [x] Migrate `!is_empty!` and `!length!` +- [x] Migrate `!zip!` and `!zip_truncated!` +- [x] Implement some kind of support for variable length methods +- [x] Migrate `!intersperse!` +- [x] Move `.to_ident()` and friends to `to_value` via `.to_stream()` +- [x] Migrate `!split!` and `!comma_split!` +- [x] Add tests for object and string as iterables and test length +- [x] Add `into_iter()` and `to_vec()` to iterable +- [x] Add `next()`, `take(N) -> Vec` and `skip(N)` to iterator, and add tests + +## Span changes + +- [x] Remove spans from `ExpressionValue`, leave only on bindings or strem contents +- [x] Add `xx.with_span(%[])` which changes the value to stream and replaces the span of every token at the top iteration level of the stream + +## Method Calls + +- [x] Migrate the following `PairedBinaryOperation` across all types to being under the `binary_operations` of its TypeData. For each, when migration is complete, add it to the // MIGRATION LIST in operations.rs to check it's fully migrated + - [x] `Addition` + - [x] `Subtraction`, `Multiplication`, `Division` and `Remainder` + - [x] `LogicalAnd` and `LogicalOr` + - [x] `BitXor`, `BitAnd` and `BitOr` + - [x] `Equal` and `NotEqual` + - [x] `LessThan`, `LessThanOrEqual`, `GreaterThanOrEqual`, `GreaterThan` +- [x] Migrate the following `IntegerBinaryOperation`: + - [x] `ShiftLeft` and `ShiftRight` + - [x] Compute SHL/SHR on `Integer` via `.checked_shl(u32)` with an attempted cast to u32 via TryInto, + which should massively reduce the number of implementataions we need to generate. + i.e. we have a `CoercedInt` wrapper type which we use as the operand of the SHL/SHR operators +- [x] Remove the `evaluate_legacy` method, the `PairedBinaryOperation`, and all the dead code +- [x] CompoundAssignment migration +- [x] Enable `x += x` to work (using disable() / enable()) methods +- [x] Add tests that e.g. `swap(x, x)` still breaks with a borrowing error and we don't get UB +- [x] Combine `PairedBinaryOperation`, `IntegerBinaryOperation` and `CompoundAssignmentOperation` into a flattened `BinaryOperation` +- [x] Migrate `UntypedInteger` to use `FallbackInteger` like `UntypedFloat` (except a little harder because integers can overflow) +- [x] Migrate <, <=, >, >= from specific integer/float and untyped values to IntegerValue and FloatValue, in a similar way that we've done for paired arithmetic operators. +- [x] Add `==` and `!=` support for all values (including streams, objects, arrays, parsers, unsupported literals, etc) and make it work with `AnyRef<..>` arguments for testing equality +- [x] Add support for non-finite float values (infinity, NaN): + - [x] Added `output_to()` method to `FloatValue` that outputs `f32::INFINITY`, `f32::NEG_INFINITY`, `f32::NAN` (and f64 equivalents) for non-finite values, and uses `Literal::f32_suffixed()`/`Literal::f64_suffixed()` for finite values + - [x] Added float type constants: `f32::MAX`, `f32::MIN`, `f32::MIN_POSITIVE`, `f32::INFINITY`, `f32::NEG_INFINITY`, `f32::NAN`, `f32::EPSILON` (and f64 equivalents) + - [x] Added `is_nan()`, `is_infinite()`, `is_finite()`, `is_sign_positive()`, `is_sign_negative()` methods to float values +- [x] Add a new test file, `operations.rs`, and add tests to cover all the operations, including: + - [x] Cover all the binary operations with all valid type combinations + - [x] For integers/streams, this will involve for each paired operator `1 x untyped/untyped`, `n x typed/typed`, `n x typed/untyped` and `n x untyped/typed` where `n` is the number of integer/float types there are. + - [x] Create various examples of combinations / operations that don't compile, and create compilation failure tests for them in an `operations` folder, brainstorm ideas, but some ideas include: + - [x] Operations between invalid types; at least one for each operation. e.g. `1 + []` or `1u32 + 3.0` + - [x] Overflows, underflows, divide by 0s, etc +- [x] Ensure all `TODO[operation-refactor]` and `TODO[compound-assignment-refactor]` are done + +## Control flow expressions (ideally requires Stream Literals) + +Create the following expressions: +- [x] Blocks `{}` +- [x] `if`, `else`, `for`, `while`, `loop` +- [x] `continue`, `break` +- [x] Refactors: + - [x] Rename `SourceExpression` => `Expression`, and inline the leaf parsing + - [x] Rename `interpreted_stream.rs` to `output_stream.rs` + - [x] Change `InterpretTo` to use `&self` + - [x] Rename `InterpretToValue` to `Evaluate` and make it use `&self` + +## Scopes & Blocks (requires control flow expressions, or at least no `!let!` command) + +- [x] Add back `#{ }` in stream literals. Despite the brackets, the code/definitions get executed *in the parent scope*. +- [x] Scopes, Definitions, References and ControlFlowSegments exist at compile time: + - [x] Scopes and segments are created everywhere they're needed + - [x] Add in algorithm to mark references as final +- [x] At execution time: + - [x] There needs to be some link between scope and stack frame + - [x] Variable places go through `Unallocated` | `Occupied` | `Removed` + - [x] Variables are read / written based on binding ids +- [x] Fix marking references as final: + - [x] Use a second pass aligned with control flow order to set up scopes, segments and variables. +- [x] Improvements to final_value + - [x] EmbeddedX should request value ownership of shared from expression land + - [x] Disable transparent clone for streams +- [x] Fix all `TODO[scopes]` + - [x] Fix / remove expensive parse stream forks +- [x] Tests + - [x] Add test macro for asserting variable binding `is_final` information, e.g. with a `x[final]` and `x[not_final]` syntax? + - [x] Add tests for things like `let x = %[1]; let x = %[2] + x; x` + - [x] Add tests for things like `y[x] = x + 1` + - [x] Add tests for things like `let x = %[1]; { let x = %[2] + x; }; x` + - [x] Add test that `let x; x = { let x = 123; x = 456; 5 }`. resolves correctly with `x = 5`. + - [x] Add tests involving for loops; and if/elsif/else blocks + +## Attempt Expression (requires Scopes & Blocks) + +- [x] Migrate remaining commands + - [x] Use `None.configure_preinterpret()` for now + - [x] Remove `!parse!` +- [x] Add `attempt` expression - See @./2025-09-vision.md + - [x] Replace `execution_err` with explicit error kinds, so we can handle them differently with respect to catching, e.g. `destructure_err`, `panic_err`, `user_err`, `assert_err`, `resolution_err`, `operation_err` + - [x] Prevent mutating parent state in revertible block + - [x] Add `if X` guards to attempt block + - [x] Add a message to uncatchable errors explaining why the attempt block does not catch them, advising to use an assertion if these errors are intended to be caught. + - [x] Add `revert` keyword and replace error `is a not caught by attempt blocks` and `Guard condition evaluated to false.` with using it + +## Loop return behaviour & emit statement + +Ideally we want to allow returning/appending easily in a loop. Currently, we're trialing loops returning a vector (or possibly a vector of non-None values). + +But this might just be unexpected / confusing. + +Alternatively, we could have consider: +* Loops not returning anything (unless a `loop` uses a `break` perhaps, like in Rust) +* Embedded expressions having access to a `stream` variable, bound to the current contents of the stream, which they can append to. + +These are things we definitely want to do: + +- [x] Add an `OutputHandler` to the interpreter, with a stack of `OutputStream`. +- [x] Add `emit` statement which outputs into the parent stream literal +- [x] Remove vec-returns from loops, replace with `emit` +- [x] Change to error at parse if people use preinterpret keywords including `emit`, `attempt` and `revert` as variable names. +- [x] Add sensible use-case tests and compilation failure tests using `emit` +- [x] Disallow `emit` in revertible segment into stream outside of segment + +## Break / Continue Improvements + +- [x] `break` can include an optional expresion, and can be used to return a value +- [x] Loops can be labelled, e.g. with `'outer: loop { .. }` or `'inner: for { .. }`. +- [x] `break` / `continue` can specify a label, and return from that label +- [x] Add tests to control_flow.rs and compilation failure tests covering various scenarios +- [x] Blocks can be labelled, and `break` can be used to return from a labelled block (https://blog.rust-lang.org/2022/11/03/Rust-1.65.0/#break-from-labeled-blocks) + +## Parser Changes + +First, read the @./2025-11-vision.md + +- [x] We store input in the interpreter +- [x] Create new `Parser` value kind +- [x] Add ParserHandle to `InputHandler` and use some generational map to store ParseStacks (or import slotmap) + - [x] Look at https://donsz.nl/blog/arenas/ + - [ ] If using slotmap / generational-arena, replace the arena implementation too +- [x] Create (temporary) `parse X => |Y| { }` expression +- [x] Bind `input` to `Parser` at the start of each parse expression +- [x] Create `@input[...]` expression + - [x] Create a `ParseTemplateLiteral` and a `ParseTemplateStream` +- [x] Add remaining parser methods below +- [x] Add `ParseTemplatePattern` pattern +- [x] Migrate tests from `transforming.rs` to `parsing.rs` etc +- [x] Delete the transformers folder +- [x] Reversion works in attempt blocks, via forking and committing or rolling back the fork, fix `TODO[parser-input-in-interpreter]` +- [x] Add tons of tests for all the methods on Parser, and for nested parse statements + +`Parser` methods: +- [x] `ident()`, `is_ident()` +- [x] `literal()`, `is_literal()` +- [x] `integer()`, `is_integer()` +- [x] `float()`, `is_float()` +- [x] `char()`, `is_char()` +- [x] `string()`, `is_string()` +- [x] `end()`, `is_end()` +- [x] `read()` - uses `stream.parse_exact_match` +- [x] `rest()` +- [x] `until(%[,])` (see until transformer) +- [x] `end()` +- [x] `any_ident()` +- [x] `error()` +- [x] `token_tree()` +- [x] `open('(')` and `close(')')` + +## More literal kinds + +- [x] `%string[ .. ]` +- [x] `%ident[ .. ]` +- [x] `%ident_camel[ .. ]` +- [x] `%ident_snake[ .. ]` +- [x] `%ident_upper_snake[ .. ]` +- [x] `%literal[ .. ]` +- [x] Update README.md for these + +## Better handling of value sub-references + +Moved to [2026-01-types-and-forms.md](./2026-01-types-and-forms.md). + +## Methods and closures + +- [ ] Consider pre-requisite work on [2026-01-types-and-forms.md](./2026-01-types-and-forms.md) for type annotations. Instead, let's move forward without support for specific types for now. To start, let's just support: `x` or `x: any`; `x: &any` and `x: &mut any`. +- [ ] Change bindings (currently just variables) to be able to store any of the following: (nb we still restrict variables to be owned for now). +```rust +enum VariableContent { + Owned(Referenceable), + Shared(Shared), + Mutable(Mutable), +} +``` +- [ ] Introduce basic function values + * Value type function `let my_func = |x, y, z| { ... };` + * Parameters can be `x` / `x: any` (Owned), `x: &any` (Shared) or `x: &mut any` (Mutable). + * To start with, they are not closures (i.e. they can't capture any outer variables) +- [ ] Break/continue label resolution in functions/closures + * Functions and closures must resolve break/continue labels statically + * Break and continue statements should not leak out of function boundaries + * This needs to be validated during the control flow pass +- [ ] New node extension in the expression parser: invocation `(...)` +- [ ] Closures + * A function may capture variable bindings from the parent scope, these are converted into a `VariableBinding::Closure()` + * The closure consists of a set of bindings attached to the function value, either: + - `ClosedVariable::Owned(Value)` if it's the last mention of the closed variable, so it can be moved in + - `ClosedVariable::Referenced(Rc>)` otherwise + * Invocation requests `CopyOnWrite`, and can be on a shared function or an owned function + (if it is the last usage of that value, as per normal red/owned binding rules) + * If invocation is on an owned function, then owned values from the closure can be consumed + by the invocation + * Otherwise, the values are only available as shared/mut +- [ ] Try to unify methods under a "resolve, then invoke" structure + * `array::push` should resolve to the method, and `array::push(arr, value)` should work - we'll likely want an explicit `function` section and `constants` section on type data; which we can merge with methods when resolving what `array::push` resolves to. + * `my_array.push` returns a closure with `my_array` bound. The LateBound `my_array` can then be deactivated whilst the rest of the arguments are resolved, like what we do at the moment. This approach avoids the horrible javascript issues with `this` not being bound when referencing `x.y`. + * And then for objects, the method wins; BUT you can use `x["obj"]` to access the field instead of the method +- [ ] Create a `preinterpret` type, and move preinterpret settings to `preinterpret::...` +- [ ] Optional arguments +- [ ] Add `iterable.map`, `iterable.filter`, `iterable.flatten`, `iterable.flatmap` +- [ ] Add `array.sort`, `array.sort_by` + +## Parser - Methods using closures + +Future methods once we have closures: +- [ ] Add `stream.parse(|input| { ... })` to replace the `parse` statement entirely. +- [ ] Add `let captured = input.capture(|input| { ... })` + * This returns the parsed input stream. It can capture the original tokens by using `let forked = input.fork()` and then `let end_cursor = input.end();` and then consuming `TokenTree`s from `forked` until `forked.cursor >= end_cursor` (making use of the PartialEq implementation) +- [ ] Something for `input.fields({ ... })` and `input.subfields({ ... })` (allows other fields not present), whose fields are closures. BUT what do we do about `optional` fields. Hmm. Maybe: +```rust +input.fields(%{ + required: %{ + hello: parser::integer, + }, + optional: %{ + world: parser::string, + }, +}) +``` +- [ ] `input.repeated(..)` as below: +```rust +input.repeated( + %{ + separator?: %[], + min?: 0, + max?: 1000000, + }, + |inner| { + // ... + } +) +``` +- [ ] `input.any_group(|inner| { })` +- [ ] `input.group('()', |inner| { })` +- [ ] `input.transparent_group(|inner| { })` + +## Parser - Better Types for Tokens + +- [ ] Add `Tokens`, `TokenTree`, `Group` as non-leaf types, for use alongside `StreamValue` and other Rust-like / syn-like objects +- [ ] Work out whether `Tokens` should be iterable or not. + * Actually I think *not* + * Some values probably want to be iterable in other ways (e.g. `Repeated` or `Punctuated`) + * Add singleton things like those under `TokenTree` should probably not be iterable + * Perhaps Tokens/Stream needs `.into_token_tree_iter()` to be iterable? + * Perhaps `Iterable` actually takes a `Box` somehow, and is an opt-in in the type hierarchy + * We likely need multi-parenting - so `Repeated` can be both `Tokens` and `Iterable` +- [ ] Add explicit values for: + - [ ] NOPE: `Span` + - [ ] NOPE: `TokenTree` <-- We don't want this. Instead we should have leaf values like `Literal`, `Ident`, `Punct` + ... but mabe + - [ ] `Ident` (parent = `TokenTree`) + - [ ] `Literal` (parent = `TokenTree`) <-- Unsupported Literal can go here + - [ ] `Punct` (parent = `TokenTree`) + - [ ] `ParenthesesGroup` (parent = `Group`) + - [ ] `BraceGroup` (parent = `Group`) + - [ ] `BracketGroup` (parent = `Group`) +- [ ] Stream methods `single_literal()`, `single_ident()`, `single_token_tree()` +- [ ] Parser methods `span()` or `cursor()` -- maybe? outputs a token with a span for outputting errors. If at end of an inner stream, it outputs the ident `END` with the span of the closing bracket. +- [ ] `preinterpret::call_site()`, `preinterpret::call_site_close()`, `preinterpret::call_site_open()` return `Span` + +Then add all of these from normal macros: +- [ ] block: a block (i.e. a block of statements and/or an expression, surrounded by braces) +- [ ] expr: an expression +- [ ] ident: an identifier (this includes keywords) +- [ ] item: an item, like a function, struct, module, impl, etc. +- [ ] lifetime: a lifetime (e.g. 'foo, 'static, …) +- [ ] literal: a literal (e.g. "Hello World!", 3.14, '🦀', …) +- [ ] meta: a meta item; the things that go inside the #[...] and #![...] attributes +- [ ] pat: a pattern +- [ ] path: a path (e.g. foo, ::std::mem::replace, transmute::<_, int>, …) +- [ ] stmt: a statement +- [ ] tt: a single token tree +- [ ] ty: a type +- [ ] vis: a possible empty visibility qualifier (e.g. pub, pub(in crate), …) + +## Utility methods + +Implement the following: +* All values: + * A `types() -> ["u32", "integer", "value"]` method which returns a logical name for all types in its hierarchy, from most to least specific. + * A `leaf_type() -> "u32"` which could be used in a `match` statement + * An `is_type("ident") -> bool` + +## Parser - Repeat Input Bindings + +Parse template repeat bindings +* `@xx[]?`, `@xx[]+`, `@xx[],+`, `@xx[]*`, `@xx[],*` +* `@(..)?`, `@(..)+`, `@(..),+`, `@(..)*`, `@(..),*` (inside a parse template literal) + +## Repeat output bindings + +- [ ] Implement option 1 below (i.e. repeat syntax). Maps will follow separately. + +-- +* Use case: Easily create the below code, similar to a procedural macro. Notably creating tuples of all sizes. + => Honestly, `map(|x| x.to_ident())` and existing `.intersperse(%[,])` is probably the cleanest combination +* We need maps or repeats. A simple join isn't enough for . Consider alternatives to the below syntax. + * Option 0: Do nothing: + - Use `for x in A..Z { let ident = x.ident(); $[x,] }` + - Use `#(generics.intersperse(%[,]))` + * Option 1: `%*(#generics,)` or `%(#generics),` like declarative macros. + * All the variable bindings in the repeat must refer to arrays or streams (i.e. iterables) of the same length, similar to proc macros. + * BUT sadly we'll often have arrays of objects, so we really want to map e.g. `arr[i].x` + * Option 2: Python style iterator comprehension `#(%[x,] for x in generics)` using a `for` extension + ... actually we already have this kinda with the for expression returning a list `for x in A..Z { x.ident() }` + * Option 3: Map methods + +Option 0 - Do nothing +```rust +// Impls `MyTrait` for tuples of size 0 to 10 +preinterpret::run! { + for N in 0..=10 { + let comma_separated_types = %[]; + for name in 'A'..'Z'.take(N) { + let ident = name.ident(); + comma_separated_types += %[#ident,]; + } + %[ + impl<#comma_separated_types> MyTrait for (#comma_separated_types) {} + ] + } +} +``` +Or even, with for expressions returning arrays: +```rust +// Impls `MyTrait` for tuples of size 0 to 10 +preinterpret::run! { + for N in 0..=10 { + let comma_separated_types = (for name in 'A'..'Z'.take(N) { name.ident() }).intersperse(%[,]); + %[ + impl<#comma_separated_types> MyTrait for (#comma_separated_types) {} + ] + } +} +``` + +Option 1 - Output repeat syntax, like declarative macros output binding +```rust +// Impls `MyTrait` for tuples of size 0 to 10 +preinterpret::run! { + for N in 0..=10 { + let types = %[A B C D E F G H I J K L M N O P Q R S T].take(N); + %[ + impl<%(#types),*> MyTrait for (%(#types,)*) {} + ] + } +} +``` + +Option 2 - Python-style for comprehensions? (or rust-style one-line for expressions) +```rust +// Impls `MyTrait` for tuples of size 0 to 10 +preinterpret::run! { + for N in 0..=10 { + let type_params = [x.ident() for x in ('A'..).take(N)]; + // OR type_params = for x in A..Z { x.ident() } + let tuple = %[( #(%[#x,] for x in type_params) )]; + let generics = %[< #(%[#x,] for x in type_params) >]; + %[ + impl#generics MyTrait for #tuple {} + ] + } +} +``` + +Option 3 - Maps: +```rust +// Impls `MyTrait` for tuples of size 0 to 10 +preinterpret::run! { + for N in 0..=10 { + let idents = ('A'..).take(N).map(|x| x.ident()); + let type_params = %[< #(idents.map(|x| %[#x,])) >]; + let tuple = %[( #(idents.map(|x| %[#x,])) )]; + %[ + impl #type_params MyTrait for #tuple {} + ] + } +} +``` + +## Error improvements + +- [x] Distinguish a runtime error from a coding error (e.g. parse error, or "no method of type") +- [ ] If method resolution fails, perhaps we try finding a method with that name on other types + +## Optimizations + +- [ ] Look at benchmarks and if anything should be sped up +- [ ] Speeding up stream literal processing + - [ ] When interpreting a stream literal, we can avoid having to go through error handling pathways to get an `output` from the intepreter by storing a `OutputInterpreter<'a>` which wraps an `&mut OutputStream` and a pointer to an Intepreter, and can be converted back into/from an `Interpreter` easily + - [ ] Possibly similarly for an `InputInterpreter<'a>` when processing a `ConsumeStream` +- [ ] Speeding up scopes at runtime: + - [ ] In the interpreter, store a flattened stack of variable values + - [ ] `no_mutation_above` can be a stack offset + - [ ] References store on them cached information - either up-front, via an `Rc>` or via a "resolve on first execute" + - Value's relative offset from the top of the stack + - An is last use flag +- [ ] Change the storage model for `OutputStream` + - [ ] Either just use `TokenStream` directly(!) (...and ignore Rust analyzer's poor handling of none groups) + - [ ] Or use an `Rc` model like https://github.com/dtolnay/proc-macro2/pull/341/files and Rust itself +- Address `TODO[performance]` + +## Deferred + +The following are less important tasks which maybe we don't even want/need to do. + +- [x] Side-project: Make LateBound better to allow this, by upgrading to mutable before use + - [x] https://rust-lang.github.io/rfcs/2025-nested-method-calls.html + - [x] x += x for x copy, by resolving Owned before Mutable / Shared +- [ ] Allow adding labels to stream literals `%'a[]` and then `emit 'a`, with `'root` being the topmost. Or maybe just `emit 'root` honestly. Can't really see the use case for the others. + - [ ] Note that `%'a[((#{ emit 'a %[x] }))]` should yield `x(())` + - [ ] Note that we need to prevent or revert outputting to root in revertible segments +- [ ] All value kinds should be generated with a macro which also generates a `#[test] list_all` method + - [ ] We should create some unit tests in `value.rs` and functions `generate_example_values(value_kind)` which returns a `Vec` for each value kind. + - [ ] We can use this to check that `eq` and `neq` are defined and work correctly for all types +- [ ] Add lexicographic ordering to arrays, if they're the same length and their values can be compared +- [ ] Make StreamPattern an exact match, and allow `%raw[]` and `%group[]` patterns too - but disallow embedding statements; and address any remaining `TODO[parsers]` +- [ ] Add a "closing span range" to the parse streams, and check for end manually to get a better error message: + - [ ] Wherever we use `parse_with` + - [ ] Wherever we drop the `ParseStreamStack` in the interpreter + - [ ] Whenever we create an output stream, we can set an optional "end_of_stream" span, which is used when the parser runs. + - [ ] Check if the compiler output in the `parser_after_rest` test is better: +```rust +let @input[{ let _ = input.rest(); let _ = input.token_tree(); }] = %[Hello World]; +``` + +## Match block [blocked on slices] + +* Delay this probably - without enums it's not super important. +* We'll need to add destructuring references, and allow destructuring `x.as_ref()` + and maybe `x.as_mut()`. +* To destructure owned objects, we probably want to do it as_ref first, and if that succeeds, we can commit to a proper owned destructuring. +* Poor man's enum with `{ type: "a", ... }` and `{ type: "b", ... }` + +## Coding challenges + +Implement 10 leet-code challenges and 10 parsing challenges (e.g. from `syn` docs) to ensure that the language is sufficiently comprehensive to use in practice. + +Also: +* `versioned!` from Scrypto +* The big state macro from Scrypto + +## Final considerations + +- [x] Merge `assignee_frames` into `value_frames` as per comment as the top of `assignee_frames` +- [x] Rename `EvaluationItem` to `RequestedValue` and consider making `RequestedValue::AssignmentCompletion` wrap an `Owned<()>` so that it becomes truly a value. +- [x] Merge `HasValueType` with `ValueLeafKind` +- [ ] Add `preinterpret::macro` - can this be a declarative macro? Would be slightly more efficient, as it just needs to wrap a call to `preinterpret::stream` or `preinterpret::run`... + - [ ] When we create `input = %raw[..]` we will need to set its `end_of_stream` span to the end of the + macro_rules! macro somehow... I'm not sure how to get that span though. +- [x] Add `Eq` support on composite types and streams +- [x] See `TODO[untyped]` - Have UntypedInteger/UntypedFloat have an inner representation of either value or literal, for improved efficiency / less weird `Span::call_site()` error handling +- [ ] Move `typed_eq` as `%[].typed_eq(..)` +- [ ] Add a `%[].structure_eq(...)` method which uses an `EqualityContext` which ignores value inequality +- [ ] We might need to auto-change the span of all outputted tokens to `Span::call_site()` to get hygiene + to be most flexible. Perhaps this can be disabled with `preinterpret::set_auto_call_site_hygiene(false)` +- [ ] Add `LiteralPattern` (wrapping a `Literal`) +- [ ] Better handling of `configure_preinterpret`: + * Move `None.configure_preinterpret` to `preinterpret::set_iteration_limit(..)` +- [ ] CastTarget revision: + * The `as untyped_int` operator is not supported for string values + * The `as char` operator is not supported for untyped integer values + * Add casts of any integer to char, via `char::from_u32(u32::try_from(x))` + * Should we remove/replace any CastTargets? +- [ ] TODO check +- [ ] Check all `#[allow(unused)]` and remove any which aren't needed + We can use `_xyz: Unused` in some places to reduce the size of types. + +## Cloning + +* Consider making Iterator non-clonable (which will unlock many more easy lazy implementations, of e.g. `take` using the non-clonable `Take`, and similar for other mapped iterators), i.e. `ExpressionValue` has a manual `clone() -> ExecutionResult` - this will simplify some things. But then, `to_string()` would want to take a `CopyOnWrite` so that where we clone, the iterator can potentially take owned, attempt clone, else error. + +## Write book / Docs + +- [ ] [PAGE] Introduction - covering: + * Motivation + * A Rust-like interpreted language with JS-like value types, built for code-generation + * Comparison with proc-macros and crabtime + * Native stream and parsing support, the `attempt` expression + - [ ] [PAGE] Cheat-sheet +- [ ] [PAGE] Syntax + - [ ] [PAGE] Values (linking to each expression value) + - [ ] [PAGE] Control Flow +- [ ] [PAGE] Expression Values - discusses expression value hierachy, expression model similar to rust, object/array similar to JS. [subpage for each main expression value kind, including sytax, examples and creating the value, and methods on the value] + - [ ] [PAGE] Stream + - [ ] [PAGE] Integers + - [ ] [PAGE] Floats + - [ ] [PAGE] Char + - [ ] [PAGE] Boolean + - [ ] [PAGE] String + - [ ] [PAGE] Array + - [ ] [PAGE] Object + - [ ] [PAGE] Range + - [ ] [PAGE] Iterable + - [ ] [PAGE] Iterator + - [ ] [PAGE] None + - [ ] Document equality methods: `==` (lenient), `typed_eq()` (errors on type mismatch), `assert_eq()` (detailed error messages). Stream equality preserves transparent groups; use `remove_transparent_groups()` to normalize. +- [ ] [PAGE] Guides + - [ ] [PAGE] Errors and Spans + - NB: If someone wants to keep a value's span, they can keep it in a stream and coerce it; or store it as a tuple of a value with its span `%{ value: $x, span: %[$x] }` + - [ ] [PAGE] Parsing + - [ ] Hygiene +- [ ] Examples (tbc) + +And then we need to: +- [ ] Update the README to point to the book +- [ ] Update the module docstring to point to the book. + +Sidenote - crabtime comparison: + * Compare to https://www.reddit.com/r/rust/comments/1j42fgi/media_introducing_eval_macro_a_new_way_to_write i.e. https://crates.io/crates/crabtime - thoughts on crabtime: +=> Looks great! +=> Likely has faster compile times compared with preinterpret +=> Why don't they use a cheap hash of the code as a cache key? + - I imagine the rust macro system takes care of not re-running it if it changes + - The cachability is a big win compared to preinterpret (although preinterpret is faster on first run) +=> I can't imagine the span-chasing / error messages are great, because everything is translated to/from strings between the processes + ... I wonder if there's any way to improve this? Plausibly you could use the proc-macro bridge encoding scheme as per https://blog.jetbrains.com/rust/2022/07/07/procedural-macros-under-the-hood-part-ii/ to send handles onwards, or even delegate directly somehow? + - The spans are better in preinterpret +=> Parsing isn't really a thing - they're not going after full macro stuff, probably wise + + +## Write marketing materials + +* Publish v1.0 +* Flashy infographic like `crabtime` with some examples. +* Mimimal readme, focusing on key use-cases, and pointing out at the docs. + +## Stream-return optimizations [OPTIONAL] + +Expression evaluation can come with an `OutputStyle::AppendToStream(&mut OutputStream)` rather than a `OutputStyle::OwnedValue`, which is handled in `ArgumentValue` (might need a new name!) + +This can be used to optimize, e.g.: + +* Methods + * Using the `StreamOutput` return +* Stream Literals + * Could output direct to the output stream +* Loops: + * If they have an output location of stream, and no `break `, we can pass the stream through as each iteration's output location + * If they have an output location of none: don't output anything +* `stream +=` and `stream.append(..)` methods + +This means that this is low-clone: +```rust +#(for i in 0..100 { %[println!("Hello world!");] }) +``` + +## Value expansions [OPTIONAL] + +* Consider somehow adding some kind of slice object? (see `TODO[slice-supprt]`) + * To do this, we basically need to have a leaf-kind of `Mutable`, so we can map an + `arr[0..2]` to a `Mutable<[AnyValue]>` - but this then can't be converted back to + a `Mutable`. This is then a binding structurally of type `&mut slice`. + * Supporting structurally type-restricted bindings is ... complicated ... + And whilst the machinery for leaf-restricted bindings is in place, their exposure and + inter-op with any-bindings is very much not (as of Jan 26). + * See [2026-01-types-and-forms.md](./2026-01-types-and-forms.md) for more details. +* Consider whether to expand to storing `ArgumentValue` or `CopyOnWriteValue` in variables instead of `OwnedValue`? + - The main issue is if it interferes with taking mutable references, but it's possibly OK, would need to see if it's a confusing problem in practice... (e.g. `let b = a[0]; a.push(1)` if `b` is a reference to `a[0]` then this is a problem when we push to `a`) + - If a mutable reference is created and there are pending references, the variable data RefCell could be replaced with a cloned value and then mutated... But this can be more expensive, because e.g. `let b = a[0]; a.push(1)` results in the whole array `a` being copied in the `CoW` case; but only the `a[0]` being cloned in the "clone on assign" case. + - Maybe we just stick to assignments being Owned/Cloned as currently + +* Support `#(x[..])` syntax for indexing streams, like with arrays + * `#(x[0])` returns the value at that position of the stream (using `INFER_TOKEN_TREE`) + * `#(x[0..3])` returns a TokenStream + * `#(x[0..=3])` returns a TokenStream + +-------------------------------------------------------------------------------- + +# Descoped for 1.0 + +* Further Performance Improvements: + * Use a small-vec optimization in some places +* Fork of syn to: + * Fix issues in Rust Analyzer (hopefully Rust Analyzer will fix their handling of transparent groups) + * Add support for a more general `TokenBuffer`, and ensure that Cursor can work in a backwards-compatible way with that buffer. Support: + * Storing a length + * Embedding tokens directly without putting them into a `Group` + * Possibling embedding a reference to a slice buffer inside a group + * Ability to parse to a TokenBuffer or TokenBufferSlice + * Possibly allowing some kind of embedding of Tokens which can be converted into a TokenStream. + * Currently, `ParseBuffer` stores `unexpected` and has drop glue which is a hacky abstraction. We'll need to think of an alternative. Perhaps we change `ParseBuffer` to operate on top of a `TokenBuffer` ?? + * Fix `any_punct()` to ignore none groups + * In future - improve performance of some other parts of syn + * Better error messages + * See e.g. invalid_content_too_short where ideally the error message would be on the last token in the stream. Perhaps End gets a span from the previous error? + * See e.g. invalid_content_too_long where `unexpected token` is quite vague. + Maybe we can't sensibly do better though... \ No newline at end of file diff --git a/plans/debugging.md b/plans/debugging.md new file mode 100644 index 00000000..a8d59a4a --- /dev/null +++ b/plans/debugging.md @@ -0,0 +1,5 @@ +The following are specific issues you might encounter, and what they might mean. + +## SIGBUS + +It appears that a stack overflow of a proc macro (at least on an M2 Macbook in Oct 2025) gives a SIGBUS error. Look for accidental recursion in the methods to identify the likely cause. \ No newline at end of file diff --git a/regenerate-compilation-failures.sh b/regenerate-compilation-failures.sh new file mode 100755 index 00000000..f1a3fd31 --- /dev/null +++ b/regenerate-compilation-failures.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +TRYBUILD=overwrite cargo test compilation_failures + +# Remove the .wip folder created sometimes by try-build +if [ -d "./wip" ]; then + rm -r ./wip +fi diff --git a/src/command.rs b/src/command.rs deleted file mode 100644 index 68ec39d5..00000000 --- a/src/command.rs +++ /dev/null @@ -1,186 +0,0 @@ -use crate::internal_prelude::*; - -pub(crate) trait CommandDefinition { - const COMMAND_NAME: &'static str; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result; -} - -macro_rules! define_commands { - ( - pub(crate) enum $enum_name:ident { - $( - $command:ident, - )* - } - ) => { - #[allow(clippy::enum_variant_names)] - pub(crate) enum $enum_name { - $( - $command, - )* - } - - impl $enum_name { - pub(crate) fn execute(self, interpreter: &mut Interpreter, argument_stream: CommandArgumentStream, command_span: Span) -> Result { - match self { - $( - Self::$command => $command::execute(interpreter, argument_stream, command_span), - )* - } - } - - pub(crate) fn attempt_parse(ident: &Ident) -> Option { - Some(match ident.to_string().as_ref() { - $( - <$command as CommandDefinition>::COMMAND_NAME => Self::$command, - )* - _ => return None, - }) - } - - const ALL_KIND_NAMES: &'static [&'static str] = &[$($command::COMMAND_NAME,)*]; - - pub(crate) fn list_all() -> String { - // TODO improve to add an "and" at the end - Self::ALL_KIND_NAMES.join(", ") - } - } - }; -} -pub(crate) use define_commands; - -pub(crate) struct CommandInvocation { - command_kind: CommandKind, - argument_stream: CommandArgumentStream, - command_span: Span, -} - -impl CommandInvocation { - pub(crate) fn new(command_kind: CommandKind, group: &Group, argument_tokens: Tokens) -> Self { - Self { - command_kind, - argument_stream: CommandArgumentStream::new(argument_tokens), - command_span: group.span(), - } - } - - pub(crate) fn execute(self, interpreter: &mut Interpreter) -> Result { - self.command_kind - .execute(interpreter, self.argument_stream, self.command_span) - } -} - -pub(crate) struct VariableSubstitution { - marker: Punct, // # - variable_name: Ident, -} - -impl VariableSubstitution { - pub(crate) fn new(marker: Punct, variable_name: Ident) -> Self { - Self { - marker, - variable_name, - } - } - - pub(crate) fn execute(self, interpreter: &mut Interpreter) -> Result { - let VariableSubstitution { - marker, - variable_name, - } = self; - match interpreter.get_variable(&variable_name.to_string()) { - Some(variable_value) => Ok(variable_value.clone()), - None => { - let marker = marker.as_char(); - let name_str = variable_name.to_string(); - let name_str = &name_str; - Err(Error::new( - variable_name.span(), - format!( - "The variable {}{} wasn't set.\nIf this wasn't intended to be a variable, work around this with [!raw! {}{}]", - marker, - name_str, - marker, - name_str, - ), - )) - } - } - } -} - -pub(crate) struct CommandArgumentStream { - tokens: Tokens, -} - -impl CommandArgumentStream { - fn new(tokens: Tokens) -> Self { - Self { tokens } - } - - pub(crate) fn interpret_and_concat_to_string( - self, - interpreter: &mut Interpreter, - ) -> Result { - let interpreted = interpreter.interpret_tokens(self.tokens)?; - Ok(concat_recursive(interpreted)) - } - - pub(crate) fn tokens(self) -> Tokens { - self.tokens - } -} - -fn concat_recursive(arguments: TokenStream) -> String { - fn concat_recursive_internal(output: &mut String, arguments: TokenStream) { - for token_tree in arguments { - match token_tree { - TokenTree::Literal(literal) => { - let lit: Lit = parse_str(&literal.to_string()).expect( - "All proc_macro2::Literal values should be decodable as a syn::Lit", - ); - match lit { - Lit::Str(lit_str) => output.push_str(&lit_str.value()), - Lit::Char(lit_char) => output.push(lit_char.value()), - _ => { - output.push_str(&literal.to_string()); - } - } - } - TokenTree::Group(group) => match group.delimiter() { - Delimiter::Parenthesis => { - output.push('('); - concat_recursive_internal(output, group.stream()); - output.push(')'); - } - Delimiter::Brace => { - output.push('{'); - concat_recursive_internal(output, group.stream()); - output.push('}'); - } - Delimiter::Bracket => { - output.push('['); - concat_recursive_internal(output, group.stream()); - output.push(']'); - } - Delimiter::None => { - concat_recursive_internal(output, group.stream()); - } - }, - TokenTree::Punct(punct) => { - output.push(punct.as_char()); - } - TokenTree::Ident(ident) => output.push_str(&ident.to_string()), - } - } - } - - let mut output = String::new(); - concat_recursive_internal(&mut output, arguments); - output -} diff --git a/src/commands/concat_commands.rs b/src/commands/concat_commands.rs deleted file mode 100644 index ac6fae22..00000000 --- a/src/commands/concat_commands.rs +++ /dev/null @@ -1,334 +0,0 @@ -use crate::internal_prelude::*; - -//======== -// Helpers -//======== - -fn string_literal(value: &str, span: Span) -> Literal { - let mut literal = Literal::string(value); - literal.set_span(span); - literal -} - -fn parse_literal(value: &str, span: Span) -> Result { - let mut literal = Literal::from_str(value) - .map_err(|err| span.error(format!("`{}` is not a valid literal: {:?}", value, err,)))?; - literal.set_span(span); - Ok(literal) -} - -fn parse_ident(value: &str, span: Span) -> Result { - let mut ident = parse_str::(value) - .map_err(|err| span.error(format!("`{}` is not a valid ident: {:?}", value, err,)))?; - ident.set_span(span); - Ok(ident) -} - -//======================================= -// Concatenating type-conversion commands -//======================================= - -pub(crate) struct StringCommand; - -impl CommandDefinition for StringCommand { - const COMMAND_NAME: &'static str = "string"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - let interpreted = argument.interpret_and_concat_to_string(interpreter)?; - let string_literal = string_literal(&interpreted, command_span); - Ok(TokenStream::from(TokenTree::Literal(string_literal))) - } -} - -pub(crate) struct IdentCommand; - -impl CommandDefinition for IdentCommand { - const COMMAND_NAME: &'static str = "ident"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - let interpreted = argument.interpret_and_concat_to_string(interpreter)?; - let parsed_ident = parse_ident(&interpreted, command_span)?; - Ok(TokenStream::from(TokenTree::Ident(parsed_ident))) - } -} - -pub(crate) struct IdentCamelCommand; - -impl CommandDefinition for IdentCamelCommand { - const COMMAND_NAME: &'static str = "ident_camel"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - let interpreted = argument.interpret_and_concat_to_string(interpreter)?; - let upper_camel_cased = to_upper_camel_case(&interpreted); - let parsed_ident = parse_ident(&upper_camel_cased, command_span)?; - Ok(TokenStream::from(TokenTree::Ident(parsed_ident))) - } -} - -pub(crate) struct IdentSnakeCommand; - -impl CommandDefinition for IdentSnakeCommand { - const COMMAND_NAME: &'static str = "ident_snake"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - let interpreted = argument.interpret_and_concat_to_string(interpreter)?; - let lower_snake_cased = to_lower_snake_case(&interpreted); - let parsed_ident = parse_ident(&lower_snake_cased, command_span)?; - Ok(TokenStream::from(TokenTree::Ident(parsed_ident))) - } -} - -pub(crate) struct IdentUpperSnakeCommand; - -impl CommandDefinition for IdentUpperSnakeCommand { - const COMMAND_NAME: &'static str = "ident_upper_snake"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - let interpreted = argument.interpret_and_concat_to_string(interpreter)?; - let upper_snake_cased = to_upper_snake_case(&interpreted); - let parsed_ident = parse_ident(&upper_snake_cased, command_span)?; - Ok(TokenStream::from(TokenTree::Ident(parsed_ident))) - } -} - -pub(crate) struct LiteralCommand; - -impl CommandDefinition for LiteralCommand { - const COMMAND_NAME: &'static str = "literal"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - let interpreted = argument.interpret_and_concat_to_string(interpreter)?; - let parsed_literal = parse_literal(&interpreted, command_span)?; - Ok(TokenStream::from(TokenTree::Literal(parsed_literal))) - } -} - -//=========================== -// String conversion commands -//=========================== - -fn concat_string_and_convert( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - conversion_fn: impl Fn(&str) -> String, -) -> Result { - let interpreted = argument.interpret_and_concat_to_string(interpreter)?; - let string_literal = string_literal(&conversion_fn(&interpreted), command_span); - Ok(TokenStream::from(TokenTree::Literal(string_literal))) -} - -pub(crate) struct UpperCommand; - -impl CommandDefinition for UpperCommand { - const COMMAND_NAME: &'static str = "upper"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert(interpreter, argument, command_span, to_uppercase) - } -} - -pub(crate) struct LowerCommand; - -impl CommandDefinition for LowerCommand { - const COMMAND_NAME: &'static str = "lower"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert(interpreter, argument, command_span, to_lowercase) - } -} - -pub(crate) struct SnakeCommand; - -impl CommandDefinition for SnakeCommand { - const COMMAND_NAME: &'static str = "snake"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - // Lower snake case is the more common casing in Rust, so default to that - LowerSnakeCommand::execute(interpreter, argument, command_span) - } -} - -pub(crate) struct LowerSnakeCommand; - -impl CommandDefinition for LowerSnakeCommand { - const COMMAND_NAME: &'static str = "lower_snake"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert(interpreter, argument, command_span, to_lower_snake_case) - } -} - -pub(crate) struct UpperSnakeCommand; - -impl CommandDefinition for UpperSnakeCommand { - const COMMAND_NAME: &'static str = "upper_snake"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert(interpreter, argument, command_span, to_upper_snake_case) - } -} - -pub(crate) struct KebabCommand; - -impl CommandDefinition for KebabCommand { - const COMMAND_NAME: &'static str = "kebab"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - // Kebab case is normally lower case (including in Rust where it's used - e.g. crate names) - // It can always be combined with other casing to get other versions - concat_string_and_convert(interpreter, argument, command_span, to_lower_kebab_case) - } -} - -pub(crate) struct CamelCommand; - -impl CommandDefinition for CamelCommand { - const COMMAND_NAME: &'static str = "camel"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - // Upper camel case is the more common casing in Rust, so default to that - UpperCamelCommand::execute(interpreter, argument, command_span) - } -} - -pub(crate) struct LowerCamelCommand; - -impl CommandDefinition for LowerCamelCommand { - const COMMAND_NAME: &'static str = "lower_camel"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert(interpreter, argument, command_span, to_lower_camel_case) - } -} - -pub(crate) struct UpperCamelCommand; - -impl CommandDefinition for UpperCamelCommand { - const COMMAND_NAME: &'static str = "upper_camel"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert(interpreter, argument, command_span, to_upper_camel_case) - } -} - -pub(crate) struct CapitalizeCommand; - -impl CommandDefinition for CapitalizeCommand { - const COMMAND_NAME: &'static str = "capitalize"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert(interpreter, argument, command_span, capitalize) - } -} - -pub(crate) struct DecapitalizeCommand; - -impl CommandDefinition for DecapitalizeCommand { - const COMMAND_NAME: &'static str = "decapitalize"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert(interpreter, argument, command_span, decapitalize) - } -} - -pub(crate) struct TitleCommand; - -impl CommandDefinition for TitleCommand { - const COMMAND_NAME: &'static str = "title"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert(interpreter, argument, command_span, title_case) - } -} - -pub(crate) struct InsertSpacesCommand; - -impl CommandDefinition for InsertSpacesCommand { - const COMMAND_NAME: &'static str = "insert_spaces"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - concat_string_and_convert( - interpreter, - argument, - command_span, - insert_spaces_between_words, - ) - } -} diff --git a/src/commands/core_commands.rs b/src/commands/core_commands.rs deleted file mode 100644 index 1ab44080..00000000 --- a/src/commands/core_commands.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::internal_prelude::*; - -pub(crate) struct SetCommand; - -impl CommandDefinition for SetCommand { - const COMMAND_NAME: &'static str = "set"; - - fn execute( - interpreter: &mut Interpreter, - argument: CommandArgumentStream, - command_span: Span, - ) -> Result { - let mut argument_tokens = argument.tokens(); - let variable_name = match parse_variable_set(&mut argument_tokens) { - Some(ident) => ident.to_string(), - None => { - return Err(command_span - .error("A set call is expected to start with `#variable_name = ..`")); - } - }; - - let result_tokens = interpreter.interpret_tokens(argument_tokens)?; - interpreter.set_variable(variable_name, result_tokens); - - Ok(TokenStream::new()) - } -} - -pub(crate) struct RawCommand; - -impl CommandDefinition for RawCommand { - const COMMAND_NAME: &'static str = "raw"; - - fn execute( - _interpreter: &mut Interpreter, - argument: CommandArgumentStream, - _command_span: Span, - ) -> Result { - Ok(argument.tokens().into_token_stream()) - } -} - -pub(crate) struct IgnoreCommand; - -impl CommandDefinition for IgnoreCommand { - const COMMAND_NAME: &'static str = "ignore"; - - fn execute(_: &mut Interpreter, _: CommandArgumentStream, _: Span) -> Result { - Ok(TokenStream::new()) - } -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs deleted file mode 100644 index b1971577..00000000 --- a/src/commands/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -mod concat_commands; -mod core_commands; - -use crate::internal_prelude::*; -use concat_commands::*; -use core_commands::*; - -define_commands! { - pub(crate) enum CommandKind { - // Core Commands - SetCommand, - RawCommand, - IgnoreCommand, - - // Concat & Type Convert Commands - StringCommand, - IdentCommand, - IdentCamelCommand, - IdentSnakeCommand, - IdentUpperSnakeCommand, - LiteralCommand, - - // Concat & String Convert Commands - UpperCommand, - LowerCommand, - SnakeCommand, - LowerSnakeCommand, - UpperSnakeCommand, - CamelCommand, - LowerCamelCommand, - UpperCamelCommand, - KebabCommand, - CapitalizeCommand, - DecapitalizeCommand, - TitleCommand, - InsertSpacesCommand, - } -} diff --git a/src/expressions/concepts/content.rs b/src/expressions/concepts/content.rs new file mode 100644 index 00000000..d33e97c7 --- /dev/null +++ b/src/expressions/concepts/content.rs @@ -0,0 +1,247 @@ +use super::*; + +/// Shorthand for representing the form F of a type T with a particular lifetime 'a. +pub(crate) type Content<'a, T, F> = ::Content<'a, F>; +pub(crate) type DynContent<'a, D, F> = + ::DynLeaf<'a, ::DynContent>; + +/// For types which have an associated value (type and form) +pub(crate) trait IsValueContent { + type Type: IsHierarchicalType; + type Form: IsHierarchicalForm; +} + +pub(crate) trait FromValueContent<'a>: IsValueContent { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self; +} + +pub(crate) trait FromSpannedValueContent<'a>: IsValueContent { + fn from_spanned_content(content: Spanned>) -> Self; +} + +impl<'a, C: FromValueContent<'a>> FromSpannedValueContent<'a> for C { + fn from_spanned_content(content: Spanned>) -> Self { + let Spanned(inner, _span_range) = content; + C::from_content(inner) + } +} + +impl IsValueContent for Spanned { + type Type = C::Type; + type Form = C::Form; +} + +impl<'a, C: FromValueContent<'a>> FromSpannedValueContent<'a> for Spanned { + fn from_spanned_content( + Spanned(content, span_range): Spanned>, + ) -> Self { + Spanned(C::from_content(content), span_range) + } +} + +pub(crate) trait IntoValueContent<'a>: IsValueContent +where + Self: Sized, +{ + fn into_content(self) -> Content<'a, Self::Type, Self::Form>; + + #[inline] + fn upcast(self) -> Content<'a, S, Self::Form> + where + Self::Type: UpcastTo, + { + ::upcast_to::(self.into_content()) + } + + #[inline] + #[allow(clippy::type_complexity)] // It's actually pretty readable + fn downcast(self) -> Result, Content<'a, Self::Type, Self::Form>> + where + U: DowncastFrom, + { + U::downcast_from::(self.into_content()) + } + + #[inline] + fn into_any(self) -> Content<'a, AnyType, Self::Form> + where + Self::Type: UpcastTo, + { + self.upcast() + } + + fn into_referenceable(self) -> Content<'a, Self::Type, BeReferenceable> + where + Self: IsValueContent, + { + map_via_leaf! { + input: (Content<'a, Self::Type, Self::Form>) = self.into_content(), + fn map_leaf(leaf) -> (Content<'a, T, BeReferenceable>) { + Rc::new(RefCell::new(leaf)) + } + } + } +} + +impl<'a, C> Spanned +where + C: IntoValueContent<'a>, + C::Type: IsHierarchicalType, + C::Form: IsHierarchicalForm, +{ + pub(crate) fn downcast_resolve>( + self, + resolution_target: &str, + ) -> ExecutionResult + where + ::Type: DowncastFrom, + { + let Spanned(value, span_range) = self; + let content = value.into_content(); + let resolved = <::Type>::resolve::( + content, + span_range, + resolution_target, + )?; + Ok(X::from_spanned_content(Spanned(resolved, span_range))) + } + + // TODO[concepts]: Change to use a FromSpannedDynContent trait, + // so that it can return a ExecutionResult and avoid needing to specify D. + pub(crate) fn dyn_resolve( + self, + resolution_target: &str, + ) -> ExecutionResult> + where + ::Type: DynResolveFrom, + C::Form: IsDynCompatibleForm, + { + let Spanned(value, span_range) = self; + let content = value.into_content(); + let resolved = + <::Type>::resolve::(content, span_range, resolution_target)?; + Ok(resolved) + } +} + +// TODO[concepts]: Remove eventually, along with IntoValue impl +impl> IntoAnyValue for X +where + X::Type: UpcastTo, +{ + fn into_any_value(self) -> AnyValue { + self.into_any() + } +} + +/// Implementations on the value contents *themselves*. +/// Typically this is reserved for things operating on references - otherwise we can +/// implement on IntoValueContent / FromValueContent. +pub(crate) trait IsSelfValueContent<'a>: IsValueContent +where + Self::Type: IsHierarchicalType = Self>, + Self::Form: IsHierarchicalForm, +{ + fn as_mut_value<'r>(&'r mut self) -> Content<'r, Self::Type, BeMut> + where + 'a: 'r, + Self::Form: LeafAsMutForm, + { + map_via_leaf! { + input: &'r mut (Content<'a, Self::Type, Self::Form>) = self, + fn map_leaf(leaf) -> (Content<'r, T, BeMut>) { + F::leaf_as_mut(leaf) + } + } + } + + fn as_ref_value<'r>(&'r self) -> Content<'r, Self::Type, BeRef> + where + 'a: 'r, + Self::Form: LeafAsRefForm, + { + map_via_leaf! { + input: &'r (Content<'a, Self::Type, Self::Form>) = self, + fn map_leaf(leaf) -> (Content<'r, T, BeRef>) { + F::leaf_as_ref(leaf) + } + } + } + + /// This method should only be used when you are certain that the value should be cloned. + /// In most situations, you may wish to use [IsSelfValueContent::clone_to_owned_transparently] + /// instead. + fn clone_to_owned_infallible<'r>(&'r self) -> Content<'static, Self::Type, BeOwned> + where + 'a: 'r, + Self::Form: LeafAsRefForm, + Self: Sized, + { + map_via_leaf! { + input: &'r (Content<'a, Self::Type, Self::Form>) = self, + fn map_leaf(leaf) -> (Content<'static, T, BeOwned>) { + F::leaf_clone_to_owned_infallible(leaf) + } + } + } + + /// A transparent clone is allowed for some types when doing method resolution. + /// * For these types, a &a can be transparently cloned into an owned a. + /// * For other types, an error is raised suggesting to use .clone() explicitly. + /// + /// See [TypeKind::supports_transparent_cloning] for more details. + fn clone_to_owned_transparently<'r>( + &'r self, + span_range: SpanRange, + ) -> ExecutionResult> + where + 'a: 'r, + Self::Form: LeafAsRefForm, + Self: Sized, + { + map_via_leaf! { + input: &'r (Content<'a, Self::Type, Self::Form>) = self, + state: SpanRange | let span_range = span_range, + fn map_leaf(leaf) -> (ExecutionResult>) { + F::leaf_clone_to_owned_transparently(leaf, span_range) + } + } + } +} + +impl<'a, X> IsSelfValueContent<'a> for X +where + X: IsValueContent, + X::Type: IsHierarchicalType = X>, + X::Form: IsHierarchicalForm, +{ +} + +impl< + X: FromValueContent<'static, Type = T, Form = F>, + F: MapFromArgument, + T: DowncastFrom, + > IsArgument for X +{ + type ValueType = T; + const OWNERSHIP: ArgumentOwnership = F::ARGUMENT_OWNERSHIP; + fn from_argument(Spanned(value, span_range): Spanned) -> ExecutionResult { + let ownership_mapped = F::from_argument_value(value)?; + let type_mapped = T::resolve(ownership_mapped, span_range, "This argument")?; + Ok(X::from_content(type_mapped)) + } +} + +impl< + X: IntoValueContent<'static, Type = T, Form = BeOwned>, + // TODO[concepts]: Migrate to BeOwned => F when it doesn't break MSRV + // due to clashes with `IntoValueContent` on Shared / Mutable + // F: MapIntoReturned, + T: UpcastTo, + > IsReturnable for X +{ + fn to_returned_value(self) -> ExecutionResult { + let type_mapped = self.into_content().upcast::(); + BeOwned::into_returned_value(type_mapped) + } +} diff --git a/src/expressions/concepts/form.rs b/src/expressions/concepts/form.rs new file mode 100644 index 00000000..f4b8e4fb --- /dev/null +++ b/src/expressions/concepts/form.rs @@ -0,0 +1,77 @@ +use super::*; + +/// A type representing a particular form of a value's ownership. +/// +/// Examples include: +/// - [BeOwned] representing [Owned] (floating) value +/// - [BeShared] representing [Shared] references +/// - [BeMutable] representing [Mutable] references +/// - [BeCopyOnWrite] representing [CopyOnWrite] values +/// +/// NB: The Sized + Clone bounds are just to make certain derive impls easier, +/// due to e.g. the poor auto-derive of Clone which requires Clone bounds on all +/// generics. +pub(crate) trait IsForm: Sized + Clone {} + +pub(crate) trait IsHierarchicalForm: IsForm { + /// The standard leaf for a hierachical type + type Leaf<'a, T: IsLeafType>: IsValueContent + + IntoValueContent<'a> + + FromValueContent<'a>; +} + +pub(crate) trait LeafAsRefForm: IsHierarchicalForm { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf; + + fn leaf_clone_to_owned_infallible<'r, 'a: 'r, T: IsLeafType>( + leaf: &'r Self::Leaf<'a, T>, + ) -> T::Leaf { + Self::leaf_as_ref(leaf).clone() + } + + fn leaf_clone_to_owned_transparently<'r, 'a: 'r, T: IsLeafType>( + leaf: &'r Self::Leaf<'a, T>, + error_span: SpanRange, + ) -> ExecutionResult { + let type_kind = T::type_kind(); + if type_kind.supports_transparent_cloning() { + Ok(Self::leaf_clone_to_owned_infallible(leaf)) + } else { + error_span.ownership_err(format!( + "An owned value is required, but a reference was received, and {} does not support transparent cloning. You may wish to use .clone() explicitly.", + type_kind.articled_display_name() + )) + } + } +} + +pub(crate) trait LeafAsMutForm: IsHierarchicalForm { + fn leaf_as_mut<'r, 'a: 'r, T: IsLeafType>(leaf: &'r mut Self::Leaf<'a, T>) -> &'r mut T::Leaf; +} + +pub(crate) trait IsDynCompatibleForm: IsHierarchicalForm { + /// The container for a dyn Trait based type. + /// The DynLeaf can be similar to the standard leaf, but must be + /// able to support an unsized D. + type DynLeaf<'a, D: 'static + ?Sized>; + + fn leaf_to_dyn<'a, T: IsLeafType, D: ?Sized + 'static>( + leaf: Self::Leaf<'a, T>, + ) -> Result, Content<'a, T, Self>> + where + T::Leaf: CastDyn; +} + +pub(crate) trait MapFromArgument: IsHierarchicalForm { + const ARGUMENT_OWNERSHIP: ArgumentOwnership; + + fn from_argument_value( + value: ArgumentValue, + ) -> ExecutionResult>; +} + +pub(crate) trait MapIntoReturned: IsHierarchicalForm { + fn into_returned_value( + value: Content<'static, AnyType, Self>, + ) -> ExecutionResult; +} diff --git a/src/expressions/concepts/forms/any_mut.rs b/src/expressions/concepts/forms/any_mut.rs new file mode 100644 index 00000000..5bbaf984 --- /dev/null +++ b/src/expressions/concepts/forms/any_mut.rs @@ -0,0 +1,66 @@ +use super::*; + +impl<'a, L: IsValueLeaf> IsValueContent for AnyMut<'a, L> { + type Type = L::Type; + type Form = BeAnyMut; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for AnyMut<'a, L> { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for AnyMut<'a, L> { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +#[derive(Copy, Clone)] +pub(crate) struct BeAnyMut; +impl IsForm for BeAnyMut {} +impl IsHierarchicalForm for BeAnyMut { + type Leaf<'a, T: IsLeafType> = AnyMut<'a, T::Leaf>; +} + +impl IsDynCompatibleForm for BeAnyMut { + type DynLeaf<'a, D: 'static + ?Sized> = AnyMut<'a, D>; + + fn leaf_to_dyn<'a, T: IsLeafType, D: ?Sized + 'static>( + leaf: Self::Leaf<'a, T>, + ) -> Result, Content<'a, T, Self>> + where + T::Leaf: CastDyn, + { + leaf.replace(|content, emplacer| match ::map_mut(content) { + Ok(mapped) => Ok(emplacer.emplace(mapped)), + Err(this) => Err(emplacer.emplace(this)), + }) + } +} + +impl LeafAsRefForm for BeAnyMut { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf { + leaf + } +} + +impl LeafAsMutForm for BeAnyMut { + fn leaf_as_mut<'r, 'a: 'r, T: IsLeafType>(leaf: &'r mut Self::Leaf<'a, T>) -> &'r mut T::Leaf { + leaf + } +} + +impl MapFromArgument for BeAnyMut { + const ARGUMENT_OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Mutable; + + fn from_argument_value( + value: ArgumentValue, + ) -> ExecutionResult> { + Ok(value + .expect_mutable() + .0 + .replace(|inner, emplacer| inner.as_mut_value().into_mutable_any_mut(emplacer))) + } +} diff --git a/src/expressions/concepts/forms/any_ref.rs b/src/expressions/concepts/forms/any_ref.rs new file mode 100644 index 00000000..3e9220cd --- /dev/null +++ b/src/expressions/concepts/forms/any_ref.rs @@ -0,0 +1,61 @@ +use super::*; + +impl<'a, L: IsValueLeaf> IsValueContent for AnyRef<'a, L> { + type Type = L::Type; + type Form = BeAnyRef; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for AnyRef<'a, L> { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for AnyRef<'a, L> { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +#[derive(Copy, Clone)] +pub(crate) struct BeAnyRef; +impl IsForm for BeAnyRef {} + +impl IsHierarchicalForm for BeAnyRef { + type Leaf<'a, T: IsLeafType> = crate::internal_prelude::AnyRef<'a, T::Leaf>; +} + +impl IsDynCompatibleForm for BeAnyRef { + type DynLeaf<'a, D: 'static + ?Sized> = crate::internal_prelude::AnyRef<'a, D>; + + fn leaf_to_dyn<'a, T: IsLeafType, D: ?Sized + 'static>( + leaf: Self::Leaf<'a, T>, + ) -> Result, Content<'a, T, Self>> + where + T::Leaf: CastDyn, + { + leaf.replace(|content, emplacer| match ::map_ref(content) { + Ok(mapped) => Ok(emplacer.emplace(mapped)), + Err(this) => Err(emplacer.emplace(this)), + }) + } +} + +impl LeafAsRefForm for BeAnyRef { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf { + leaf + } +} + +impl MapFromArgument for BeAnyRef { + const ARGUMENT_OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Shared; + + fn from_argument_value( + value: ArgumentValue, + ) -> ExecutionResult> { + Ok(value + .expect_shared() + .0 + .replace(|inner, emplacer| inner.as_ref_value().into_shared_any_ref(emplacer))) + } +} diff --git a/src/expressions/concepts/forms/argument.rs b/src/expressions/concepts/forms/argument.rs new file mode 100644 index 00000000..a2ac0e4d --- /dev/null +++ b/src/expressions/concepts/forms/argument.rs @@ -0,0 +1,44 @@ +use super::*; + +pub(crate) enum Argument { + Owned(L), + CopyOnWrite(QqqCopyOnWrite), + Mutable(QqqMutable), + Assignee(QqqAssignee), + Shared(QqqShared), +} + +impl IsValueContent for Argument { + type Type = L::Type; + type Form = BeArgument; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for Argument { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for Argument { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +#[derive(Copy, Clone)] +pub(crate) struct BeArgument; +impl IsForm for BeArgument {} + +impl IsHierarchicalForm for BeArgument { + type Leaf<'a, T: IsLeafType> = Argument; +} + +impl MapFromArgument for BeArgument { + const ARGUMENT_OWNERSHIP: ArgumentOwnership = ArgumentOwnership::AsIs; + + fn from_argument_value( + value: ArgumentValue, + ) -> ExecutionResult> { + todo!("Argument") + } +} diff --git a/src/expressions/concepts/forms/assignee.rs b/src/expressions/concepts/forms/assignee.rs new file mode 100644 index 00000000..1a20eef6 --- /dev/null +++ b/src/expressions/concepts/forms/assignee.rs @@ -0,0 +1,68 @@ +use super::*; + +pub(crate) struct QqqAssignee(pub(crate) MutableSubRcRefCell); + +impl IsValueContent for QqqAssignee { + type Type = L::Type; + type Form = BeAssignee; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for QqqAssignee { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for QqqAssignee { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +#[derive(Copy, Clone)] +pub(crate) struct BeAssignee; +impl IsForm for BeAssignee {} + +impl IsHierarchicalForm for BeAssignee { + type Leaf<'a, T: IsLeafType> = QqqAssignee; +} + +impl IsDynCompatibleForm for BeAssignee { + type DynLeaf<'a, D: 'static + ?Sized> = QqqAssignee; + + fn leaf_to_dyn<'a, T: IsLeafType, D: ?Sized + 'static>( + leaf: Self::Leaf<'a, T>, + ) -> Result, Content<'a, T, Self>> + where + T::Leaf: CastDyn, + { + leaf.0 + .replace(|content, emplacer| match ::map_mut(content) { + Ok(mapped) => Ok(QqqAssignee(emplacer.emplace(mapped))), + Err(this) => Err(QqqAssignee(emplacer.emplace(this))), + }) + } +} + +impl LeafAsRefForm for BeAssignee { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf { + &leaf.0 + } +} + +impl LeafAsMutForm for BeAssignee { + fn leaf_as_mut<'r, 'a: 'r, T: IsLeafType>(leaf: &'r mut Self::Leaf<'a, T>) -> &'r mut T::Leaf { + &mut leaf.0 + } +} + +impl MapFromArgument for BeAssignee { + const ARGUMENT_OWNERSHIP: ArgumentOwnership = + ArgumentOwnership::Assignee { auto_create: false }; + + fn from_argument_value( + value: ArgumentValue, + ) -> ExecutionResult> { + Ok(value.expect_assignee().into_content()) + } +} diff --git a/src/expressions/concepts/forms/copy_on_write.rs b/src/expressions/concepts/forms/copy_on_write.rs new file mode 100644 index 00000000..2e6ea9a0 --- /dev/null +++ b/src/expressions/concepts/forms/copy_on_write.rs @@ -0,0 +1,318 @@ +use super::*; + +pub(crate) enum QqqCopyOnWrite { + /// An owned value that can be used directly + Owned(Owned), + /// For use when the CopyOnWrite value effectively represents the owned value (post-clone). + /// In this case, returning a Cow is just an optimization and we can always clone infallibly. + SharedWithInfallibleCloning(QqqShared), + /// For use when the CopyOnWrite value represents a pre-cloned read-only value. + /// A transparent clone may fail in this case at use time. + SharedWithTransparentCloning(QqqShared), +} + +impl IsValueContent for QqqCopyOnWrite { + type Type = L::Type; + type Form = BeCopyOnWrite; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for QqqCopyOnWrite { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for QqqCopyOnWrite { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +#[derive(Copy, Clone)] +pub(crate) struct BeCopyOnWrite; +impl IsForm for BeCopyOnWrite {} + +impl IsHierarchicalForm for BeCopyOnWrite { + type Leaf<'a, T: IsLeafType> = QqqCopyOnWrite; +} + +impl BeCopyOnWrite { + pub(crate) fn new_owned<'a, C: IntoValueContent<'a, Form = BeOwned>>( + owned: C, + ) -> Content<'a, C::Type, BeCopyOnWrite> { + map_via_leaf! { + input: (Content<'a, C::Type, BeOwned>) = owned.into_content(), + fn map_leaf(leaf) -> (Content<'a, T, BeCopyOnWrite>) { + QqqCopyOnWrite::Owned(leaf) + } + } + } + + pub(crate) fn new_shared_in_place_of_owned<'a, C: IntoValueContent<'a, Form = BeShared>>( + shared: C, + ) -> Content<'a, C::Type, BeCopyOnWrite> { + map_via_leaf! { + input: (Content<'a, C::Type, BeShared>) = shared.into_content(), + fn map_leaf(leaf) -> (Content<'a, T, BeCopyOnWrite>) { + QqqCopyOnWrite::SharedWithInfallibleCloning(leaf) + } + } + } + + pub(crate) fn new_shared_in_place_of_shared<'a, C: IntoValueContent<'a, Form = BeShared>>( + shared: C, + ) -> Content<'a, C::Type, BeCopyOnWrite> { + map_via_leaf! { + input: (Content<'a, C::Type, BeShared>) = shared.into_content(), + fn map_leaf(leaf) -> (Content<'a, T, BeCopyOnWrite>) { + QqqCopyOnWrite::SharedWithTransparentCloning(leaf) + } + } + } +} + +impl MapFromArgument for BeCopyOnWrite { + const ARGUMENT_OWNERSHIP: ArgumentOwnership = ArgumentOwnership::CopyOnWrite; + + fn from_argument_value( + value: ArgumentValue, + ) -> ExecutionResult> { + match value.expect_copy_on_write().inner { + CopyOnWriteInner::Owned(owned) => Ok(BeCopyOnWrite::new_owned(owned)), + CopyOnWriteInner::SharedWithInfallibleCloning(shared) => { + Ok(BeCopyOnWrite::new_shared_in_place_of_owned(shared)) + } + CopyOnWriteInner::SharedWithTransparentCloning(shared) => { + Ok(BeCopyOnWrite::new_shared_in_place_of_shared(shared)) + } + } + } +} + +impl LeafAsRefForm for BeCopyOnWrite { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf { + match leaf { + QqqCopyOnWrite::Owned(owned) => owned, + QqqCopyOnWrite::SharedWithInfallibleCloning(shared) => shared, + QqqCopyOnWrite::SharedWithTransparentCloning(shared) => shared, + } + } + + fn leaf_clone_to_owned_infallible<'r, 'a: 'r, T: IsLeafType>( + leaf: &'r Self::Leaf<'a, T>, + ) -> T::Leaf { + match leaf { + QqqCopyOnWrite::Owned(owned) => owned.clone(), + QqqCopyOnWrite::SharedWithInfallibleCloning(shared) => { + BeShared::leaf_clone_to_owned_infallible::(shared) + } + QqqCopyOnWrite::SharedWithTransparentCloning(shared) => { + BeShared::leaf_clone_to_owned_infallible::(shared) + } + } + } + + fn leaf_clone_to_owned_transparently<'r, 'a: 'r, T: IsLeafType>( + leaf: &'r Self::Leaf<'a, T>, + error_span: SpanRange, + ) -> ExecutionResult { + match leaf { + QqqCopyOnWrite::Owned(owned) => Ok(owned.clone()), + QqqCopyOnWrite::SharedWithInfallibleCloning(shared) => { + Ok(BeShared::leaf_clone_to_owned_infallible::(shared)) + } + QqqCopyOnWrite::SharedWithTransparentCloning(shared) => { + BeShared::leaf_clone_to_owned_transparently::(shared, error_span) + } + } + } +} + +pub(crate) trait IsSelfCopyOnWriteContent<'a>: IsSelfValueContent<'a> +where + Self: IsValueContent, + Self::Type: IsHierarchicalType = Self>, +{ + fn into_any_level_copy_on_write(self) -> AnyLevelCopyOnWrite<'a, Self::Type> + where + Self: Sized, + { + map_via_leaf! { + input: (Content<'a, Self::Type, Self::Form>) = self, + fn map_leaf(leaf) -> (AnyLevelCopyOnWrite<'a, T>) { + match leaf { + QqqCopyOnWrite::Owned(owned) => AnyLevelCopyOnWrite::Owned(owned), + QqqCopyOnWrite::SharedWithInfallibleCloning(shared) => AnyLevelCopyOnWrite::SharedWithInfallibleCloning(shared), + QqqCopyOnWrite::SharedWithTransparentCloning(shared) => AnyLevelCopyOnWrite::SharedWithTransparentCloning(shared), + } + } + } + } + + fn into_owned_infallible(self) -> Content<'static, Self::Type, BeOwned> + where + Self: Sized, + { + map_via_leaf! { + input: (Content<'a, Self::Type, Self::Form>) = self, + fn map_leaf(leaf) -> (Content<'static, T, BeOwned>) { + match leaf { + QqqCopyOnWrite::Owned(owned) => owned, + QqqCopyOnWrite::SharedWithInfallibleCloning(shared) => BeShared::leaf_clone_to_owned_infallible::(&shared), + QqqCopyOnWrite::SharedWithTransparentCloning(shared) => BeShared::leaf_clone_to_owned_infallible::(&shared), + } + } + } + } + + fn into_owned_transparently( + self, + error_span: SpanRange, + ) -> ExecutionResult> + where + Self: Sized, + { + map_via_leaf! { + input: (Content<'a, Self::Type, Self::Form>) = self, + state: SpanRange | let error_span = error_span, + fn map_leaf(leaf) -> (ExecutionResult>) { + Ok(match leaf { + QqqCopyOnWrite::Owned(owned) => owned, + QqqCopyOnWrite::SharedWithInfallibleCloning(shared) => BeShared::leaf_clone_to_owned_infallible::(&shared), + QqqCopyOnWrite::SharedWithTransparentCloning(shared) => BeShared::leaf_clone_to_owned_transparently::(&shared, error_span)?, + }) + } + } + } + + fn acts_as_shared_reference(&self) -> bool { + map_via_leaf! { + input: &'r (Content<'a, Self::Type, Self::Form>) = self, + fn map_leaf(leaf) -> (bool) { + match leaf { + QqqCopyOnWrite::Owned(_) => false, + QqqCopyOnWrite::SharedWithInfallibleCloning(_) => false, + QqqCopyOnWrite::SharedWithTransparentCloning(_) => true, + } + } + } + } + + // TODO - Find alternative implementation or replace + fn map(self, mapper: M) -> ExecutionResult> + where + 'a: 'static, + M: OwnedTypeMapper + RefTypeMapper, + M: TypeMapper, // u32 is temporary to see if we can make it work without adding generics to `map_via_leaf!` + Self: Sized, + Self: IsValueContent, // Temporary to see if we can make it work without adding generics to `map_via_leaf!` + { + Ok(match self.into_any_level_copy_on_write() { + AnyLevelCopyOnWrite::Owned(owned) => BeCopyOnWrite::new_owned(mapper.map_owned(owned)?), + AnyLevelCopyOnWrite::SharedWithInfallibleCloning(shared) => { + // // Apply map, get Shared> + // let shared = map_via_leaf! { + // input: (Content<'a, Self::Type, BeShared>) = shared, + // fn map_leaf(leaf) -> (ExecutionResult>>) { + // leaf.try_map(|value_ref| { + // let from_ref = value_ref.into_any(); + // inner_map_ref(from_ref) // &Content + + // // If inner_map_ref returned a Content<'a, M::TTo, BeRef> + // // then we'd need to move the leaf map inside here, but it'd work the same + // }) + // } + // }?; + // // Migrate Shared into leaf + // let shared_content = shared.replace(|content, emplacer| { + // // TODO - use two lifetimes here + // // ---- + // map_via_leaf! { + // input: &'r (Content<'a, AnyType, BeOwned>) = content, + // state: | <'e> SharedEmplacer<'e, AnyValue, AnyValue> | let emplacer = emplacer, + // fn map_leaf(leaf) -> (Content<'static, T, BeShared>) { + // emplacer.emplace(leaf) + // } + // } + // }); + // BeCopyOnWrite::new_shared_in_place_of_owned::(shared_content) + todo!() + } + AnyLevelCopyOnWrite::SharedWithTransparentCloning(shared) => { + todo!() + } + }) + } +} + +fn inner_map_ref<'a>( + ref_value: Content<'a, AnyType, BeRef>, +) -> ExecutionResult<&'a Content<'static, AnyType, BeOwned>> { + unimplemented!() +} + +/// Using the leaf-based type [`Content<'a, T, BeCopyOnWrite>`] is usually preferred, +/// but in some instances (particularly around supporting old code), we may want the +/// partitioning to be at a higher level (e.g. an `Owned(OwnedValue)` or `Shared(SharedValue)`). +/// +/// That is what this type represents. +pub(crate) enum AnyLevelCopyOnWrite<'a, T: IsHierarchicalType> { + Owned(Content<'a, T, BeOwned>), + SharedWithInfallibleCloning(Content<'a, T, BeShared>), + SharedWithTransparentCloning(Content<'a, T, BeShared>), +} + +impl<'a, T: IsHierarchicalType> AnyLevelCopyOnWrite<'a, T> { + pub fn into_copy_on_write(self) -> Content<'a, T, BeCopyOnWrite> { + match self { + AnyLevelCopyOnWrite::Owned(owned) => BeCopyOnWrite::new_owned(owned), + AnyLevelCopyOnWrite::SharedWithInfallibleCloning(shared) => { + BeCopyOnWrite::new_shared_in_place_of_owned(shared) + } + AnyLevelCopyOnWrite::SharedWithTransparentCloning(shared) => { + BeCopyOnWrite::new_shared_in_place_of_shared(shared) + } + } + } +} + +// TODO[concepts]: COPY ON WRITE MAPPING +// How map can work: +// - Create AnyLevelCopyOnWrite<'a, T>: +// * AnyLevelCopyOnWrite::<'a, T>::Owned(Content<'a, T, BeOwned>) +// * AnyLevelCopyOnWrite::<'a, T>::SharedAsX(Content<'a, T, BeShared>) +// - Use a leaf mapper to convert Content<'a, TFrom, BeCopyOnWrite> to AnyLevelCopyOnWrite::<'a, TFrom> +// - Then map the inner Content via TFrom::map_to::() +// - Then convert AnyLevelCopyOnWrite<'a, TTo> back to Content<'a, TTo, BeCopyOnWrite> via leaf mapping `Owned` / `Shared` *to* copy on write (see e.g. as_referencable) +// - Create an AnyValuePropertyAccessor and an AnyValueIndexer +pub(crate) trait TypeMapper { + type TFrom: IsHierarchicalType; + type TTo: IsHierarchicalType; +} +pub(crate) trait OwnedTypeMapper: TypeMapper { + fn map_owned<'a>( + self, + owned_value: Content<'a, Self::TFrom, BeOwned>, + ) -> ExecutionResult>; +} + +pub(crate) trait RefTypeMapper: TypeMapper { + fn map_ref<'a>( + self, + ref_value: Content<'a, Self::TFrom, BeRef>, + ) -> ExecutionResult<&'a Content<'a, Self::TTo, BeOwned>>; +} + +pub(crate) trait MutTypeMapper: TypeMapper { + fn map_mut<'a>( + self, + mut_value: Content<'a, Self::TFrom, BeMut>, + ) -> ExecutionResult<&'a mut Content<'a, Self::TTo, BeOwned>>; +} + +impl<'a, C: IsSelfValueContent<'a>> IsSelfCopyOnWriteContent<'a> for C +where + Self: IsValueContent, + Self::Type: IsHierarchicalType = Self>, +{ +} diff --git a/src/expressions/concepts/forms/late_bound.rs b/src/expressions/concepts/forms/late_bound.rs new file mode 100644 index 00000000..0395e1ca --- /dev/null +++ b/src/expressions/concepts/forms/late_bound.rs @@ -0,0 +1,58 @@ +use super::*; + +pub(crate) enum QqqLateBound { + /// An owned value that can be converted to any ownership type + Owned(QqqLateBoundOwned), + /// A copy-on-write value that can be converted to an owned value + CopyOnWrite(QqqCopyOnWrite), + /// A mutable reference + Mutable(QqqMutable), + /// A shared reference where mutable access failed for a specific reason + Shared(QqqLateBoundShared), +} + +impl IsValueContent for QqqLateBound { + type Type = L::Type; + type Form = BeLateBound; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for QqqLateBound { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for QqqLateBound { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +/// Universal value type that can resolve to any concrete ownership type. +/// +/// Sometimes, a value can be accessed, but we don't yet know *how* we need to access it. +/// In this case, we attempt to load it with the most powerful access we can have, and convert it later. +/// +/// ## Example of requirement +/// For example, if we have `x[a].method()`, we first need to resolve the type of `x[a]` to know +/// whether the method needs `x[a]` to be a shared reference, mutable reference or an owned value. +/// +/// So instead, we take the most powerful access we can have for `x[a]`, and convert it later. +#[derive(Copy, Clone)] +pub(crate) struct BeLateBound; +impl IsForm for BeLateBound {} + +impl IsHierarchicalForm for BeLateBound { + type Leaf<'a, T: IsLeafType> = QqqLateBound; +} + +pub(crate) struct QqqLateBoundOwned { + pub(crate) owned: O, + pub(crate) is_from_last_use: bool, +} + +/// A shared value where mutable access failed for a specific reason +pub(crate) struct QqqLateBoundShared { + pub(crate) shared: QqqShared, + pub(crate) reason_not_mutable: syn::Error, +} diff --git a/src/expressions/concepts/forms/mod.rs b/src/expressions/concepts/forms/mod.rs new file mode 100644 index 00000000..3f42e697 --- /dev/null +++ b/src/expressions/concepts/forms/mod.rs @@ -0,0 +1,28 @@ +#![allow(unused)] // Whilst we're building it out +use super::*; + +mod any_mut; +mod any_ref; +mod argument; +mod assignee; +mod copy_on_write; +mod late_bound; +mod mutable; +mod owned; +mod referenceable; +mod shared; +mod simple_mut; +mod simple_ref; + +pub(crate) use any_mut::*; +pub(crate) use any_ref::*; +pub(crate) use argument::*; +pub(crate) use assignee::*; +pub(crate) use copy_on_write::*; +pub(crate) use late_bound::*; +pub(crate) use mutable::*; +pub(crate) use owned::*; +pub(crate) use referenceable::*; +pub(crate) use shared::*; +pub(crate) use simple_mut::*; +pub(crate) use simple_ref::*; diff --git a/src/expressions/concepts/forms/mutable.rs b/src/expressions/concepts/forms/mutable.rs new file mode 100644 index 00000000..b17e1c43 --- /dev/null +++ b/src/expressions/concepts/forms/mutable.rs @@ -0,0 +1,74 @@ +use super::*; + +pub(crate) type QqqMutable = MutableSubRcRefCell; + +impl IsValueContent for QqqMutable { + type Type = L::Type; + type Form = BeMutable; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for QqqMutable { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for QqqMutable { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +#[derive(Copy, Clone)] +pub(crate) struct BeMutable; +impl IsForm for BeMutable {} + +impl IsHierarchicalForm for BeMutable { + type Leaf<'a, T: IsLeafType> = QqqMutable; +} + +impl IsDynCompatibleForm for BeMutable { + type DynLeaf<'a, D: 'static + ?Sized> = QqqMutable; + + fn leaf_to_dyn<'a, T: IsLeafType, D: ?Sized + 'static>( + leaf: Self::Leaf<'a, T>, + ) -> Result, Content<'a, T, Self>> + where + T::Leaf: CastDyn, + { + leaf.replace(|content, emplacer| match ::map_mut(content) { + Ok(mapped) => Ok(emplacer.emplace(mapped)), + Err(this) => Err(emplacer.emplace(this)), + }) + } +} + +impl LeafAsRefForm for BeMutable { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf { + leaf + } +} + +impl LeafAsMutForm for BeMutable { + fn leaf_as_mut<'r, 'a: 'r, T: IsLeafType>(leaf: &'r mut Self::Leaf<'a, T>) -> &'r mut T::Leaf { + leaf + } +} + +impl MapFromArgument for BeMutable { + const ARGUMENT_OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Mutable; + + fn from_argument_value( + value: ArgumentValue, + ) -> ExecutionResult> { + Ok(value.expect_mutable().into_content()) + } +} + +// impl MapIntoReturned for BeMutable { +// fn into_returned_value( +// content: Content<'static, AnyType, Self>, +// ) -> ExecutionResult { +// todo!("Return mutable") +// } +// } diff --git a/src/expressions/concepts/forms/owned.rs b/src/expressions/concepts/forms/owned.rs new file mode 100644 index 00000000..5095ac42 --- /dev/null +++ b/src/expressions/concepts/forms/owned.rs @@ -0,0 +1,93 @@ +use super::*; + +/// Just [`T`]! This exists simply to be a name for symmetry with e.g. Shared or Mutable. +pub(crate) type Owned = T; + +// NOTE: Attempting to blanket implement IsValueContent etc for all T: IsValueLeaf causes +// a clash (inlike the other forms) - so instead we work around this with (a) manual impls +// in impl_value_content_traits for each L, and (b) bounds on L: IsValueLeaf. + +/// Represents floating owned values. +/// +/// If you need span information, wrap with `Spanned`. For example, with `x.y[4]`, this would capture both: +/// * The output owned value +/// * The lexical span of the tokens `x.y[4]` +#[derive(Copy, Clone)] +pub(crate) struct BeOwned; +impl IsForm for BeOwned {} + +impl IsHierarchicalForm for BeOwned { + type Leaf<'a, T: IsLeafType> = T::Leaf; +} + +impl IsDynCompatibleForm for BeOwned { + type DynLeaf<'a, D: 'static + ?Sized> = Box; + + fn leaf_to_dyn<'a, T: IsLeafType, D: ?Sized + 'static>( + leaf: Self::Leaf<'a, T>, + ) -> Result, Content<'a, T, Self>> + where + T::Leaf: CastDyn, + { + ::map_boxed(Box::new(leaf)).map_err(|boxed| *boxed) + } +} + +impl LeafAsRefForm for BeOwned { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf { + leaf + } +} + +impl LeafAsMutForm for BeOwned { + fn leaf_as_mut<'r, 'a: 'r, T: IsLeafType>(leaf: &'r mut Self::Leaf<'a, T>) -> &'r mut T::Leaf { + leaf + } +} + +impl MapFromArgument for BeOwned { + const ARGUMENT_OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Owned; + + fn from_argument_value( + value: ArgumentValue, + ) -> ExecutionResult> { + Ok(value.expect_owned()) + } +} + +impl MapIntoReturned for BeOwned { + fn into_returned_value( + content: Content<'static, AnyType, Self>, + ) -> ExecutionResult { + Ok(ReturnedValue::Owned(content)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn can_resolve_owned() { + let owned_value: Owned<_> = 42u64; + let resolved = owned_value + .spanned(Span::call_site().span_range()) + .downcast_resolve::("My value") + .unwrap(); + assert_eq!(resolved, 42u64); + } + + #[test] + fn can_as_ref_owned() { + let owned_value = 42u64; + let as_ref: Content = owned_value.as_ref_value(); + assert_eq!(*as_ref, 42u64); + } + + #[test] + fn can_as_mut_owned() { + let mut owned_value = 42u64; + *owned_value.as_mut_value() = 41u64; + assert_eq!(owned_value, 41u64); + } +} diff --git a/src/expressions/concepts/forms/referenceable.rs b/src/expressions/concepts/forms/referenceable.rs new file mode 100644 index 00000000..47b7b622 --- /dev/null +++ b/src/expressions/concepts/forms/referenceable.rs @@ -0,0 +1,33 @@ +use super::*; + +pub(crate) type Referenceable = Rc>; + +impl IsValueContent for Referenceable { + type Type = L::Type; + type Form = BeReferenceable; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for Referenceable { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for Referenceable { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +/// Roughly equivalent to an owned, but wrapped so that it can be turned into a Shared/Mutable easily. +/// This is useful for the content of variables. +/// +/// Note that Referenceable form does not support dyn casting, because Rc> cannot be +/// directly cast to Rc>. +#[derive(Copy, Clone)] +pub(crate) struct BeReferenceable; +impl IsForm for BeReferenceable {} + +impl IsHierarchicalForm for BeReferenceable { + type Leaf<'a, T: IsLeafType> = Referenceable; +} diff --git a/src/expressions/concepts/forms/shared.rs b/src/expressions/concepts/forms/shared.rs new file mode 100644 index 00000000..106cf736 --- /dev/null +++ b/src/expressions/concepts/forms/shared.rs @@ -0,0 +1,68 @@ +use super::*; + +pub(crate) type QqqShared = SharedSubRcRefCell; + +impl IsValueContent for QqqShared { + type Type = L::Type; + type Form = BeShared; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for QqqShared { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for QqqShared { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +#[derive(Copy, Clone)] +pub(crate) struct BeShared; +impl IsForm for BeShared {} + +impl IsHierarchicalForm for BeShared { + type Leaf<'a, T: IsLeafType> = QqqShared; +} + +impl IsDynCompatibleForm for BeShared { + type DynLeaf<'a, D: 'static + ?Sized> = QqqShared; + + fn leaf_to_dyn<'a, T: IsLeafType, D: ?Sized + 'static>( + leaf: Self::Leaf<'a, T>, + ) -> Result, Content<'a, T, Self>> + where + T::Leaf: CastDyn, + { + leaf.replace(|content, emplacer| match ::map_ref(content) { + Ok(mapped) => Ok(emplacer.emplace(mapped)), + Err(this) => Err(emplacer.emplace(this)), + }) + } +} + +impl LeafAsRefForm for BeShared { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf { + leaf + } +} + +impl MapFromArgument for BeShared { + const ARGUMENT_OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Shared; + + fn from_argument_value( + value: ArgumentValue, + ) -> ExecutionResult> { + Ok(value.expect_shared().into_content()) + } +} + +// impl MapIntoReturned for BeShared { +// fn into_returned_value( +// content: Content<'static, AnyType, Self>, +// ) -> ExecutionResult { +// todo!("Return shared") +// } +// } diff --git a/src/expressions/concepts/forms/simple_mut.rs b/src/expressions/concepts/forms/simple_mut.rs new file mode 100644 index 00000000..fcd4bbe4 --- /dev/null +++ b/src/expressions/concepts/forms/simple_mut.rs @@ -0,0 +1,154 @@ +use super::*; + +impl IsValueContent for &mut L { + type Type = L::Type; + type Form = BeMut; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for &'a mut L { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for &'a mut L { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +/// It can't be an argument because arguments must be owned in some way; +/// so that the drop glue can work properly (because they may come from +/// e.g. a reference counted Shared handle) +#[derive(Copy, Clone)] +pub(crate) struct BeMut; +impl IsForm for BeMut {} + +impl IsHierarchicalForm for BeMut { + type Leaf<'a, T: IsLeafType> = &'a mut T::Leaf; +} + +impl IsDynCompatibleForm for BeMut { + type DynLeaf<'a, D: 'static + ?Sized> = &'a mut D; + + fn leaf_to_dyn<'a, T: IsLeafType, D: ?Sized + 'static>( + leaf: Self::Leaf<'a, T>, + ) -> Result, Content<'a, T, Self>> + where + T::Leaf: CastDyn, + { + ::map_mut(leaf) + } +} + +impl LeafAsRefForm for BeMut { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf { + leaf + } +} + +pub(crate) trait IsSelfMutContent<'a>: IsSelfValueContent<'a> +where + Self: IsValueContent, + Self::Type: IsHierarchicalType = Self>, +{ + fn into_mutable<'b, T: 'static>( + self, + emplacer: &'b mut MutableEmplacer<'a, T>, + ) -> Content<'static, Self::Type, BeMutable> + where + Self: Sized, + { + struct __InlineMapper<'b, 'e2, X: 'static> { + emplacer: &'b mut MutableEmplacer<'e2, X>, + } + impl<'b, 'e2, X> LeafMapper for __InlineMapper<'b, 'e2, X> { + type Output<'a, T: IsHierarchicalType> = Content<'static, T, BeMutable>; + + fn to_parent_output<'a, T: IsChildType>( + output: Self::Output<'a, T>, + ) -> Self::Output<'a, T::ParentType> { + T::into_parent(output) + } + + fn map_leaf<'l, T: IsLeafType>( + self, + leaf: ::Leaf<'l, T>, + ) -> Self::Output<'l, T> { + // SAFETY: 'l = 'a = 'e so this is valid + unsafe { self.emplacer.emplace_unchecked(leaf) } + } + }; + let __mapper = __InlineMapper { emplacer }; + ::map_with::(__mapper, self) + } + + fn into_assignee<'b, T: 'static>( + self, + emplacer: &'b mut MutableEmplacer<'a, T>, + ) -> Content<'static, Self::Type, BeAssignee> + where + Self: Sized, + { + struct __InlineMapper<'b, 'e2, X: 'static> { + emplacer: &'b mut MutableEmplacer<'e2, X>, + } + impl<'b, 'e2, X> LeafMapper for __InlineMapper<'b, 'e2, X> { + type Output<'a, T: IsHierarchicalType> = Content<'static, T, BeAssignee>; + + fn to_parent_output<'a, T: IsChildType>( + output: Self::Output<'a, T>, + ) -> Self::Output<'a, T::ParentType> { + T::into_parent(output) + } + + fn map_leaf<'l, T: IsLeafType>( + self, + leaf: ::Leaf<'l, T>, + ) -> Self::Output<'l, T> { + // SAFETY: 'l = 'a = 'e so this is valid + unsafe { QqqAssignee(self.emplacer.emplace_unchecked(leaf)) } + } + }; + let __mapper = __InlineMapper { emplacer }; + ::map_with::(__mapper, self) + } + + fn into_mutable_any_mut<'b, T: 'static>( + self, + emplacer: &'b mut MutableEmplacer<'a, T>, + ) -> Content<'static, Self::Type, BeAnyMut> + where + Self: Sized, + { + struct __InlineMapper<'b, 'e2, X: 'static> { + emplacer: &'b mut MutableEmplacer<'e2, X>, + } + impl<'b, 'e2, X> LeafMapper for __InlineMapper<'b, 'e2, X> { + type Output<'a, T: IsHierarchicalType> = Content<'static, T, BeAnyMut>; + + fn to_parent_output<'a, T: IsChildType>( + output: Self::Output<'a, T>, + ) -> Self::Output<'a, T::ParentType> { + T::into_parent(output) + } + + fn map_leaf<'l, T: IsLeafType>( + self, + leaf: ::Leaf<'l, T>, + ) -> Self::Output<'l, T> { + // SAFETY: 'l = 'a = 'e so this is valid + unsafe { Mutable(self.emplacer.emplace_unchecked(leaf)).into() } + } + }; + let __mapper = __InlineMapper { emplacer }; + ::map_with::(__mapper, self) + } +} + +impl<'a, C: IsSelfValueContent<'a>> IsSelfMutContent<'a> for C +where + Self: IsValueContent, + Self::Type: IsHierarchicalType = Self>, +{ +} diff --git a/src/expressions/concepts/forms/simple_ref.rs b/src/expressions/concepts/forms/simple_ref.rs new file mode 100644 index 00000000..d466b492 --- /dev/null +++ b/src/expressions/concepts/forms/simple_ref.rs @@ -0,0 +1,123 @@ +use super::*; + +impl IsValueContent for &L { + type Type = L::Type; + type Form = BeRef; +} + +impl<'a, L: IsValueLeaf> IntoValueContent<'a> for &'a L { + fn into_content(self) -> Content<'a, Self::Type, Self::Form> { + self + } +} + +impl<'a, L: IsValueLeaf> FromValueContent<'a> for &'a L { + fn from_content(content: Content<'a, Self::Type, Self::Form>) -> Self { + content + } +} + +/// It can't be an argument because arguments must be owned in some way; +/// so that the drop glue can work properly (because they may come from +/// e.g. a reference counted Shared handle) +#[derive(Copy, Clone)] +pub(crate) struct BeRef; +impl IsForm for BeRef {} + +impl IsHierarchicalForm for BeRef { + type Leaf<'a, T: IsLeafType> = &'a T::Leaf; +} + +impl IsDynCompatibleForm for BeRef { + type DynLeaf<'a, D: 'static + ?Sized> = &'a D; + + fn leaf_to_dyn<'a, T: IsLeafType, D: ?Sized + 'static>( + leaf: Self::Leaf<'a, T>, + ) -> Result, Content<'a, T, Self>> + where + T::Leaf: CastDyn, + { + ::map_ref(leaf) + } +} + +impl LeafAsRefForm for BeRef { + fn leaf_as_ref<'r, 'a: 'r, T: IsLeafType>(leaf: &'r Self::Leaf<'a, T>) -> &'r T::Leaf { + leaf + } +} + +pub(crate) trait IsSelfRefContent<'a>: IsSelfValueContent<'a> +where + Self: IsValueContent, + Self::Type: IsHierarchicalType = Self>, +{ + fn into_shared<'b, T: 'static>( + self, + emplacer: &'b mut SharedEmplacer<'a, T>, + ) -> Content<'static, Self::Type, BeShared> + where + Self: Sized, + { + struct __InlineMapper<'b, 'e2, X: 'static> { + emplacer: &'b mut SharedEmplacer<'e2, X>, + } + impl<'b, 'e2, X> LeafMapper for __InlineMapper<'b, 'e2, X> { + type Output<'a, T: IsHierarchicalType> = Content<'static, T, BeShared>; + + fn to_parent_output<'a, T: IsChildType>( + output: Self::Output<'a, T>, + ) -> Self::Output<'a, T::ParentType> { + T::into_parent(output) + } + + fn map_leaf<'l, T: IsLeafType>( + self, + leaf: ::Leaf<'l, T>, + ) -> Self::Output<'l, T> { + // SAFETY: 'l = 'a = 'e so this is valid + unsafe { self.emplacer.emplace_unchecked(leaf) } + } + }; + let __mapper = __InlineMapper { emplacer }; + ::map_with::(__mapper, self) + } + + fn into_shared_any_ref<'b, T: 'static>( + self, + emplacer: &'b mut SharedEmplacer<'a, T>, + ) -> Content<'static, Self::Type, BeAnyRef> + where + Self: Sized, + { + struct __InlineMapper<'b, 'e2, X: 'static> { + emplacer: &'b mut SharedEmplacer<'e2, X>, + } + impl<'b, 'e2, X> LeafMapper for __InlineMapper<'b, 'e2, X> { + type Output<'a, T: IsHierarchicalType> = Content<'static, T, BeAnyRef>; + + fn to_parent_output<'a, T: IsChildType>( + output: Self::Output<'a, T>, + ) -> Self::Output<'a, T::ParentType> { + T::into_parent(output) + } + + fn map_leaf<'l, T: IsLeafType>( + self, + leaf: ::Leaf<'l, T>, + ) -> Self::Output<'l, T> { + // SAFETY: 'l = 'a = 'e so this is valid + unsafe { Shared(self.emplacer.emplace_unchecked(leaf)).into() } + } + }; + let __mapper = __InlineMapper { emplacer }; + ::map_with::(__mapper, self) + } +} + +impl<'a, C: IsSelfValueContent<'a>> IsSelfRefContent<'a> for C +where + Self: IsValueContent, + Self::Type: IsHierarchicalType = Self>, +{ +} diff --git a/src/expressions/concepts/mapping.rs b/src/expressions/concepts/mapping.rs new file mode 100644 index 00000000..6b58a307 --- /dev/null +++ b/src/expressions/concepts/mapping.rs @@ -0,0 +1,188 @@ +use super::*; + +pub(crate) trait LeafMapper { + type Output<'a, T: IsHierarchicalType>; + + fn to_parent_output<'a, T: IsChildType>( + output: Self::Output<'a, T>, + ) -> Self::Output<'a, T::ParentType>; + + fn map_leaf<'a, T: IsLeafType>(self, leaf: F::Leaf<'a, T>) -> Self::Output<'a, T>; +} + +pub(crate) trait RefLeafMapper { + type Output<'r, 'a: 'r, T: IsHierarchicalType>; + + fn to_parent_output<'r, 'a: 'r, T: IsChildType>( + output: Self::Output<'r, 'a, T>, + ) -> Self::Output<'r, 'a, T::ParentType>; + + fn map_leaf<'r, 'a: 'r, T: IsLeafType>( + self, + leaf: &'r F::Leaf<'a, T>, + ) -> Self::Output<'r, 'a, T>; +} + +pub(crate) trait MutLeafMapper { + type Output<'r, 'a: 'r, T: IsHierarchicalType>; + + fn to_parent_output<'r, 'a: 'r, T: IsChildType>( + output: Self::Output<'r, 'a, T>, + ) -> Self::Output<'r, 'a, T::ParentType>; + + fn map_leaf<'r, 'a: 'r, T: IsLeafType>( + self, + leaf: &'r mut F::Leaf<'a, T>, + ) -> Self::Output<'r, 'a, T>; +} + +/// Macro for creating and immediately applying a leaf mapper inline. +/// +/// This macro creates an anonymous mapper struct, implements the appropriate mapper trait +/// (LeafMapper, RefLeafMapper, or MutLeafMapper) based on the leaf pattern, and applies +/// it to the input content. +/// +/// Specific output type forms are supported for parent propogation. +/// If you get a cryptic error in the to_parent method, you may need to add +/// an explicit implementation into [`__map_via_leaf_to_parent`] +/// +/// # Syntax +/// +/// ```ignore +/// map_via_leaf! { +/// input: [ | &'r | &'r mut] (Content<'a, InputType, InputForm>) = input_value, +/// state: StateType | let state_binding = state_init, // Optional +/// fn map_leaf | : | = ], T>(leaf) -> (OutputType) +/// [where <...>] // Optional +/// { +/// // mapper body +/// } +/// } +/// ``` +macro_rules! map_via_leaf { + ( + input: $(&$r:lifetime $($mut:ident)?)? (Content<$a:lifetime, $input_type:ty, $input_form:ty>) = $input_value:expr, + $(state: $(| <$($state_l:lifetime,)* $($state_t:ident = $state_t_ty:ty,)*>)? $state_type:ty | let $state_pat:pat = $state_init:expr,)? + fn map_leaf <$f:ident $(: $fb:ident $(+ $fbe:ident )*)? $(= $fixed_form:ty)?, $t:ident> ($leaf:ident) -> ($($output_type:tt)+) + $(where $($where_clause:tt)*)? + $body:block + ) => {{ + struct __InlineMapper $($(<$($state_l,)* $($state_t,)*>)?)? { + state: $crate::if_exists!{ {$($state_type)?} {$($state_type)?} {()} }, + } + + $crate::__map_via_leaf_trait_impl!{ + $(@fixed_form $fixed_form |)? impl<$f $(: $fb $(+ $fbe)*)?> @mutability $(&$r $($mut)?)? for __InlineMapper $($(<$($state_l,)* $($state_t,)*>)?)? $(where $($where_clause)*)? + { + type Output<$($r,)? $a $(: $r)?, $t: $crate::expressions::concepts::IsHierarchicalType> = $($output_type)+; + + fn to_parent_output<$($r,)? $a $(: $r)?, $t: $crate::expressions::concepts::IsChildType>( + output: Self::Output<$($r,)? $a, $t>, + ) -> Self::Output<$($r,)? $a, $t::ParentType> { + $crate::__map_via_leaf_to_parent!(output -> $($output_type)+) + } + + #[allow(unused_mut)] + fn map_leaf<$($r,)? $a $(: $r)?, $t: $crate::expressions::concepts::IsLeafType>( + self, + $leaf: $crate::__map_via_leaf_leaf_type!($(@fixed_form $fixed_form |)? $(&$r $($mut)?)? | $f::Leaf<$a, $t>), + ) -> Self::Output<$($r,)? 'a, T> { + $(let $state_pat = self.state;)? + $body + } + } + }; + + let __mapper = __InlineMapper { + state: $crate::if_exists!{ {$($state_type)?} {$($state_init)?} {()} }, + }; + $crate::__map_via_leaf_output!( + <$input_type, $input_form> @mutability $(&$r $($mut)?)? ( + __mapper, + $input_value + ) + ) + }}; +} + +#[doc(hidden)] +macro_rules! __map_via_leaf_trait_impl { + (impl<$f:ident $(: $fb:ident $(+ $fbe:ident )*)?> @mutability &$r:lifetime mut for $mapper:ident $(<$($state_l:lifetime,)* $($state_t:ident,)*>)? $(where $($where_clause:tt)*)? { $($body:tt)* } ) => { impl$(<$($state_l,)* $($state_t,)*>)? <$f $(: $fb $(+ $fbe)*)?> MutLeafMapper<$f> for $mapper $(<$($state_l,)* $($state_t,)*>)? $(where $($where_clause)*)? { $($body)* } }; + (impl<$f:ident $(: $fb:ident $(+ $fbe:ident )*)?> @mutability &$r:lifetime for $mapper:ident $(<$($state_l:lifetime,)* $($state_t:ident,)*>)? $(where $($where_clause:tt)*)? { $($body:tt)* } ) => { impl$(<$($state_l,)* $($state_t,)*>)? <$f $(: $fb $(+ $fbe)*)?> RefLeafMapper<$f> for $mapper $(<$($state_l,)* $($state_t,)*>)? $(where $($where_clause)*)? { $($body)* } }; + (impl<$f:ident $(: $fb:ident $(+ $fbe:ident )*)?> @mutability for $mapper:ident $(<$($state_l:lifetime,)* $($state_t:ident,)*>)? $(where $($where_clause:tt)*)? { $($body:tt)* } ) => { impl$(<$($state_l,)* $($state_t,)*>)? <$f $(: $fb $(+ $fbe)*)?> LeafMapper<$f> for $mapper $(<$($state_l,)* $($state_t,)*>)? $(where $($where_clause)*)? { $($body)* } }; + (@fixed_form $fixed_form:ty | impl<$f:ident> @mutability &$r:lifetime mut for $mapper:ident $(<$($state_l:lifetime,)* $($state_t:ident,)*>)? $(where $($where_clause:tt)*)? { $($body:tt)* } ) => { impl$(<$($state_l,)* $($state_t,)*>)? MutLeafMapper<$fixed_form> for $mapper $(<$($state_l,)* $($state_t,)*>)? $(where $($where_clause)*)? { $($body)* } }; + (@fixed_form $fixed_form:ty | impl<$f:ident> @mutability &$r:lifetime for $mapper:ident $(<$($state_l:lifetime,)* $($state_t:ident,)*>)? $(where $($where_clause:tt)*)? { $($body:tt)* } ) => { impl$(<$($state_l,)* $($state_t,)*>)? RefLeafMapper<$fixed_form> for $mapper $(<$($state_l,)* $($state_t,)*>)? $(where $($where_clause)*)? { $($body)* } }; + (@fixed_form $fixed_form:ty | impl<$f:ident> @mutability for $mapper:ident $(<$($state_l:lifetime,)* $($state_t:ident,)*>)? $(where $($where_clause:tt)*)? { $($body:tt)* } ) => { impl$(<$($state_l,)* $($state_t,)*>)? LeafMapper<$fixed_form> for $mapper $(<$($state_l,)* $($state_t,)*>)? $(where $($where_clause)*)? { $($body)* } }; +} + +#[doc(hidden)] +macro_rules! __map_via_leaf_output { + (<$input_type:ty, $input_form:ty> @mutability &$r:lifetime mut ($mapper:expr, $input:expr) ) => { + <$input_type>::map_mut_with::<$input_form, _>($mapper, $input) + }; + (<$input_type:ty, $input_form:ty> @mutability &$r:lifetime ($mapper:expr, $input:expr) ) => { + <$input_type>::map_ref_with::<$input_form, _>($mapper, $input) + }; + (<$input_type:ty, $input_form:ty> @mutability ($mapper:expr, $input:expr) ) => { + <$input_type>::map_with::<$input_form, _>($mapper, $input) + }; +} + +#[doc(hidden)] +macro_rules! __map_via_leaf_leaf_type { + (@fixed_form $fixed_form:ty | $(&$r:lifetime $($mut:ident)?)? | $f:ident::Leaf<$a:lifetime, $t:ident>) => { + $(&$r $($mut)?)? <$fixed_form as IsHierarchicalForm>::Leaf<$a, $t> + }; + ($(&$r:lifetime $($mut:ident)?)? | $f:ident::Leaf<$a:lifetime, $t:ident>) => { + $(&$r $($mut)?)? <$f>::Leaf<$a, $t> + }; +} + +#[doc(hidden)] +macro_rules! __map_via_leaf_to_parent { + ($output:ident -> Content<$lt:lifetime, $t:ident, $form:ty>) => { + $t::into_parent($output) + }; + ($output:ident -> ExecutionResult>) => { + Ok($t::into_parent($output?)) + }; + ($output:ident -> Result, $err:ty>) => { + Ok($t::into_parent($output?)) + }; + ($output:ident -> AnyLevelCopyOnWrite<$lt:lifetime, $t:ident>) => { + match $output { + AnyLevelCopyOnWrite::Owned(owned) => AnyLevelCopyOnWrite::Owned($t::into_parent(owned)), + AnyLevelCopyOnWrite::SharedWithInfallibleCloning(shared) => { + AnyLevelCopyOnWrite::SharedWithInfallibleCloning($t::into_parent(shared)) + } + AnyLevelCopyOnWrite::SharedWithTransparentCloning(shared) => { + AnyLevelCopyOnWrite::SharedWithTransparentCloning($t::into_parent(shared)) + } + } + }; + ($output:ident -> $ty:ty) => { + $output + }; +} + +pub(crate) use { + __map_via_leaf_leaf_type, __map_via_leaf_output, __map_via_leaf_to_parent, + __map_via_leaf_trait_impl, map_via_leaf, +}; + +#[cfg(test)] +mod test_macro { + use super::*; + + #[test] + fn test_map_via_leaf_owned() { + let mut x = 4; + let owned = map_via_leaf! { + input: &'r mut (Content<'a, U8Type, BeOwned>) = &mut x, + fn map_leaf(leaf) -> (Content<'static, T, BeOwned>) { + F::leaf_clone_to_owned_infallible(leaf) + } + }; + assert_eq!(owned, 4); + } +} diff --git a/src/expressions/concepts/mod.rs b/src/expressions/concepts/mod.rs new file mode 100644 index 00000000..4e1d0177 --- /dev/null +++ b/src/expressions/concepts/mod.rs @@ -0,0 +1,14 @@ +#![allow(dead_code)] // Whilst we're building it out +use super::*; + +mod content; +mod form; +mod forms; +mod mapping; +mod type_traits; + +pub(crate) use content::*; +pub(crate) use form::*; +pub(crate) use forms::*; +pub(crate) use mapping::*; +pub(crate) use type_traits::*; diff --git a/src/expressions/concepts/type_traits.rs b/src/expressions/concepts/type_traits.rs new file mode 100644 index 00000000..e1bd9f26 --- /dev/null +++ b/src/expressions/concepts/type_traits.rs @@ -0,0 +1,871 @@ +use super::*; + +pub(crate) trait TypeVariant {} + +pub(crate) struct HierarchicalTypeVariant; +impl TypeVariant for HierarchicalTypeVariant {} + +pub(crate) struct DynTypeVariant; +impl TypeVariant for DynTypeVariant {} + +pub(crate) trait IsType: Sized + TypeData { + type Variant: TypeVariant; + + const SOURCE_TYPE_NAME: &'static str; + const ARTICLED_DISPLAY_NAME: &'static str; + + fn type_kind() -> TypeKind; + fn type_kind_from_source_name(name: &str) -> Option; +} + +pub(crate) trait IsHierarchicalType: IsType { + type Content<'a, F: IsHierarchicalForm>: IsValueContent + + IntoValueContent<'a> + + FromValueContent<'a>; + type LeafKind: IsLeafKind; + + fn map_with<'a, F: IsHierarchicalForm, M: LeafMapper>( + mapper: M, + content: Self::Content<'a, F>, + ) -> M::Output<'a, Self>; + + fn map_ref_with<'r, 'a: 'r, F: IsHierarchicalForm, M: RefLeafMapper>( + mapper: M, + content: &'r Self::Content<'a, F>, + ) -> M::Output<'r, 'a, Self>; + + fn map_mut_with<'r, 'a: 'r, F: IsHierarchicalForm, M: MutLeafMapper>( + mapper: M, + content: &'r mut Self::Content<'a, F>, + ) -> M::Output<'r, 'a, Self>; + + fn content_to_leaf_kind( + content: &Self::Content<'_, F>, + ) -> Self::LeafKind; +} + +pub(crate) trait IsLeafType: + IsHierarchicalType + // NOTE: We can't create a universal bound over F: IsHierarchicalForm in rust + // If you need a generic interconversion over all such F, we'll need to reintroduce + // e.g. a fn content_to_leaf(content) method, with body { self } in each impl. + // For now though, listing out all the forms here is sufficient. + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + for<'a> IsHierarchicalType = ::Leaf<'a, Self>> + + UpcastTo +{ + type Leaf: IsValueLeaf; + + fn leaf_kind() -> Self::LeafKind; +} + +pub(crate) trait IsDynType: IsType { + type DynContent: ?Sized + 'static; +} + +pub(crate) trait UpcastTo: IsHierarchicalType { + fn upcast_to<'a, F: IsHierarchicalForm>(content: Content<'a, Self, F>) -> Content<'a, T, F>; +} + +pub(crate) trait DowncastFrom: IsHierarchicalType { + fn downcast_from<'a, F: IsHierarchicalForm>( + content: Content<'a, T, F>, + ) -> Result, Content<'a, T, F>>; + + fn resolve<'a, F: IsHierarchicalForm>( + content: Content<'a, T, F>, + span_range: SpanRange, + resolution_target: &str, + ) -> ExecutionResult> { + let content = match Self::downcast_from(content) { + Ok(c) => c, + Err(existing) => { + let leaf_kind = T::content_to_leaf_kind::(&existing); + return span_range.value_err(format!( + "{} is expected to be {}, but it is {}", + resolution_target, + Self::ARTICLED_DISPLAY_NAME, + leaf_kind.articled_display_name(), + )); + } + }; + Ok(content) + } +} + +pub(crate) trait DynResolveFrom: IsDynType { + fn downcast_from<'a, F: IsHierarchicalForm + IsDynCompatibleForm>( + content: Content<'a, T, F>, + ) -> Result, Content<'a, T, F>>; + + fn resolve<'a, F: IsHierarchicalForm + IsDynCompatibleForm>( + content: Content<'a, T, F>, + span_range: SpanRange, + resolution_target: &str, + ) -> ExecutionResult> { + let content = match Self::downcast_from(content) { + Ok(c) => c, + Err(existing) => { + let leaf_kind = T::content_to_leaf_kind::(&existing); + return span_range.value_err(format!( + "{} is expected to be {}, but it is {}", + resolution_target, + Self::ARTICLED_DISPLAY_NAME, + leaf_kind.articled_display_name(), + )); + } + }; + Ok(content) + } +} + +pub(crate) trait IsChildType: IsHierarchicalType { + type ParentType: IsHierarchicalType; + + fn into_parent<'a, F: IsHierarchicalForm>( + content: Self::Content<'a, F>, + ) -> Content<'a, Self::ParentType, F>; + + fn from_parent<'a, F: IsHierarchicalForm>( + content: ::Content<'a, F>, + ) -> Result, Content<'a, Self::ParentType, F>>; +} + +macro_rules! impl_type_feature_resolver { + (impl TypeFeatureResolver for $type_def:ty: [$($type_defs:ty)+]) => { + impl TypeFeatureResolver for $type_def { + fn resolve_method(&self, method_name: &str) -> Option { + $( + if let Some(method) = <$type_defs as TypeData>::resolve_own_method(method_name) { + return Some(method); + }; + )+ + None + } + + fn resolve_unary_operation( + &self, + operation: &UnaryOperation, + ) -> Option { + $( + if let Some(operation) = <$type_defs as TypeData>::resolve_own_unary_operation(operation) { + return Some(operation); + }; + )+ + None + } + + fn resolve_binary_operation( + &self, + operation: &BinaryOperation, + ) -> Option { + $( + if let Some(operation) = <$type_defs as TypeData>::resolve_own_binary_operation(operation) { + return Some(operation); + }; + )+ + None + } + + fn resolve_type_property(&self, property_name: &str) -> Option { + // Purposefully doesn't resolve parents, but TBC if this is right + <$type_def as TypeData>::resolve_type_property(property_name) + } + + fn resolve_property_access(&self) -> Option { + $( + if let Some(interface) = <$type_defs as TypeData>::resolve_own_property_access() { + return Some(interface); + }; + )+ + None + } + + fn resolve_index_access(&self) -> Option { + $( + if let Some(interface) = <$type_defs as TypeData>::resolve_own_index_access() { + return Some(interface); + }; + )+ + None + } + } + }; +} + +pub(crate) use impl_type_feature_resolver; + +macro_rules! impl_ancestor_chain_conversions { + ($child:ty $(=> $parent:ident ($parent_content:ident :: $parent_variant:ident) $(=> $ancestor:ty)*)?) => { + impl DowncastFrom<$child> for $child + { + fn downcast_from<'a, F: IsHierarchicalForm>( + content: Content<'a, Self, F>, + ) -> Result, Content<'a, Self, F>> { + Ok(content) + } + } + + impl UpcastTo<$child> for $child + { + fn upcast_to<'a, F: IsHierarchicalForm>( + content: Content<'a, Self, F>, + ) -> Content<'a, Self, F> { + content + } + } + + $( + impl IsChildType for $child { + type ParentType = $parent; + + fn into_parent<'a, F: IsHierarchicalForm>( + content: Self::Content<'a, F>, + ) -> ::Content<'a, F> { + $parent_content::$parent_variant(content) + } + + fn from_parent<'a, F: IsHierarchicalForm>( + content: ::Content<'a, F>, + ) -> Result, Content<'a, Self::ParentType, F>> { + match content { + $parent_content::$parent_variant(i) => Ok(i), + other => Err(other), + } + } + } + + impl DowncastFrom<$parent> for $child + { + fn downcast_from<'a, F: IsHierarchicalForm>( + content: Content<'a, $parent, F>, + ) -> Result, Content<'a, $parent, F>> { + <$child as IsChildType>::from_parent(content) + } + } + + impl UpcastTo<$parent> for $child + { + fn upcast_to<'a, F: IsHierarchicalForm>( + content: Content<'a, $child, F>, + ) -> Content<'a, $parent, F> { + <$child as IsChildType>::into_parent(content) + } + } + + $( + impl DowncastFrom<$ancestor> for $child { + fn downcast_from<'a, F: IsHierarchicalForm>( + content: Content<'a, $ancestor, F>, + ) -> Result, Content<'a, $ancestor, F>> { + let inner = <$parent as DowncastFrom<$ancestor>>::downcast_from::(content)?; + match <$child as DowncastFrom<$parent>>::downcast_from::(inner) { + Ok(c) => Ok(c), + Err(existing) => Err(<$parent as UpcastTo<$ancestor>>::upcast_to::(existing)), + } + } + } + + impl UpcastTo<$ancestor> for $child { + fn upcast_to<'a, F: IsHierarchicalForm>( + content: Content<'a, $child, F>, + ) -> Content<'a, $ancestor, F> { + <$parent as UpcastTo<$ancestor>>::upcast_to::(<$child as UpcastTo<$parent>>::upcast_to::(content)) + } + } + )* + )? + }; +} + +pub(crate) use impl_ancestor_chain_conversions; + +pub(crate) trait IsValueLeaf: + 'static + // This whole creation of IsLeafValueContent and the LeafType bound is a workaround to + // add an implied bound to IsValueLeaf that Self::Type: IsLeafType + // - This "use an associated type equality constraint" workaround came from a rust thread + // related to implied bounds, which of course I can't find now. + // - The main caveat is that order matters (i.e. I had to set Type == LeafType) and bound the + // correct one of Type or LeafType in other places to avoid circularity and ensure the one way + // resolution logic works correctly. + + IsValueContent::LeafType, Form = BeOwned> + + for<'a> IntoValueContent<'a> + + for<'a> FromValueContent<'a> + + IsLeafValueContent + + CastDyn + + Clone +{ +} + +pub(crate) trait IsLeafValueContent: IsValueContent { + type LeafType: IsLeafType; +} + +impl IsLeafValueContent for L +where + L::Type: IsLeafType, +{ + type LeafType = L::Type; +} + +pub(crate) trait IsDynLeaf: 'static { + type Type: IsDynType; +} + +pub(crate) trait CastDyn { + fn map_boxed(self: Box) -> Result, Box> { + Err(self) + } + fn map_ref(&self) -> Result<&T, &Self> { + Err(self) + } + fn map_mut(&mut self) -> Result<&mut T, &mut Self> { + Err(self) + } +} + +pub(crate) trait HasLeafKind { + type LeafKind: IsLeafKind; + + fn kind(&self) -> Self::LeafKind; + + fn value_kind(&self) -> AnyValueLeafKind { + self.kind().into() + } + + fn articled_kind(&self) -> &'static str { + self.kind().articled_display_name() + } +} + +// TODO[concepts]: Remove when we get rid of impl_resolvable_argument_for +impl HasLeafKind for &T { + type LeafKind = T::LeafKind; + + fn kind(&self) -> Self::LeafKind { + (**self).kind() + } +} + +// TODO[concepts]: Remove when we get rid of impl_resolvable_argument_for +impl HasLeafKind for &mut T { + type LeafKind = T::LeafKind; + + fn kind(&self) -> Self::LeafKind { + (**self).kind() + } +} + +macro_rules! define_parent_type { + ( + $type_def_vis:vis $type_def:ident $(=> $parent:ident($parent_content:ident :: $parent_variant:ident) $(=> $ancestor:ty)*)?, + content: $content_vis:vis $content:ident, + leaf_kind: $leaf_kind_vis:vis $leaf_kind:ident, + type_kind: ParentTypeKind::$parent_kind:ident($type_kind_vis:vis $type_kind:ident), + variants: { + $($variant:ident => $variant_type:ty,)* + }, + type_name: $source_type_name:literal, + articled_display_name: $articled_display_name:literal, + ) => { + #[derive(Copy, Clone)] + $type_def_vis struct $type_def; + + impl IsType for $type_def { + type Variant = HierarchicalTypeVariant; + + const SOURCE_TYPE_NAME: &'static str = $source_type_name; + const ARTICLED_DISPLAY_NAME: &'static str = $articled_display_name; + + fn type_kind() -> TypeKind { + TypeKind::Parent(ParentTypeKind::$parent_kind($type_kind)) + } + + #[inline(always)] + fn type_kind_from_source_name(name: &str) -> Option { + if name == Self::SOURCE_TYPE_NAME { + return Some(Self::type_kind()); + } + $( + if let Some(type_kind) = <$variant_type as IsType>::type_kind_from_source_name(name) { + return Some(type_kind); + } + )* + None + } + } + + impl_type_feature_resolver! { + impl TypeFeatureResolver for $type_def: [ + $type_def $( $parent $( $ancestor )* )? + ] + } + + impl IsHierarchicalType for $type_def { + type Content<'a, F: IsHierarchicalForm> = $content<'a, F>; + type LeafKind = $leaf_kind; + + fn map_with<'a, F: IsHierarchicalForm, M: LeafMapper>( + mapper: M, + content: Self::Content<'a, F>, + ) -> M::Output<'a, Self> { + match content { + $( $content::$variant(x) => M::to_parent_output::<'a, $variant_type>( + <$variant_type>::map_with::<'a, F, M>(mapper, x) + ), )* + } + } + + fn map_ref_with<'r, 'a: 'r, F: IsHierarchicalForm, M: RefLeafMapper>( + mapper: M, + content: &'r Self::Content<'a, F>, + ) -> M::Output<'r, 'a, Self> { + match content { + $( $content::$variant(x) => M::to_parent_output::<'r, 'a, $variant_type>( + <$variant_type>::map_ref_with::<'r, 'a, F, M>(mapper, x) + ), )* + } + } + + fn map_mut_with<'r, 'a: 'r, F: IsHierarchicalForm, M: MutLeafMapper>( + mapper: M, + content: &'r mut Self::Content<'a, F>, + ) -> M::Output<'r, 'a, Self> { + match content { + $( $content::$variant(x) => M::to_parent_output::<'r, 'a, $variant_type>( + <$variant_type>::map_mut_with::<'r, 'a, F, M>(mapper, x) + ), )* + } + } + + fn content_to_leaf_kind( + content: &Self::Content<'_, F>, + ) -> Self::LeafKind { + content.kind() + } + } + + $content_vis enum $content<'a, F: IsHierarchicalForm> { + $( $variant(Content<'a, $variant_type, F>), )* + } + + impl<'a, F: IsHierarchicalForm> Clone for $content<'a, F> + where + $( Content<'a, $variant_type, F>: Clone ),* + { + fn clone(&self) -> Self { + match self { + $( $content::$variant(x) => $content::$variant(x.clone()), )* + } + } + } + + + impl<'a, F: IsHierarchicalForm> Copy for $content<'a, F> + where + $( Content<'a, $variant_type, F>: Copy ),* + {} + + impl_value_content_traits!(parent: $type_def, $content); + + impl<'a, F: IsHierarchicalForm> HasLeafKind for $content<'a, F> { + type LeafKind = $leaf_kind; + + fn kind(&self) -> Self::LeafKind { + match self { + $($content::$variant(x) => $leaf_kind::$variant( + <$variant_type as IsHierarchicalType>::content_to_leaf_kind::(x) + ),)* + } + } + } + + #[derive(Clone, Copy, PartialEq, Eq)] + $type_kind_vis struct $type_kind; + + impl $type_kind { + pub(crate) fn articled_display_name(&self) -> &'static str { + $articled_display_name + } + + pub(crate) fn source_type_name(&self) -> &'static str { + $source_type_name + } + + pub(crate) fn feature_resolver(&self) -> &'static dyn TypeFeatureResolver { + &$type_def + } + } + + #[derive(Clone, Copy, PartialEq, Eq)] + $leaf_kind_vis enum $leaf_kind { + $( $variant(<$variant_type as IsHierarchicalType>::LeafKind), )* + } + + $( + impl From<$leaf_kind> for AnyValueLeafKind { + fn from(kind: $leaf_kind) -> Self { + let as_parent_kind = <$parent as IsHierarchicalType>::LeafKind::$parent_variant(kind); + AnyValueLeafKind::from(as_parent_kind) + } + } + )? + + impl IsLeafKind for $leaf_kind { + fn source_type_name(&self) -> &'static str { + match self { + $( Self::$variant(x) => x.source_type_name(), )* + } + } + + fn articled_display_name(&self) -> &'static str { + match self { + $( Self::$variant(x) => x.articled_display_name(), )* + } + } + + fn feature_resolver(&self) -> &'static dyn TypeFeatureResolver { + match self { + $( Self::$variant(x) => x.feature_resolver(), )* + } + } + } + + impl_ancestor_chain_conversions!( + $type_def $(=> $parent($parent_content :: $parent_variant) $(=> $ancestor)*)? + ); + }; +} + +pub(crate) use define_parent_type; + +macro_rules! impl_fallback_is_iterable { + ({IsIterable $($haystack:tt)*} for $content_type:ty) => { + // Already implemented, ignoring + }; + ({$discard:tt $($haystack:tt)*} for $content_type:ty) => { + // Recurse to check the rest + impl_is_iterable_if_missing!({$($haystack)*} for $content_type) + }; + ({} for $content_type:ty) => { + // Implement fallback + impl CastDyn for $content_type {} + }; +} + +pub(crate) use impl_fallback_is_iterable; + +macro_rules! define_leaf_type { + ( + $type_def_vis:vis $type_def:ident => $parent:ident($parent_content:ident :: $parent_variant:ident) $(=> $ancestor:ty)*, + content: $content_type:ty, + kind: $kind_vis:vis $kind:ident, + type_name: $source_type_name:literal, + articled_display_name: $articled_display_name:literal, + dyn_impls: { + $($dyn_type:ty: impl $dyn_trait:ident { $($dyn_trait_impl:tt)* })* + }, + ) => { + #[derive(Copy, Clone)] + $type_def_vis struct $type_def; + + impl IsType for $type_def { + type Variant = HierarchicalTypeVariant; + + const SOURCE_TYPE_NAME: &'static str = $source_type_name; + const ARTICLED_DISPLAY_NAME: &'static str = $articled_display_name; + + fn type_kind() -> TypeKind { + TypeKind::Leaf(AnyValueLeafKind::from($kind)) + } + + // This is called for every leaf in a row as part of parsing + // Use inline(always) to avoid function call overhead, even in Debug builds + // which are used when macros are run during development + #[inline(always)] + fn type_kind_from_source_name(name: &str) -> Option { + if name == Self::SOURCE_TYPE_NAME { + Some(Self::type_kind()) + } else { + None + } + } + } + + impl IsHierarchicalType for $type_def { + type Content<'a, F: IsHierarchicalForm> = F::Leaf<'a, Self>; + type LeafKind = $kind; + + fn map_with<'a, F: IsHierarchicalForm, M: LeafMapper>( + mapper: M, + content: Self::Content<'a, F>, + ) -> M::Output<'a, Self> { + mapper.map_leaf::(content) + } + + fn map_ref_with<'r, 'a: 'r, F: IsHierarchicalForm, M: RefLeafMapper>( + mapper: M, + content: &'r Self::Content<'a, F>, + ) -> M::Output<'r, 'a, Self> { + mapper.map_leaf::(content) + } + + fn map_mut_with<'r, 'a: 'r, F: IsHierarchicalForm, M: MutLeafMapper>( + mapper: M, + content: &'r mut Self::Content<'a, F>, + ) -> M::Output<'r, 'a, Self> { + mapper.map_leaf::(content) + } + + fn content_to_leaf_kind( + _content: &Self::Content<'_, F>, + ) -> Self::LeafKind { + $kind + } + } + + impl IsLeafType for $type_def { + type Leaf = $content_type; + + fn leaf_kind() -> $kind { + $kind + } + } + + impl_type_feature_resolver! { + impl TypeFeatureResolver for $type_def: [ + $type_def $($dyn_type)* $parent $( $ancestor )* + ] + } + + #[derive(Clone, Copy, PartialEq, Eq)] + $kind_vis struct $kind; + + impl From<$kind> for AnyValueLeafKind { + fn from(kind: $kind) -> Self { + let as_parent_kind = <$parent as IsHierarchicalType>::LeafKind::$parent_variant(kind); + AnyValueLeafKind::from(as_parent_kind) + } + } + + impl IsLeafKind for $kind { + fn source_type_name(&self) -> &'static str { + $source_type_name + } + + fn articled_display_name(&self) -> &'static str { + $articled_display_name + } + + fn feature_resolver(&self) -> &'static dyn TypeFeatureResolver { + &$type_def + } + } + + impl HasLeafKind for $content_type { + type LeafKind = $kind; + + fn kind(&self) -> Self::LeafKind { + $kind + } + } + + impl_value_content_traits!(leaf: $type_def, $content_type); + + impl IsValueLeaf for $content_type {} + + $( + impl $dyn_trait for $content_type { + $($dyn_trait_impl)* + } + impl CastDyn for $content_type { + fn map_boxed(self: Box) -> Result, Box> { + Ok(self) + } + fn map_ref(&self) -> Result<&dyn $dyn_trait, &Self> { + Ok(self) + } + fn map_mut(&mut self) -> Result<&mut dyn $dyn_trait, &mut Self> { + Ok(self) + } + } + )* + impl_fallback_is_iterable!({$($dyn_trait)*} for $content_type); + + impl_ancestor_chain_conversions!( + $type_def => $parent($parent_content :: $parent_variant) $(=> $ancestor)* + ); + }; +} + +pub(crate) use define_leaf_type; + +pub(crate) struct DynMapper(std::marker::PhantomData); + +impl DynMapper { + pub(crate) const fn new() -> Self { + Self(std::marker::PhantomData) + } +} + +/// Implements `IsValueContent`, `IntoValueContent`, and `FromValueContent` for various +/// form wrappers of a content type. This macro deduplicates code across `define_leaf_type`, +/// `define_parent_type`, and `define_dyn_type`. +macro_rules! impl_value_content_traits { + // For leaf types - implements for all common form wrappers + (leaf: $type_def:ty, $content_type:ty) => { + // NB - we can't blanket implement this for all L: IsValueLeaf (unlike all the other forms) + // because it potentially conflicts with the &'a L and &'a mut L blanket implementations. + + // BeOwned: content is X + impl IsValueContent for $content_type { + type Type = $type_def; + type Form = BeOwned; + } + impl<'a> IntoValueContent<'a> for $content_type { + fn into_content(self) -> Self { + self + } + } + impl<'a> FromValueContent<'a> for $content_type { + fn from_content(content: Self) -> Self { + content + } + } + }; + + // For parent types - $content<'a, F> where F: IsHierarchicalForm + (parent: $type_def:ty, $content:ident) => { + impl<'a, F: IsHierarchicalForm> IsValueContent for $content<'a, F> { + type Type = $type_def; + type Form = F; + } + impl<'a, F: IsHierarchicalForm> IntoValueContent<'a> for $content<'a, F> { + fn into_content(self) -> Self { + self + } + } + impl<'a, F: IsHierarchicalForm> FromValueContent<'a> for $content<'a, F> { + fn from_content(content: Self) -> Self { + content + } + } + }; +} + +pub(crate) use impl_value_content_traits; + +macro_rules! define_dyn_type { + ( + $type_def_vis:vis $type_def:ident, + content: $dyn_type:ty, + dyn_kind: DynTypeKind::$dyn_kind:ident, + type_name: $source_type_name:literal, + articled_display_name: $articled_display_name:literal, + ) => { + #[derive(Copy, Clone)] + $type_def_vis struct $type_def; + + impl IsType for $type_def { + type Variant = DynTypeVariant; + + const SOURCE_TYPE_NAME: &'static str = $source_type_name; + const ARTICLED_DISPLAY_NAME: &'static str = $articled_display_name; + + fn type_kind() -> TypeKind { + TypeKind::Dyn(DynTypeKind::$dyn_kind) + } + + // This is called for every leaf in a row as part of parsing + // Use inline(always) to avoid function call overhead, even in Debug builds + // which are used when macros are run during development + #[inline(always)] + fn type_kind_from_source_name(name: &str) -> Option { + if name == Self::SOURCE_TYPE_NAME { + Some(Self::type_kind()) + } else { + None + } + } + } + + impl IsDynType for $type_def { + type DynContent = $dyn_type; + } + + impl_type_feature_resolver! { + impl TypeFeatureResolver for $type_def: [$type_def] + } + + impl IsDynLeaf for $dyn_type { + type Type = $type_def; + } + + impl IsArgument for Box<$dyn_type> { + type ValueType = $type_def; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Owned; + fn from_argument(Spanned(value, span_range): Spanned) -> ExecutionResult { + let form_mapped = BeOwned::from_argument_value(value)?; + <$type_def as DynResolveFrom>::resolve(form_mapped, span_range, "This argument") + } + } + + impl<'a> IsArgument for AnyRef<'a, $dyn_type> { + type ValueType = $type_def; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Shared; + fn from_argument(Spanned(value, span_range): Spanned) -> ExecutionResult { + let form_mapped = BeAnyRef::from_argument_value(value)?; + <$type_def as DynResolveFrom>::resolve(form_mapped, span_range, "This argument") + } + } + + impl<'a> IsArgument for AnyMut<'a, $dyn_type> { + type ValueType = $type_def; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Mutable; + fn from_argument(Spanned(value, span_range): Spanned) -> ExecutionResult { + let form_mapped = BeAnyMut::from_argument_value(value)?; + <$type_def as DynResolveFrom>::resolve(form_mapped, span_range, "This argument") + } + } + + impl DynResolveFrom for $type_def + { + fn downcast_from<'a, F: IsHierarchicalForm + IsDynCompatibleForm>(content: Content<'a, T, F>) -> Result, Content<'a, T, F>> { + T::map_with::<'a, F, _>(DynMapper::<$dyn_type>::new(), content) + } + } + + impl LeafMapper for DynMapper<$dyn_type> { + type Output<'a, T: IsHierarchicalType> = Result, Content<'a, T, F>>; + + fn to_parent_output<'a, T: IsChildType>( + output: Self::Output<'a, T>, + ) -> Self::Output<'a, T::ParentType> { + match output { + Ok(dyn_content) => Ok(dyn_content), + Err(content) => Err(T::into_parent(content)), + } + } + + fn map_leaf<'a, T: IsLeafType>( + self, + leaf: F::Leaf<'a, T>, + ) -> Self::Output<'a, T> { + F::leaf_to_dyn(leaf) + } + } + }; +} + +pub(crate) use define_dyn_type; diff --git a/src/expressions/control_flow.rs b/src/expressions/control_flow.rs new file mode 100644 index 00000000..1080af25 --- /dev/null +++ b/src/expressions/control_flow.rs @@ -0,0 +1,631 @@ +use super::*; + +pub(crate) struct IfExpression { + if_token: Ident, + condition: Expression, + then_code: ScopedBlock, + else_ifs: Vec<(Expression, ScopedBlock)>, + else_code: Option, +} + +impl HasSpanRange for IfExpression { + fn span_range(&self) -> SpanRange { + let start = self.if_token.span(); + let end = if let Some(else_code) = &self.else_code { + else_code.span() + } else if let Some((_, last_else_if_code)) = self.else_ifs.last() { + last_else_if_code.span() + } else { + self.then_code.span() + }; + SpanRange::new_between(start, end) + } +} + +impl ParseSource for IfExpression { + fn parse(input: SourceParser) -> ParseResult { + let if_token = input.parse_ident_matching("if")?; + + let condition = input.parse()?; + let then_code = input.parse()?; + + let mut else_ifs = Vec::new(); + let mut else_code = None; + while input.peek_ident_matching("else") { + let _ = input.parse_ident_matching("else")?; + if input.peek_ident_matching("if") { + input.parse_ident_matching("if")?; + + let condition = input.parse()?; + let then_code = input.parse()?; + + else_ifs.push((condition, then_code)); + } else { + else_code = Some(input.parse()?); + break; + } + } + + Ok(Self { + if_token, + condition, + then_code, + else_ifs, + else_code, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + // In terms of control-flow segments, the possible execution paths + // look like this, so we model that with path-based segments: + // + // IfCondA < BlockA + // ^< ElseIfCondB < BlockB + // ^< ElseIfCondC < BlockC + // ^<----------- ElseBlock + let outer_segment = context.enter_next_segment(SegmentKind::PathBased); + + let mut cond_segment = context.enter_path_segment(None, SegmentKind::Sequential); + self.condition.control_flow_pass(context)?; + context.exit_segment(cond_segment); + + let block_segment = context.enter_path_segment(Some(cond_segment), SegmentKind::Sequential); + self.then_code.control_flow_pass(context)?; + context.exit_segment(block_segment); + + for (condition, code) in &mut self.else_ifs { + cond_segment = context.enter_path_segment(Some(cond_segment), SegmentKind::Sequential); + condition.control_flow_pass(context)?; + context.exit_segment(cond_segment); + + let block_segment = + context.enter_path_segment(Some(cond_segment), SegmentKind::Sequential); + code.control_flow_pass(context)?; + context.exit_segment(block_segment); + } + + if let Some(else_code) = &mut self.else_code { + let block_segment = + context.enter_path_segment(Some(cond_segment), SegmentKind::Sequential); + else_code.control_flow_pass(context)?; + context.exit_segment(block_segment); + } + + context.exit_segment(outer_segment); + + Ok(()) + } +} + +impl Evaluate for IfExpression { + fn evaluate( + &self, + interpreter: &mut Interpreter, + requested_ownership: RequestedOwnership, + ) -> ExecutionResult { + let evaluated_condition: bool = self + .condition + .evaluate_owned(interpreter)? + .resolve_as("An if condition")?; + + if evaluated_condition { + return self.then_code.evaluate(interpreter, requested_ownership); + } + + for (condition, code) in &self.else_ifs { + let evaluated_condition: bool = condition + .evaluate_owned(interpreter)? + .resolve_as("An else if condition")?; + if evaluated_condition { + return code.evaluate(interpreter, requested_ownership); + } + } + + if let Some(else_code) = &self.else_code { + return else_code.evaluate(interpreter, requested_ownership); + } + + requested_ownership + .map_from_owned(Spanned(().into_any_value(), self.span_range())) + .map(|spanned| spanned.0) + } +} + +pub(crate) struct WhileExpression { + label: Option, + while_token: Ident, + condition: Expression, + body: ScopedBlock, + catch_location: CatchLocationId, +} + +impl HasSpanRange for WhileExpression { + fn span_range(&self) -> SpanRange { + // We ignore the label, as it's not really part of the expression + SpanRange::new_between(self.while_token.span(), self.body.span()) + } +} + +impl ParseSource for WhileExpression { + fn parse(input: SourceParser) -> ParseResult { + let label = input.parse_optional()?; + let while_token = input.parse_ident_matching("while")?; + let condition = input.parse()?; + let body = input.parse()?; + + Ok(Self { + label, + while_token, + condition, + body, + catch_location: CatchLocationId::new_placeholder(), + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.catch_location = context.register_catch_location(CatchLocationData::Loop { + label: self.label.as_ref().map(|l| l.ident_string()), + }); + + let segment = context.enter_next_segment(SegmentKind::LoopingSequential); + self.condition.control_flow_pass(context)?; + + context.enter_catch(self.catch_location); + self.body.control_flow_pass(context)?; + context.exit_catch(self.catch_location); + + context.exit_segment(segment); + Ok(()) + } +} + +impl Evaluate for WhileExpression { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + let span = self.body.span(); + let mut iteration_counter = interpreter.start_iteration_counter(&span); + + let scope = interpreter.current_scope_id(); + while self + .condition + .evaluate_owned(interpreter)? + .resolve_as("A while condition")? + { + iteration_counter.increment_and_check()?; + let body_result = self.body.evaluate_owned(interpreter); + match interpreter.catch_control_flow(body_result, self.catch_location, scope)? { + ExecutionOutcome::Value(value) => { + value.into_statement_result()?; + } + ExecutionOutcome::ControlFlow(control_flow_interrupt) => { + match control_flow_interrupt { + ControlFlowInterrupt::Break(break_interrupt) => { + return break_interrupt + .into_requested_value(self.span_range(), ownership); + } + ControlFlowInterrupt::Continue { .. } => { + continue; + } + ControlFlowInterrupt::Revert(_) => { + unreachable!("A revert should not match to a loop catch location") + } + } + } + } + } + ownership + .map_none(self.span_range()) + .map(|spanned| spanned.0) + } +} + +pub(crate) struct LoopExpression { + catch_location: CatchLocationId, + label: Option, + loop_token: Ident, + body: ScopedBlock, +} + +impl HasSpanRange for LoopExpression { + fn span_range(&self) -> SpanRange { + // We ignore the label, because it's not really part of the span of the resultant value + SpanRange::new_between(self.loop_token.span(), self.body.span()) + } +} + +impl ParseSource for LoopExpression { + fn parse(input: SourceParser) -> ParseResult { + let label = input.parse_optional()?; + let loop_token = input.parse_ident_matching("loop")?; + let body = input.parse()?; + Ok(Self { + catch_location: CatchLocationId::new_placeholder(), + label, + loop_token, + body, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.catch_location = context.register_catch_location(CatchLocationData::Loop { + label: self.label.as_ref().map(|l| l.ident_string()), + }); + + let segment = context.enter_next_segment(SegmentKind::LoopingSequential); + + context.enter_catch(self.catch_location); + self.body.control_flow_pass(context)?; + context.exit_catch(self.catch_location); + + context.exit_segment(segment); + Ok(()) + } +} + +impl Evaluate for LoopExpression { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + let span = self.body.span(); + let mut iteration_counter = interpreter.start_iteration_counter(&span); + + let scope = interpreter.current_scope_id(); + loop { + iteration_counter.increment_and_check()?; + + let body_result = self.body.evaluate_owned(interpreter); + match interpreter.catch_control_flow(body_result, self.catch_location, scope)? { + ExecutionOutcome::Value(value) => { + value.into_statement_result()?; + } + ExecutionOutcome::ControlFlow(control_flow_interrupt) => { + match control_flow_interrupt { + ControlFlowInterrupt::Break(break_interrupt) => { + return break_interrupt + .into_requested_value(self.span_range(), ownership); + } + ControlFlowInterrupt::Continue { .. } => { + continue; + } + ControlFlowInterrupt::Revert(_) => { + unreachable!("A revert should not match to a loop catch location") + } + } + } + } + } + } +} + +pub(crate) struct ForExpression { + iteration_scope: ScopeId, + catch_location: CatchLocationId, + label: Option, + for_token: Ident, + pattern: Pattern, + _in_token: Ident, + iterable: Expression, + body: ScopedBlock, +} + +impl HasSpanRange for ForExpression { + fn span_range(&self) -> SpanRange { + // We ignore the label, because it's not really part of the span of the resultant value + SpanRange::new_between(self.for_token.span(), self.body.span()) + } +} + +impl ParseSource for ForExpression { + fn parse(input: SourceParser) -> ParseResult { + let label = input.parse_optional()?; + let for_token = input.parse_ident_matching("for")?; + let pattern = input.parse()?; + let in_token = input.parse_ident_matching("in")?; + let iterable = input.parse()?; + let body = input.parse()?; + + Ok(Self { + iteration_scope: ScopeId::new_placeholder(), + catch_location: CatchLocationId::new_placeholder(), + label, + for_token, + pattern, + _in_token: in_token, + iterable, + body, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.catch_location = context.register_catch_location(CatchLocationData::Loop { + label: self.label.as_ref().map(|l| l.ident_string()), + }); + + context.register_scope(&mut self.iteration_scope); + self.iterable.control_flow_pass(context)?; + + let segment = context.enter_next_segment(SegmentKind::LoopingSequential); + context.enter_scope(self.iteration_scope); + + context.enter_catch(self.catch_location); + self.pattern.control_flow_pass(context)?; + self.body.control_flow_pass(context)?; + context.exit_catch(self.catch_location); + + context.exit_scope(self.iteration_scope); + context.exit_segment(segment); + + Ok(()) + } +} + +impl Evaluate for ForExpression { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + let iterable: IterableValue = self + .iterable + .evaluate_owned(interpreter)? + .dyn_resolve::("A for loop iterable")?; + + let span = self.body.span(); + let scope = interpreter.current_scope_id(); + let mut iteration_counter = interpreter.start_iteration_counter(&span); + + for item in iterable.into_iterator()? { + iteration_counter.increment_and_check()?; + + interpreter.enter_scope(self.iteration_scope); + self.pattern.handle_destructure(interpreter, item)?; + + let body_result = self.body.evaluate_owned(interpreter); + match interpreter.catch_control_flow(body_result, self.catch_location, scope)? { + ExecutionOutcome::Value(value) => { + value.into_statement_result()?; + } + ExecutionOutcome::ControlFlow(control_flow_interrupt) => { + match control_flow_interrupt { + ControlFlowInterrupt::Break(break_interrupt) => { + return break_interrupt + .into_requested_value(self.span_range(), ownership); + } + ControlFlowInterrupt::Continue { .. } => { + continue; + } + ControlFlowInterrupt::Revert(_) => { + unreachable!("A revert should not match to a loop catch location") + } + } + } + } + interpreter.exit_scope(self.iteration_scope); + } + ownership + .map_none(self.span_range()) + .map(|spanned| spanned.0) + } +} + +pub(crate) struct AttemptExpression { + catch_location: CatchLocationId, + label: Option, + attempt: AttemptKeyword, + braces: Braces, + arms: Vec, +} + +struct AttemptArm { + arm_scope: ScopeId, + // The LHS's scope extends into the RHS + lhs: UnscopedBlock, + guard: Option<(Token![if], Expression)>, + _arrow: Token![=>], + rhs: Expression, +} + +impl HasSpanRange for AttemptExpression { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.attempt.span(), self.braces.close()) + } +} + +impl ParseSource for AttemptExpression { + fn parse(input: SourceParser) -> ParseResult { + let label = input.parse_optional()?; + let attempt = input.parse()?; + let (braces, inner) = input.parse_braces()?; + let mut arms = vec![]; + while !inner.is_empty() { + let lhs = inner.parse()?; + let guard = if inner.peek_ident_matching("if") { + let if_token = inner.parse()?; + let condition = inner.parse()?; + Some((if_token, condition)) + } else { + None + }; + let arrow = inner.parse()?; + let rhs: Expression = inner.parse()?; + if inner.peek(Token![,]) { + let _ = inner.parse::()?; + } else if !rhs.is_block() { + inner.parse_err("Expected trailing comma after previous non-block attempt arm")?; + } + arms.push(AttemptArm { + arm_scope: ScopeId::new_placeholder(), + lhs, + guard, + _arrow: arrow, + rhs, + }); + } + Ok(Self { + catch_location: CatchLocationId::new_placeholder(), + label, + attempt, + braces, + arms, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.catch_location = context.register_catch_location(CatchLocationData::AttemptBlock { + label: self.label.as_ref().map(|l| l.ident_string()), + }); + + let segment = context.enter_next_segment(SegmentKind::PathBased); + let mut previous_attempt_segment = None; + for arm in &mut self.arms { + context.register_scope(&mut arm.arm_scope); + + context.enter_scope(arm.arm_scope); + let attempt_segment = context + .enter_path_segment(previous_attempt_segment, SegmentKind::RevertibleSequential); + + context.enter_catch(self.catch_location); + arm.lhs.control_flow_pass(context)?; + context.exit_catch(self.catch_location); + + if let Some((_, guard_expression)) = &mut arm.guard { + guard_expression.control_flow_pass(context)?; + } + + context.exit_segment(attempt_segment); + previous_attempt_segment = Some(attempt_segment); + + let action_segment = + context.enter_path_segment(previous_attempt_segment, SegmentKind::Sequential); + arm.rhs.control_flow_pass(context)?; + context.exit_segment(action_segment); + context.exit_scope(arm.arm_scope); + } + context.exit_segment(segment); + + Ok(()) + } +} + +impl Evaluate for AttemptExpression { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + // We need a separate method to correctly capture the lifetimes of the guard clause + fn guard_clause<'a>( + guard: Option<&'a (Token![if], Expression)>, + ) -> Option FnOnce(&'b mut Interpreter) -> ExecutionResult + 'a> + { + guard.map(|(_, guard_expression)| { + move |interpreter: &mut Interpreter| -> ExecutionResult { + guard_expression + .evaluate_owned(interpreter)? + .resolve_as("The guard condition of an attempt arm") + } + }) + } + for arm in self.arms.iter() { + let attempt_outcome = interpreter.enter_scope_starting_with_revertible_segment( + arm.arm_scope, + self.catch_location, + |interpreter| -> ExecutionResult<()> { + arm.lhs + .evaluate_owned(interpreter)? + .resolve_as("The returned value from the left half of an attempt arm") + }, + guard_clause(arm.guard.as_ref()), + MutationBlockReason::AttemptRevertibleSegment, + )?; + match attempt_outcome { + AttemptOutcome::Completed(()) => { /* proceed to rhs */ } + AttemptOutcome::Reverted => { + interpreter.exit_scope(arm.arm_scope); + continue; + } + } + let Spanned(output, _) = arm.rhs.evaluate(interpreter, ownership)?; + interpreter.exit_scope(arm.arm_scope); + return Ok(output); + } + self.braces.control_flow_err("No attempt arm ran successfully. You may wish to add a fallback arm `{} => { None }` to ignore the error or to propagate a better message: `{} => { %[].error(\"Error message\") }`.") + } +} + +pub(crate) struct ParseExpression { + parse_ident: ParseKeyword, + input: Expression, + _fat_arrow: Unused]>, + _left_bar: Unused, + parser_variable: VariableDefinition, + _right_bar: Unused, + scope: ScopeId, + body: UnscopedBlock, +} + +impl HasSpanRange for ParseExpression { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.parse_ident.span(), self.body.span()) + } +} + +impl ParseSource for ParseExpression { + fn parse(input: SourceParser) -> ParseResult { + let parse_ident = input.parse()?; + let input_expression = input.parse()?; + let _fat_arrow = input.parse()?; + let _left_bar = input.parse()?; + let parser_variable = input.parse()?; + let _right_bar = input.parse()?; + let body = input.parse()?; + Ok(Self { + parse_ident, + input: input_expression, + _fat_arrow, + _left_bar, + parser_variable, + _right_bar, + scope: ScopeId::new_placeholder(), + body, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + context.register_scope(&mut self.scope); + self.input.control_flow_pass(context)?; + context.enter_scope(self.scope); + self.parser_variable.control_flow_pass(context)?; + self.body.control_flow_pass(context)?; + context.exit_scope(self.scope); + Ok(()) + } +} + +impl Evaluate for ParseExpression { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + let input = self + .input + .evaluate_owned(interpreter)? + .resolve_as("The input to a parse expression")?; + + interpreter.enter_scope(self.scope); + + let output = interpreter.start_parse(input, |interpreter, handle| { + self.parser_variable.define(interpreter, handle); + self.body.evaluate(interpreter, ownership) + })?; + + interpreter.exit_scope(self.scope); + Ok(output) + } +} diff --git a/src/expressions/equality.rs b/src/expressions/equality.rs new file mode 100644 index 00000000..4f3b78f2 --- /dev/null +++ b/src/expressions/equality.rs @@ -0,0 +1,578 @@ +use super::*; + +// ============================================================================ +// Equality Context - Controls behavior of value equality comparisons +// ============================================================================ + +/// A segment in the path to the current comparison location. +#[derive(Clone, Debug)] +pub(crate) enum PathSegment { + ArrayIndex(usize), + ObjectKey(String), + IteratorIndex(usize), + RangeStart, + RangeEnd, +} + +impl PathSegment { + fn fmt_path(path: &[PathSegment]) -> String { + let mut result = String::new(); + for segment in path { + match segment { + PathSegment::ArrayIndex(i) => result.push_str(&format!("[{}]", i)), + PathSegment::ObjectKey(k) => { + if syn::parse_str::(k).is_ok() { + result.push_str(&format!(".{}", k)) + } else { + result.push_str(&format!("[{:?}]", k)) + } + } + PathSegment::IteratorIndex(i) => result.push_str(&format!("[{}]", i)), + PathSegment::RangeStart => result.push_str(".start"), + PathSegment::RangeEnd => result.push_str(".end"), + } + } + result + } +} + +/// Context trait for controlling equality comparison behavior. +/// +/// Different implementations allow for: +/// - Simple equality: returns `false` on type mismatch (like JS `===`) +/// - Typed equality: errors on type mismatch, tracks path for error messages +/// - Assert equality: errors on any difference, useful for assert_eq +pub(crate) trait EqualityContext { + /// The result type returned by equality comparisons. + type Result; + + /// Values are equal. + fn values_equal(&mut self) -> Self::Result; + + /// Values of the same type are not equal. + fn leaf_values_not_equal(&mut self, lhs: &T, rhs: &T) -> Self::Result; + + /// Values have different kinds. + fn kind_mismatch(&mut self, lhs: &L, rhs: &R) -> Self::Result; + + /// Range values have different structures. + fn range_structure_mismatch( + &mut self, + lhs: RangeStructure, + rhs: RangeStructure, + ) -> Self::Result; + + /// Arrays or iterators have different lengths. + fn lengths_unequal(&mut self, lhs_len: Option, rhs_len: Option) -> Self::Result; + + /// Object is missing a key that the other has. + fn missing_key(&mut self, key: &str, missing_on: MissingSide) -> Self::Result; + + fn iteration_limit_exceeded(&mut self, limit: usize) -> Self::Result { + let message = format!("iteration limit {} exceeded", limit); + self.leaf_values_not_equal(&message, &message) + } + + /// Wrap a comparison within an array index context. + fn with_array_index(&mut self, index: usize, f: impl FnOnce(&mut Self) -> R) -> R; + + /// Wrap a comparison within an object key context. + fn with_object_key(&mut self, key: &str, f: impl FnOnce(&mut Self) -> R) -> R; + + /// Wrap a comparison within an iterator index context. + fn with_iterator_index(&mut self, index: usize, f: impl FnOnce(&mut Self) -> R) -> R; + + /// Wrap a comparison within a range start context. + fn with_range_start(&mut self, f: impl FnOnce(&mut Self) -> R) -> R; + + /// Wrap a comparison within a range end context. + fn with_range_end(&mut self, f: impl FnOnce(&mut Self) -> R) -> R; + + /// Returns true if the result indicates we should stop comparing and return early. + /// This is true when the result indicates "not equal" or an error occurred. + fn should_short_circuit(&self, result: &Self::Result) -> bool; +} + +/// Simple equality context - returns `false` on type mismatch, no path tracking. +/// This is the most efficient option when you just need a bool result. +pub(crate) struct SimpleEquality; + +impl EqualityContext for SimpleEquality { + type Result = bool; + + #[inline] + fn values_equal(&mut self) -> bool { + true + } + + #[inline] + fn leaf_values_not_equal(&mut self, _lhs: &T, _rhs: &T) -> bool { + false + } + + #[inline] + fn kind_mismatch(&mut self, _lhs: &L, _rhs: &R) -> bool { + false + } + + #[inline] + fn range_structure_mismatch(&mut self, _lhs: RangeStructure, _rhs: RangeStructure) -> bool { + false + } + + #[inline] + fn lengths_unequal(&mut self, _lhs_len: Option, _rhs_len: Option) -> bool { + false + } + + #[inline] + fn missing_key(&mut self, _key: &str, _missing_on: MissingSide) -> bool { + false + } + + #[inline] + fn with_array_index(&mut self, _index: usize, f: impl FnOnce(&mut Self) -> R) -> R { + f(self) + } + + #[inline] + fn with_object_key(&mut self, _key: &str, f: impl FnOnce(&mut Self) -> R) -> R { + f(self) + } + + #[inline] + fn with_iterator_index(&mut self, _index: usize, f: impl FnOnce(&mut Self) -> R) -> R { + f(self) + } + + #[inline] + fn with_range_start(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + f(self) + } + + #[inline] + fn with_range_end(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + f(self) + } + + #[inline] + fn should_short_circuit(&self, result: &bool) -> bool { + !*result + } +} + +/// Typed equality context - errors on type mismatch, tracks path for error messages. +pub(crate) struct TypedEquality { + path: Vec, + error_span: SpanRange, +} + +impl TypedEquality { + pub(crate) fn new(error_span: SpanRange) -> Self { + Self { + path: Vec::new(), + error_span, + } + } +} + +impl EqualityContext for TypedEquality { + type Result = ExecutionResult; + + #[inline] + fn values_equal(&mut self) -> ExecutionResult { + Ok(true) + } + + #[inline] + fn leaf_values_not_equal(&mut self, _lhs: &T, _rhs: &T) -> ExecutionResult { + Ok(false) + } + + fn kind_mismatch( + &mut self, + lhs: &L, + rhs: &R, + ) -> ExecutionResult { + let path_str = PathSegment::fmt_path(&self.path); + Err(self.error_span.type_error(format!( + "lhs{} is {}, but rhs{} is {}", + path_str, + lhs.articled_kind(), + path_str, + rhs.articled_kind() + ))) + } + + fn range_structure_mismatch( + &mut self, + lhs: RangeStructure, + rhs: RangeStructure, + ) -> Self::Result { + let path_str = PathSegment::fmt_path(&self.path); + Err(self.error_span.type_error(format!( + "lhs{} is {}, but rhs{} is {}", + path_str, + lhs.articled_display_name(), + path_str, + rhs.articled_display_name() + ))) + } + + #[inline] + fn lengths_unequal( + &mut self, + _lhs_len: Option, + _rhs_len: Option, + ) -> ExecutionResult { + Ok(false) + } + + #[inline] + fn missing_key(&mut self, _key: &str, _missing_on: MissingSide) -> ExecutionResult { + Ok(false) + } + + #[inline] + fn with_array_index(&mut self, index: usize, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::ArrayIndex(index)); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn with_object_key(&mut self, key: &str, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::ObjectKey(key.to_string())); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn with_iterator_index(&mut self, index: usize, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::IteratorIndex(index)); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn with_range_start(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::RangeStart); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn with_range_end(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::RangeEnd); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn should_short_circuit(&self, result: &ExecutionResult) -> bool { + // Short-circuit on Ok(false) or Err(_) + !matches!(result, Ok(true)) + } +} + +// ============================================================================ +// Debug Equality - Captures detailed information about equality failures +// ============================================================================ + +/// Which side of the comparison is missing a key. +#[derive(Debug, Clone, Copy)] +pub(crate) enum MissingSide { + #[allow(dead_code)] + Lhs, + Rhs, +} + +/// The reason why two values were not equal. +#[derive(Clone)] +pub(crate) enum DebugInequalityReason { + /// Values of the same type have different values. + ValueMismatch { + lhs_display: String, + rhs_display: String, + }, + /// Values have incompatible value kinds. + ValueLeafKindMismatch { + lhs_kind: AnyValueLeafKind, + rhs_kind: AnyValueLeafKind, + }, + /// Ranges have incompatible structures. + RangeStructureMismatch { + lhs_structure: RangeStructure, + rhs_structure: RangeStructure, + }, + /// Collections have different lengths. + LengthMismatch { + lhs_len: Option, + rhs_len: Option, + }, + /// Object is missing a key on one side. + MissingKey { + key: String, + missing_on: MissingSide, + }, +} + +pub(crate) struct DebugEqualityError { + inner: Box, +} + +struct DebugEqualityErrorInner { + path: Vec, + reason: DebugInequalityReason, +} + +impl DebugEqualityError { + pub fn format_message(&self) -> String { + let inner = &self.inner; + let path_str = PathSegment::fmt_path(&inner.path); + + match &inner.reason { + DebugInequalityReason::ValueMismatch { + lhs_display, + rhs_display, + } => { + format!( + "lhs{} != rhs{}: {} != {}", + path_str, path_str, lhs_display, rhs_display + ) + } + DebugInequalityReason::ValueLeafKindMismatch { lhs_kind, rhs_kind } => { + format!( + "lhs{} is {}, but rhs{} is {}", + path_str, + lhs_kind.articled_display_name(), + path_str, + rhs_kind.articled_display_name() + ) + } + DebugInequalityReason::RangeStructureMismatch { + lhs_structure: lhs_kind, + rhs_structure: rhs_kind, + } => { + format!( + "lhs{} is {}, but rhs{} is {}", + path_str, + lhs_kind.articled_display_name(), + path_str, + rhs_kind.articled_display_name() + ) + } + DebugInequalityReason::LengthMismatch { lhs_len, rhs_len } => { + format!( + "lhs{} has length {}, but rhs{} has length {}", + path_str, + lhs_len.unwrap_or(0), + path_str, + rhs_len.unwrap_or(0) + ) + } + DebugInequalityReason::MissingKey { key, missing_on } => match missing_on { + MissingSide::Lhs => { + format!( + "lhs{} is missing key {:?}, compared to rhs{}", + path_str, key, path_str + ) + } + MissingSide::Rhs => { + format!( + "lhs{} has extra key {:?}, compared to rhs{}", + path_str, key, path_str + ) + } + }, + } + } +} + +impl From for DebugEqualityError { + fn from(inner: DebugEqualityErrorInner) -> Self { + Self { + inner: Box::new(inner), + } + } +} + +/// Debug equality context - captures detailed information about why values differ. +/// This is useful for assertion failures where you want to show exactly where +/// the mismatch occurred. +pub(crate) struct DebugEquality { + path: Vec, +} + +impl DebugEquality { + pub(crate) fn new() -> Self { + Self { path: Vec::new() } + } +} + +impl EqualityContext for DebugEquality { + type Result = Result<(), DebugEqualityError>; + + #[inline] + fn values_equal(&mut self) -> Result<(), DebugEqualityError> { + Ok(()) + } + + #[inline] + fn leaf_values_not_equal( + &mut self, + lhs: &T, + rhs: &T, + ) -> Result<(), DebugEqualityError> { + Err(DebugEqualityErrorInner { + path: self.path.clone(), + reason: DebugInequalityReason::ValueMismatch { + lhs_display: format!("{:?}", lhs), + rhs_display: format!("{:?}", rhs), + }, + })? + } + + fn kind_mismatch( + &mut self, + lhs: &L, + rhs: &R, + ) -> Result<(), DebugEqualityError> { + Err(DebugEqualityErrorInner { + path: self.path.clone(), + reason: DebugInequalityReason::ValueLeafKindMismatch { + lhs_kind: lhs.value_kind(), + rhs_kind: rhs.value_kind(), + }, + })? + } + + fn range_structure_mismatch( + &mut self, + lhs: RangeStructure, + rhs: RangeStructure, + ) -> Result<(), DebugEqualityError> { + Err(DebugEqualityErrorInner { + path: self.path.clone(), + reason: DebugInequalityReason::RangeStructureMismatch { + lhs_structure: lhs, + rhs_structure: rhs, + }, + })? + } + + #[inline] + fn lengths_unequal( + &mut self, + lhs_len: Option, + rhs_len: Option, + ) -> Result<(), DebugEqualityError> { + Err(DebugEqualityErrorInner { + path: self.path.clone(), + reason: DebugInequalityReason::LengthMismatch { lhs_len, rhs_len }, + })? + } + + #[inline] + fn missing_key( + &mut self, + key: &str, + missing_on: MissingSide, + ) -> Result<(), DebugEqualityError> { + Err(DebugEqualityErrorInner { + path: self.path.clone(), + reason: DebugInequalityReason::MissingKey { + key: key.to_string(), + missing_on, + }, + })? + } + + #[inline] + fn with_array_index(&mut self, index: usize, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::ArrayIndex(index)); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn with_object_key(&mut self, key: &str, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::ObjectKey(key.to_string())); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn with_iterator_index(&mut self, index: usize, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::IteratorIndex(index)); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn with_range_start(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::RangeStart); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn with_range_end(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + self.path.push(PathSegment::RangeEnd); + let result = f(self); + self.path.pop(); + result + } + + #[inline] + fn should_short_circuit(&self, result: &Result<(), DebugEqualityError>) -> bool { + result.is_err() + } +} + +// ============================================================================ +// ValuesEqual trait - Value equality with configurable context +// ============================================================================ + +/// A trait for comparing values for equality with preinterpret semantics. +/// +/// This is NOT the same as Rust's `PartialEq`/`Eq` traits because: +/// - **Type coercion**: Untyped integers/floats can equal typed ones (e.g., `5 == 5u32`) +/// - **Structural comparison**: Arrays, objects, and iterators are compared element-wise +/// - **Token comparison**: Streams and unsupported literals compare via token string representation +/// - **Float semantics**: Floats use Rust's `==`, so `NaN != NaN` +/// +/// The comparison behavior is controlled by the `EqualityContext`: +/// - `SimpleEquality`: Returns `false` on type mismatch (like JS `===`) +/// - `TypedEquality`: Errors on type mismatch with path information +/// - `DebugEquality`: Returns detailed error info for assertion messages +pub(crate) trait ValuesEqual: Sized + HasLeafKind { + /// Compare two values for equality using the given context. + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result; + + /// Lenient equality - returns `false` for incompatible types instead of erroring. + /// Behaves like JavaScript's `===` operator. + fn lenient_eq(&self, other: &Self) -> bool { + self.test_equality(other, &mut SimpleEquality) + } + + /// Strict equality check that errors on incompatible types. + fn typed_eq(&self, other: &Self, error_span: SpanRange) -> ExecutionResult { + self.test_equality(other, &mut TypedEquality::new(error_span)) + } + + /// Debug equality check - returns detailed information about why values differ. + /// Useful for generating informative assertion failure messages. + fn debug_eq(&self, other: &Self) -> Result<(), DebugEqualityError> { + self.test_equality(other, &mut DebugEquality::new()) + } +} diff --git a/src/expressions/evaluation/assignment_frames.rs b/src/expressions/evaluation/assignment_frames.rs new file mode 100644 index 00000000..4d858b5c --- /dev/null +++ b/src/expressions/evaluation/assignment_frames.rs @@ -0,0 +1,335 @@ +use super::*; + +pub(crate) struct AssignmentCompletion; + +/// Handlers which return an AssignmentCompletion +pub(super) enum AnyAssignmentFrame { + Assignee(AssigneeAssigner), + Grouped(GroupedAssigner), + Array(ArrayBasedAssigner), + Object(Box), +} + +impl AnyAssignmentFrame { + pub(super) fn handle_next( + self, + context: Context, + value: Spanned, + ) -> ExecutionResult { + match self { + Self::Assignee(frame) => frame.handle_next(context, value), + Self::Grouped(frame) => frame.handle_next(context, value), + Self::Object(frame) => frame.handle_next(context, value), + Self::Array(frame) => frame.handle_next(context, value), + } + } +} + +struct PrivateUnit; + +pub(super) struct AssigneeAssigner { + value: AnyValue, +} + +impl AssigneeAssigner { + pub(super) fn start( + context: AssignmentContext, + assignee: ExpressionNodeId, + value: AnyValue, + ) -> NextAction { + let frame = Self { value }; + context.request_assignee(frame, assignee, true) + } +} + +impl EvaluationFrame for AssigneeAssigner { + type ReturnType = ReturnsAssignmentCompletion; + + fn into_any(self) -> AnyAssignmentFrame { + AnyAssignmentFrame::Assignee(self) + } + + fn handle_next( + self, + context: AssignmentContext, + Spanned(value, span): Spanned, + ) -> ExecutionResult { + let mut assignee = value.expect_assignee(); + let value = self.value; + assignee.set(value); + Ok(context.return_assignment_completion(span)) + } +} + +pub(super) struct GroupedAssigner(PrivateUnit); + +impl GroupedAssigner { + pub(super) fn start( + context: AssignmentContext, + inner: ExpressionNodeId, + value: AnyValue, + ) -> NextAction { + let frame = Self(PrivateUnit); + context.request_assignment(frame, inner, value) + } +} + +impl EvaluationFrame for GroupedAssigner { + type ReturnType = ReturnsAssignmentCompletion; + + fn into_any(self) -> AnyAssignmentFrame { + AnyAssignmentFrame::Grouped(self) + } + + fn handle_next( + self, + context: AssignmentContext, + Spanned(value, span): Spanned, + ) -> ExecutionResult { + let AssignmentCompletion = value.expect_assignment_completion(); + Ok(context.return_assignment_completion(span)) + } +} + +pub(super) struct ArrayBasedAssigner { + span_range: SpanRange, + assignee_stack: Vec<(ExpressionNodeId, AnyValue)>, +} + +impl ArrayBasedAssigner { + pub(super) fn start( + context: AssignmentContext, + nodes: &Arena, + brackets: &Brackets, + assignee_item_node_ids: &[ExpressionNodeId], + value: AnyValue, + ) -> ExecutionResult { + let frame = Self::new(nodes, brackets.join(), assignee_item_node_ids, value)?; + Ok(frame.handle_next_subassignment(context)) + } + + /// See also `ArrayPattern` in `patterns.rs` + fn new( + nodes: &Arena, + assignee_span: Span, + assignee_item_node_ids: &[ExpressionNodeId], + value: AnyValue, + ) -> ExecutionResult { + let span_range = assignee_span.span_range(); + let array: ArrayValue = + Spanned(value, span_range).resolve_as("The value destructured as an array")?; + let mut has_seen_dot_dot = false; + let mut prefix_assignees = Vec::new(); + let mut suffix_assignees = Vec::new(); + for node in assignee_item_node_ids.iter() { + match nodes.get(*node) { + ExpressionNode::Range { + left: None, + range_limits: syn::RangeLimits::HalfOpen(_), + right: None, + } => { + if has_seen_dot_dot { + return assignee_span + .syntax_err("Only one .. is allowed in an array assignee"); + } + has_seen_dot_dot = true; + } + _ => { + if has_seen_dot_dot { + suffix_assignees.push(*node); + } else { + prefix_assignees.push(*node); + } + } + } + } + + let array_length = array.items.len(); + + let mut assignee_pairs: Vec<_> = if has_seen_dot_dot { + let total_assignees = prefix_assignees.len() + suffix_assignees.len(); + if total_assignees > array_length { + return assignee_span.value_err(format!( + "The array has {} items, but the assignee expected at least {}", + array_length, total_assignees, + )); + } + let discarded_count = + array.items.len() - prefix_assignees.len() - suffix_assignees.len(); + let assignees = prefix_assignees + .into_iter() + .map(Some) + .chain(std::iter::repeat(None).take(discarded_count)) + .chain(suffix_assignees.into_iter().map(Some)); + + assignees + .zip(array.items) + .filter_map(|(assignee, value)| Some((assignee?, value))) + .collect() + } else { + let total_assignees = prefix_assignees.len(); + if total_assignees != array_length { + return assignee_span.value_err(format!( + "The array has {} items, but the assignee expected {}", + array_length, total_assignees, + )); + } + prefix_assignees.into_iter().zip(array.items).collect() + }; + Ok(Self { + span_range, + assignee_stack: { + assignee_pairs.reverse(); + assignee_pairs + }, + }) + } + + fn handle_next_subassignment(mut self, context: AssignmentContext) -> NextAction { + match self.assignee_stack.pop() { + Some((node, value)) => context.request_assignment(self, node, value), + None => context.return_assignment_completion(self.span_range), + } + } +} + +impl EvaluationFrame for ArrayBasedAssigner { + type ReturnType = ReturnsAssignmentCompletion; + + fn into_any(self) -> AnyAssignmentFrame { + AnyAssignmentFrame::Array(self) + } + + fn handle_next( + self, + context: AssignmentContext, + Spanned(value, _span): Spanned, + ) -> ExecutionResult { + let AssignmentCompletion = value.expect_assignment_completion(); + Ok(self.handle_next_subassignment(context)) + } +} + +pub(super) struct ObjectBasedAssigner { + span_range: SpanRange, + entries: BTreeMap, + already_used_keys: HashSet, + unresolved_stack: Vec<(ObjectKey, ExpressionNodeId)>, + state: ObjectAssignmentState, +} + +enum ObjectAssignmentState { + WaitingForSubassignment, + ResolvingIndex { + assignee_node: ExpressionNodeId, + access: IndexAccess, + }, +} + +impl ObjectBasedAssigner { + pub(super) fn start( + context: AssignmentContext, + braces: &Braces, + assignee_pairs: &[(ObjectKey, ExpressionNodeId)], + value: AnyValue, + ) -> ExecutionResult { + let frame = Box::new(Self::new(braces.join(), assignee_pairs, value)?); + frame.handle_next_subassignment(context) + } + + fn new( + assignee_span: Span, + assignee_pairs: &[(ObjectKey, ExpressionNodeId)], + value: AnyValue, + ) -> ExecutionResult { + let span_range = assignee_span.span_range(); + let object: ObjectValue = + Spanned(value, span_range).resolve_as("The value destructured as an object")?; + + Ok(Self { + span_range, + entries: object.entries, + already_used_keys: HashSet::with_capacity(assignee_pairs.len()), + unresolved_stack: assignee_pairs.iter().rev().cloned().collect(), + state: ObjectAssignmentState::WaitingForSubassignment, + }) + } + + fn handle_index_value( + mut self: Box, + context: AssignmentContext, + access: IndexAccess, + index: AnyValueRef, + assignee_node: ExpressionNodeId, + ) -> ExecutionResult { + let key: &str = index + .spanned(access.span_range()) + .downcast_resolve("An object key")?; + let value = self.resolve_value(key.to_string(), access.span())?; + Ok(context.request_assignment(self, assignee_node, value)) + } + + fn handle_next_subassignment( + mut self: Box, + context: AssignmentContext, + ) -> ExecutionResult { + Ok(match self.unresolved_stack.pop() { + Some((ObjectKey::Identifier(ident), assignee_node)) => { + let key = ident.to_string(); + let value = self.resolve_value(key, ident.span())?; + self.state = ObjectAssignmentState::WaitingForSubassignment; + context.request_assignment(self, assignee_node, value) + } + Some((ObjectKey::Indexed { index, access }, assignee_node)) => { + self.state = ObjectAssignmentState::ResolvingIndex { + assignee_node, + access, + }; + // This only needs to be read-only, as we are just using it to work out which field/s to assign + context.request_shared(self, index) + } + None => context.return_assignment_completion(self.span_range), + }) + } + + fn resolve_value(&mut self, key: String, key_span: Span) -> ExecutionResult { + if self.already_used_keys.contains(&key) { + return key_span.syntax_err(format!("The key `{}` has already used", key)); + } + let value = self + .entries + .remove(&key) + .map(|entry| entry.value) + .unwrap_or_else(|| ().into_any_value()); + self.already_used_keys.insert(key); + Ok(value) + } +} + +impl EvaluationFrame for Box { + type ReturnType = ReturnsAssignmentCompletion; + + fn into_any(self) -> AnyAssignmentFrame { + AnyAssignmentFrame::Object(self) + } + + fn handle_next( + self, + context: AssignmentContext, + Spanned(value, _span): Spanned, + ) -> ExecutionResult { + match self.state { + ObjectAssignmentState::ResolvingIndex { + assignee_node, + access, + } => { + let index_place = value.expect_shared(); + self.handle_index_value(context, access, index_place.as_ref_value(), assignee_node) + } + ObjectAssignmentState::WaitingForSubassignment => { + let AssignmentCompletion = value.expect_assignment_completion(); + self.handle_next_subassignment(context) + } + } + } +} diff --git a/src/expressions/evaluation/control_flow_analysis.rs b/src/expressions/evaluation/control_flow_analysis.rs new file mode 100644 index 00000000..e26f3782 --- /dev/null +++ b/src/expressions/evaluation/control_flow_analysis.rs @@ -0,0 +1,125 @@ +use super::*; + +pub(in super::super) fn control_flow_visit( + root: ExpressionNodeId, + nodes: &mut Arena, + context: FlowCapturer, +) -> ParseResult<()> { + let mut stack = ControlFlowStack::new(); + stack.push(root); + while let Some(node_id) = stack.pop() { + let node = nodes.get_mut(node_id); + match node { + ExpressionNode::Leaf(leaf) => { + leaf.control_flow_pass(context)?; + } + ExpressionNode::Grouped { inner, .. } => { + stack.push(*inner); + } + ExpressionNode::Array { items, .. } => { + stack.push_reversed(items.iter().copied()); + } + ExpressionNode::Object { entries, .. } => { + let mut nodes = vec![]; + for (key, node_id) in entries.iter() { + match key { + ObjectKey::Identifier(_) => {} + ObjectKey::Indexed { index, .. } => { + nodes.push(*index); + } + } + nodes.push(*node_id); + } + stack.push_reversed(nodes); + } + ExpressionNode::UnaryOperation { input, .. } => { + stack.push(*input); + } + ExpressionNode::BinaryOperation { + left_input, + right_input, + .. + } => { + stack.push_reversed([*left_input, *right_input]); + } + ExpressionNode::Property { node, .. } => { + stack.push(*node); + } + ExpressionNode::MethodCall { + node, parameters, .. + } => { + stack.push_reversed(parameters.iter().copied()); + stack.push(*node); // This is a stack so this executes first + } + ExpressionNode::Index { node, index, .. } => { + stack.push_reversed([*node, *index]); + } + ExpressionNode::Range { left, right, .. } => { + // This is a stack, so left is activated first + if let Some(right) = right { + stack.push(*right); + } + if let Some(left) = left { + stack.push(*left); + } + } + ExpressionNode::Assignment { + assignee, + equals_token: _, + value, + } => { + stack.push_reversed([*value, *assignee]); + } + } + } + Ok(()) +} + +struct ControlFlowStack { + nodes: Vec, +} + +impl ControlFlowStack { + fn new() -> Self { + Self { nodes: vec![] } + } + + fn push_reversed>( + &mut self, + node: impl IntoIterator, + ) { + self.nodes.extend(node.into_iter().rev()); + } + + fn push(&mut self, node: ExpressionNodeId) { + self.nodes.push(node); + } + + fn pop(&mut self) -> Option { + self.nodes.pop() + } +} + +impl Leaf { + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + match self { + Leaf::Variable(variable) => variable.control_flow_pass(context), + Leaf::TypeProperty(type_property) => type_property.control_flow_pass(context), + Leaf::Block(block) => block.control_flow_pass(context), + Leaf::StreamLiteral(stream_literal) => stream_literal.control_flow_pass(context), + Leaf::ParseTemplateLiteral(consume_literal) => { + consume_literal.control_flow_pass(context) + } + Leaf::IfExpression(if_expression) => if_expression.control_flow_pass(context), + Leaf::LoopExpression(loop_expression) => loop_expression.control_flow_pass(context), + Leaf::WhileExpression(while_expression) => while_expression.control_flow_pass(context), + Leaf::ForExpression(for_expression) => for_expression.control_flow_pass(context), + Leaf::AttemptExpression(attempt_expression) => { + attempt_expression.control_flow_pass(context) + } + Leaf::ParseExpression(parse_expression) => parse_expression.control_flow_pass(context), + Leaf::Discarded(_) => Ok(()), + Leaf::Value(_) => Ok(()), + } + } +} diff --git a/src/expressions/evaluation/evaluator.rs b/src/expressions/evaluation/evaluator.rs new file mode 100644 index 00000000..0348ec2d --- /dev/null +++ b/src/expressions/evaluation/evaluator.rs @@ -0,0 +1,521 @@ +use super::*; + +/// Trait for types that can be evaluated to produce a value with span information. +/// +/// Types implementing this trait provide `evaluate()` which returns the raw value, +/// and get a default `evaluate_spanned()` implementation that wraps the result with the type's span. +pub(crate) trait Evaluate: HasSpanRange { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult; + + fn evaluate_spanned( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult> { + let value = self.evaluate(interpreter, ownership)?; + Ok(Spanned(value, self.span_range())) + } +} + +pub(in crate::expressions) struct ExpressionEvaluator<'a> { + nodes: &'a Arena, + stack: EvaluationStack, +} + +impl<'a> ExpressionEvaluator<'a> { + pub(in super::super) fn new(nodes: &'a Arena) -> Self { + Self { + nodes, + stack: EvaluationStack::new(), + } + } + + pub(in super::super) fn evaluate( + mut self, + root: ExpressionNodeId, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult> { + let mut next_action = NextActionInner::ReadNodeAsValue(root, ownership); + + loop { + match self.step(next_action, interpreter)? { + StepResult::Continue(continue_action) => { + next_action = continue_action.0; + } + StepResult::Return(value) => { + return Ok(value); + } + } + } + } + + fn step( + &mut self, + action: NextActionInner, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + Ok(StepResult::Continue(match action { + NextActionInner::ReadNodeAsValue(node, ownership) => { + self.nodes.get(node).handle_as_value(Context { + request: ownership, + interpreter, + stack: &mut self.stack, + })? + } + NextActionInner::ReadNodeAsAssignmentTarget(node, value) => { + self.nodes.get(node).handle_as_assignment_target( + Context { + stack: &mut self.stack, + interpreter, + request: (), + }, + self.nodes, + node, + value, + )? + } + NextActionInner::HandleReturnedValue(item) => { + let top_of_stack = match self.stack.handlers.pop() { + Some(top) => top, + None => { + return Ok(StepResult::Return(item)); + } + }; + top_of_stack.handle_next(interpreter, &mut self.stack, item)? + } + })) + } +} + +pub(super) struct EvaluationStack { + /// The stack of operations which are waiting to handle an item / value / assignment completion to continue execution + handlers: Vec, +} + +impl EvaluationStack { + pub(super) fn new() -> Self { + Self { + handlers: Vec::new(), + } + } +} + +pub(super) enum StepResult { + Continue(NextAction), + Return(Spanned), +} + +pub(super) struct NextAction(NextActionInner); + +impl NextAction { + fn return_requested(value: Spanned) -> Self { + NextActionInner::HandleReturnedValue(value).into() + } +} + +enum NextActionInner { + /// Enters an expression node to output a value + ReadNodeAsValue(ExpressionNodeId, RequestedOwnership), + // Enters an expression node for assignment purposes + // This covers atomic assignments and composite assignments + // (similar to patterns but for existing values/reassignments) + // let a = ["x", "y"]; let b; [a[1], .. b] = [1, 2, 3, 4] + ReadNodeAsAssignmentTarget(ExpressionNodeId, AnyValue), + HandleReturnedValue(Spanned), +} + +impl From for NextAction { + fn from(value: NextActionInner) -> Self { + Self(value) + } +} + +/// This value is the result of evaluating an expression, with the requested ownership applied +/// to the result. It should always be paired with a corresponding [`RequestedOwnership`] which +/// is used to produce it. +/// +/// See [`RequestedOwnership`] and [`ArgumentOwnership`] for more details on these different types. +/// +/// See also [`ReturnedValue`] which is used for returned values which don't necessarily yet +/// align with the requested ownership. +pub(crate) enum RequestedValue { + // RequestedOwnership::Concrete(_) + // ------------------------------- + Owned(AnyValueOwned), + Shared(AnyValueShared), + Mutable(AnyValueMutable), + CopyOnWrite(CopyOnWriteValue), + Assignee(AssigneeValue), + + // RequestedOwnership::LateBound + // ------------------------------- + LateBound(LateBoundValue), + + // Marks completion of an assignment frame + // --------------------------------------- + AssignmentCompletion(AssignmentCompletion), +} + +impl RequestedValue { + pub(crate) fn expect_owned(self) -> AnyValueOwned { + match self { + RequestedValue::Owned(value) => value, + _ => panic!("expect_owned() called on non-owned RequestedValue"), + } + } + + pub(crate) fn expect_shared(self) -> AnyValueShared { + match self { + RequestedValue::Shared(shared) => shared, + _ => panic!("expect_shared() called on non-shared RequestedValue"), + } + } + + pub(super) fn expect_assignee(self) -> AnyValueAssignee { + match self { + RequestedValue::Assignee(assignee) => assignee, + _ => panic!("expect_assignee() called on non-assignee RequestedValue"), + } + } + + pub(crate) fn expect_late_bound(self) -> LateBoundValue { + match self { + RequestedValue::LateBound(late_bound) => late_bound, + _ => panic!("expect_late_bound() called on non-late-bound RequestedValue"), + } + } + + pub(super) fn expect_assignment_completion(self) -> AssignmentCompletion { + match self { + RequestedValue::AssignmentCompletion(completion) => completion, + _ => panic!( + "expect_assignment_completion() called on non-assignment-completion RequestedValue" + ), + } + } + + pub(crate) fn expect_argument_value(self) -> ArgumentValue { + match self { + RequestedValue::Owned(value) => ArgumentValue::Owned(value), + RequestedValue::Mutable(mutable) => ArgumentValue::Mutable(mutable), + RequestedValue::Assignee(assignee) => ArgumentValue::Assignee(assignee), + RequestedValue::Shared(shared) => ArgumentValue::Shared(shared), + RequestedValue::CopyOnWrite(copy_on_write) => ArgumentValue::CopyOnWrite(copy_on_write), + RequestedValue::LateBound(_) | RequestedValue::AssignmentCompletion(_) => { + panic!("expect_resolved_value() called on non-value RequestedValue") + } + } + } + + /// Returns the leaf kind of the underlying value, for type resolution purposes. + pub(crate) fn value_kind(&self) -> AnyValueLeafKind { + match self { + RequestedValue::Owned(value) => value.value_kind(), + RequestedValue::Shared(shared) => shared.value_kind(), + RequestedValue::Mutable(mutable) => mutable.value_kind(), + RequestedValue::CopyOnWrite(cow) => cow.value_kind(), + RequestedValue::Assignee(assignee) => assignee.value_kind(), + RequestedValue::LateBound(late_bound) => late_bound.value_kind(), + RequestedValue::AssignmentCompletion(_) => { + panic!("value_kind() called on AssignmentCompletion") + } + } + } + + pub(crate) fn expect_any_value_and_map( + self, + map_shared: impl FnOnce(AnyValueShared) -> ExecutionResult, + map_mutable: impl FnOnce(AnyValueMutable) -> ExecutionResult, + map_owned: impl FnOnce(AnyValueOwned) -> ExecutionResult, + ) -> ExecutionResult { + Ok(match self { + RequestedValue::LateBound(late_bound) => { + RequestedValue::LateBound(late_bound.map_any(map_shared, map_mutable, map_owned)?) + } + RequestedValue::Owned(value) => RequestedValue::Owned(map_owned(value)?), + RequestedValue::Assignee(assignee) => { + RequestedValue::Assignee(Assignee(map_mutable(assignee.0)?)) + } + RequestedValue::Mutable(mutable) => RequestedValue::Mutable(map_mutable(mutable)?), + RequestedValue::Shared(shared) => RequestedValue::Shared(map_shared(shared)?), + RequestedValue::CopyOnWrite(cow) => { + RequestedValue::CopyOnWrite(cow.map(map_shared, map_owned)?) + } + RequestedValue::AssignmentCompletion(_) => { + panic!("expect_any_value_and_map() called on non-value RequestedValue") + } + }) + } +} + +#[allow(unused)] +impl Spanned { + #[inline] + pub(crate) fn expect_owned(self) -> Spanned { + self.map(|v| v.expect_owned()) + } + + #[inline] + pub(crate) fn expect_shared(self) -> Spanned { + self.map(|v| v.expect_shared()) + } + + #[inline] + pub(crate) fn expect_assignee(self) -> Spanned { + self.map(|v| v.expect_assignee()) + } + + #[inline] + pub(crate) fn expect_late_bound(self) -> Spanned { + self.map(|v| v.expect_late_bound()) + } + + #[inline] + pub(crate) fn expect_argument_value(self) -> Spanned { + self.map(|v| v.expect_argument_value()) + } + + #[inline] + pub(crate) fn expect_assignment_completion(self) -> Spanned { + self.map(|v| v.expect_assignment_completion()) + } +} + +/// See the [rust reference] for a good description of assignee vs place. +/// +/// [rust reference]: https://doc.rust-lang.org/reference/expressions.html#place-expressions-and-value-expressions +pub(super) enum AnyEvaluationHandler { + Value(AnyValueFrame, RequestedOwnership), + Assignment(AnyAssignmentFrame), +} + +impl AnyEvaluationHandler { + fn handle_next( + self, + interpreter: &mut Interpreter, + stack: &mut EvaluationStack, + value: Spanned, + ) -> ExecutionResult { + match self { + AnyEvaluationHandler::Value(handler, ownership) => handler.handle_next( + Context { + interpreter, + stack, + request: ownership, + }, + value, + ), + AnyEvaluationHandler::Assignment(handler) => handler.handle_next( + Context { + interpreter, + stack, + request: (), + }, + value, + ), + } + } +} + +pub(super) struct Context<'a, T: RequestedValueType> { + interpreter: &'a mut Interpreter, + stack: &'a mut EvaluationStack, + request: T::RequestConstraints, +} + +impl<'a, T: RequestedValueType> Context<'a, T> { + pub(super) fn request_owned>( + self, + handler: H, + node: ExpressionNodeId, + ) -> NextAction { + self.request_argument_value(handler, node, ArgumentOwnership::Owned) + } + + pub(super) fn request_shared>( + self, + handler: H, + node: ExpressionNodeId, + ) -> NextAction { + self.request_argument_value(handler, node, ArgumentOwnership::Shared) + } + + pub(super) fn request_assignee>( + self, + handler: H, + node: ExpressionNodeId, + auto_create: bool, + ) -> NextAction { + self.request_argument_value(handler, node, ArgumentOwnership::Assignee { auto_create }) + } + + pub(super) fn request_late_bound>( + self, + handler: H, + node: ExpressionNodeId, + ) -> NextAction { + self.request_any_value(handler, node, RequestedOwnership::LateBound) + } + + pub(super) fn request_any_value>( + self, + handler: H, + node: ExpressionNodeId, + requested_ownership: RequestedOwnership, + ) -> NextAction { + self.stack + .handlers + .push(T::into_unkinded_handler(handler.into_any(), self.request)); + NextActionInner::ReadNodeAsValue(node, requested_ownership).into() + } + + pub(super) fn request_argument_value>( + self, + handler: H, + node: ExpressionNodeId, + argument_ownership: ArgumentOwnership, + ) -> NextAction { + self.request_any_value(handler, node, argument_ownership.into()) + } + + pub(super) fn request_assignment>( + self, + handler: H, + node: ExpressionNodeId, + value: AnyValue, + ) -> NextAction { + self.stack + .handlers + .push(T::into_unkinded_handler(handler.into_any(), self.request)); + NextActionInner::ReadNodeAsAssignmentTarget(node, value).into() + } + + pub(super) fn interpreter(&mut self) -> &mut Interpreter { + self.interpreter + } +} + +pub(super) trait EvaluationFrame: Sized { + type ReturnType: RequestedValueType; + + fn into_any(self) -> ::AnyHandler; + + fn handle_next( + self, + context: Context, + value: Spanned, + ) -> ExecutionResult; +} + +pub(super) trait RequestedValueType { + type RequestConstraints; + type AnyHandler; + fn into_unkinded_handler( + handler: Self::AnyHandler, + request: Self::RequestConstraints, + ) -> AnyEvaluationHandler; +} + +pub(super) struct ReturnsValue; +pub(super) type ValueContext<'a> = Context<'a, ReturnsValue>; + +impl RequestedValueType for ReturnsValue { + type RequestConstraints = RequestedOwnership; + type AnyHandler = AnyValueFrame; + + fn into_unkinded_handler( + handler: Self::AnyHandler, + request: Self::RequestConstraints, + ) -> AnyEvaluationHandler { + AnyEvaluationHandler::Value(handler, request) + } +} + +impl<'a> Context<'a, ReturnsValue> { + pub(super) fn requested_ownership(&self) -> RequestedOwnership { + self.request + } + + pub(super) fn evaluate( + self, + f: impl FnOnce(&mut Interpreter, RequestedOwnership) -> ExecutionResult>, + ) -> ExecutionResult { + let value = f(self.interpreter, self.request)?; + self.return_not_necessarily_matching_requested(value) + } + + pub(super) fn return_late_bound( + self, + late_bound: Spanned, + ) -> ExecutionResult { + let value = self.request.map_from_late_bound(late_bound)?; + Ok(NextAction::return_requested(value)) + } + + pub(super) fn return_argument_value( + self, + value: Spanned, + ) -> ExecutionResult { + let value = self.request.map_from_argument(value)?; + Ok(NextAction::return_requested(value)) + } + + pub(super) fn return_returned_value( + self, + value: Spanned, + ) -> ExecutionResult { + let value = self.request.map_from_returned(value)?; + Ok(NextAction::return_requested(value)) + } + + /// Note: This doesn't assume that the requested ownership matches the value's ownership. + /// + /// This allows the value to come from `requested.replace_owned_with_copy_on_write()`, + /// and this resolver then maps back to the requested ownership. + /// See e.g. [`ValuePropertyAccessBuilder`]. + pub(super) fn return_not_necessarily_matching_requested( + self, + value: Spanned, + ) -> ExecutionResult { + let value = self.request.map_from_requested(value)?; + Ok(NextAction::return_requested(value)) + } + + pub(super) fn return_value( + self, + value: Spanned, + ) -> ExecutionResult { + let value = self + .request + .map_from_returned(value.try_map(|v| v.to_returned_value())?)?; + Ok(NextAction::return_requested(value)) + } +} + +pub(super) struct ReturnsAssignmentCompletion; + +pub(super) type AssignmentContext<'a> = Context<'a, ReturnsAssignmentCompletion>; + +impl RequestedValueType for ReturnsAssignmentCompletion { + type RequestConstraints = (); + type AnyHandler = AnyAssignmentFrame; + + fn into_unkinded_handler(handler: Self::AnyHandler, _request: ()) -> AnyEvaluationHandler { + AnyEvaluationHandler::Assignment(handler) + } +} + +impl<'a> Context<'a, ReturnsAssignmentCompletion> { + pub(super) fn return_assignment_completion(self, span_range: SpanRange) -> NextAction { + NextActionInner::HandleReturnedValue(Spanned( + RequestedValue::AssignmentCompletion(AssignmentCompletion), + span_range, + )) + .into() + } +} diff --git a/src/expressions/evaluation/mod.rs b/src/expressions/evaluation/mod.rs new file mode 100644 index 00000000..67a7ffc2 --- /dev/null +++ b/src/expressions/evaluation/mod.rs @@ -0,0 +1,12 @@ +mod assignment_frames; +mod control_flow_analysis; +mod evaluator; +mod node_conversion; +mod value_frames; + +use super::*; +use assignment_frames::*; +pub(super) use control_flow_analysis::*; +pub(super) use evaluator::*; +pub(crate) use evaluator::{Evaluate, RequestedValue}; +pub(crate) use value_frames::*; diff --git a/src/expressions/evaluation/node_conversion.rs b/src/expressions/evaluation/node_conversion.rs new file mode 100644 index 00000000..8ceb680d --- /dev/null +++ b/src/expressions/evaluation/node_conversion.rs @@ -0,0 +1,161 @@ +use super::*; + +impl ExpressionNode { + pub(super) fn handle_as_value( + &self, + mut context: Context, + ) -> ExecutionResult { + Ok(match self { + ExpressionNode::Leaf(leaf) => { + match leaf { + Leaf::Discarded(token) => { + return token.syntax_err("This cannot be used in a value expression."); + } + Leaf::Variable(variable) => match context.requested_ownership() { + RequestedOwnership::LateBound => { + let late_bound = variable.resolve_late_bound(context.interpreter())?; + context.return_late_bound(late_bound)? + } + RequestedOwnership::Concrete(ownership) => { + let resolved = + variable.resolve_concrete(context.interpreter(), ownership)?; + context + .return_argument_value(Spanned(resolved, variable.span_range()))? + } + }, + Leaf::TypeProperty(type_property) => { + context.evaluate(|_, ownership| type_property.resolve_spanned(ownership))? + } + Leaf::Block(block) => context.evaluate(|interpreter, ownership| { + block.evaluate_spanned(interpreter, ownership) + })?, + Leaf::Value(Spanned(value, span_range)) => { + // We return a freely clonable CopyOnWrite in order to delay the clone of the literal if it's not necessary + // This allows something like e.g. x[0][5][2] to only clone the innermost value instead of the full multi-dimensional array + let shared_cloned = Shared::clone(value); + let cow = CopyOnWriteValue::shared_in_place_of_owned(shared_cloned); + context.return_returned_value(Spanned( + ReturnedValue::CopyOnWrite(cow), + *span_range, + ))? + } + Leaf::StreamLiteral(stream_literal) => { + let span = stream_literal.span_range(); + let value = context + .interpreter() + .capture_output(|interpreter| stream_literal.interpret(interpreter))?; + context.return_value(Spanned(value, span))? + } + Leaf::ParseTemplateLiteral(consume_literal) => { + context.evaluate(|interpreter, ownership| { + consume_literal.evaluate_spanned(interpreter, ownership) + })? + } + Leaf::IfExpression(if_expression) => { + context.evaluate(|interpreter, ownership| { + if_expression.evaluate_spanned(interpreter, ownership) + })? + } + Leaf::LoopExpression(loop_expression) => { + context.evaluate(|interpreter, ownership| { + loop_expression.evaluate_spanned(interpreter, ownership) + })? + } + Leaf::WhileExpression(while_expression) => { + context.evaluate(|interpreter, ownership| { + while_expression.evaluate_spanned(interpreter, ownership) + })? + } + Leaf::ForExpression(for_expression) => { + context.evaluate(|interpreter, ownership| { + for_expression.evaluate_spanned(interpreter, ownership) + })? + } + Leaf::AttemptExpression(attempt_expression) => { + context.evaluate(|interpreter, ownership| { + attempt_expression.evaluate_spanned(interpreter, ownership) + })? + } + Leaf::ParseExpression(parse_expression) => { + context.evaluate(|interpreter, ownership| { + parse_expression.evaluate_spanned(interpreter, ownership) + })? + } + } + } + ExpressionNode::Grouped { delim_span, inner } => { + GroupBuilder::start(context, delim_span, *inner) + } + ExpressionNode::Array { brackets, items } => { + ArrayBuilder::start(context, brackets, items)? + } + ExpressionNode::Object { braces, entries } => { + ObjectBuilder::start(context, braces, entries)? + } + ExpressionNode::UnaryOperation { operation, input } => { + UnaryOperationBuilder::start(context, operation.clone(), *input) + } + ExpressionNode::BinaryOperation { + operation, + left_input, + right_input, + } => BinaryOperationBuilder::start(context, *operation, *left_input, *right_input), + ExpressionNode::Property { node, access } => { + ValuePropertyAccessBuilder::start(context, access.clone(), *node) + } + ExpressionNode::Index { + node, + access, + index, + } => ValueIndexAccessBuilder::start(context, *node, *access, *index), + ExpressionNode::Range { + left, + range_limits, + right, + } => RangeBuilder::start(context, left, range_limits, right)?, + ExpressionNode::Assignment { + assignee, + equals_token, + value, + } => AssignmentBuilder::start(context, *assignee, *equals_token, *value), + ExpressionNode::MethodCall { + node, + method, + parameters, + } => MethodCallBuilder::start(context, *node, method.clone(), parameters), + }) + } + + pub(super) fn handle_as_assignment_target( + &self, + context: AssignmentContext, + nodes: &Arena, + self_node_id: ExpressionNodeId, + // NB: This might intrisically be a part of a larger value, and might have been + // created many lines previously, so doesn't have an obvious span associated with it + // Instead, we put errors on the assignee syntax side + value: AnyValue, + ) -> ExecutionResult { + Ok(match self { + ExpressionNode::Leaf(Leaf::Discarded(underscore)) => { + context.return_assignment_completion(underscore.span_range()) + } + ExpressionNode::Array { + brackets, + items: assignee_item_node_ids, + } => { + ArrayBasedAssigner::start(context, nodes, brackets, assignee_item_node_ids, value)? + } + ExpressionNode::Object { braces, entries } => { + ObjectBasedAssigner::start(context, braces, entries, value)? + } + ExpressionNode::Grouped { inner, .. } => GroupedAssigner::start(context, *inner, value), + // This handles: + // - Standard Variable assignment + // - Property assignment (allowing for creation of fields) + // - Index assignment (allowing for creation of keys) + // - Assignment to any mutable value (e.g. x.as_mut()) + _ => AssigneeAssigner::start(context, self_node_id, value), + }) + } +} diff --git a/src/expressions/evaluation/value_frames.rs b/src/expressions/evaluation/value_frames.rs new file mode 100644 index 00000000..c0debd2a --- /dev/null +++ b/src/expressions/evaluation/value_frames.rs @@ -0,0 +1,1426 @@ +use super::*; + +/// A [`ArgumentValue`] represents a value which has had its ownership concretely +/// resolved for use in a specific operation (e.g. method call, property access, etc). +/// +/// It is typically paired with an [`ArgumentOwnership`] (maybe wrapped in a [`RequestedOwnership`]) +/// which indicates what ownership type to resolve to. +pub(crate) enum ArgumentValue { + Owned(AnyValueOwned), + CopyOnWrite(CopyOnWriteValue), + Mutable(AnyValueMutable), + Assignee(AnyValueAssignee), + Shared(AnyValueShared), +} + +impl ArgumentValue { + pub(crate) fn expect_owned(self) -> AnyValueOwned { + match self { + ArgumentValue::Owned(value) => value, + _ => panic!("expect_owned() called on a non-owned ArgumentValue"), + } + } + + pub(crate) fn expect_copy_on_write(self) -> CopyOnWriteValue { + match self { + ArgumentValue::CopyOnWrite(value) => value, + _ => panic!("expect_copy_on_write() called on a non-copy-on-write ArgumentValue"), + } + } + + pub(crate) fn expect_mutable(self) -> AnyValueMutable { + match self { + ArgumentValue::Mutable(value) => value, + _ => panic!("expect_mutable() called on a non-mutable ArgumentValue"), + } + } + + pub(crate) fn expect_assignee(self) -> AnyValueAssignee { + match self { + ArgumentValue::Assignee(value) => value, + _ => panic!("expect_assignee() called on a non-assignee ArgumentValue"), + } + } + + pub(crate) fn expect_shared(self) -> AnyValueShared { + match self { + ArgumentValue::Shared(value) => value, + _ => panic!("expect_shared() called on a non-shared ArgumentValue"), + } + } +} + +impl Spanned { + #[inline] + pub(crate) fn expect_owned(self) -> Spanned { + self.map(|value| value.expect_owned()) + } + + #[inline] + pub(crate) fn expect_mutable(self) -> Spanned { + self.map(|value| value.expect_mutable()) + } + + #[inline] + pub(crate) fn expect_assignee(self) -> Spanned { + self.map(|value| value.expect_assignee()) + } + + #[inline] + pub(crate) fn expect_shared(self) -> Spanned { + self.map(|value| value.expect_shared()) + } +} + +// Note: ArgumentValue no longer implements HasSpanRange or WithSpanRangeExt +// since the inner value types no longer carry spans internally. +// Spans should be tracked separately at a higher level if needed. + +impl Spanned<&mut ArgumentValue> { + /// SAFETY: + /// * Must be paired with a call to `enable()` before any further use of the value. + /// * Must not use the value while disabled. + pub(crate) unsafe fn disable(&mut self) { + match &mut self.0 { + ArgumentValue::Owned(_) => {} + ArgumentValue::CopyOnWrite(copy_on_write) => copy_on_write.spanned(self.1).disable(), + ArgumentValue::Mutable(mutable) => mutable.spanned(self.1).disable(), + ArgumentValue::Assignee(assignee) => (&mut assignee.0).spanned(self.1).disable(), + ArgumentValue::Shared(shared) => shared.spanned(self.1).disable(), + } + } + + /// SAFETY: + /// * Must only be used after a call to `disable()`. + /// + /// Returns an ownership error if re-enabling fails (e.g., due to conflicting borrows). + pub(crate) unsafe fn enable(&mut self) -> ExecutionResult<()> { + match &mut self.0 { + ArgumentValue::Owned(_) => Ok(()), + ArgumentValue::CopyOnWrite(copy_on_write) => copy_on_write.spanned(self.1).enable(), + ArgumentValue::Mutable(mutable) => mutable.spanned(self.1).enable(), + ArgumentValue::Assignee(assignee) => (&mut assignee.0).spanned(self.1).enable(), + ArgumentValue::Shared(shared) => shared.spanned(self.1).enable(), + } + } +} + +impl Deref for ArgumentValue { + type Target = AnyValue; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl AsMut for ArgumentValue { + fn as_mut(&mut self) -> &mut Self { + self + } +} + +impl AsRef for ArgumentValue { + fn as_ref(&self) -> &AnyValue { + match self { + ArgumentValue::Owned(owned) => owned, + ArgumentValue::Mutable(mutable) => mutable, + ArgumentValue::Assignee(assignee) => &assignee.0, + ArgumentValue::Shared(shared) => shared, + ArgumentValue::CopyOnWrite(copy_on_write) => copy_on_write, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum RequestedOwnership { + /// Receives any of Owned, SharedReference or MutableReference, depending on what + /// is available. + /// This can then be used to resolve the value kind, and use the correct one. + LateBound, + /// A concrete value of the correct type. + Concrete(ArgumentOwnership), +} + +impl RequestedOwnership { + pub(crate) fn owned() -> Self { + RequestedOwnership::Concrete(ArgumentOwnership::Owned) + } + + pub(crate) fn shared() -> Self { + RequestedOwnership::Concrete(ArgumentOwnership::Shared) + } + + pub(crate) fn replace_owned_with_copy_on_write(self) -> Self { + match self { + RequestedOwnership::Concrete(ArgumentOwnership::Owned) => { + RequestedOwnership::Concrete(ArgumentOwnership::CopyOnWrite) + } + _ => self, + } + } + + pub(crate) fn requests_auto_create(&self) -> bool { + match self { + RequestedOwnership::Concrete(ArgumentOwnership::Assignee { auto_create }) => { + *auto_create + } + _ => false, + } + } + + pub(crate) fn map_none(self, span: SpanRange) -> ExecutionResult> { + self.map_from_owned(Spanned(().into_any_value(), span)) + } + + pub(crate) fn map_from_late_bound( + &self, + Spanned(late_bound, span): Spanned, + ) -> ExecutionResult> { + Ok(Spanned( + match self { + RequestedOwnership::LateBound => RequestedValue::LateBound(late_bound), + RequestedOwnership::Concrete(_) => { + panic!("Returning a late-bound reference when concrete ownership was requested") + } + }, + span, + )) + } + + pub(crate) fn map_from_argument( + &self, + Spanned(value, span): Spanned, + ) -> ExecutionResult> { + match value { + ArgumentValue::Owned(owned) => self.map_from_owned(Spanned(owned, span)), + ArgumentValue::Mutable(mutable) => self.map_from_mutable(Spanned(mutable, span)), + ArgumentValue::Assignee(assignee) => self.map_from_assignee(Spanned(assignee, span)), + ArgumentValue::Shared(shared) => self.map_from_shared(Spanned(shared, span)), + ArgumentValue::CopyOnWrite(copy_on_write) => { + self.map_from_copy_on_write(Spanned(copy_on_write, span)) + } + } + } + + pub(crate) fn map_from_returned( + &self, + Spanned(value, span): Spanned, + ) -> ExecutionResult> { + match value { + ReturnedValue::Owned(owned) => self.map_from_owned(Spanned(owned, span)), + ReturnedValue::Mutable(mutable) => self.map_from_mutable(Spanned(mutable, span)), + ReturnedValue::Shared(shared) => self.map_from_shared(Spanned(shared, span)), + ReturnedValue::CopyOnWrite(copy_on_write) => { + self.map_from_copy_on_write(Spanned(copy_on_write, span)) + } + } + } + + /// This ensures the requested value's type aligns with the requested ownership. + pub(crate) fn map_from_requested( + &self, + Spanned(requested, span): Spanned, + ) -> ExecutionResult> { + match requested { + RequestedValue::Owned(owned) => self.map_from_owned(Spanned(owned, span)), + RequestedValue::Shared(shared) => self.map_from_shared(Spanned(shared, span)), + RequestedValue::Mutable(mutable) => self.map_from_mutable(Spanned(mutable, span)), + RequestedValue::Assignee(assignee) => self.map_from_assignee(Spanned(assignee, span)), + RequestedValue::LateBound(late_bound_value) => { + self.map_from_late_bound(Spanned(late_bound_value, span)) + } + RequestedValue::CopyOnWrite(copy_on_write) => { + self.map_from_copy_on_write(Spanned(copy_on_write, span)) + } + RequestedValue::AssignmentCompletion { .. } => { + panic!("Returning a non-value item from a value context") + } + } + } + + pub(crate) fn map_from_owned( + &self, + Spanned(value, span): Spanned, + ) -> ExecutionResult> { + Ok(Spanned( + match self { + RequestedOwnership::LateBound => { + RequestedValue::LateBound(LateBoundValue::Owned(LateBoundOwnedValue { + owned: value, + is_from_last_use: false, + })) + } + RequestedOwnership::Concrete(requested) => { + Self::item_from_argument(requested.map_from_owned(Spanned(value, span))?) + } + }, + span, + )) + } + + pub(crate) fn map_from_copy_on_write( + &self, + Spanned(cow, span): Spanned, + ) -> ExecutionResult> { + Ok(Spanned( + match self { + RequestedOwnership::LateBound => { + RequestedValue::LateBound(LateBoundValue::CopyOnWrite(cow)) + } + RequestedOwnership::Concrete(requested) => { + Self::item_from_argument(requested.map_from_copy_on_write(Spanned(cow, span))?) + } + }, + span, + )) + } + + pub(crate) fn map_from_mutable( + &self, + Spanned(mutable, span): Spanned, + ) -> ExecutionResult> { + Ok(Spanned( + match self { + RequestedOwnership::LateBound => { + RequestedValue::LateBound(LateBoundValue::Mutable(mutable)) + } + RequestedOwnership::Concrete(requested) => { + Self::item_from_argument(requested.map_from_mutable(Spanned(mutable, span))?) + } + }, + span, + )) + } + + pub(crate) fn map_from_assignee( + &self, + Spanned(assignee, span): Spanned, + ) -> ExecutionResult> { + Ok(Spanned( + match self { + RequestedOwnership::LateBound => { + RequestedValue::LateBound(LateBoundValue::Mutable(assignee.0)) + } + RequestedOwnership::Concrete(requested) => { + Self::item_from_argument(requested.map_from_assignee(Spanned(assignee, span))?) + } + }, + span, + )) + } + + pub(crate) fn map_from_shared( + &self, + Spanned(shared, span): Spanned, + ) -> ExecutionResult> { + Ok(Spanned( + match self { + RequestedOwnership::LateBound => RequestedValue::LateBound( + LateBoundValue::CopyOnWrite(CopyOnWrite::shared_in_place_of_shared(shared)), + ), + RequestedOwnership::Concrete(requested) => { + Self::item_from_argument(requested.map_from_shared(Spanned(shared, span))?) + } + }, + span, + )) + } + + fn item_from_argument(value: ArgumentValue) -> RequestedValue { + match value { + ArgumentValue::Owned(owned) => RequestedValue::Owned(owned), + ArgumentValue::Mutable(mutable) => RequestedValue::Mutable(mutable), + ArgumentValue::Assignee(assignee) => RequestedValue::Assignee(assignee), + ArgumentValue::Shared(shared) => RequestedValue::Shared(shared), + ArgumentValue::CopyOnWrite(copy_on_write) => RequestedValue::CopyOnWrite(copy_on_write), + } + } +} + +impl From for RequestedOwnership { + fn from(ownership: ArgumentOwnership) -> Self { + RequestedOwnership::Concrete(ownership) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// The ownership that a method might concretely request +pub(crate) enum ArgumentOwnership { + Owned, + Shared, + Mutable, + /// Approximately equivalent to Mutable, but explicitly for use as an assignee. + /// (e.g. `x[0] = 1` or `obj.field = 2` or `x.swap(y)`) + /// In rust, an assignee is a special type of place, and if you want to assign + /// to a mutable reference, you have to explicitly dereference it with *. + /// (Under Niko's Overwrite proposal, this would require that the value is also + /// Overwrite). + /// + /// The distinction from mutable allows for different handling in ownership conversion. + /// For example, whilst an owned value can be freely converted to a mutable reference + /// for use in a method, it cannot be freely converted to an assignee. + /// This prevents (1 = 2) = 3 style issues, where it would be insane to allow assignment + /// to a floating owned value. + /// + /// The auto_create flag indicates whether the assignee should create missing entries, + /// for example, it enables `obj.new_field = value` to work by auto-creating `new_field` in `obj`. + Assignee { + auto_create: bool, + }, + /// Approximately equivalent to Owned, but more flexible to avoid cloning large values unnecessarily + /// e.g. array indexing operations should take CopyOnWrite instead of Owned + /// If a method needs to create an owned value, that method can transparently or infallibly copy it, + /// as per the method's own requirements/expectations. + CopyOnWrite, + /// A niche resolved value which passes through the value uncoerced, for + /// handling in the method itself - notably this is used in the `.as_mut()` method. + /// + /// Currently this is handled as a ArgumentValue, but it might be better to handle + /// it as LateBound (so that we don't drop the shared-conversion error reason). + AsIs, +} + +impl ArgumentOwnership { + pub(crate) fn map_from_late_bound( + &self, + Spanned(late_bound, span): Spanned, + ) -> ExecutionResult { + match late_bound { + LateBoundValue::Owned(owned) => self.map_from_owned_with_is_last_use( + Spanned(owned.owned, span), + owned.is_from_last_use, + ), + LateBoundValue::CopyOnWrite(copy_on_write) => { + self.map_from_copy_on_write(Spanned(copy_on_write, span)) + } + LateBoundValue::Mutable(mutable) => { + self.map_from_mutable_inner(Spanned(mutable, span), true) + } + LateBoundValue::Shared(late_bound_shared) => self + .map_from_shared_with_error_reason(Spanned(late_bound_shared.shared, span), |_| { + ExecutionInterrupt::ownership_error(late_bound_shared.reason_not_mutable) + }), + } + } + + pub(crate) fn map_from_copy_on_write( + &self, + Spanned(copy_on_write, span): Spanned, + ) -> ExecutionResult { + match self { + ArgumentOwnership::Owned => Ok(ArgumentValue::Owned( + copy_on_write.clone_to_owned_transparently(span)?, + )), + ArgumentOwnership::Shared => Ok(ArgumentValue::Shared(copy_on_write.into_shared())), + ArgumentOwnership::Mutable => { + if copy_on_write.acts_as_shared_reference() { + span.ownership_err("A mutable reference is required, but a shared reference was received, this indicates a possible bug as the updated value won't be accessible. To proceed regardless, use `.clone()` to get a mutable reference to a cloned value.") + } else { + Ok(ArgumentValue::Mutable(Mutable::new_from_owned( + copy_on_write.clone_to_owned_transparently(span)?, + ))) + } + } + ArgumentOwnership::Assignee { .. } => { + if copy_on_write.acts_as_shared_reference() { + span.ownership_err("A shared reference cannot be assigned to.") + } else { + span.ownership_err("An owned value cannot be assigned to.") + } + } + ArgumentOwnership::CopyOnWrite | ArgumentOwnership::AsIs => { + Ok(ArgumentValue::CopyOnWrite(copy_on_write)) + } + } + } + + pub(crate) fn map_from_shared( + &self, + shared: Spanned, + ) -> ExecutionResult { + self.map_from_shared_with_error_reason( + shared, + |span| span.ownership_error("A mutable reference is required, but a shared reference was received, this indicates a possible bug as the updated value won't be accessible. To proceed regardless, use `.clone().as_mut()` to get a mutable reference."), + ) + } + + fn map_from_shared_with_error_reason( + &self, + Spanned(shared, span): Spanned, + mutable_error: impl FnOnce(SpanRange) -> ExecutionInterrupt, + ) -> ExecutionResult { + match self { + ArgumentOwnership::Owned => Ok(ArgumentValue::Owned( + Spanned(shared, span).transparent_clone()?, + )), + ArgumentOwnership::CopyOnWrite => Ok(ArgumentValue::CopyOnWrite( + CopyOnWrite::shared_in_place_of_shared(shared), + )), + ArgumentOwnership::Assignee { .. } => Err(mutable_error(span)), + ArgumentOwnership::Mutable => Err(mutable_error(span)), + ArgumentOwnership::Shared | ArgumentOwnership::AsIs => { + Ok(ArgumentValue::Shared(shared)) + } + } + } + + pub(crate) fn map_from_mutable( + &self, + spanned_mutable: Spanned, + ) -> ExecutionResult { + self.map_from_mutable_inner(spanned_mutable, false) + } + + pub(crate) fn map_from_assignee( + &self, + Spanned(assignee, span): Spanned, + ) -> ExecutionResult { + self.map_from_mutable_inner(Spanned(assignee.0, span), false) + } + + fn map_from_mutable_inner( + &self, + Spanned(mutable, span): Spanned, + is_late_bound: bool, + ) -> ExecutionResult { + match self { + ArgumentOwnership::Owned => { + if is_late_bound { + Ok(ArgumentValue::Owned( + Spanned(mutable, span).transparent_clone()?, + )) + } else { + span.ownership_err("An owned value is required, but a mutable reference was received. This indicates a possible bug. If this was intended, use `.clone()` to get an owned value.") + } + } + ArgumentOwnership::CopyOnWrite => Ok(ArgumentValue::CopyOnWrite( + CopyOnWrite::shared_in_place_of_shared(mutable.into_shared()), + )), + ArgumentOwnership::Mutable | ArgumentOwnership::AsIs => { + Ok(ArgumentValue::Mutable(mutable)) + } + ArgumentOwnership::Assignee { .. } => Ok(ArgumentValue::Assignee(Assignee(mutable))), + ArgumentOwnership::Shared => Ok(ArgumentValue::Shared(mutable.into_shared())), + } + } + + pub(crate) fn map_from_owned( + &self, + owned: Spanned, + ) -> ExecutionResult { + self.map_from_owned_with_is_last_use(owned, false) + } + + fn map_from_owned_with_is_last_use( + &self, + Spanned(owned, span): Spanned, + is_from_last_use: bool, + ) -> ExecutionResult { + match self { + ArgumentOwnership::Owned | ArgumentOwnership::AsIs => Ok(ArgumentValue::Owned(owned)), + ArgumentOwnership::CopyOnWrite => { + Ok(ArgumentValue::CopyOnWrite(CopyOnWrite::owned(owned))) + } + ArgumentOwnership::Mutable => { + Ok(ArgumentValue::Mutable(Mutable::new_from_owned(owned))) + } + ArgumentOwnership::Assignee { .. } => { + if is_from_last_use { + span.ownership_err("The final usage of a variable cannot be assigned to. You can use `let _ = ..` to discard a value.") + } else { + span.ownership_err("An owned value cannot be assigned to.") + } + } + ArgumentOwnership::Shared => Ok(ArgumentValue::Shared(Shared::new_from_owned(owned))), + } + } +} + +/// Handlers which return a Value +pub(super) enum AnyValueFrame { + Group(GroupBuilder), + Array(ArrayBuilder), + Object(Box), + UnaryOperation(UnaryOperationBuilder), + BinaryOperation(BinaryOperationBuilder), + PropertyAccess(ValuePropertyAccessBuilder), + IndexAccess(ValueIndexAccessBuilder), + Range(RangeBuilder), + Assignment(AssignmentBuilder), + MethodCall(MethodCallBuilder), +} + +impl AnyValueFrame { + pub(super) fn handle_next( + self, + context: Context, + value: Spanned, + ) -> ExecutionResult { + match self { + AnyValueFrame::Group(frame) => frame.handle_next(context, value), + AnyValueFrame::Array(frame) => frame.handle_next(context, value), + AnyValueFrame::Object(frame) => frame.handle_next(context, value), + AnyValueFrame::UnaryOperation(frame) => frame.handle_next(context, value), + AnyValueFrame::BinaryOperation(frame) => frame.handle_next(context, value), + AnyValueFrame::PropertyAccess(frame) => frame.handle_next(context, value), + AnyValueFrame::IndexAccess(frame) => frame.handle_next(context, value), + AnyValueFrame::Range(frame) => frame.handle_next(context, value), + AnyValueFrame::Assignment(frame) => frame.handle_next(context, value), + AnyValueFrame::MethodCall(frame) => frame.handle_next(context, value), + } + } +} + +pub(super) struct GroupBuilder { + span: Span, +} + +impl GroupBuilder { + pub(super) fn start( + context: ValueContext, + delim_span: &DelimSpan, + inner: ExpressionNodeId, + ) -> NextAction { + let frame = Self { + span: delim_span.join(), + }; + context.request_owned(frame, inner) + } +} + +impl EvaluationFrame for GroupBuilder { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Group(self) + } + + fn handle_next( + self, + context: ValueContext, + Spanned(value, _span): Spanned, + ) -> ExecutionResult { + let inner = value.expect_owned(); + // Use the grouped expression's span, not the inner expression's span + context.return_value(Spanned(inner, self.span.span_range())) + } +} + +pub(super) struct ArrayBuilder { + span: Span, + unevaluated_items: Vec, + evaluated_items: Vec, +} + +impl ArrayBuilder { + pub(super) fn start( + context: ValueContext, + brackets: &Brackets, + items: &[ExpressionNodeId], + ) -> ExecutionResult { + Self { + span: brackets.join(), + unevaluated_items: items.to_vec(), + evaluated_items: Vec::with_capacity(items.len()), + } + .next(context) + } + + pub(super) fn next(self, context: ValueContext) -> ExecutionResult { + Ok( + match self + .unevaluated_items + .get(self.evaluated_items.len()) + .cloned() + { + Some(next) => context.request_owned(self, next), + None => { + context.return_value(Spanned(self.evaluated_items, self.span.span_range()))? + } + }, + ) + } +} + +impl EvaluationFrame for ArrayBuilder { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Array(self) + } + + fn handle_next( + mut self, + context: ValueContext, + Spanned(value, _span): Spanned, + ) -> ExecutionResult { + let value = value.expect_owned(); + self.evaluated_items.push(value); + self.next(context) + } +} + +pub(super) struct ObjectBuilder { + span: Span, + pending: Option, + unevaluated_entries: Vec<(ObjectKey, ExpressionNodeId)>, + evaluated_entries: BTreeMap, +} + +enum PendingEntryPath { + OnIndexKeyBranch { + access: IndexAccess, + value_node: ExpressionNodeId, + }, + OnValueBranch { + key: String, + key_span: Span, + }, +} + +impl ObjectBuilder { + pub(super) fn start( + context: ValueContext, + braces: &Braces, + entries: &[(ObjectKey, ExpressionNodeId)], + ) -> ExecutionResult { + Box::new(Self { + span: braces.join(), + unevaluated_entries: entries.to_vec(), + evaluated_entries: BTreeMap::new(), + pending: None, + }) + .next(context) + } + + fn next(mut self: Box, context: ValueContext) -> ExecutionResult { + Ok( + match self + .unevaluated_entries + .get(self.evaluated_entries.len()) + .cloned() + { + Some((ObjectKey::Identifier(ident), value_node)) => { + let key = ident.to_string(); + if self.evaluated_entries.contains_key(&key) { + return ident.syntax_err(format!("The key {} has already been set", key)); + } + self.pending = Some(PendingEntryPath::OnValueBranch { + key, + key_span: ident.span(), + }); + context.request_owned(self, value_node) + } + Some((ObjectKey::Indexed { access, index }, value_node)) => { + self.pending = Some(PendingEntryPath::OnIndexKeyBranch { access, value_node }); + context.request_owned(self, index) + } + None => { + context.return_value(Spanned(self.evaluated_entries, self.span.span_range()))? + } + }, + ) + } +} + +impl EvaluationFrame for Box { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Object(self) + } + + fn handle_next( + mut self, + context: ValueContext, + Spanned(value, span): Spanned, + ) -> ExecutionResult { + let pending = self.pending.take(); + Ok(match pending { + Some(PendingEntryPath::OnIndexKeyBranch { access, value_node }) => { + let value = value.expect_owned(); + let key: String = Spanned(value, span).resolve_as("An object key")?; + if self.evaluated_entries.contains_key(&key) { + return access.syntax_err(format!("The key {} has already been set", key)); + } + self.pending = Some(PendingEntryPath::OnValueBranch { + key, + key_span: access.span(), + }); + context.request_owned(self, value_node) + } + Some(PendingEntryPath::OnValueBranch { key, key_span }) => { + let value = value.expect_owned(); + let entry = ObjectEntry { key_span, value }; + self.evaluated_entries.insert(key, entry); + self.next(context)? + } + None => { + unreachable!("Should not receive a value without a pending handler set") + } + }) + } +} + +pub(super) struct UnaryOperationBuilder { + operation: UnaryOperation, +} + +impl UnaryOperationBuilder { + pub(super) fn start( + context: ValueContext, + operation: UnaryOperation, + input: ExpressionNodeId, + ) -> NextAction { + let frame = Self { operation }; + // Use late-bound evaluation to allow method resolution to determine ownership requirements + context.request_late_bound(frame, input) + } +} + +impl EvaluationFrame for UnaryOperationBuilder { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::UnaryOperation(self) + } + + fn handle_next( + self, + context: ValueContext, + operand: Spanned, + ) -> ExecutionResult { + let operand = operand.expect_late_bound(); + let operand_kind = operand.kind(); + + // Try method resolution first + if let Some(interface) = operand_kind + .feature_resolver() + .resolve_unary_operation(&self.operation) + { + let operand_span = operand.span_range(); + let resolved_value = operand.resolve(interface.argument_ownership())?; + let result = + interface.execute(Spanned(resolved_value, operand_span), &self.operation)?; + return context.return_returned_value(result); + } + self.operation.type_err(format!( + "The {} operator is not supported for {}", + self.operation, + operand.articled_kind(), + )) + } +} + +pub(super) struct BinaryOperationBuilder { + operation: BinaryOperation, + state: BinaryPath, +} + +enum BinaryPath { + OnLeftBranch { + right: ExpressionNodeId, + }, + OnRightBranch { + left: Spanned, + interface: BinaryOperationInterface, + }, +} + +impl BinaryOperationBuilder { + pub(super) fn start( + context: ValueContext, + operation: BinaryOperation, + left: ExpressionNodeId, + right: ExpressionNodeId, + ) -> NextAction { + let frame = Self { + operation, + state: BinaryPath::OnLeftBranch { right }, + }; + // Use late-bound evaluation to allow method resolution to determine ownership requirements + context.request_late_bound(frame, left) + } +} + +impl EvaluationFrame for BinaryOperationBuilder { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::BinaryOperation(self) + } + + fn handle_next( + mut self, + context: ValueContext, + value: Spanned, + ) -> ExecutionResult { + Ok(match self.state { + BinaryPath::OnLeftBranch { right } => { + let Spanned(left, left_span) = value.expect_late_bound(); + + // Check for lazy evaluation first (short-circuit operators) + // Use operator span for type errors since the error is about the operation's requirements + let left_value = Spanned(left.as_value(), left_span); + if let Some(result) = self.operation.lazy_evaluate(left_value)? { + // For short-circuit, the result span is just the left operand's span + // (the right operand was never evaluated) + context + .return_returned_value(Spanned(ReturnedValue::Owned(result), left_span))? + } else { + // Try method resolution based on left operand's kind and resolve left operand immediately + let interface = left + .kind() + .feature_resolver() + .resolve_binary_operation(&self.operation); + + match interface { + Some(interface) => { + let rhs_ownership = interface.rhs_ownership(); + let left = interface + .lhs_ownership() + .map_from_late_bound(Spanned(left, left_span))?; + let mut left = Spanned(left, left_span); + + unsafe { + // SAFETY: We re-enable it below and don't use it while disabled + left.to_mut().disable(); + } + + self.state = BinaryPath::OnRightBranch { left, interface }; + context.request_argument_value(self, right, rhs_ownership) + } + None => { + return self.operation.type_err(format!( + "The {} operator is not supported for {} operand", + self.operation.symbolic_description(), + left.articled_kind(), + )); + } + } + } + } + BinaryPath::OnRightBranch { + mut left, + interface, + } => { + let mut right = value.expect_argument_value(); + + // NOTE: + // - This disable/enable flow allows us to do x += x without issues + // - Read https://rust-lang.github.io/rfcs/2025-nested-method-calls.html for more details + // - We enable left-to-right for more intuitive error messages: + // If left and right clash, then the error message should be on the right, not the left + unsafe { + // SAFETY: We disabled left above + right.to_mut().disable(); + // SAFETY: enable() may fail if left and right reference the same variable + left.to_mut().enable()?; + right.to_mut().enable()?; + } + let result = interface.execute(left, right, &self.operation)?; + return context.return_returned_value(result); + } + }) + } +} + +pub(super) struct ValuePropertyAccessBuilder { + access: PropertyAccess, +} + +impl ValuePropertyAccessBuilder { + pub(super) fn start( + context: ValueContext, + access: PropertyAccess, + node: ExpressionNodeId, + ) -> NextAction { + let frame = Self { access }; + // If we need an owned value, we can try resolving a reference and + // clone the outputted value if needed - which can be much cheaper. + // e.g. `let x = arr[0]` only copies `arr[0]` instead of the whole array. + let ownership_request = context + .requested_ownership() + .replace_owned_with_copy_on_write(); + context.request_any_value(frame, node, ownership_request) + } +} + +impl EvaluationFrame for ValuePropertyAccessBuilder { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::PropertyAccess(self) + } + + fn handle_next( + self, + context: ValueContext, + Spanned(value, source_span): Spanned, + ) -> ExecutionResult { + // Get the source kind to check if property access is supported + let source_kind = value.value_kind(); + + // Query type resolution for property access capability + let Some(interface) = source_kind.feature_resolver().resolve_property_access() else { + return self.access.type_err(format!( + "Cannot access properties on {}", + source_kind.articled_display_name() + )); + }; + + let ctx = PropertyAccessCallContext { + property: &self.access, + }; + let auto_create = context.requested_ownership().requests_auto_create(); + + // Execute the access via the interface + let mapped = value.expect_any_value_and_map( + |shared| shared.try_map(|value| (interface.shared_access)(ctx, value)), + |mutable| mutable.try_map(|value| (interface.mutable_access)(ctx, value, auto_create)), + |owned| (interface.owned_access)(ctx, owned), + )?; + + // The result span covers source through property + let result_span = SpanRange::new_between(source_span, self.access.span_range()); + context.return_not_necessarily_matching_requested(Spanned(mapped, result_span)) + } +} + +pub(super) struct ValueIndexAccessBuilder { + access: IndexAccess, + state: IndexPath, +} + +enum IndexPath { + OnSourceBranch { + index: ExpressionNodeId, + }, + OnIndexBranch { + source: Spanned, + interface: IndexAccessInterface, + }, +} + +impl ValueIndexAccessBuilder { + pub(super) fn start( + context: ValueContext, + source: ExpressionNodeId, + access: IndexAccess, + index: ExpressionNodeId, + ) -> NextAction { + let frame = Self { + access, + state: IndexPath::OnSourceBranch { index }, + }; + // If we need an owned value, we can try resolving a reference and + // clone the outputted value if needed - which can be much cheaper. + // e.g. `let x = obj.xyz` only copies `obj.xyz` instead of the whole object. + let ownership_request = context + .requested_ownership() + .replace_owned_with_copy_on_write(); + context.request_any_value(frame, source, ownership_request) + } +} + +impl EvaluationFrame for ValueIndexAccessBuilder { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::IndexAccess(self) + } + + fn handle_next( + mut self, + context: ValueContext, + Spanned(value, span): Spanned, + ) -> ExecutionResult { + Ok(match self.state { + IndexPath::OnSourceBranch { index } => { + // Get the source kind to check if index access is supported + let source_kind = value.value_kind(); + + // Query type resolution for index access capability + let Some(interface) = source_kind.feature_resolver().resolve_index_access() else { + return self.access.type_err(format!( + "Cannot index into {}", + source_kind.articled_display_name() + )); + }; + + // Store the interface for the next phase + let index_ownership = interface.index_ownership; + self.state = IndexPath::OnIndexBranch { + source: Spanned(value, span), + interface, + }; + + // Request the index with ownership specified by the interface + context.request_argument_value(self, index, index_ownership) + } + IndexPath::OnIndexBranch { + source: Spanned(source, source_span), + interface, + } => { + let index = value.expect_shared(); + let index = index.as_ref_value().spanned(span); + + let ctx = IndexAccessCallContext { + access: &self.access, + }; + let auto_create = context.requested_ownership().requests_auto_create(); + + // Execute the access via the interface + let result = source.expect_any_value_and_map( + |shared| shared.try_map(|value| (interface.shared_access)(ctx, value, index)), + |mutable| { + mutable.try_map(|value| { + (interface.mutable_access)(ctx, value, index, auto_create) + }) + }, + |owned| (interface.owned_access)(ctx, owned, index), + )?; + let result_span = SpanRange::new_between(source_span, self.access.span_range()); + context.return_not_necessarily_matching_requested(Spanned(result, result_span))? + } + }) + } +} + +pub(super) struct RangeBuilder { + range_limits: syn::RangeLimits, + state: RangePath, +} + +enum RangePath { + OnLeftBranch { right: Option }, + OnRightBranch { left: Option }, +} + +impl RangeBuilder { + pub(super) fn start( + context: ValueContext, + left: &Option, + range_limits: &syn::RangeLimits, + right: &Option, + ) -> ExecutionResult { + Ok(match (left, right) { + (None, None) => match range_limits { + syn::RangeLimits::HalfOpen(token) => { + let inner = RangeValueInner::RangeFull { token: *token }; + context.return_value(Spanned(inner, token.span_range()))? + } + syn::RangeLimits::Closed(_) => { + unreachable!( + "A closed range should have been given a right in continue_range(..)" + ) + } + }, + (None, Some(right)) => context.request_owned( + Self { + range_limits: *range_limits, + state: RangePath::OnRightBranch { left: None }, + }, + *right, + ), + (Some(left), right) => context.request_owned( + Self { + range_limits: *range_limits, + state: RangePath::OnLeftBranch { right: *right }, + }, + *left, + ), + }) + } +} + +impl EvaluationFrame for RangeBuilder { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Range(self) + } + + fn handle_next( + mut self, + context: ValueContext, + Spanned(value, _span): Spanned, + ) -> ExecutionResult { + let value = value.expect_owned(); + Ok(match (self.state, self.range_limits) { + (RangePath::OnLeftBranch { right: Some(right) }, _) => { + self.state = RangePath::OnRightBranch { left: Some(value) }; + context.request_owned(self, right) + } + (RangePath::OnLeftBranch { right: None }, syn::RangeLimits::HalfOpen(token)) => { + let inner = RangeValueInner::RangeFrom { + start_inclusive: value, + token, + }; + context.return_value(Spanned(inner, token.span_range()))? + } + (RangePath::OnLeftBranch { right: None }, syn::RangeLimits::Closed(_)) => { + unreachable!("A closed range should have been given a right in continue_range(..)") + } + (RangePath::OnRightBranch { left: Some(left) }, syn::RangeLimits::HalfOpen(token)) => { + let inner = RangeValueInner::Range { + start_inclusive: left, + token, + end_exclusive: value, + }; + context.return_value(Spanned(inner, token.span_range()))? + } + (RangePath::OnRightBranch { left: Some(left) }, syn::RangeLimits::Closed(token)) => { + let inner = RangeValueInner::RangeInclusive { + start_inclusive: left, + token, + end_inclusive: value, + }; + context.return_value(Spanned(inner, token.span_range()))? + } + (RangePath::OnRightBranch { left: None }, syn::RangeLimits::HalfOpen(token)) => { + let inner = RangeValueInner::RangeTo { + token, + end_exclusive: value, + }; + context.return_value(Spanned(inner, token.span_range()))? + } + (RangePath::OnRightBranch { left: None }, syn::RangeLimits::Closed(token)) => { + let inner = RangeValueInner::RangeToInclusive { + token, + end_inclusive: value, + }; + context.return_value(Spanned(inner, token.span_range()))? + } + }) + } +} + +pub(super) struct AssignmentBuilder { + #[allow(unused)] + equals_token: Token![=], + state: AssignmentPath, +} + +enum AssignmentPath { + OnValueBranch { assignee: ExpressionNodeId }, + OnAwaitingAssignment, +} + +impl AssignmentBuilder { + pub(super) fn start( + context: ValueContext, + assignee: ExpressionNodeId, + equals_token: Token![=], + value: ExpressionNodeId, + ) -> NextAction { + let frame = Self { + equals_token, + state: AssignmentPath::OnValueBranch { assignee }, + }; + context.request_owned(frame, value) + } +} + +impl EvaluationFrame for AssignmentBuilder { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Assignment(self) + } + + fn handle_next( + mut self, + context: ValueContext, + Spanned(value, span): Spanned, + ) -> ExecutionResult { + Ok(match self.state { + AssignmentPath::OnValueBranch { assignee } => { + let value = value.expect_owned(); + self.state = AssignmentPath::OnAwaitingAssignment; + context.request_assignment(self, assignee, value) + } + AssignmentPath::OnAwaitingAssignment => { + let AssignmentCompletion = value.expect_assignment_completion(); + context.return_value(Spanned((), span))? + } + }) + } +} + +pub(super) struct MethodCallBuilder { + method: MethodAccess, + unevaluated_parameters_stack: Vec<(ExpressionNodeId, ArgumentOwnership)>, + state: MethodCallPath, +} + +enum MethodCallPath { + CallerPath, + ArgumentsPath { + method: MethodInterface, + disabled_evaluated_arguments_including_caller: Vec>, + }, +} + +impl MethodCallBuilder { + pub(super) fn start( + context: ValueContext, + caller: ExpressionNodeId, + method: MethodAccess, + parameters: &[ExpressionNodeId], + ) -> NextAction { + let frame = Self { + method, + unevaluated_parameters_stack: parameters + .iter() + .rev() + .map(|x| { + // This is just a placeholder - we'll fix it up shortly + (*x, ArgumentOwnership::Owned) + }) + .collect(), + state: MethodCallPath::CallerPath, + }; + context.request_late_bound(frame, caller) + } +} + +impl EvaluationFrame for MethodCallBuilder { + type ReturnType = ReturnsValue; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::MethodCall(self) + } + + fn handle_next( + mut self, + mut context: ValueContext, + Spanned(value, span): Spanned, + ) -> ExecutionResult { + // Handle expected item based on current state + match self.state { + MethodCallPath::CallerPath => { + let caller_span = span; + let caller = value.expect_late_bound(); + let method = caller + .kind() + .feature_resolver() + .resolve_method(self.method.method.to_string().as_str()); + let method = match method { + Some(m) => m, + None => { + return self.method.method.type_err(format!( + "The method {} does not exist on {}", + self.method.method, + caller.articled_kind(), + )) + } + }; + let non_caller_arguments = self.unevaluated_parameters_stack.len(); + let (argument_ownerships, min_arguments) = method.argument_ownerships(); + let max_arguments = argument_ownerships.len(); + assert!( + max_arguments >= 1 && min_arguments >= 1, + "Method calls must have at least one argument (the caller)" + ); + let non_caller_min_arguments = min_arguments - 1; + let non_caller_max_arguments = max_arguments - 1; + + if non_caller_arguments < non_caller_min_arguments + || non_caller_arguments > non_caller_max_arguments + { + return self.method.method.type_err(format!( + "The method {} expects {} non-self argument/s, but {} were provided", + self.method.method, + if non_caller_min_arguments == non_caller_max_arguments { + (non_caller_min_arguments).to_string() + } else { + format!( + "{} to {}", + (non_caller_min_arguments), + (non_caller_max_arguments) + ) + }, + non_caller_arguments, + )); + } + let caller = + argument_ownerships[0].map_from_late_bound(Spanned(caller, caller_span))?; + let mut caller = Spanned(caller, caller_span); + + // We skip 1 to ignore the caller + let non_self_argument_ownerships: iter::Skip< + std::slice::Iter<'_, ArgumentOwnership>, + > = argument_ownerships.iter().skip(1); + for ((_, requested_ownership), ownership) in self + .unevaluated_parameters_stack + .iter_mut() + .rev() // Swap the stack back to the normal order + .zip(non_self_argument_ownerships) + { + *requested_ownership = *ownership; + } + + self.state = MethodCallPath::ArgumentsPath { + disabled_evaluated_arguments_including_caller: { + let mut params = + Vec::with_capacity(1 + self.unevaluated_parameters_stack.len()); + unsafe { + // SAFETY: We enable it again before use + caller.to_mut().disable(); + } + params.push(caller); + params + }, + method, + }; + } + MethodCallPath::ArgumentsPath { + ref mut disabled_evaluated_arguments_including_caller, + .. + } => { + let argument = value.expect_argument_value(); + let mut argument = Spanned(argument, span); + unsafe { + // SAFETY: We enable it again before use + argument.to_mut().disable(); + } + disabled_evaluated_arguments_including_caller.push(argument); + } + }; + // Now plan the next action + Ok(match self.unevaluated_parameters_stack.pop() { + Some((parameter, ownership)) => { + context.request_any_value(self, parameter, RequestedOwnership::Concrete(ownership)) + } + None => { + let (arguments, method) = match self.state { + MethodCallPath::CallerPath => unreachable!("Already updated above"), + MethodCallPath::ArgumentsPath { + disabled_evaluated_arguments_including_caller: mut arguments, + method, + } => { + // NOTE: + // - This disable/enable flow allows us to do things like vec.push(vec.len()) + // - Read https://rust-lang.github.io/rfcs/2025-nested-method-calls.html for more details + // - We enable left-to-right for intuitive error messages: later borrows will report errors + unsafe { + for argument in &mut arguments { + // SAFETY: We disabled them above + // NOTE: enable() may fail if arguments conflict (e.g., same variable) + argument.to_mut().enable()?; + } + } + (arguments, method) + } + }; + let mut call_context = MethodCallContext { + output_span_range: self.method.span_range(), + interpreter: context.interpreter(), + }; + let output = method.execute(arguments, &mut call_context)?; + context.return_returned_value(output)? + } + }) + } +} diff --git a/src/expressions/expression.rs b/src/expressions/expression.rs new file mode 100644 index 00000000..3912aa90 --- /dev/null +++ b/src/expressions/expression.rs @@ -0,0 +1,202 @@ +use super::*; + +pub(crate) struct Expression { + root: ExpressionNodeId, + nodes: Arena, +} + +impl ParseSource for Expression { + fn parse(input: SourceParser) -> ParseResult { + ExpressionParser::parse(input) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + control_flow_visit(self.root, &mut self.nodes, context) + } +} + +impl Expression { + pub(super) fn new( + root: ExpressionNodeId, + nodes: Arena, + ) -> Self { + Self { root, nodes } + } + + pub(super) fn is_block(&self) -> bool { + matches!( + self.nodes.get(self.root), + ExpressionNode::Leaf(Leaf::Block(_)) + ) + } + + pub(super) fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult> { + ExpressionEvaluator::new(&self.nodes).evaluate(self.root, interpreter, ownership) + } + + pub(crate) fn evaluate_owned( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult> { + Ok(self + .evaluate(interpreter, RequestedOwnership::owned())? + .expect_owned()) + } + + pub(crate) fn evaluate_shared( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult> { + Ok(self + .evaluate(interpreter, RequestedOwnership::shared())? + .expect_shared()) + } + + pub(crate) fn is_valid_as_statement_without_semicolon(&self) -> bool { + // Must align with evaluate_as_statement + matches!( + &self.nodes.get(self.root), + ExpressionNode::Leaf(x) if x.is_valid_as_statement_without_semicolon(), + ) + } + + pub(crate) fn evaluate_as_statement( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult<()> { + // This must align with is_valid_as_statement_without_semicolon + match &self.nodes.get(self.root) { + ExpressionNode::Leaf(Leaf::Block(block)) => block + .evaluate_spanned(interpreter, RequestedOwnership::owned())? + .expect_owned() + .into_statement_result(), + ExpressionNode::Leaf(Leaf::IfExpression(if_expression)) => if_expression + .evaluate_spanned(interpreter, RequestedOwnership::owned())? + .expect_owned() + .into_statement_result(), + ExpressionNode::Leaf(Leaf::LoopExpression(loop_expression)) => loop_expression + .evaluate_spanned(interpreter, RequestedOwnership::owned())? + .expect_owned() + .into_statement_result(), + ExpressionNode::Leaf(Leaf::WhileExpression(while_expression)) => while_expression + .evaluate_spanned(interpreter, RequestedOwnership::owned())? + .expect_owned() + .into_statement_result(), + ExpressionNode::Leaf(Leaf::ForExpression(for_expression)) => for_expression + .evaluate_spanned(interpreter, RequestedOwnership::owned())? + .expect_owned() + .into_statement_result(), + _ => self.evaluate_owned(interpreter)?.into_statement_result(), + } + } +} + +pub(super) enum ExpressionNode { + Leaf(Leaf), + Grouped { + delim_span: DelimSpan, + inner: ExpressionNodeId, + }, + Array { + brackets: Brackets, + items: Vec, + }, + Object { + braces: Braces, + entries: Vec<(ObjectKey, ExpressionNodeId)>, + }, + UnaryOperation { + operation: UnaryOperation, + input: ExpressionNodeId, + }, + BinaryOperation { + operation: BinaryOperation, + left_input: ExpressionNodeId, + right_input: ExpressionNodeId, + }, + Property { + node: ExpressionNodeId, + access: PropertyAccess, + }, + MethodCall { + node: ExpressionNodeId, + method: MethodAccess, + parameters: Vec, + }, + Index { + node: ExpressionNodeId, + access: IndexAccess, + index: ExpressionNodeId, + }, + Range { + left: Option, + range_limits: syn::RangeLimits, + right: Option, + }, + Assignment { + assignee: ExpressionNodeId, + equals_token: Token![=], + value: ExpressionNodeId, + }, +} + +// We Box some of these variants to reduce the size of ExpressionNode +pub(super) enum Leaf { + Block(Box), + Variable(VariableReference), + TypeProperty(TypeProperty), + Discarded(Token![_]), + Value(Spanned), + StreamLiteral(StreamLiteral), + ParseTemplateLiteral(ParseTemplateLiteral), + IfExpression(Box), + LoopExpression(Box), + WhileExpression(Box), + ForExpression(Box), + AttemptExpression(Box), + ParseExpression(Box), +} + +impl HasSpanRange for Leaf { + fn span_range(&self) -> SpanRange { + match self { + Leaf::Variable(variable) => variable.span_range(), + Leaf::TypeProperty(type_property) => type_property.span_range(), + Leaf::Discarded(token) => token.span_range(), + Leaf::Block(block) => block.span_range(), + Leaf::Value(value) => value.span_range(), + Leaf::StreamLiteral(stream) => stream.span_range(), + Leaf::ParseTemplateLiteral(stream) => stream.span_range(), + Leaf::IfExpression(expression) => expression.span_range(), + Leaf::LoopExpression(expression) => expression.span_range(), + Leaf::WhileExpression(expression) => expression.span_range(), + Leaf::ForExpression(expression) => expression.span_range(), + Leaf::AttemptExpression(expression) => expression.span_range(), + Leaf::ParseExpression(expression) => expression.span_range(), + } + } +} + +impl Leaf { + fn is_valid_as_statement_without_semicolon(&self) -> bool { + match self { + Leaf::Block(_) + | Leaf::IfExpression(_) + | Leaf::LoopExpression(_) + | Leaf::WhileExpression(_) + | Leaf::ForExpression(_) + | Leaf::AttemptExpression(_) + | Leaf::ParseExpression(_) => true, + Leaf::Variable(_) + | Leaf::TypeProperty(_) + | Leaf::Discarded(_) + | Leaf::Value(_) + | Leaf::StreamLiteral(_) + | Leaf::ParseTemplateLiteral(_) => false, + } + } +} diff --git a/src/expressions/expression_block.rs b/src/expressions/expression_block.rs new file mode 100644 index 00000000..cacdfcca --- /dev/null +++ b/src/expressions/expression_block.rs @@ -0,0 +1,337 @@ +use super::*; + +pub(crate) struct EmbeddedExpression { + marker: Token![#], + parentheses: Parentheses, + content: Expression, +} + +impl ParseSource for EmbeddedExpression { + fn parse(input: SourceParser) -> ParseResult { + let marker = input.parse()?; + let (parentheses, inner) = input.parse_parentheses()?; + let content = inner.parse()?; + Ok(Self { + marker, + parentheses, + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.content.control_flow_pass(context) + } +} + +impl HasSpanRange for EmbeddedExpression { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.marker.span, self.parentheses.close()) + } +} + +impl Interpret for EmbeddedExpression { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + let value = self.content.evaluate_shared(interpreter)?; + value.as_ref_value().output_to( + Grouping::Flattened, + &mut ToStreamContext::new(interpreter.output(self)?, self.span_range()), + ) + } +} + +pub(crate) struct EmbeddedStatements { + marker: Token![#], + braces: Braces, + content: ExpressionBlockContent, +} + +impl ParseSource for EmbeddedStatements { + fn parse(input: SourceParser) -> ParseResult { + let marker = input.parse()?; + let (braces, inner) = input.parse_braces()?; + let content = inner.parse()?; + Ok(Self { + marker, + braces, + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.content.control_flow_pass(context) + } +} + +impl HasSpanRange for EmbeddedStatements { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.marker.span, self.braces.close()) + } +} + +impl Interpret for EmbeddedStatements { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + let value = self + .content + .evaluate_spanned(interpreter, self.span_range(), RequestedOwnership::shared())? + .0 + .expect_shared(); + value.as_ref_value().output_to( + Grouping::Flattened, + &mut ToStreamContext::new(interpreter.output(self)?, self.span_range()), + ) + } +} + +impl EmbeddedStatements { + pub(crate) fn consume(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + self.content + .evaluate_spanned(interpreter, self.span_range(), RequestedOwnership::owned())? + .map(|v| v.expect_owned()) + .into_statement_result() + } +} + +pub(crate) struct ExpressionBlock { + pub(super) label: Option<(CatchLabel, CatchLocationId)>, + pub(super) scoped_block: ScopedBlock, +} + +impl ParseSource for ExpressionBlock { + fn parse(input: SourceParser) -> ParseResult { + let label = input.parse_optional()?; + + // We add some special error handling here to help users avoid confusion + // between object literals and blocks. + let (inner, delim_span) = match input.cursor().any_group() { + Some((inner, Delimiter::Brace, delim_span, _)) => (inner, delim_span), + _ => { + return input.parse_err("Expected `{ ... }` to start an expression block."); + } + }; + if inner.eof() { + return delim_span.open().parse_err("An empty object literal is written `%{}` with a `%` prefix. If you intend to use an empty block here, instead use `{ None }`."); + } + if let Some((_, next)) = inner.ident() { + if next.punct_matching(':').is_some() || next.punct_matching(',').is_some() { + return delim_span.open().parse_err("An object literal must be prefixed with %, e.g. `%{ field: 1 }`. Without such a prefix, { .. } defines a block."); + } + } + + let scoped_block = input.parse()?; + Ok(Self { + label: label.map(|l| (l, CatchLocationId::new_placeholder())), + scoped_block, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + if let Some((label, location_id)) = &mut self.label { + *location_id = context.register_catch_location(CatchLocationData::LabeledBlock { + label: label.ident_string(), + }); + context.enter_catch(*location_id); + self.scoped_block.control_flow_pass(context)?; + context.exit_catch(*location_id); + Ok(()) + } else { + self.scoped_block.control_flow_pass(context) + } + } +} + +impl HasSpan for ExpressionBlock { + fn span(&self) -> Span { + // We ignore the label, because it's not really part of the span of the resultant value + self.scoped_block.span() + } +} + +impl Evaluate for ExpressionBlock { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + let scope = interpreter.current_scope_id(); + + // If this block has a label, catch breaks targeting this specific catch location + let output = if let Some((_, catch_location)) = &self.label { + let output_result = self.scoped_block.evaluate_spanned(interpreter, ownership); + match interpreter.catch_control_flow(output_result, *catch_location, scope)? { + ExecutionOutcome::Value(Spanned(value, _)) => value, + ExecutionOutcome::ControlFlow(ControlFlowInterrupt::Break(break_interrupt)) => { + break_interrupt.into_requested_value(self.span_range(), ownership)? + } + ExecutionOutcome::ControlFlow(_) => { + unreachable!("Only break control flow should be catchable by labeled blocks") + } + } + } else { + // No label, just evaluate the block normally + self.scoped_block.evaluate(interpreter, ownership)? + }; + Ok(output) + } +} + +pub(crate) struct ScopedBlock { + pub(super) braces: Braces, + pub(super) scope: ScopeId, + pub(super) content: ExpressionBlockContent, +} + +impl ParseSource for ScopedBlock { + fn parse(input: SourceParser) -> ParseResult { + let (braces, inner) = input.parse_braces()?; + let content = inner.parse()?; + Ok(Self { + braces, + scope: ScopeId::new_placeholder(), + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + context.register_scope(&mut self.scope); + context.enter_scope(self.scope); + self.content.control_flow_pass(context)?; + context.exit_scope(self.scope); + Ok(()) + } +} + +impl HasSpan for ScopedBlock { + fn span(&self) -> Span { + self.braces.join() + } +} + +impl Evaluate for ScopedBlock { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + interpreter.enter_scope(self.scope); + let output = self + .content + .evaluate_spanned(interpreter, self.span().into(), ownership)?; + interpreter.exit_scope(self.scope); + Ok(output.0) + } +} + +impl ScopedBlock { + pub(crate) fn evaluate_owned( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult> { + let span_range = self.span().span_range(); + self.evaluate(interpreter, RequestedOwnership::owned()) + .map(|value| Spanned(value.expect_owned(), span_range)) + } +} + +pub(crate) struct UnscopedBlock { + pub(super) braces: Braces, + pub(super) content: ExpressionBlockContent, +} + +impl ParseSource for UnscopedBlock { + fn parse(input: SourceParser) -> ParseResult { + let (braces, inner) = input.parse_braces()?; + let content = inner.parse()?; + Ok(Self { braces, content }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.content.control_flow_pass(context) + } +} + +impl HasSpan for UnscopedBlock { + fn span(&self) -> Span { + self.braces.join() + } +} + +impl Evaluate for UnscopedBlock { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + self.content + .evaluate_spanned(interpreter, self.span().into(), ownership) + .map(|v| v.0) + } +} + +impl UnscopedBlock { + pub(crate) fn evaluate_owned( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult> { + let span_range = self.span().span_range(); + self.evaluate(interpreter, RequestedOwnership::owned()) + .map(|value| Spanned(value.expect_owned(), span_range)) + } +} + +pub(crate) struct ExpressionBlockContent { + statements: Vec<(Statement, Option)>, +} + +impl ParseSource for ExpressionBlockContent { + fn parse(input: SourceParser) -> ParseResult { + let mut statements = Vec::new(); + while !input.is_empty() { + let statement: Statement = input.parse()?; + let requires_semicolon = statement.requires_semicolon(input.is_empty()); + match (requires_semicolon, input.peek(Token![;])) { + (true, false) => { + if input.is_empty() { + return input.parse_err("Expected `;` at the end of this statement."); + } else { + return input.parse_err("Invalid statement continuation. Possibly the previous statement is missing a semicolon?"); + } + } + (_, true) => { + let semicolon = input.parse()?; + statements.push((statement, Some(semicolon))); + } + (false, false) => { + statements.push((statement, None)); + } + } + } + Ok(Self { statements }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + for (statement, _semicolon) in self.statements.iter_mut() { + statement.control_flow_pass(context)?; + } + Ok(()) + } +} + +impl ExpressionBlockContent { + pub(crate) fn evaluate_spanned( + &self, + interpreter: &mut Interpreter, + output_span: SpanRange, + ownership: RequestedOwnership, + ) -> ExecutionResult> { + for (i, (statement, semicolon)) in self.statements.iter().enumerate() { + let is_last = i == self.statements.len() - 1; + if is_last && semicolon.is_none() { + let value = statement.evaluate_as_returning_expression(interpreter, ownership)?; + return Ok(value.spanned(output_span)); + } else { + statement.evaluate_as_statement(interpreter)?; + } + } + ownership.map_from_owned(Spanned(().into_any_value(), output_span)) + } +} diff --git a/src/expressions/expression_label.rs b/src/expressions/expression_label.rs new file mode 100644 index 00000000..43299777 --- /dev/null +++ b/src/expressions/expression_label.rs @@ -0,0 +1,29 @@ +use super::*; + +pub(crate) struct CatchLabel { + label: syn::Lifetime, + _colon: Unused, +} + +impl CatchLabel { + pub(crate) fn ident_string(&self) -> String { + self.label.ident.to_string() + } +} + +impl ParseSource for CatchLabel { + fn parse(input: SourceParser) -> ParseResult { + let lifetime = input.parse()?; + let _colon = input.parse()?; + Ok(Self { + label: lifetime, + _colon, + }) + } + + fn control_flow_pass(&mut self, _context: FlowCapturer) -> ParseResult<()> { + Ok(()) + } +} + +impl ParseSourceOptional for CatchLabel {} diff --git a/src/expressions/expression_parsing.rs b/src/expressions/expression_parsing.rs new file mode 100644 index 00000000..2fc27fdf --- /dev/null +++ b/src/expressions/expression_parsing.rs @@ -0,0 +1,1094 @@ +use syn::token; + +use super::*; + +/// ## Overview +/// +/// Expression parsing is quite complicated, but this is possibly slightly more complicated in some ways than it needs to be. +/// Its design is intended to make expression parsing more intuitive (although I'm not sure that's been achieved really), +/// and to avoid recursion by making use of an explicit stack frame approach with [`ExpressionStackFrame`]s. +/// +/// This allows parsing of very long expressions (e.g. a sum of 1 million terms) without hitting recursion limits. +/// +/// ## Intuition +/// +/// You can think of these frames in two ways - as: +/// (a) the local variables of a function in an un-flattened parser call stack +/// (b) the specifics of a parent operator, which can allow judging if/when an introduced expression with a possible +/// extension should either bind to its parent (ignoring the extension for now, and retrying the extension with +/// its parent) or bind to the extension itself. +/// +/// ## Examples +/// +/// See the rust doc on the [`ExpressionStackFrame`] for further details. +pub(super) struct ExpressionParser<'a> { + streams: ParseStack<'a, Source>, + nodes: ExpressionNodes, + expression_stack: Vec, +} + +impl<'a> ExpressionParser<'a> { + pub(super) fn parse(input: SourceParser<'a>) -> ParseResult { + Self { + streams: ParseStack::new(input), + nodes: ExpressionNodes::new(), + expression_stack: Vec::with_capacity(10), + } + .run() + } + + fn run(mut self) -> ParseResult { + let mut work_item = self.push_stack_frame(ExpressionStackFrame::Root); + loop { + work_item = match work_item { + WorkItem::RequireUnaryAtom => { + let unary_atom = Self::parse_unary_atom(&mut self.streams)?; + self.extend_with_unary_atom(unary_atom)? + } + WorkItem::TryParseAndApplyExtension { node } => { + let extension = Self::parse_extension( + &mut self.streams, + self.expression_stack.last().unwrap(), + )?; + self.attempt_extension(node, extension)? + } + WorkItem::TryApplyAlreadyParsedExtension { node, extension } => { + self.attempt_extension(node, extension)? + } + WorkItem::ContinueRange { lhs, range_limits } => { + self.continue_range(lhs, range_limits)? + } + WorkItem::Finished { root } => { + return Ok(self.nodes.complete(root)); + } + } + } + } + + fn parse_unary_atom(input: &mut ParseStack) -> ParseResult { + Ok(match input.peek_grammar() { + SourcePeekMatch::EmbeddedVariable | SourcePeekMatch::EmbeddedExpression | SourcePeekMatch::EmbeddedStatements => { + return input.parse_err( + "In an expression, the # variable prefix is not allowed. The # prefix should only be used when embedding a variable into an output stream, e.g. %[#var + #(..expressions..)]", + ) + } + SourcePeekMatch::Group(Delimiter::None | Delimiter::Parenthesis) => { + let (_, delim_span) = input.parse_and_enter_group(None)?; + UnaryAtom::Group(delim_span) + } + SourcePeekMatch::Group(Delimiter::Brace) => { + UnaryAtom::Leaf(Leaf::Block(Box::new(input.parse()?))) + } + SourcePeekMatch::Group(Delimiter::Bracket) => { + // This could be handled as parsing a vector of SourceExpressions, + // but it's more efficient to handle nested vectors as a single expression + // in the expression parser + let (_, delim_span) = input.parse_and_enter_group(None)?; + UnaryAtom::Array(Brackets { delim_span }) + } + SourcePeekMatch::Punct(punct) => { + if punct.as_char() == '\'' && input.cursor().lifetime().is_some() { + if let Some((_, cursor_after_lifetime)) = input.cursor().lifetime() { + if let Some((_, cursor_after_colon)) = cursor_after_lifetime.punct_matching(':') { + if let Some((ident, _)) = cursor_after_colon.ident() { + match ident.to_string().as_str() { + "loop" => return Ok(UnaryAtom::Leaf(Leaf::LoopExpression(Box::new(input.parse()?)))), + "while" => return Ok(UnaryAtom::Leaf(Leaf::WhileExpression(Box::new(input.parse()?)))), + "for" => return Ok(UnaryAtom::Leaf(Leaf::ForExpression(Box::new(input.parse()?)))), + "attempt" => return Ok(UnaryAtom::Leaf(Leaf::AttemptExpression(Box::new(input.parse()?)))), + _ => {} + } + } + if cursor_after_colon.group_matching(Delimiter::Brace).is_some() { + return Ok(UnaryAtom::Leaf(Leaf::Block(Box::new(input.parse()?)))); + } + } + } + } + if punct.as_char() == '@' { + UnaryAtom::Leaf(Leaf::ParseTemplateLiteral(input.parse()?)) + } else if punct.as_char() == '.' { + UnaryAtom::Range(input.parse()?) + } else if punct.as_char() == '-' || punct.as_char() == '!' { + UnaryAtom::PrefixUnaryOperation(input.parse()?) + } else { + return input.parse_err("Expected an expression"); + } + } + SourcePeekMatch::Ident(ident) => { + match ident.to_string().as_str() { + "_" => UnaryAtom::Leaf(Leaf::Discarded(input.parse()?)), + "true" | "false" => { + let bool = input.parse::()?; + UnaryAtom::Leaf(Leaf::Value(Spanned( + SharedValue::new_from_owned(bool.value.into_any_value()), + bool.span.span_range(), + ))) + } + "if" => UnaryAtom::Leaf(Leaf::IfExpression(Box::new(input.parse()?))), + "loop" => UnaryAtom::Leaf(Leaf::LoopExpression(Box::new(input.parse()?))), + "while" => UnaryAtom::Leaf(Leaf::WhileExpression(Box::new(input.parse()?))), + "for" => UnaryAtom::Leaf(Leaf::ForExpression(Box::new(input.parse()?))), + "attempt" => UnaryAtom::Leaf(Leaf::AttemptExpression(Box::new(input.parse()?))), + "parse" => return Ok(UnaryAtom::Leaf(Leaf::ParseExpression(Box::new(input.parse()?)))), + "None" => { + let none_ident = input.parse_any_ident()?; // consume the "None" token + UnaryAtom::Leaf(Leaf::Value(Spanned( + SharedValue::new_from_owned(().into_any_value()), + none_ident.span().span_range(), + ))) + } + _ => { + let (_ident, next) = input.cursor().ident().unwrap(); + if let Some((_, next)) = next.punct_matching(':') { + if let Some((_, _)) = next.punct_matching(':') { + return Ok(UnaryAtom::Leaf(Leaf::TypeProperty(input.parse()?))); + } + } + UnaryAtom::Leaf(Leaf::Variable(input.parse()?)) + } + } + }, + SourcePeekMatch::Literal(_) => { + let lit: syn::Lit = input.parse()?; + let span_range = lit.span().span_range(); + UnaryAtom::Leaf(Leaf::Value(Spanned( + SharedValue::new_from_owned(AnyValue::for_syn_lit(lit)), + span_range, + ))) + }, + SourcePeekMatch::StreamLiteral => { + UnaryAtom::Leaf(Leaf::StreamLiteral(input.parse()?)) + } + SourcePeekMatch::ObjectLiteral => { + let _: Token![%] = input.parse()?; + let (_, delim_span) = input.parse_and_enter_group(None)?; + UnaryAtom::Object(Braces { delim_span }) + } + SourcePeekMatch::End => return input.parse_err("Expected an expression"), + }) + } + + fn parse_extension( + input: &mut ParseStack, + parent_stack_frame: &ExpressionStackFrame, + ) -> ParseResult { + // We fall through if we have no match + match input.peek_grammar() { + SourcePeekMatch::Group(Delimiter::Bracket) => { + let (_, delim_span) = input.parse_and_enter_group(None)?; + return Ok(NodeExtension::Index(IndexAccess { + brackets: Brackets { delim_span }, + })); + } + SourcePeekMatch::Punct(punct) if punct.as_char() == ',' => { + match parent_stack_frame { + ExpressionStackFrame::NonEmptyArray { .. } + | ExpressionStackFrame::NonEmptyMethodCallParametersList { .. } + | ExpressionStackFrame::NonEmptyObject { + state: ObjectStackFrameState::EntryValue { .. }, + .. + } => { + input.parse::()?; + if input.is_current_empty() { + return Ok(NodeExtension::EndOfStreamOrGroup); + } else { + return Ok(NodeExtension::NonTerminalComma); + } + } + ExpressionStackFrame::Group { .. } => { + return input.parse_err("Commas are only permitted inside preinterpret arrays []. Preinterpret arrays [a, b] can be used as a drop-in replacement for rust tuples (a, b).") + } + // Fall through for an unmatched extension + _ => {} + } + } + SourcePeekMatch::Punct(punct) => { + // TODO[performance]: Get rid of the try_parse_or_revert and convert this into + // a parse tree + if punct.as_char() == '.' && input.peek2(syn::Ident) { + let dot = input.parse()?; + let ident = input.parse()?; + if input.peek(token::Paren) { + let (_, delim_span) = input.parse_and_enter_group(None)?; + return Ok(NodeExtension::MethodCall(MethodAccess { + dot, + method: ident, + parentheses: Parentheses { delim_span }, + })); + } + return Ok(NodeExtension::Property(PropertyAccess { + dot, + property: ident, + })); + } + if let Some((punct, _)) = input.cursor().punct_matching('=') { + // Ensure that a guard expression `{} if XX => ` can be parsed correctly. + if punct.spacing() == Spacing::Alone { + return Ok(NodeExtension::AssignmentOperation(input.parse()?)); + } + } + if let Ok(operation) = input.try_parse_or_revert() { + return Ok(NodeExtension::BinaryOperation(operation)); + } + if let Ok(range_limits) = input.try_parse_or_revert() { + return Ok(NodeExtension::Range(range_limits)); + } + } + SourcePeekMatch::Ident(ident) if ident == "as" => { + let cast_operation = + UnaryOperation::for_cast_operation(input.parse()?, input.parse_any_ident()?)?; + return Ok(NodeExtension::PostfixOperation(cast_operation)); + } + SourcePeekMatch::End => return Ok(NodeExtension::EndOfStreamOrGroup), + _ => {} + }; + // We are not at the end of the stream, but the tokens which follow are + // not a valid extension... + match parent_stack_frame { + ExpressionStackFrame::Root => Ok(NodeExtension::NoValidExtensionForCurrentParent), + ExpressionStackFrame::Group { .. } => input.parse_err("Expected ) or operator"), + ExpressionStackFrame::NonEmptyArray { .. } => { + input.parse_err("Expected comma, ], or operator") + } + ExpressionStackFrame::IncompleteIndex { .. } + | ExpressionStackFrame::NonEmptyObject { + state: ObjectStackFrameState::EntryIndex { .. }, + .. + } => input.parse_err("Expected ], or operator"), + ExpressionStackFrame::NonEmptyObject { + state: ObjectStackFrameState::EntryValue { .. }, + .. + } => input.parse_err("Expected comma, }, or operator"), + ExpressionStackFrame::NonEmptyMethodCallParametersList { .. } => { + input.parse_err("Expected comma, ) or operator") + } + // e.g. I've just matched the true in !true or false || true, + // and I want to see if there's an extension (e.g. a cast). + // There's nothing matching, so we fall through to an EndOfFrame + ExpressionStackFrame::IncompleteUnaryPrefixOperation { .. } + | ExpressionStackFrame::IncompleteBinaryOperation { .. } + | ExpressionStackFrame::IncompleteRange { .. } + | ExpressionStackFrame::IncompleteAssignment { .. } => { + Ok(NodeExtension::NoValidExtensionForCurrentParent) + } + } + } + + fn extend_with_unary_atom(&mut self, unary_atom: UnaryAtom) -> ParseResult { + Ok(match unary_atom { + UnaryAtom::Leaf(leaf) => self.add_leaf(leaf), + UnaryAtom::Group(delim_span) => { + self.push_stack_frame(ExpressionStackFrame::Group { delim_span }) + } + UnaryAtom::Array(brackets) => { + if self.streams.is_current_empty() { + self.streams.exit_group(None)?; + WorkItem::TryParseAndApplyExtension { + node: self.nodes.add_node(ExpressionNode::Array { + brackets, + items: Vec::new(), + }), + } + } else { + self.push_stack_frame(ExpressionStackFrame::NonEmptyArray { + brackets, + items: Vec::new(), + }) + } + } + UnaryAtom::Object(braces) => self.continue_object(braces, Vec::new())?, + UnaryAtom::PrefixUnaryOperation(operation) => { + self.push_stack_frame(ExpressionStackFrame::IncompleteUnaryPrefixOperation { + operation, + }) + } + UnaryAtom::Range(range_limits) => WorkItem::ContinueRange { + lhs: None, + range_limits, + }, + }) + } + + fn attempt_extension( + &mut self, + node: ExpressionNodeId, + extension: NodeExtension, + ) -> ParseResult { + let parent_precedence = self.parent_precedence(); + let extension_precendence = extension.precedence(); + if extension_precendence > parent_precedence { + // If the extension has a higher precedence than the parent, then the left_node gets pulled into + // becoming part of the new operation + Ok(match extension { + NodeExtension::PostfixOperation(operation) => WorkItem::TryParseAndApplyExtension { + node: self.nodes.add_node(ExpressionNode::UnaryOperation { + operation, + input: node, + }), + }, + NodeExtension::BinaryOperation(operation) => { + self.push_stack_frame(ExpressionStackFrame::IncompleteBinaryOperation { + lhs: node, + operation, + }) + } + NodeExtension::NonTerminalComma => { + let next_item = match self.expression_stack.last_mut().unwrap() { + ExpressionStackFrame::NonEmptyMethodCallParametersList { parameters, .. } => { + parameters.push(node); + Some(WorkItem::RequireUnaryAtom) + } + ExpressionStackFrame::NonEmptyArray { items, .. } => { + items.push(node); + Some(WorkItem::RequireUnaryAtom) + } + ExpressionStackFrame::NonEmptyObject { .. } => { + None // Placeholder to indicate separate handling below + } + _ => unreachable!( + "NonTerminalComma is only returned under an Array, Object or MethodCallParametersList parent." + ), + }; + match next_item { + Some(item) => item, + None => { + // Indicates object + match self.expression_stack.pop().unwrap() { + ExpressionStackFrame::NonEmptyObject { + braces, + state: ObjectStackFrameState::EntryValue(key, _), + mut complete_entries, + } => { + complete_entries.push((key, node)); + self.continue_object(braces, complete_entries)? + } + _ => unreachable!( + "This None code path is only reachable under an object" + ), + } + } + } + } + NodeExtension::Property(access) => WorkItem::TryParseAndApplyExtension { + node: self + .nodes + .add_node(ExpressionNode::Property { node, access }), + }, + NodeExtension::MethodCall(method) => { + if self.streams.is_current_empty() { + self.streams.exit_group(None)?; + let node = self.nodes.add_node(ExpressionNode::MethodCall { + node, + method, + parameters: Vec::new(), + }); + WorkItem::TryParseAndApplyExtension { node } + } else { + self.push_stack_frame( + ExpressionStackFrame::NonEmptyMethodCallParametersList { + node, + method, + parameters: Vec::new(), + }, + ) + } + } + NodeExtension::Index(access) => { + self.push_stack_frame(ExpressionStackFrame::IncompleteIndex { node, access }) + } + NodeExtension::Range(range_limits) => WorkItem::ContinueRange { + lhs: Some(node), + range_limits, + }, + NodeExtension::AssignmentOperation(equals_token) => { + self.push_stack_frame(ExpressionStackFrame::IncompleteAssignment { + assignee: node, + equals_token, + }) + } + NodeExtension::EndOfStreamOrGroup + | NodeExtension::NoValidExtensionForCurrentParent => { + unreachable!("Not possible, as these have minimum precedence") + } + }) + } else { + // Otherwise, the node gets combined into the parent stack frame, and the extension + // is attempted to be applied against the next parent stack frame + let parent_stack_frame = self.pop_stack_frame(); + Ok(match parent_stack_frame { + ExpressionStackFrame::Root => { + assert!(matches!( + extension, + NodeExtension::EndOfStreamOrGroup + | NodeExtension::NoValidExtensionForCurrentParent + )); + WorkItem::Finished { root: node } + } + ExpressionStackFrame::Group { delim_span } => { + assert!(matches!(extension, NodeExtension::EndOfStreamOrGroup)); + self.streams.exit_group(None)?; + WorkItem::TryParseAndApplyExtension { + node: self.nodes.add_node(ExpressionNode::Grouped { + delim_span, + inner: node, + }), + } + } + ExpressionStackFrame::NonEmptyArray { + mut items, + brackets, + } => { + assert!(matches!(extension, NodeExtension::EndOfStreamOrGroup)); + items.push(node); + self.streams.exit_group(None)?; + WorkItem::TryParseAndApplyExtension { + node: self + .nodes + .add_node(ExpressionNode::Array { brackets, items }), + } + } + ExpressionStackFrame::NonEmptyMethodCallParametersList { + node: source, + mut parameters, + method, + } => { + assert!(matches!(extension, NodeExtension::EndOfStreamOrGroup)); + parameters.push(node); + self.streams.exit_group(None)?; + let node = self.nodes.add_node(ExpressionNode::MethodCall { + node: source, + method, + parameters, + }); + WorkItem::TryParseAndApplyExtension { node } + } + ExpressionStackFrame::NonEmptyObject { + braces, + complete_entries, + state: ObjectStackFrameState::EntryIndex(access), + } => { + assert!(matches!(extension, NodeExtension::EndOfStreamOrGroup)); + self.streams.exit_group(None)?; + let colon = self.streams.parse()?; + self.expression_stack + .push(ExpressionStackFrame::NonEmptyObject { + braces, + complete_entries, + state: ObjectStackFrameState::EntryValue( + ObjectKey::Indexed { + access, + index: node, + }, + colon, + ), + }); + WorkItem::RequireUnaryAtom + } + ExpressionStackFrame::NonEmptyObject { + braces, + complete_entries: mut entries, + state: ObjectStackFrameState::EntryValue(key, _), + } => { + assert!(matches!(extension, NodeExtension::EndOfStreamOrGroup)); + self.streams.exit_group(None)?; + entries.push((key, node)); + let node = self + .nodes + .add_node(ExpressionNode::Object { braces, entries }); + WorkItem::TryParseAndApplyExtension { node } + } + ExpressionStackFrame::IncompleteUnaryPrefixOperation { operation } => { + let node = self.nodes.add_node(ExpressionNode::UnaryOperation { + operation: operation.into(), + input: node, + }); + extension.into_post_operation_completion_work_item(node) + } + ExpressionStackFrame::IncompleteBinaryOperation { lhs, operation } => { + let node = self.nodes.add_node(ExpressionNode::BinaryOperation { + operation, + left_input: lhs, + right_input: node, + }); + extension.into_post_operation_completion_work_item(node) + } + ExpressionStackFrame::IncompleteIndex { + node: source, + access, + } => { + assert!(matches!(extension, NodeExtension::EndOfStreamOrGroup)); + self.streams.exit_group(None)?; + let node = self.nodes.add_node(ExpressionNode::Index { + node: source, + access, + index: node, + }); + WorkItem::TryParseAndApplyExtension { node } + } + ExpressionStackFrame::IncompleteRange { lhs, range_limits } => { + let node = self.nodes.add_node(ExpressionNode::Range { + left: lhs, + range_limits, + right: Some(node), + }); + extension.into_post_operation_completion_work_item(node) + } + ExpressionStackFrame::IncompleteAssignment { + assignee, + equals_token, + } => { + let node = self.nodes.add_node(ExpressionNode::Assignment { + assignee, + equals_token, + value: node, + }); + extension.into_post_operation_completion_work_item(node) + } + }) + } + } + + fn continue_range( + &mut self, + lhs: Option, + range_limits: syn::RangeLimits, + ) -> ParseResult { + // See https://doc.rust-lang.org/reference/expressions/range-expr.html + match &range_limits { + syn::RangeLimits::Closed(_) => { + // A closed range requires a right hand side, so we can just + // go straight to matching a UnaryAtom for it + return Ok( + self.push_stack_frame(ExpressionStackFrame::IncompleteRange { + lhs, + range_limits, + }), + ); + } + syn::RangeLimits::HalfOpen(_) => {} + } + // Otherwise, we have a half-open range, and need to work out whether + // we can parse a UnaryAtom to be the right side of the range or whether + // it will have no right side. + // * Has right side: 1..2 or x..y or '1'..'3' + // * Has no rhs: `[3.., 4]`, `[3..]`, `let x = 3..;`, `3...take(10)` + let should_parse_range_lhs = match self.streams.peek_grammar() { + SourcePeekMatch::Punct(punct) => !matches!(punct.as_char(), ',' | ';' | '.'), + SourcePeekMatch::End => false, + _ => true, // Literals, Idents(variables/methods), Commands/expressions + }; + if should_parse_range_lhs { + // A unary atom can be parsed so let's attempt to complete the range with it + Ok(self.push_stack_frame(ExpressionStackFrame::IncompleteRange { lhs, range_limits })) + } else { + // No unary atom can be parsed, so let's complete the range + let node = self.nodes.add_node(ExpressionNode::Range { + left: lhs, + range_limits, + right: None, + }); + Ok(WorkItem::TryParseAndApplyExtension { node }) + } + } + + fn continue_object( + &mut self, + braces: Braces, + mut complete_entries: Vec<(ObjectKey, ExpressionNodeId)>, + ) -> ParseResult { + const ERROR_MESSAGE: &str = r##"Expected an object entry (`field,` `field: ..,` or `["field"]: ..,`). If you meant to start a new block, use #{ ... } instead."##; + let state = loop { + if self.streams.is_current_empty() { + self.streams.exit_group(None)?; + let node = self.nodes.add_node(ExpressionNode::Object { + braces, + entries: complete_entries, + }); + return Ok(WorkItem::TryParseAndApplyExtension { node }); + } else if self.streams.peek(syn::Ident) { + let key: Ident = self.streams.parse()?; + + if self.streams.is_current_empty() { + // Fall through + } else if self.streams.peek(token::Comma) { + self.streams.parse::()?; + // Fall through + } else if self.streams.peek(token::Colon) { + let colon = self.streams.parse()?; + break ObjectStackFrameState::EntryValue(ObjectKey::Identifier(key), colon); + } else { + return self.streams.parse_err(ERROR_MESSAGE); + } + + let node = + self.nodes + .add_node(ExpressionNode::Leaf(Leaf::Variable(VariableReference { + ident: key.clone(), + id: VariableReferenceId::new_placeholder(), + #[cfg(feature = "debug")] + assertion: FinalUseAssertion::None, + }))); + complete_entries.push((ObjectKey::Identifier(key), node)); + continue; + } else if self.streams.peek(token::Bracket) { + let (_, delim_span) = self.streams.parse_and_enter_group(None)?; + break ObjectStackFrameState::EntryIndex(IndexAccess { + brackets: Brackets { delim_span }, + }); + } else { + return self.streams.parse_err(ERROR_MESSAGE); + } + }; + Ok(self.push_stack_frame(ExpressionStackFrame::NonEmptyObject { + braces, + complete_entries, + state, + })) + } + + fn add_leaf(&mut self, leaf: Leaf) -> WorkItem { + let node = self.nodes.add_node(ExpressionNode::Leaf(leaf)); + WorkItem::TryParseAndApplyExtension { node } + } + + fn push_stack_frame(&mut self, frame: ExpressionStackFrame) -> WorkItem { + self.expression_stack.push(frame); + WorkItem::RequireUnaryAtom + } + + /// Panics if called after the Root has been popped + fn pop_stack_frame(&mut self) -> ExpressionStackFrame { + self.expression_stack + .pop() + .expect("There should always be at least a root") + } + + /// Panics if called after the Root has been popped + fn parent_precedence(&self) -> OperatorPrecendence { + self.expression_stack + .last() + .map(|s| s.precedence_to_bind_to_child()) + .unwrap() + } +} + +new_key!(pub(crate) ExpressionNodeId); + +pub(super) struct ExpressionNodes { + nodes: Arena, +} + +impl ExpressionNodes { + pub(super) fn new() -> Self { + Self { + nodes: Arena::new(), + } + } + + pub(super) fn add_node(&mut self, node: ExpressionNode) -> ExpressionNodeId { + self.nodes.add(node) + } + + pub(super) fn complete(self, root: ExpressionNodeId) -> Expression { + Expression::new(root, self.nodes) + } +} + +//=============================================================================================== +// The OperatorPrecendence is an amended copy of the Precedence enum from Syn. +// +// Syn is dual-licensed under MIT and Apache, and a subset of it is reproduced from version 2.0.96 +// of syn, and then further edited as a derivative work as part of preinterpret, which is released +// under the same licenses. +// +// LICENSE-MIT: https://github.com/dtolnay/syn/blob/2.0.96/LICENSE-MIT +// LICENSE-APACHE: https://github.com/dtolnay/syn/blob/2.0.96/LICENSE-APACHE +//=============================================================================================== + +/// This is an amended copy of the `Precedence` enum from Syn. +/// +/// Reference: https://doc.rust-lang.org/reference/expressions.html#expression-precedence +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[allow(unused)] +enum OperatorPrecendence { + // return, break, closures + Jump, + // [PREINTERPRET ADDITION] + // Assignment should bind more tightly than arrays. + // e.g. [x = 3, 2] should parse as [(x = 3), 2] rather than [x = (3, 2)] + NonTerminalComma, + /// = += -= *= /= %= &= |= ^= <<= >>= + Assign, + // [PREINTERPRET ADDITION] + // By giving assign extensions slightly higher priority than existing assign + // frames, this effectively makes them right-associative, so that: + // a = b = c parses as a = (b = c) instead of (a = b) = c + AssignExtension, + // .. ..= + Range, + // || + Or, + // && + And, + // let + Let, + /// == != < > <= >= + Compare, + // | + BitOr, + // ^ + BitXor, + // & + BitAnd, + // << >> + Shift, + // + - + Sum, + // * / % + Product, + // as + Cast, + // unary - * ! & &mut + Prefix, + // paths, loops, function calls, array indexing, field expressions, method calls + Unambiguous, +} + +impl OperatorPrecendence { + const MIN: Self = OperatorPrecendence::Jump; + + fn of_prefix_unary_operation(op: &PrefixUnaryOperation) -> Self { + match op { + PrefixUnaryOperation::Neg { .. } | PrefixUnaryOperation::Not { .. } => Self::Prefix, + } + } + + fn of_unary_operation(op: &UnaryOperation) -> Self { + match op { + UnaryOperation::Cast { .. } => Self::Cast, + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => Self::Prefix, + } + } + + fn of_binary_operation(op: &BinaryOperation) -> Self { + match op { + // Arithmetic + BinaryOperation::Addition { .. } | BinaryOperation::Subtraction { .. } => Self::Sum, + BinaryOperation::Multiplication { .. } + | BinaryOperation::Division { .. } + | BinaryOperation::Remainder { .. } => Self::Product, + // Logical + BinaryOperation::LogicalAnd { .. } => Self::And, + BinaryOperation::LogicalOr { .. } => Self::Or, + // Bitwise + BinaryOperation::BitXor { .. } => Self::BitXor, + BinaryOperation::BitAnd { .. } => Self::BitAnd, + BinaryOperation::BitOr { .. } => Self::BitOr, + BinaryOperation::ShiftLeft { .. } | BinaryOperation::ShiftRight { .. } => Self::Shift, + // Comparison + BinaryOperation::Equal { .. } + | BinaryOperation::NotEqual { .. } + | BinaryOperation::LessThan { .. } + | BinaryOperation::LessThanOrEqual { .. } + | BinaryOperation::GreaterThan { .. } + | BinaryOperation::GreaterThanOrEqual { .. } => Self::Compare, + // Compound assignment + BinaryOperation::AddAssign { .. } + | BinaryOperation::SubAssign { .. } + | BinaryOperation::MulAssign { .. } + | BinaryOperation::DivAssign { .. } + | BinaryOperation::RemAssign { .. } + | BinaryOperation::BitAndAssign { .. } + | BinaryOperation::BitOrAssign { .. } + | BinaryOperation::BitXorAssign { .. } + | BinaryOperation::ShlAssign { .. } + | BinaryOperation::ShrAssign { .. } => Self::Assign, + } + } +} + +/// Use of a stack avoids recursion, which hits limits with long/deep expressions. +/// +/// ## Worked Algorithm Sketch +/// +/// Let ParseStream P be: +/// a b cdx e fg h i j k +/// 1 + -(1) + (2 + 4) * 3 +/// +/// The algorithm proceeds as follows: +/// ```text +/// => Start +/// ===> Set parse stack to have a root parsebuffer of [P] +/// ===> Stack: [Root] +/// ===> WorkItem::RequireUnaryAtom +/// => Read leaf a:1 +/// ===> PushEvalNode(A: Leaf(a:1)) +/// ===> Stack: [Root] +/// ===> WorkItem::TryParseAndApplyExtension(A) with precedence >MIN from parent=Root +/// => Read binop b:+ +/// ===> Stack: [Root, BinOp(A, b:+)] +/// ===> WorkItem::RequireUnaryAtom +/// => Read unop c:- +/// ===> Stack: [Root, BinOp(A, b:+), PrefixOp(c:-)] +/// ===> WorkItem::RequireUnaryAtom +/// => Read group d:([D]) +/// ===> PushParseBuffer([D]) +/// ===> Stack: [Root, BinOp(A, b:+), PrefixOp(c:-), Group(d:Paren)] +/// ===> WorkItem::RequireUnaryAtom +/// => Read leaf x:1 +/// ===> PushEvalNode(X: Leaf(x:1), NewParentPrecedence: Group => >MIN) +/// ===> Stack: [Root, BinOp(A, +), PrefixOp(c:-), Group(d:Paren)] +/// ===> WorkItem::TryParseAndApplyExtension(X) with precedence >MIN from parent=Group +/// => Extension of None is not valid, cascade X with Group: +/// ===> PopStack: It's a group, so PopParseBuffer, PushEvalNode(D: UnOp(d:(), X)) +/// ===> Stack: [Root, BinOp(A, b:+), PrefixOp(c:-), TryExtend(D, >PREFIX)] +/// ===> WorkItem::TryParseAndApplyExtension(X) with precedence >PREFIX from parent=PrefixOp(c:-) +/// => Extension of + is not valid, cascade D with PrefixOp: +/// ===> PopStack: PushEvalNode(C: UnOp(c:-, D)) +/// ===> Stack: [Root, BinOp(A, b:+)] +/// ===> WorkItem::TryApplyAlreadyParsedExtension(C, +) with precedence >SUM from parent=BinOp(A, b:+) +/// => Extension of + is not valid, so cascade C with BinOp: +/// ===> PopStack: PushEvalNode(B: BinOp(A, b:+, C)) +/// ===> Stack: [Root] +/// ===> WorkItem::TryApplyAlreadyParsedExtension(B, +) with precedence >MIN from parent=Root +/// => Read binop e:+ +/// ===> Stack: [Root, BinOp(B, e:+)] +/// ===> WorkItem::RequireUnaryAtom +/// => Read group f:([F]) +/// ===> PushParseBuffer([F]) +/// ===> Stack: [Root, BinOp(B, e:+), Group(f:Paren)] +/// ===> WorkItem::RequireUnaryAtom +/// => Read leaf g:2 +/// ===> PushEvalNode(G: Leaf(g:2)) +/// ===> Stack: [Root, BinOp(B, e:+), Group(f:Paren)] +/// ===> WorkItem::TryParseAndApplyExtension(G) with precedence >MIN from parent=Group +/// => Read binop h:+ +/// ===> Stack: [Root, BinOp(B, e:+), Group(f:Paren), BinOp(G, h:+)] +/// ===> WorkItem::RequireUnaryAtom +/// => Read leaf i:2 +/// ===> PushEvalNode(I: Leaf(i:2)) +/// ===> Stack: [Root, BinOp(B, e:+), Group(f:Paren), BinOp(G, h:+)] +/// ===> WorkItem::TryParseAndApplyExtension(I) with precedence >SUM from parent=BinOp(G, h:+) +/// => Extension of None is not valid, so cascade I with BinOp: +/// ===> PopStack: PushEvalNode(H: BinOp(G, h:+, I)) +/// ===> Stack: [Root, BinOp(B, e:+), Group(f:Paren)] +/// ===> WorkItem::TryApplyAlreadyParsedExtension(H, None) with precedence >MIN from parent=Group +/// => Extension of None is not valid, so cascade H with Group: +/// ===> PopStack: It's a group, so PopParseBuffer, PushEvalNode(F: UnOp(f:Paren, H)) +/// ===> Stack: [Root, BinOp(B, e:+)] +/// ===> WorkItem::TryParseAndApplyExtension(F) with precedence >SUM from parent=BinOp(B, e:+) +/// => Read binop j:* +/// ===> Stack: [Root, BinOp(B, e:+), BinOp(F, j:*)] +/// ===> WorkItem::RequireUnaryAtom +/// => Read leaf k:3 +/// ===> PushEvalNode(K: Leaf(k:3)) +/// ===> Stack: [Root, BinOp(B, e:+), BinOp(F, j:*)] +/// ===> WorkItem::TryParseAndApplyExtension(K) with precedence >PRODUCT from parent=BinOp(F, j:*) +/// => Extension of None is not valid, so cascade K with BinOp: +/// ===> PopStack: PushEvalNode(J: BinOp(F, j:*, K)) +/// ===> Stack: [Root, BinOp(B, e:+)] +/// ===> WorkItem::TryApplyAlreadyParsedExtension(J, None) with precedence >SUM from parent=BinOp(B, e:+) +/// => Extension of None is not valid, so cascade J with BinOp: +/// ===> PopStack: PushEvalNode(E: BinOp(B, e:*, J)) +/// ===> Stack: [Root] +/// ===> WorkItem::TryApplyAlreadyParsedExtension(E, None) with precedence >MIN from parent=Root +/// => Extension of None is not valid, so cascade E with Root: +/// ===> Stack: [] +/// ===> WorkItem::Finished(E) +/// ``` +pub(super) enum ExpressionStackFrame { + /// A marker for the root of the expression + Root, + /// A marker for the parenthesized or transparent group. + /// * When the group is opened, we add its inside to the parse stream stack + /// * When the group is closed, we pop it from the parse stream stack + Group { delim_span: DelimSpan }, + /// A marker for the bracketed array. + /// * When the array is opened, we add its inside to the parse stream stack + /// * When the array is closed, we pop it from the parse stream stack + NonEmptyArray { + brackets: Brackets, + items: Vec, + }, + /// A marker for an object literal. + /// * When the object is opened, we add its inside to the parse stream stack + /// * When the object is closed, we pop it from the parse stream stack + NonEmptyObject { + braces: Braces, + complete_entries: Vec<(ObjectKey, ExpressionNodeId)>, + state: ObjectStackFrameState, + }, + /// A method call with a possibly incomplete list of parameters. + /// * When the method parameters list is opened, we add its inside to the parse stream stack + /// * When the method parameters list is closed, we pop it from the parse stream stack + NonEmptyMethodCallParametersList { + node: ExpressionNodeId, + method: MethodAccess, + parameters: Vec, + }, + /// An incomplete unary prefix operation + /// NB: unary postfix operations such as `as` casting go straight to ExtendableNode + IncompleteUnaryPrefixOperation { operation: PrefixUnaryOperation }, + /// An incomplete binary operation + IncompleteBinaryOperation { + lhs: ExpressionNodeId, + operation: BinaryOperation, + }, + /// An incomplete indexing access + IncompleteIndex { + node: ExpressionNodeId, + access: IndexAccess, + }, + /// An incomplete assignment operation + /// It's left side is an assignee expression, according to the [rust reference]. + /// + /// [rust reference]: https://doc.rust-lang.org/reference/expressions.html#place-expressions-and-value-expressions + IncompleteAssignment { + assignee: ExpressionNodeId, + equals_token: Token![=], + }, + /// A range which will be followed by a rhs + IncompleteRange { + lhs: Option, + range_limits: syn::RangeLimits, + }, +} + +#[derive(Clone)] +pub(super) enum ObjectKey { + Identifier(Ident), + Indexed { + access: IndexAccess, + index: ExpressionNodeId, + }, +} + +pub(super) enum ObjectStackFrameState { + EntryIndex(IndexAccess), + #[allow(unused)] + EntryValue(ObjectKey, Token![:]), +} + +impl ExpressionStackFrame { + fn precedence_to_bind_to_child(&self) -> OperatorPrecendence { + match self { + ExpressionStackFrame::Root => OperatorPrecendence::MIN, + ExpressionStackFrame::Group { .. } => OperatorPrecendence::MIN, + ExpressionStackFrame::NonEmptyArray { .. } => OperatorPrecendence::MIN, + ExpressionStackFrame::NonEmptyObject { .. } => OperatorPrecendence::MIN, + ExpressionStackFrame::NonEmptyMethodCallParametersList { .. } => { + OperatorPrecendence::MIN + } + ExpressionStackFrame::IncompleteIndex { .. } => OperatorPrecendence::MIN, + ExpressionStackFrame::IncompleteRange { .. } => OperatorPrecendence::Range, + ExpressionStackFrame::IncompleteAssignment { .. } => OperatorPrecendence::Assign, + ExpressionStackFrame::IncompleteUnaryPrefixOperation { operation, .. } => { + OperatorPrecendence::of_prefix_unary_operation(operation) + } + ExpressionStackFrame::IncompleteBinaryOperation { operation, .. } => { + OperatorPrecendence::of_binary_operation(operation) + } + } + } +} + +enum WorkItem { + /// We require reading a UnaryAtom, such as a command, variable, literal, or unary operation + RequireUnaryAtom, + /// We have a partial expression, which can possibly be extended to the right. + /// => If on the right, there is an operation of higher precedence than its parent, + /// it gets pulled into being part of the right expression + /// => Otherwise it will be combined into its parent + TryParseAndApplyExtension { + node: ExpressionNodeId, + }, + /// The same as [`WorkItem::TryParseAndApplyExtension`], except that we have already + /// parsed the extension, and we are attempting to apply it again. + /// This happens if an extension is of lower precedence than an existing unary/binary + /// operation, and so we complete the existing operation and try to re-apply the extension + /// on the result. For example, the + in `2 * 3 + 4` fails to apply to 3, but can + /// then apply to (2 * 3). + TryApplyAlreadyParsedExtension { + node: ExpressionNodeId, + extension: NodeExtension, + }, + /// Range parsing behaviour is quite unique, so we have a special case for it. + ContinueRange { + lhs: Option, + range_limits: syn::RangeLimits, + }, + Finished { + root: ExpressionNodeId, + }, +} + +pub(super) enum UnaryAtom { + Leaf(Leaf), + Group(DelimSpan), + Array(Brackets), + Object(Braces), + PrefixUnaryOperation(PrefixUnaryOperation), + Range(syn::RangeLimits), +} + +pub(super) enum NodeExtension { + PostfixOperation(UnaryOperation), + BinaryOperation(BinaryOperation), + /// Used under Arrays, Objects, and Method Parameter List parents + NonTerminalComma, + Property(PropertyAccess), + MethodCall(MethodAccess), + Index(IndexAccess), + Range(syn::RangeLimits), + AssignmentOperation(Token![=]), + EndOfStreamOrGroup, + NoValidExtensionForCurrentParent, +} + +impl NodeExtension { + fn precedence(&self) -> OperatorPrecendence { + match self { + NodeExtension::PostfixOperation(op) => OperatorPrecendence::of_unary_operation(op), + NodeExtension::BinaryOperation(op) => OperatorPrecendence::of_binary_operation(op), + NodeExtension::NonTerminalComma => OperatorPrecendence::NonTerminalComma, + NodeExtension::Property { .. } => OperatorPrecendence::Unambiguous, + NodeExtension::MethodCall { .. } => OperatorPrecendence::Unambiguous, + NodeExtension::Index { .. } => OperatorPrecendence::Unambiguous, + NodeExtension::Range(_) => OperatorPrecendence::Range, + NodeExtension::EndOfStreamOrGroup => OperatorPrecendence::MIN, + NodeExtension::AssignmentOperation(_) => OperatorPrecendence::AssignExtension, + NodeExtension::NoValidExtensionForCurrentParent => OperatorPrecendence::MIN, + } + } + + fn into_post_operation_completion_work_item(self, node: ExpressionNodeId) -> WorkItem { + match self { + // These extensions are valid/correct for any parent, + // so can be re-used without parsing again. + extension @ (NodeExtension::PostfixOperation { .. } + | NodeExtension::BinaryOperation { .. } + | NodeExtension::Property { .. } + | NodeExtension::MethodCall { .. } + | NodeExtension::Index { .. } + | NodeExtension::Range { .. } + | NodeExtension::AssignmentOperation { .. } + | NodeExtension::EndOfStreamOrGroup) => { + WorkItem::TryApplyAlreadyParsedExtension { node, extension } + } + NodeExtension::NonTerminalComma => { + unreachable!( + "Comma is only possible on method parameter list or array or object parent" + ) + } + NodeExtension::NoValidExtensionForCurrentParent => { + // We have to reparse in case the extension is valid for the new parent. + // e.g. consider [2 + 3, 4] - initially the peeked comma has a parent of an + // incomplete binary operation 2 + 3 which is invalid. + // When the binary operation is completed, the comma now gets parsed against + // the parent array, which can succeed. + WorkItem::TryParseAndApplyExtension { node } + } + } + } +} diff --git a/src/expressions/mod.rs b/src/expressions/mod.rs new file mode 100644 index 00000000..e24690e4 --- /dev/null +++ b/src/expressions/mod.rs @@ -0,0 +1,31 @@ +// Marked as use for expression sub-modules to use with a `use super::*` statement +use crate::internal_prelude::*; +use expression_parsing::*; + +mod concepts; +mod control_flow; +mod equality; +mod evaluation; +mod expression; +mod expression_block; +mod expression_label; +mod expression_parsing; +mod operations; +mod patterns; +mod statements; +mod type_resolution; +mod values; + +#[allow(unused_imports)] // Whilst we're building it out +pub(crate) use concepts::*; +pub(crate) use control_flow::*; +pub(crate) use equality::*; +pub(crate) use evaluation::*; +pub(crate) use expression::*; +pub(crate) use expression_block::*; +pub(crate) use expression_label::*; +pub(crate) use operations::*; +pub(crate) use patterns::*; +pub(crate) use statements::*; +pub(crate) use type_resolution::*; +pub(crate) use values::*; diff --git a/src/expressions/operations.rs b/src/expressions/operations.rs new file mode 100644 index 00000000..271198be --- /dev/null +++ b/src/expressions/operations.rs @@ -0,0 +1,526 @@ +use std::fmt::{Display, Formatter}; + +use super::*; + +pub(crate) trait Operation: HasSpanRange { + fn symbolic_description(&self) -> &'static str; +} + +pub(super) enum PrefixUnaryOperation { + Neg(Token![-]), + Not(Token![!]), +} + +impl SynParse for PrefixUnaryOperation { + fn parse(input: SynParseStream) -> SynResult { + if input.peek(Token![-]) { + Ok(Self::Neg(input.parse()?)) + } else if input.peek(Token![!]) { + Ok(Self::Not(input.parse()?)) + } else { + Err(input.error("Expected ! or -")) + } + } +} + +impl HasSpan for PrefixUnaryOperation { + fn span(&self) -> Span { + match self { + PrefixUnaryOperation::Neg(token) => token.span, + PrefixUnaryOperation::Not(token) => token.span, + } + } +} + +impl From for UnaryOperation { + fn from(operation: PrefixUnaryOperation) -> Self { + match operation { + PrefixUnaryOperation::Neg(token) => Self::Neg { token }, + PrefixUnaryOperation::Not(token) => Self::Not { token }, + } + } +} + +#[derive(Clone)] +pub(crate) enum UnaryOperation { + Neg { + token: Token![-], + }, + Not { + token: Token![!], + }, + Cast { + as_token: Token![as], + target_ident: Ident, + target: CastTarget, + }, +} + +impl UnaryOperation { + pub(super) fn for_cast_operation( + as_token: Token![as], + target_ident: Ident, + ) -> ParseResult { + let target = TypeIdent::from_ident(&target_ident)?; + let target = CastTarget::from_source_type(target)?; + + Ok(Self::Cast { + as_token, + target, + target_ident, + }) + } + + pub(super) fn output_span_range(&self, mut operand_span_range: SpanRange) -> SpanRange { + match self { + UnaryOperation::Neg { token } => operand_span_range.set_start(token.span), + UnaryOperation::Not { token } => operand_span_range.set_start(token.span), + UnaryOperation::Cast { target_ident, .. } => { + operand_span_range.set_end(target_ident.span()) + } + }; + operand_span_range + } + + pub(super) fn evaluate( + &self, + Spanned(input, input_span): Spanned, + ) -> ExecutionResult> { + let input = input.into_any_value(); + let method = input + .kind() + .feature_resolver() + .resolve_unary_operation(self) + .ok_or_else(|| { + self.type_error(format!( + "The {} operator is not supported for {} operand", + self, + input.articled_kind(), + )) + })?; + let input = method + .argument_ownership + .map_from_owned(Spanned(input, input_span))?; + method.execute(Spanned(input, input_span), self) + } +} + +#[derive(Copy, Clone)] +pub(crate) struct CastTarget(pub(crate) AnyValueLeafKind); + +impl CastTarget { + fn from_source_type(s: TypeIdent) -> ParseResult { + match s.kind { + TypeKind::Parent(ParentTypeKind::Integer(_)) => s.parse_err(format!( + "This type is not supported in cast expressions. Perhaps you want 'as {}'?", + UntypedIntegerKind.source_type_name() + )), + TypeKind::Parent(ParentTypeKind::Float(_)) => s.parse_err(format!( + "This type is not supported in cast expressions. Perhaps you want 'as {}'?", + UntypedFloatKind.source_type_name() + )), + TypeKind::Leaf(leaf_kind) => Ok(CastTarget(leaf_kind)), + _ => s.parse_err("This type is not supported in cast expressions"), + } + } + + /// Denotes that this cast target for a singleton iterable + /// (e.g. array or stream) + pub(crate) fn is_singleton_target(&self) -> bool { + matches!( + self.0, + AnyValueLeafKind::Bool(_) + | AnyValueLeafKind::Char(_) + | AnyValueLeafKind::Integer(_) + | AnyValueLeafKind::Float(_) + ) + } +} + +impl Display for UnaryOperation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + UnaryOperation::Neg { .. } => write!(f, "-"), + UnaryOperation::Not { .. } => write!(f, "!"), + UnaryOperation::Cast { target, .. } => write!(f, "as {}", target.0.source_type_name()), + } + } +} + +impl HasSpan for UnaryOperation { + fn span(&self) -> Span { + match self { + UnaryOperation::Neg { token } => token.span, + UnaryOperation::Not { token } => token.span, + UnaryOperation::Cast { as_token, .. } => as_token.span, + } + } +} + +/// Flattened binary operation enum containing all binary operators. +/// +/// This includes: +/// - Arithmetic: Addition (+), Subtraction (-), Multiplication (*), Division (/), Remainder (%) +/// - Logical: LogicalAnd (&&), LogicalOr (||) +/// - Bitwise: BitXor (^), BitAnd (&), BitOr (|), ShiftLeft (<<), ShiftRight (>>) +/// - Comparison: Equal (==), NotEqual (!=), LessThan (<), LessThanOrEqual (<=), GreaterThan (>), GreaterThanOrEqual (>=) +/// - Compound Assignment: AddAssign (+=), SubAssign (-=), MulAssign (*=), DivAssign (/=), RemAssign (%=), +/// BitAndAssign (&=), BitOrAssign (|=), BitXorAssign (^=), ShlAssign (<<=), ShrAssign (>>=) +#[derive(Copy, Clone)] +pub(crate) enum BinaryOperation { + // Arithmetic operations + Addition(Token![+]), + Subtraction(Token![-]), + Multiplication(Token![*]), + Division(Token![/]), + Remainder(Token![%]), + // Logical operations + LogicalAnd(Token![&&]), + LogicalOr(Token![||]), + // Bitwise operations + BitXor(Token![^]), + BitAnd(Token![&]), + BitOr(Token![|]), + ShiftLeft(Token![<<]), + ShiftRight(Token![>>]), + // Comparison operations + Equal(Token![==]), + NotEqual(Token![!=]), + LessThan(Token![<]), + LessThanOrEqual(Token![<=]), + GreaterThan(Token![>]), + GreaterThanOrEqual(Token![>=]), + // Compound assignment operations + AddAssign(Token![+=]), + SubAssign(Token![-=]), + MulAssign(Token![*=]), + DivAssign(Token![/=]), + RemAssign(Token![%=]), + BitAndAssign(Token![&=]), + BitOrAssign(Token![|=]), + BitXorAssign(Token![^=]), + ShlAssign(Token![<<=]), + ShrAssign(Token![>>=]), +} + +impl SynParse for BinaryOperation { + fn parse(input: SynParseStream) -> SynResult { + // In line with Syn's BinOp, we use peek instead of lookahead + // ...I assume for slightly increased performance + // ...Or because 30 alternative options in the error message is too many + // NOTE: Order is important here - longer tokens must be checked first, + // because e.g. Token![+] doesn't check for Spacing::Alone so matches the + // start of Token![+=] + // [TODO-performance]: Convert this into a much more efficient parse-tree + if input.peek(Token![+=]) { + Ok(Self::AddAssign(input.parse()?)) + } else if input.peek(Token![+]) { + Ok(Self::Addition(input.parse()?)) + } else if input.peek(Token![-=]) { + Ok(Self::SubAssign(input.parse()?)) + } else if input.peek(Token![-]) { + Ok(Self::Subtraction(input.parse()?)) + } else if input.peek(Token![*=]) { + Ok(Self::MulAssign(input.parse()?)) + } else if input.peek(Token![*]) { + Ok(Self::Multiplication(input.parse()?)) + } else if input.peek(Token![/=]) { + Ok(Self::DivAssign(input.parse()?)) + } else if input.peek(Token![/]) { + Ok(Self::Division(input.parse()?)) + } else if input.peek(Token![%=]) { + Ok(Self::RemAssign(input.parse()?)) + } else if input.peek(Token![%]) { + Ok(Self::Remainder(input.parse()?)) + } else if input.peek(Token![&&]) { + Ok(Self::LogicalAnd(input.parse()?)) + } else if input.peek(Token![||]) { + Ok(Self::LogicalOr(input.parse()?)) + } else if input.peek(Token![==]) { + Ok(Self::Equal(input.parse()?)) + } else if input.peek(Token![!=]) { + Ok(Self::NotEqual(input.parse()?)) + } else if input.peek(Token![>=]) { + Ok(Self::GreaterThanOrEqual(input.parse()?)) + } else if input.peek(Token![<=]) { + Ok(Self::LessThanOrEqual(input.parse()?)) + } else if input.peek(Token![<<=]) { + Ok(Self::ShlAssign(input.parse()?)) + } else if input.peek(Token![<<]) { + Ok(Self::ShiftLeft(input.parse()?)) + } else if input.peek(Token![>>=]) { + Ok(Self::ShrAssign(input.parse()?)) + } else if input.peek(Token![>>]) { + Ok(Self::ShiftRight(input.parse()?)) + } else if input.peek(Token![>]) { + Ok(Self::GreaterThan(input.parse()?)) + } else if input.peek(Token![<]) { + Ok(Self::LessThan(input.parse()?)) + } else if input.peek(Token![&=]) { + Ok(Self::BitAndAssign(input.parse()?)) + } else if input.peek(Token![&]) { + Ok(Self::BitAnd(input.parse()?)) + } else if input.peek(Token![|=]) { + Ok(Self::BitOrAssign(input.parse()?)) + } else if input.peek(Token![|]) { + Ok(Self::BitOr(input.parse()?)) + } else if input.peek(Token![^=]) { + Ok(Self::BitXorAssign(input.parse()?)) + } else if input.peek(Token![^]) { + Ok(Self::BitXor(input.parse()?)) + } else { + Err(input.error("Expected one of + - * / % && || ^ & | == < <= != >= > << >> += -= *= /= %= &= |= ^= <<= or >>=")) + } + } +} + +impl BinaryOperation { + pub(super) fn lazy_evaluate( + &self, + left: Spanned<&AnyValue>, + ) -> ExecutionResult> { + match self { + BinaryOperation::LogicalAnd { .. } => { + let bool: Spanned<&bool> = left.resolve_as("The left operand to &&")?; + if !**bool { + Ok(Some((*bool).into_any_value())) + } else { + Ok(None) + } + } + BinaryOperation::LogicalOr { .. } => { + let bool: Spanned<&bool> = left.resolve_as("The left operand to ||")?; + if **bool { + Ok(Some((*bool).into_any_value())) + } else { + Ok(None) + } + } + _ => Ok(None), + } + } + + #[allow(unused)] + pub(crate) fn evaluate( + &self, + Spanned(left, left_span): Spanned, + Spanned(right, right_span): Spanned, + ) -> ExecutionResult> { + let left = left.into_any_value(); + let right = right.into_any_value(); + match left + .kind() + .feature_resolver() + .resolve_binary_operation(self) + { + Some(interface) => { + let left = interface + .lhs_ownership + .map_from_owned(Spanned(left, left_span))?; + let right = interface + .rhs_ownership + .map_from_owned(Spanned(right, right_span))?; + interface.execute(Spanned(left, left_span), Spanned(right, right_span), self) + } + None => self.type_err(format!( + "The {} operator is not supported for {} operand", + self.symbolic_description(), + left.articled_kind(), + )), + } + } +} + +impl HasSpanRange for BinaryOperation { + fn span_range(&self) -> SpanRange { + match self { + // Arithmetic + BinaryOperation::Addition(op) => op.span_range(), + BinaryOperation::Subtraction(op) => op.span_range(), + BinaryOperation::Multiplication(op) => op.span_range(), + BinaryOperation::Division(op) => op.span_range(), + BinaryOperation::Remainder(op) => op.span_range(), + // Logical + BinaryOperation::LogicalAnd(op) => op.span_range(), + BinaryOperation::LogicalOr(op) => op.span_range(), + // Bitwise + BinaryOperation::BitXor(op) => op.span_range(), + BinaryOperation::BitAnd(op) => op.span_range(), + BinaryOperation::BitOr(op) => op.span_range(), + BinaryOperation::ShiftLeft(op) => op.span_range(), + BinaryOperation::ShiftRight(op) => op.span_range(), + // Comparison + BinaryOperation::Equal(op) => op.span_range(), + BinaryOperation::NotEqual(op) => op.span_range(), + BinaryOperation::LessThan(op) => op.span_range(), + BinaryOperation::LessThanOrEqual(op) => op.span_range(), + BinaryOperation::GreaterThan(op) => op.span_range(), + BinaryOperation::GreaterThanOrEqual(op) => op.span_range(), + // Compound assignment + BinaryOperation::AddAssign(op) => op.span_range(), + BinaryOperation::SubAssign(op) => op.span_range(), + BinaryOperation::MulAssign(op) => op.span_range(), + BinaryOperation::DivAssign(op) => op.span_range(), + BinaryOperation::RemAssign(op) => op.span_range(), + BinaryOperation::BitAndAssign(op) => op.span_range(), + BinaryOperation::BitOrAssign(op) => op.span_range(), + BinaryOperation::BitXorAssign(op) => op.span_range(), + BinaryOperation::ShlAssign(op) => op.span_range(), + BinaryOperation::ShrAssign(op) => op.span_range(), + } + } +} + +impl Operation for BinaryOperation { + fn symbolic_description(&self) -> &'static str { + match self { + // Arithmetic + BinaryOperation::Addition { .. } => "+", + BinaryOperation::Subtraction { .. } => "-", + BinaryOperation::Multiplication { .. } => "*", + BinaryOperation::Division { .. } => "/", + BinaryOperation::Remainder { .. } => "%", + // Logical + BinaryOperation::LogicalAnd { .. } => "&&", + BinaryOperation::LogicalOr { .. } => "||", + // Bitwise + BinaryOperation::BitXor { .. } => "^", + BinaryOperation::BitAnd { .. } => "&", + BinaryOperation::BitOr { .. } => "|", + BinaryOperation::ShiftLeft { .. } => "<<", + BinaryOperation::ShiftRight { .. } => ">>", + // Comparison + BinaryOperation::Equal { .. } => "==", + BinaryOperation::NotEqual { .. } => "!=", + BinaryOperation::LessThan { .. } => "<", + BinaryOperation::LessThanOrEqual { .. } => "<=", + BinaryOperation::GreaterThan { .. } => ">", + BinaryOperation::GreaterThanOrEqual { .. } => ">=", + // Compound assignment + BinaryOperation::AddAssign { .. } => "+=", + BinaryOperation::SubAssign { .. } => "-=", + BinaryOperation::MulAssign { .. } => "*=", + BinaryOperation::DivAssign { .. } => "/=", + BinaryOperation::RemAssign { .. } => "%=", + BinaryOperation::BitAndAssign { .. } => "&=", + BinaryOperation::BitOrAssign { .. } => "|=", + BinaryOperation::BitXorAssign { .. } => "^=", + BinaryOperation::ShlAssign { .. } => "<<=", + BinaryOperation::ShrAssign { .. } => ">>=", + } + } +} + +pub(super) trait HandleBinaryOperation: Sized + std::fmt::Display + Copy { + fn type_name() -> &'static str; + + fn binary_overflow_error( + context: BinaryOperationCallContext, + lhs: Self, + rhs: impl std::fmt::Display, + ) -> ExecutionInterrupt { + context.error(format!( + "The {} operation {} {} {} overflowed", + Self::type_name(), + lhs, + context.operation.symbolic_description(), + rhs + )) + } + + fn paired_operation>( + self, + rhs: impl ResolveAs>, + context: BinaryOperationCallContext, + perform_fn: fn(Self, Self) -> Option, + ) -> ExecutionResult { + let lhs = self; + let rhs = rhs.resolve_as("This operand")?; + perform_fn(lhs, rhs.0) + .map(|r| r.into()) + .ok_or_else(|| Self::binary_overflow_error(context, lhs, rhs.0)) + } + + fn paired_operation_no_overflow>( + self, + rhs: impl ResolveAs>, + perform_fn: fn(Self, Self) -> Self, + ) -> ExecutionResult { + let lhs = self; + let rhs = rhs.resolve_as("This operand")?; + Ok(perform_fn(lhs, rhs.0).into()) + } + + fn paired_comparison( + self, + rhs: impl ResolveAs>, + compare_fn: fn(Self, Self) -> bool, + ) -> ExecutionResult { + let lhs = self; + let rhs = rhs.resolve_as("This operand")?; + Ok(compare_fn(lhs, rhs.0)) + } + + fn shift_operation>( + self, + rhs: u32, + context: BinaryOperationCallContext, + perform_fn: impl FnOnce(Self, u32) -> Option, + ) -> ExecutionResult { + let lhs = self; + perform_fn(lhs, rhs) + .map(|r| r.into()) + .ok_or_else(|| Self::binary_overflow_error(context, lhs, rhs)) + } +} + +impl Operation for syn::RangeLimits { + fn symbolic_description(&self) -> &'static str { + match self { + syn::RangeLimits::HalfOpen(_) => "..", + syn::RangeLimits::Closed(_) => "..=", + } + } +} + +impl HasSpanRange for syn::RangeLimits { + fn span_range(&self) -> SpanRange { + self.span_range_from_iterating_over_all_tokens() + } +} + +#[derive(Clone)] +pub(crate) struct PropertyAccess { + pub(super) dot: Token![.], + pub(crate) property: Ident, +} + +impl HasSpanRange for PropertyAccess { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.dot.span, self.property.span()) + } +} + +#[derive(Clone)] +pub(crate) struct MethodAccess { + pub(super) dot: Token![.], + pub(crate) method: Ident, + pub(crate) parentheses: Parentheses, +} + +impl HasSpanRange for MethodAccess { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.dot.span, self.parentheses.span()) + } +} + +#[derive(Copy, Clone)] +pub(crate) struct IndexAccess { + pub(crate) brackets: Brackets, +} + +impl HasSpan for IndexAccess { + fn span(&self) -> Span { + self.brackets.join() + } +} diff --git a/src/expressions/patterns.rs b/src/expressions/patterns.rs new file mode 100644 index 00000000..1f07033c --- /dev/null +++ b/src/expressions/patterns.rs @@ -0,0 +1,406 @@ +use crate::internal_prelude::*; + +pub(crate) trait HandleDestructure { + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: AnyValue, + ) -> ExecutionResult<()>; +} + +pub(crate) enum Pattern { + Variable(VariablePattern), + Array(ArrayPattern), + Object(ObjectPattern), + Stream(StreamPattern), + ParseTemplatePattern(ParseTemplatePattern), + Discarded(Unused), +} + +impl ParseSource for Pattern { + fn parse(input: SourceParser) -> ParseResult { + let lookahead = input.lookahead1(); + if lookahead.peek(syn::Ident) { + Ok(Pattern::Variable(input.parse()?)) + } else if lookahead.peek(syn::token::Bracket) { + Ok(Pattern::Array(input.parse()?)) + } else if lookahead.peek(Token![%]) { + let (_, next) = input.cursor().punct_matching('%').unwrap(); + if next.group_matching(Delimiter::Brace).is_some() { + Ok(Pattern::Object(input.parse()?)) + } else if next.group_matching(Delimiter::Bracket).is_some() { + Ok(Pattern::Stream(input.parse()?)) + } else if next.ident_matching("raw").is_some() { + input.parse_err( + "Use `@input[#{ input.read(%raw[...]); }]` to match `%raw[...]` stream literal content", + ) + } else if next.ident_matching("group").is_some() { + input.parse_err( + "Use `@input[#{ input.read(%group[...]); }]` to match `%group[...]` stream literal content. Although this is likely not necessary, as transparent groups are typically silently ignored when parsing, unless explicitly parsing a token tree or group.", + ) + } else { + input.parse_err("Expected a pattern, such as an object pattern `%{ ... }` or stream pattern `%[ ... ]`") + } + } else if lookahead.peek(Token![@]) { + Ok(Pattern::ParseTemplatePattern(input.parse()?)) + } else if lookahead.peek(Token![_]) { + Ok(Pattern::Discarded(input.parse()?)) + } else if input.peek(Token![#]) { + input.parse_err("Use `var` instead of `#var` in a destructuring") + } else { + Err(lookahead.error().into()) + } + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + match self { + Pattern::Variable(variable) => variable.control_flow_pass(context), + Pattern::Array(array) => array.control_flow_pass(context), + Pattern::Object(object) => object.control_flow_pass(context), + Pattern::Stream(stream) => stream.control_flow_pass(context), + Pattern::ParseTemplatePattern(pattern) => pattern.control_flow_pass(context), + Pattern::Discarded(_) => Ok(()), + } + } +} + +impl HandleDestructure for Pattern { + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: AnyValue, + ) -> ExecutionResult<()> { + match self { + Pattern::Variable(variable) => variable.handle_destructure(interpreter, value), + Pattern::Array(array) => array.handle_destructure(interpreter, value), + Pattern::Object(object) => object.handle_destructure(interpreter, value), + Pattern::Stream(stream) => stream.handle_destructure(interpreter, value), + Pattern::ParseTemplatePattern(pattern) => { + pattern.handle_destructure(interpreter, value) + } + Pattern::Discarded(_) => Ok(()), + } + } +} + +pub struct ArrayPattern { + #[allow(unused)] + brackets: Brackets, + items: Punctuated, +} + +impl ParseSource for ArrayPattern { + fn parse(input: SourceParser) -> ParseResult { + let (brackets, inner) = input.parse_brackets()?; + Ok(Self { + brackets, + items: inner.parse_terminated()?, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + for item in self.items.iter_mut() { + item.control_flow_pass(context)?; + } + Ok(()) + } +} + +impl HandleDestructure for ArrayPattern { + /// See also `ArrayAssigneeStackFrame` in `evaluation.rs` + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: AnyValue, + ) -> ExecutionResult<()> { + let array: ArrayValue = Spanned(value, self.brackets.span_range()) + .resolve_as("The value destructured with an array pattern")?; + let mut has_seen_dot_dot = false; + let mut prefix_assignees = Vec::new(); + let mut suffix_assignees = Vec::new(); + for pattern in self.items.iter() { + match pattern { + PatternOrDotDot::DotDot(dot_dot) => { + if has_seen_dot_dot { + return dot_dot.syntax_err("Only one .. is allowed in an array pattern"); + } + has_seen_dot_dot = true; + } + PatternOrDotDot::Pattern(pattern) => { + if has_seen_dot_dot { + suffix_assignees.push(pattern); + } else { + prefix_assignees.push(pattern); + } + } + } + } + let array_length = array.items.len(); + + let assignee_pairs: Vec<_> = if has_seen_dot_dot { + let total_assignees = prefix_assignees.len() + suffix_assignees.len(); + if total_assignees > array_length { + return self.brackets.value_err(format!( + "The array has {} items, but the pattern expected at least {}", + array_length, total_assignees, + )); + } + let discarded_count = + array.items.len() - prefix_assignees.len() - suffix_assignees.len(); + let assignees = prefix_assignees + .into_iter() + .map(Some) + .chain(std::iter::repeat(None).take(discarded_count)) + .chain(suffix_assignees.into_iter().map(Some)); + + assignees + .zip(array.items) + .filter_map(|(assignee, value)| Some((assignee?, value))) + .collect() + } else { + let total_assignees = prefix_assignees.len(); + if total_assignees != array_length { + return self.brackets.value_err(format!( + "The array has {} items, but the pattern expected {}", + array_length, total_assignees, + )); + } + prefix_assignees.into_iter().zip(array.items).collect() + }; + for (pattern, value) in assignee_pairs { + pattern.handle_destructure(interpreter, value)?; + } + Ok(()) + } +} + +enum PatternOrDotDot { + Pattern(Pattern), + DotDot(Token![..]), +} + +impl ParseSource for PatternOrDotDot { + fn parse(input: SourceParser) -> ParseResult { + if input.peek(Token![..]) { + Ok(PatternOrDotDot::DotDot(input.parse()?)) + } else { + Ok(PatternOrDotDot::Pattern(input.parse()?)) + } + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + match self { + PatternOrDotDot::Pattern(pattern) => pattern.control_flow_pass(context), + PatternOrDotDot::DotDot(_) => Ok(()), + } + } +} + +pub struct ObjectPattern { + _prefix: Unused, + braces: Braces, + entries: Punctuated, +} + +impl ParseSource for ObjectPattern { + fn parse(input: SourceParser) -> ParseResult { + let _prefix = input.parse()?; + let (_braces, inner) = input.parse_braces()?; + Ok(Self { + _prefix, + braces: _braces, + entries: inner.parse_terminated()?, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + for entry in self.entries.iter_mut() { + entry.control_flow_pass(context)?; + } + Ok(()) + } +} + +impl HandleDestructure for ObjectPattern { + /// See also `ObjectAssigneeStackFrame` in `evaluation.rs` + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: AnyValue, + ) -> ExecutionResult<()> { + let object: ObjectValue = Spanned(value, self.braces.span_range()) + .resolve_as("The value destructured with an object pattern")?; + let mut value_map = object.entries; + let mut already_used_keys = HashSet::with_capacity(self.entries.len()); + for entry in self.entries.iter() { + let (key, key_span, pattern) = match entry { + ObjectEntry::KeyOnly { field, pattern } => { + (field.to_string(), field.span(), pattern) + } + ObjectEntry::KeyValue { field, pattern, .. } => { + (field.to_string(), field.span(), pattern) + } + ObjectEntry::IndexValue { + access, + key, + pattern, + .. + } => (key.value(), access.span(), pattern), + }; + if already_used_keys.contains(&key) { + return key_span.syntax_err(format!("The key `{}` has already used", key)); + } + let value = value_map + .remove(&key) + .map(|entry| entry.value) + .unwrap_or_else(|| ().into_any_value()); + already_used_keys.insert(key); + pattern.handle_destructure(interpreter, value)?; + } + Ok(()) + } +} + +enum ObjectEntry { + KeyOnly { + field: Ident, + pattern: Pattern, + }, + KeyValue { + field: Ident, + _colon: Token![:], + pattern: Pattern, + }, + IndexValue { + access: IndexAccess, + key: syn::LitStr, + _colon: Token![:], + pattern: Pattern, + }, +} + +impl ParseSource for ObjectEntry { + fn parse(input: SourceParser) -> ParseResult { + if input.peek(syn::Ident) { + let field = input.parse()?; + if input.peek(Token![:]) { + Ok(ObjectEntry::KeyValue { + field, + _colon: input.parse()?, + pattern: input.parse()?, + }) + } else if input.peek(Token![,]) || input.is_empty() { + let pattern = Pattern::Variable(VariablePattern { + definition: VariableDefinition { + ident: field.clone(), + id: VariableDefinitionId::new_placeholder(), + }, + }); + Ok(ObjectEntry::KeyOnly { field, pattern }) + } else { + input.parse_err("Expected `:` or `,`") + } + } else if input.peek(syn::token::Bracket) { + let (access, key) = { + let (brackets, inner) = input.parse_brackets()?; + (IndexAccess { brackets }, inner.parse()?) + }; + Ok(ObjectEntry::IndexValue { + access, + key, + _colon: input.parse()?, + pattern: input.parse()?, + }) + } else { + input.parse_err("Expected `property: ` or `[\"property\"]: `") + } + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + match self { + ObjectEntry::KeyOnly { pattern, .. } => pattern.control_flow_pass(context), + ObjectEntry::KeyValue { pattern, .. } => pattern.control_flow_pass(context), + ObjectEntry::IndexValue { pattern, .. } => pattern.control_flow_pass(context), + } + } +} + +pub struct StreamPattern { + _prefix: Unused, + brackets: Brackets, + // TODO[parsers]: Replace with a distinct type that doesn't allow embedded statements, but does allow %[group] and %[raw] + content: ParseTemplateStream, +} + +impl ParseSource for StreamPattern { + fn parse(input: SourceParser) -> ParseResult { + let _prefix = input.parse()?; + let (brackets, inner) = input.parse_brackets()?; + Ok(Self { + _prefix, + brackets, + content: ParseTemplateStream::parse_with_span(&inner, brackets.span())?, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.content.control_flow_pass(context) + } +} + +impl HandleDestructure for StreamPattern { + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: AnyValue, + ) -> ExecutionResult<()> { + let stream = Spanned(value, self.brackets.span_range()) + .downcast_resolve("The value destructured with a stream pattern")?; + interpreter.start_parse(stream, |interpreter, _| self.content.consume(interpreter)) + } +} + +/// Note: This is very similar to a [`ParseTemplateLiteral`], but here, the ident is a *definition*, +/// and used to capture the consumed stream into a variable. There, the ident is a reference +/// to an existing variable, whose value is expected to be a parser. +pub(crate) struct ParseTemplatePattern { + _prefix: Unused, + parser_definition: VariableDefinition, + brackets: Brackets, + content: ParseTemplateStream, +} + +impl ParseSource for ParseTemplatePattern { + fn parse(input: SourceParser) -> ParseResult { + let _prefix = input.parse()?; + let parser_definition = input.parse()?; + let (brackets, inner) = input.parse_brackets()?; + let content = ParseTemplateStream::parse_with_span(&inner, brackets.span())?; + Ok(Self { + _prefix, + parser_definition, + brackets, + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.parser_definition.control_flow_pass(context)?; + self.content.control_flow_pass(context) + } +} + +impl HandleDestructure for ParseTemplatePattern { + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: AnyValue, + ) -> ExecutionResult<()> { + let stream = Spanned(value, self.brackets.span_range()) + .downcast_resolve("The value destructured with a parse template pattern")?; + interpreter.start_parse(stream, |interpreter, handle| { + self.parser_definition.define(interpreter, handle); + self.content.consume(interpreter) + }) + } +} diff --git a/src/expressions/statements.rs b/src/expressions/statements.rs new file mode 100644 index 00000000..e0d4fca3 --- /dev/null +++ b/src/expressions/statements.rs @@ -0,0 +1,377 @@ +use super::*; + +pub(crate) enum Statement { + LetStatement(LetStatement), + BreakStatement(BreakStatement), + ContinueStatement(ContinueStatement), + RevertStatement(RevertStatement), + EmitStatement(EmitStatement), + Expression(Expression), +} + +impl Statement { + pub(crate) fn requires_semicolon(&self, last_line: bool) -> bool { + match self { + Statement::LetStatement(_) => true, + Statement::BreakStatement(_) => true, + Statement::ContinueStatement(_) => true, + Statement::RevertStatement(_) => true, + Statement::EmitStatement(_) => true, + Statement::Expression(expression) => { + !(expression.is_valid_as_statement_without_semicolon() || last_line) + } + } + } +} + +impl ParseSource for Statement { + fn parse(input: SourceParser) -> ParseResult { + Ok(if let Some((ident, _)) = input.cursor().ident() { + match ident.to_string().as_str() { + "let" => Statement::LetStatement(input.parse()?), + "break" => Statement::BreakStatement(input.parse()?), + "continue" => Statement::ContinueStatement(input.parse()?), + keyword::REVERT => Statement::RevertStatement(input.parse()?), + keyword::EMIT => Statement::EmitStatement(input.parse()?), + _ => Statement::Expression(input.parse()?), + } + } else { + Statement::Expression(input.parse()?) + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + match self { + Statement::LetStatement(statement) => statement.control_flow_pass(context), + Statement::Expression(expression) => expression.control_flow_pass(context), + Statement::BreakStatement(statement) => statement.control_flow_pass(context), + Statement::ContinueStatement(statement) => statement.control_flow_pass(context), + Statement::RevertStatement(statement) => statement.control_flow_pass(context), + Statement::EmitStatement(statement) => statement.control_flow_pass(context), + } + } +} + +impl Statement { + pub(crate) fn evaluate_as_statement( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult<()> { + match self { + Statement::LetStatement(statement) => statement.evaluate_as_statement(interpreter), + Statement::Expression(expression) => expression.evaluate_as_statement(interpreter), + Statement::BreakStatement(statement) => statement.evaluate_as_statement(interpreter), + Statement::ContinueStatement(statement) => statement.evaluate_as_statement(interpreter), + Statement::RevertStatement(statement) => statement.evaluate_as_statement(interpreter), + Statement::EmitStatement(statement) => statement.evaluate_as_statement(interpreter), + } + } + + pub(crate) fn evaluate_as_returning_expression( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + match self { + Statement::Expression(expression) => { + expression.evaluate(interpreter, ownership).map(|v| v.0) + } + Statement::LetStatement(_) + | Statement::BreakStatement(_) + | Statement::ContinueStatement(_) + | Statement::RevertStatement(_) + | Statement::EmitStatement(_) => { + panic!("Statements cannot be used as returning expressions") + } + } + } +} + +/// Note a `let x = ...;` is very different to `x = ...;` inside an expression. +/// In the former, `x` is a pattern, and any identifiers creates new variable/bindings. +/// In the latter, `x` is a place expression, and identifiers can be either place references or +/// values, e.g. `a.x[y[0]][3] = ...` has `y[0]` evaluated as a value. +pub(crate) struct LetStatement { + _let_token: Token![let], + pattern: Pattern, + assignment: Option, +} + +struct LetStatementAssignment { + #[allow(unused)] + equals: Token![=], + expression: Expression, +} + +impl ParseSource for LetStatement { + fn parse(input: SourceParser) -> ParseResult { + let let_token = input.parse()?; + let pattern = input.parse()?; + if input.peek(Token![=]) { + Ok(Self { + _let_token: let_token, + pattern, + assignment: Some(LetStatementAssignment { + equals: input.parse()?, + expression: input.parse()?, + }), + }) + } else if input.is_empty() || input.peek(Token![;]) { + Ok(Self { + _let_token: let_token, + pattern, + assignment: None, + }) + } else { + input.parse_err("Expected = or ;") + } + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + if let Some(assignment) = self.assignment.as_mut() { + assignment.expression.control_flow_pass(context)?; + } + self.pattern.control_flow_pass(context)?; + Ok(()) + } +} + +impl LetStatement { + fn evaluate_as_statement(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + let LetStatement { + _let_token: _, + pattern, + assignment, + } = self; + let value = match assignment { + Some(assignment) => assignment.expression.evaluate_owned(interpreter)?.0, + None => ().into_any_value(), + }; + pattern.handle_destructure(interpreter, value)?; + Ok(()) + } +} + +pub(crate) struct InterruptLabel { + label: syn::Lifetime, +} + +impl ParseSource for InterruptLabel { + fn parse(input: SourceParser) -> ParseResult { + let label = input.parse()?; + Ok(Self { label }) + } + + fn control_flow_pass(&mut self, _context: FlowCapturer) -> ParseResult<()> { + Ok(()) + } +} + +impl ParseSourceOptional for InterruptLabel {} + +impl HasSpanRange for InterruptLabel { + fn span_range(&self) -> SpanRange { + self.label.span_range() + } +} + +impl InterruptLabel { + pub(crate) fn ident_string(&self) -> String { + self.label.ident.to_string() + } +} + +pub(crate) struct BreakStatement { + break_token: Token![break], + label: Option, + value: Option, + target_catch_location: CatchLocationId, +} + +impl HasSpanRange for BreakStatement { + fn span_range(&self) -> SpanRange { + let last = self + .label + .as_ref() + .map(|l| l.end_span()) + .unwrap_or_else(|| self.break_token.span); + SpanRange::new_between(self.break_token.span, last) + } +} + +impl ParseSource for BreakStatement { + fn parse(input: SourceParser) -> ParseResult { + let break_token = input.parse()?; + let label = input.parse_optional()?; + + // Try to parse an optional expression value + let value = if !input.is_empty() && !input.peek(Token![;]) { + Some(input.parse()?) + } else { + None + }; + + Ok(Self { + break_token, + label, + value, + target_catch_location: CatchLocationId::new_placeholder(), + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.target_catch_location = + context.resolve_catch_for_interrupt(InterruptDetails::Break { + break_token: &self.break_token, + label: self.label.as_ref(), + })?; + if let Some(value) = &mut self.value { + value.control_flow_pass(context)?; + } + Ok(()) + } +} + +impl BreakStatement { + pub(crate) fn evaluate_as_statement( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult<()> { + let value = if let Some(expr) = &self.value { + Some(expr.evaluate_owned(interpreter)?.0) + } else { + None + }; + + Err(ExecutionInterrupt::control_flow( + ControlFlowInterrupt::new_break(self.target_catch_location, value), + )) + } +} + +pub(crate) struct ContinueStatement { + target_catch_location: CatchLocationId, + continue_token: Token![continue], + label: Option, +} + +impl HasSpanRange for ContinueStatement { + fn span_range(&self) -> SpanRange { + let last = self + .label + .as_ref() + .map(|l| l.end_span()) + .unwrap_or_else(|| self.continue_token.span); + SpanRange::new_between(self.continue_token.span, last) + } +} + +impl ParseSource for ContinueStatement { + fn parse(input: SourceParser) -> ParseResult { + let continue_token = input.parse()?; + let label = input.parse_optional()?; + + Ok(Self { + target_catch_location: CatchLocationId::new_placeholder(), + continue_token, + label, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.target_catch_location = + context.resolve_catch_for_interrupt(InterruptDetails::Continue { + label: self.label.as_ref(), + continue_token: &self.continue_token, + })?; + Ok(()) + } +} + +impl ContinueStatement { + pub(crate) fn evaluate_as_statement(&self, _: &mut Interpreter) -> ExecutionResult<()> { + Err(ExecutionInterrupt::control_flow( + ControlFlowInterrupt::new_continue(self.target_catch_location), + )) + } +} + +pub(crate) struct RevertStatement { + revert: RevertKeyword, + label: Option, + target_catch_location: CatchLocationId, +} + +impl HasSpanRange for RevertStatement { + fn span_range(&self) -> SpanRange { + let last = self + .label + .as_ref() + .map(|l| l.end_span()) + .unwrap_or_else(|| self.revert.span()); + SpanRange::new_between(self.revert.span(), last) + } +} + +impl ParseSource for RevertStatement { + fn parse(input: SourceParser) -> ParseResult { + Ok(Self { + revert: input.parse()?, + label: input.parse_optional()?, + target_catch_location: CatchLocationId::new_placeholder(), + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.target_catch_location = + context.resolve_catch_for_interrupt(InterruptDetails::Revert { + revert_token: &self.revert, + label: self.label.as_ref(), + })?; + Ok(()) + } +} + +impl RevertStatement { + pub(crate) fn evaluate_as_statement(&self, _: &mut Interpreter) -> ExecutionResult<()> { + Err(ExecutionInterrupt::control_flow( + ControlFlowInterrupt::new_revert(self.target_catch_location), + )) + } +} + +pub(crate) struct EmitStatement { + emit: EmitKeyword, + expression: Expression, +} + +impl HasSpan for EmitStatement { + fn span(&self) -> Span { + self.emit.span() + } +} + +impl ParseSource for EmitStatement { + fn parse(input: SourceParser) -> ParseResult { + let emit = input.parse()?; + let expression = input.parse()?; + Ok(Self { emit, expression }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.expression.control_flow_pass(context) + } +} + +impl EmitStatement { + pub(crate) fn evaluate_as_statement( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult<()> { + let Spanned(value, span) = self.expression.evaluate_owned(interpreter)?; + value.as_ref_value().output_to( + Grouping::Flattened, + &mut ToStreamContext::new(interpreter.output(&self.emit)?, span), + ) + } +} diff --git a/src/expressions/type_resolution/arguments.rs b/src/expressions/type_resolution/arguments.rs new file mode 100644 index 00000000..4e272095 --- /dev/null +++ b/src/expressions/type_resolution/arguments.rs @@ -0,0 +1,450 @@ +#![allow(unused)] +// TODO[unused-clearup] +use super::*; + +/// Used for resolution of integers and floats from either typed or untyped literals. +pub(crate) struct OptionalSuffix(pub(crate) T); + +pub(crate) struct ResolutionContext<'a> { + span_range: &'a SpanRange, + resolution_target: &'a str, +} + +impl<'a> ResolutionContext<'a> { + pub(crate) fn new(span_range: &'a SpanRange, resolution_target: &'a str) -> Self { + Self { + span_range, + resolution_target, + } + } + + #[inline] + pub(crate) fn value_span_range(&self) -> SpanRange { + *self.span_range + } + + #[inline] + pub(crate) fn error_span_range(&self) -> SpanRange { + *self.span_range + } + + /// Create an error for the resolution context. + pub(crate) fn err( + &self, + articled_expected_value_kind: &str, + value: V, + ) -> ExecutionResult { + self.span_range.type_err(format!( + "{} is expected to be {}, but it is {}", + self.resolution_target, + articled_expected_value_kind, + value.articled_kind() + )) + } +} + +pub(crate) trait IsArgument: Sized { + type ValueType: TypeData; + const OWNERSHIP: ArgumentOwnership; + fn from_argument(value: Spanned) -> ExecutionResult; +} + +impl IsArgument for ArgumentValue { + type ValueType = AnyType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::AsIs; + + fn from_argument(Spanned(value, _): Spanned) -> ExecutionResult { + Ok(value) + } +} + +impl + ResolvableArgumentTarget + ?Sized> IsArgument for Shared { + type ValueType = T::ValueType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Shared; + + fn from_argument(argument: Spanned) -> ExecutionResult { + T::resolve_shared(argument.expect_shared(), "This argument") + } +} + +// impl IsArgument for AnyRef<'static, T> +// where +// Shared: IsArgument, +// { +// type ValueType = as IsArgument>::ValueType; +// const OWNERSHIP: ArgumentOwnership = as IsArgument>::OWNERSHIP; + +// fn from_argument(argument: Spanned) -> ExecutionResult { +// Ok(Shared::::from_argument(argument)?.into()) +// } +// } + +impl + ResolvableArgumentTarget + ?Sized> IsArgument + for Assignee +{ + type ValueType = T::ValueType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Assignee { auto_create: false }; + + fn from_argument(argument: Spanned) -> ExecutionResult { + T::resolve_assignee(argument.expect_assignee(), "This argument") + } +} + +impl + ResolvableArgumentTarget + ?Sized> IsArgument for Mutable { + type ValueType = T::ValueType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Mutable; + + fn from_argument(argument: Spanned) -> ExecutionResult { + T::resolve_mutable(argument.expect_mutable(), "This argument") + } +} + +// impl IsArgument for AnyMut<'static, T> +// where +// Mutable: IsArgument, +// { +// type ValueType = as IsArgument>::ValueType; +// const OWNERSHIP: ArgumentOwnership = as IsArgument>::OWNERSHIP; + +// fn from_argument(argument: Spanned) -> ExecutionResult { +// Ok(Mutable::::from_argument(argument)?.into()) +// } +// } + +// impl + ResolvableArgumentTarget> IsArgument for T { +// type ValueType = T::ValueType; +// const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Owned; + +// fn from_argument(argument: Spanned) -> ExecutionResult { +// T::resolve_value(argument.expect_owned(), "This argument") +// } +// } + +impl + ResolvableArgumentTarget + ToOwned> IsArgument + for CopyOnWrite +where + T::Owned: ResolvableOwned, +{ + type ValueType = T::ValueType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::CopyOnWrite; + + fn from_argument(Spanned(value, span): Spanned) -> ExecutionResult { + value.expect_copy_on_write().map( + |v| T::resolve_shared(v.spanned(span), "This argument"), + |v| { + >::resolve_value( + v.spanned(span), + "This argument", + ) + }, + ) + } +} + +impl IsArgument for Spanned { + type ValueType = ::ValueType; + const OWNERSHIP: ArgumentOwnership = ::OWNERSHIP; + + fn from_argument(argument: Spanned) -> ExecutionResult { + let span = argument.1; + Ok(Spanned(T::from_argument(argument)?, span)) + } +} + +pub(crate) trait ResolveAs { + /// The `resolution_target` should be capitalized, e.g. "This argument" or "The value destructed with an object pattern" + fn resolve_as(self, resolution_target: &str) -> ExecutionResult; +} + +// Sadly this can't be changed Value => V because of spurious issues with +// https://github.com/rust-lang/rust/issues/48869 +// Instead, we could introduce a different trait ResolveAs2 if needed. +impl> ResolveAs for Spanned { + fn resolve_as(self, resolution_target: &str) -> ExecutionResult { + T::resolve_value(self, resolution_target) + } +} + +impl + ?Sized> ResolveAs> for Spanned { + fn resolve_as(self, resolution_target: &str) -> ExecutionResult> { + T::resolve_shared(self, resolution_target) + } +} + +impl<'a, T: ResolvableShared + ?Sized> ResolveAs<&'a T> for Spanned<&'a AnyValue> { + fn resolve_as(self, resolution_target: &str) -> ExecutionResult<&'a T> { + T::resolve_ref(self, resolution_target) + } +} + +impl<'a, T: ResolvableShared + ?Sized> ResolveAs> + for Spanned<&'a AnyValue> +{ + fn resolve_as(self, resolution_target: &str) -> ExecutionResult> { + T::resolve_spanned_ref(self, resolution_target) + } +} + +impl + ?Sized> ResolveAs> for Spanned { + fn resolve_as(self, resolution_target: &str) -> ExecutionResult> { + T::resolve_mutable(self, resolution_target) + } +} + +impl<'a, T: ResolvableMutable + ?Sized> ResolveAs<&'a mut T> + for Spanned<&'a mut AnyValue> +{ + fn resolve_as(self, resolution_target: &str) -> ExecutionResult<&'a mut T> { + T::resolve_ref_mut(self, resolution_target) + } +} + +impl<'a, T: ResolvableMutable + ?Sized> ResolveAs> + for Spanned<&'a mut AnyValue> +{ + fn resolve_as(self, resolution_target: &str) -> ExecutionResult> { + T::resolve_spanned_ref_mut(self, resolution_target) + } +} + +pub(crate) trait ResolvableArgumentTarget { + type ValueType: TypeData; +} + +pub(crate) trait ResolvableOwned: Sized { + fn resolve_from_value(value: T, context: ResolutionContext) -> ExecutionResult; + + fn resolve_spanned_from_value( + value: T, + context: ResolutionContext, + ) -> ExecutionResult>> { + let span_range = context.span_range; + Self::resolve_from_value(value, context).map(|x| x.spanned(*span_range)) + } + + /// The `resolution_target` should be capitalized, e.g. "This argument" or "The value destructed with an object pattern" + fn resolve_value( + Spanned(value, span): Spanned, + resolution_target: &str, + ) -> ExecutionResult { + let context = ResolutionContext { + span_range: &span, + resolution_target, + }; + Self::resolve_from_value(value, context) + } +} + +pub(crate) trait ResolvableShared { + fn resolve_from_ref<'a>(value: &'a T, context: ResolutionContext) -> ExecutionResult<&'a Self>; + + /// The `resolution_target` should be capitalized, e.g. "This argument" or "The value destructed with an object pattern" + fn resolve_shared( + Spanned(value, span): Spanned>, + resolution_target: &str, + ) -> ExecutionResult> { + value.try_map(|v| { + Self::resolve_from_ref( + v, + ResolutionContext { + span_range: &span, + resolution_target, + }, + ) + }) + } + + fn resolve_ref<'a>( + Spanned(value, span): Spanned<&'a T>, + resolution_target: &str, + ) -> ExecutionResult<&'a Self> { + Self::resolve_from_ref( + value, + ResolutionContext { + span_range: &span, + resolution_target, + }, + ) + } + + fn resolve_spanned_ref<'a>( + Spanned(value, span): Spanned<&'a T>, + resolution_target: &str, + ) -> ExecutionResult> { + Spanned(value, span).try_map(|v| { + Self::resolve_from_ref( + v, + ResolutionContext { + span_range: &span, + resolution_target, + }, + ) + }) + } +} + +pub(crate) trait ResolvableMutable { + fn resolve_from_mut<'a>( + value: &'a mut T, + context: ResolutionContext, + ) -> ExecutionResult<&'a mut Self>; + + fn resolve_assignee( + Spanned(value, span): Spanned>, + resolution_target: &str, + ) -> ExecutionResult> { + Ok(Assignee(Self::resolve_mutable( + Spanned(value.0, span), + resolution_target, + )?)) + } + + fn resolve_mutable( + Spanned(value, span): Spanned>, + resolution_target: &str, + ) -> ExecutionResult> { + value.try_map(|v| { + Self::resolve_from_mut( + v, + ResolutionContext { + span_range: &span, + resolution_target, + }, + ) + }) + } + + fn resolve_ref_mut<'a>( + Spanned(value, span): Spanned<&'a mut T>, + resolution_target: &str, + ) -> ExecutionResult<&'a mut Self> { + Self::resolve_from_mut( + value, + ResolutionContext { + span_range: &span, + resolution_target, + }, + ) + } + + fn resolve_spanned_ref_mut<'a>( + Spanned(value, span): Spanned<&'a mut T>, + resolution_target: &str, + ) -> ExecutionResult> { + Spanned(value, span).try_map(|value| { + Self::resolve_from_mut( + value, + ResolutionContext { + span_range: &span, + resolution_target, + }, + ) + }) + } +} + +impl ResolvableArgumentTarget for AnyValue { + type ValueType = AnyType; +} + +impl ResolvableOwned for AnyValue { + fn resolve_from_value(value: AnyValue, _context: ResolutionContext) -> ExecutionResult { + Ok(value) + } +} +impl ResolvableShared for AnyValue { + fn resolve_from_ref<'a>( + value: &'a AnyValue, + _context: ResolutionContext, + ) -> ExecutionResult<&'a Self> { + Ok(value) + } +} +impl ResolvableMutable for AnyValue { + fn resolve_from_mut<'a>( + value: &'a mut AnyValue, + _context: ResolutionContext, + ) -> ExecutionResult<&'a mut Self> { + Ok(value) + } +} + +macro_rules! impl_resolvable_argument_for { + ($value_type:ty, ($value:ident, $context:ident) -> $type:ty $body:block) => { + impl ResolvableArgumentTarget for $type { + type ValueType = $value_type; + } + + impl ResolvableOwned for $type { + fn resolve_from_value( + $value: AnyValue, + $context: ResolutionContext, + ) -> ExecutionResult { + $body + } + } + + impl ResolvableShared for $type { + fn resolve_from_ref<'a>( + $value: &'a AnyValue, + $context: ResolutionContext, + ) -> ExecutionResult<&'a Self> { + $body + } + } + + impl ResolvableMutable for $type { + fn resolve_from_mut<'a>( + $value: &'a mut AnyValue, + $context: ResolutionContext, + ) -> ExecutionResult<&'a mut Self> { + $body + } + } + }; +} + +pub(crate) use impl_resolvable_argument_for; + +macro_rules! impl_delegated_resolvable_argument_for { + (($value:ident: $delegate:ty) -> $type:ty { $expr:expr }) => { + impl ResolvableArgumentTarget for $type { + type ValueType = <$delegate as ResolvableArgumentTarget>::ValueType; + } + + impl ResolvableOwned for $type { + fn resolve_from_value( + input_value: Value, + context: ResolutionContext, + ) -> ExecutionResult { + let $value: $delegate = + ResolvableOwned::::resolve_from_value(input_value, context)?; + Ok($expr) + } + } + + impl ResolvableShared for $type { + fn resolve_from_ref<'a>( + input_value: &'a Value, + context: ResolutionContext, + ) -> ExecutionResult<&'a Self> { + let $value: &$delegate = + ResolvableShared::::resolve_from_ref(input_value, context)?; + Ok(&$expr) + } + } + + impl ResolvableMutable for $type { + fn resolve_from_mut<'a>( + input_value: &'a mut Value, + context: ResolutionContext, + ) -> ExecutionResult<&'a mut Self> { + let $value: &mut $delegate = + ResolvableMutable::::resolve_from_mut(input_value, context)?; + Ok(&mut $expr) + } + } + }; +} + +pub(crate) use impl_delegated_resolvable_argument_for; diff --git a/src/expressions/type_resolution/interface_macros.rs b/src/expressions/type_resolution/interface_macros.rs new file mode 100644 index 00000000..0786469f --- /dev/null +++ b/src/expressions/type_resolution/interface_macros.rs @@ -0,0 +1,653 @@ +use super::*; + +macro_rules! ignore_all { + ($($_:tt)*) => {}; +} + +macro_rules! if_empty { + ([] [$($output:tt)*]) => { + $($output)* + }; + ([$($input:tt)+] [$($output:tt)*]) => { + $($input)* + }; +} + +#[cfg(test)] +macro_rules! handle_first_arg_type { + ([NEXT] : $type:ty) => { + $type + }; + ([NEXT] : $type:ty, $($rest:tt)*) => { + $type + }; + ([NEXT] $consumed:tt $($rest:tt)*) => { + handle_first_arg_type!([NEXT] $($rest)*) + }; + // Start + ($first:tt $($rest:tt)*) => { + handle_first_arg_type!([NEXT] $($rest)*) + }; +} + +macro_rules! generate_unary_interface { + (REQUIRED [$ty1:ty,] OPTIONAL [] ARGS[$method:path]) => { + UnaryOperationInterface { + method: |context, a| apply_unary_fn($method, a, context), + argument_ownership: <$ty1 as IsArgument>::OWNERSHIP, + } + }; +} + +macro_rules! generate_binary_interface { + (REQUIRED [$ty1:ty, $ty2:ty,] OPTIONAL [] ARGS[$method:path]) => { + BinaryOperationInterface { + method: |context, a, b| apply_binary_fn($method, a, b, context), + lhs_ownership: <$ty1 as IsArgument>::OWNERSHIP, + rhs_ownership: <$ty2 as IsArgument>::OWNERSHIP, + } + }; +} + +macro_rules! generate_method_interface { + (REQUIRED [] OPTIONAL [] ARGS[$method:path]) => { + MethodInterface::Arity0 { + method: |context| apply_fn0($method, context), + argument_ownership: [], + } + }; + (REQUIRED [$ty1:ty,] OPTIONAL [] ARGS[$method:path]) => { + MethodInterface::Arity1 { + method: |context, a| apply_fn1($method, a, context), + argument_ownership: [<$ty1 as IsArgument>::OWNERSHIP], + } + }; + (REQUIRED [$ty1:ty, $ty2:ty,] OPTIONAL [] ARGS[$method:path]) => { + MethodInterface::Arity2 { + method: |context, a, b| apply_fn2($method, a, b, context), + argument_ownership: [ + <$ty1 as IsArgument>::OWNERSHIP, + <$ty2 as IsArgument>::OWNERSHIP, + ], + } + }; + (REQUIRED [$ty1:ty, $ty2:ty, $ty3:ty,] OPTIONAL [] ARGS[$method:path]) => { + MethodInterface::Arity3 { + method: |context, a, b, c| apply_fn3($method, a, b, c, context), + argument_ownership: [ + <$ty1 as IsArgument>::OWNERSHIP, + <$ty2 as IsArgument>::OWNERSHIP, + <$ty3 as IsArgument>::OWNERSHIP, + ], + } + }; + (REQUIRED [$ty1:ty,] OPTIONAL [$ty2:ty,] ARGS[$method:path]) => { + MethodInterface::Arity1PlusOptional1 { + method: |context, a, b| apply_fn1_optional1($method, a, b, context), + argument_ownership: [ + <$ty1 as IsArgument>::OWNERSHIP, + <$ty2 as IsArgument>::OWNERSHIP, + ], + } + }; + (REQUIRED [$ty1:ty, $ty2:ty,] OPTIONAL [$ty3:ty,] ARGS[$method:path]) => { + MethodInterface::Arity2PlusOptional1 { + method: |context, a, b, c| apply_fn2_optional1($method, a, b, c, context), + argument_ownership: [ + <$ty1 as IsArgument>::OWNERSHIP, + <$ty2 as IsArgument>::OWNERSHIP, + <$ty3 as IsArgument>::OWNERSHIP, + ], + } + }; + (REQUIRED [$ty1:ty, $ty2:ty, $ty3:ty,] OPTIONAL [$ty4:ty,] ARGS[$method:path]) => { + MethodInterface::Arity3PlusOptional1 { + method: |context, a, b, c, d| apply_fn3_optional1($method, a, b, c, d, context), + argument_ownership: [ + <$ty1 as IsArgument>::OWNERSHIP, + <$ty2 as IsArgument>::OWNERSHIP, + <$ty3 as IsArgument>::OWNERSHIP, + <$ty4 as IsArgument>::OWNERSHIP, + ], + } + }; + (REQUIRED $req:tt OPTIONAL $opt:tt ARGS[$($arg:tt)*]) => { + compile_error!(stringify!("This method arity is currently unsupported - add support in `MethodInterface` and `generate_method_interface`: ", $($arg)*)); + }; +} + +macro_rules! parse_arg_types { + // Nothing left - activate callback + ([REQUIRED $req:tt OPTIONAL $opt:tt => $callback:ident! $callback_args:tt]) => { + // log_syntax!(REQUIRED $req OPTIONAL $opt ARGS $callback_args) + $callback!(REQUIRED $req OPTIONAL $opt ARGS $callback_args) + }; + // Next tokens are `: Option` - we have an optional argument + ([REQUIRED $req:tt OPTIONAL[$($opt:tt)*] => $callback:ident! $callback_args:tt] : Option<$type:ty> $($rest:tt)*) => { + parse_arg_types!([REQUIRED $req OPTIONAL[$($opt)* $type,] => $callback! $callback_args] $($rest)*) + }; + // Next tokens are `: X)` - we have a required argument (variant 1) + ([REQUIRED [$($req:tt)*] OPTIONAL [] => $callback:ident! $callback_args:tt] : $type:ty) => { + parse_arg_types!([REQUIRED[$($req)* $type,] OPTIONAL [] => $callback! $callback_args]) + }; + ([REQUIRED [$($req:tt)*] OPTIONAL $opt:tt => $callback:ident! $callback_args:tt] : $type:ty) => { + compile_error!(stringify!("Required arguments must come before optional arguments:" $type)); + }; + // Next tokens are `: X, ...` - we have a required argument (variant 2) + ([REQUIRED [$($req:tt)*] OPTIONAL [] => $callback:ident! $callback_args:tt] : $type:ty, $($rest:tt)*) => { + parse_arg_types!([REQUIRED[$($req)* $type,] OPTIONAL [] => $callback! $callback_args] $($rest)*) + }; + ([REQUIRED [$($req:tt)*] OPTIONAL $opt:tt => $callback:ident! $callback_args:tt] : $type:ty, $($rest:tt)*) => { + compile_error!(stringify!("Required arguments must come before optional arguments:" $type)); + }; + // Next tokens are something else - ignore it and look at next + ([REQUIRED $req:tt OPTIONAL $opt:tt => $callback:ident! $callback_args:tt] $consumed:tt $($rest:tt)*) => { + parse_arg_types!([REQUIRED $req OPTIONAL $opt => $callback! $callback_args] $($rest)*) + }; + // Start + ([CALLBACK: $callback:ident! $callback_args:tt] $($rest:tt)*) => { + parse_arg_types!([REQUIRED[] OPTIONAL[] => $callback! $callback_args] $($rest)*) + }; +} + +// NOTE: We use function pointers here rather than generics to avoid monomorphization bloat. +// This means that we only need to compile the mapping glue combination once for each (A, B) -> C combination + +#[allow(unused)] +pub(crate) fn apply_fn0( + f: fn(&mut MethodCallContext) -> R, + context: &mut MethodCallContext, +) -> ExecutionResult +where + R: IsReturnable, +{ + let _output_span_range = context.output_span_range; + f(context).to_returned_value() +} + +pub(crate) fn apply_fn1( + f: fn(&mut MethodCallContext, A) -> R, + a: Spanned, + context: &mut MethodCallContext, +) -> ExecutionResult +where + A: IsArgument, + R: IsReturnable, +{ + f(context, A::from_argument(a)?).to_returned_value() +} + +#[allow(unused)] +pub(crate) fn apply_fn1_optional1( + f: fn(&mut MethodCallContext, A, Option) -> C, + a: Spanned, + b: Option>, + context: &mut MethodCallContext, +) -> ExecutionResult +where + A: IsArgument, + B: IsArgument, + C: IsReturnable, +{ + let output_span_range = context.output_span_range; + f( + context, + A::from_argument(a)?, + b.map(|b| B::from_argument(b)).transpose()?, + ) + .to_returned_value() +} + +pub(crate) fn apply_fn2( + f: fn(&mut MethodCallContext, A, B) -> C, + a: Spanned, + b: Spanned, + context: &mut MethodCallContext, +) -> ExecutionResult +where + A: IsArgument, + B: IsArgument, + C: IsReturnable, +{ + f(context, A::from_argument(a)?, B::from_argument(b)?).to_returned_value() +} + +pub(crate) fn apply_fn2_optional1( + f: fn(&mut MethodCallContext, A, B, Option) -> D, + a: Spanned, + b: Spanned, + c: Option>, + context: &mut MethodCallContext, +) -> ExecutionResult +where + A: IsArgument, + B: IsArgument, + C: IsArgument, + D: IsReturnable, +{ + f( + context, + A::from_argument(a)?, + B::from_argument(b)?, + c.map(|c| C::from_argument(c)).transpose()?, + ) + .to_returned_value() +} + +#[allow(unused)] +pub(crate) fn apply_fn3( + f: fn(&mut MethodCallContext, A, B, C) -> R, + a: Spanned, + b: Spanned, + c: Spanned, + context: &mut MethodCallContext, +) -> ExecutionResult +where + A: IsArgument, + B: IsArgument, + C: IsArgument, + R: IsReturnable, +{ + f( + context, + A::from_argument(a)?, + B::from_argument(b)?, + C::from_argument(c)?, + ) + .to_returned_value() +} + +pub(crate) fn apply_fn3_optional1( + f: fn(&mut MethodCallContext, A, B, C, Option) -> R, + a: Spanned, + b: Spanned, + c: Spanned, + d: Option>, + context: &mut MethodCallContext, +) -> ExecutionResult +where + A: IsArgument, + B: IsArgument, + C: IsArgument, + D: IsArgument, + R: IsReturnable, +{ + f( + context, + A::from_argument(a)?, + B::from_argument(b)?, + C::from_argument(c)?, + d.map(|d| D::from_argument(d)).transpose()?, + ) + .to_returned_value() +} + +pub(crate) fn apply_unary_fn( + f: fn(UnaryOperationCallContext, A) -> R, + a: Spanned, + context: UnaryOperationCallContext, +) -> ExecutionResult +where + A: IsArgument, + R: IsReturnable, +{ + f(context, A::from_argument(a)?).to_returned_value() +} + +pub(crate) fn apply_binary_fn( + f: fn(BinaryOperationCallContext, A, B) -> R, + lhs: Spanned, + rhs: Spanned, + context: BinaryOperationCallContext, +) -> ExecutionResult +where + A: IsArgument, + B: IsArgument, + R: IsReturnable, +{ + f(context, A::from_argument(lhs)?, B::from_argument(rhs)?).to_returned_value() +} + +// ============================================================================ +// Property Access Wrapper Functions +// ============================================================================ + +pub(crate) fn apply_property_shared<'a, S: ResolvableShared + ?Sized + 'a>( + f: for<'b> fn(PropertyAccessCallContext, &'b S) -> ExecutionResult<&'b AnyValue>, + ctx: PropertyAccessCallContext, + source: &'a AnyValue, +) -> ExecutionResult<&'a AnyValue> { + let source = S::resolve_from_ref( + source, + ResolutionContext::new(&ctx.property.span_range(), "The property access source"), + )?; + f(ctx, source) +} + +pub(crate) fn apply_property_mutable<'a, S: ResolvableMutable + ?Sized + 'a>( + f: for<'b> fn(PropertyAccessCallContext, &'b mut S, bool) -> ExecutionResult<&'b mut AnyValue>, + ctx: PropertyAccessCallContext, + source: &'a mut AnyValue, + auto_create: bool, +) -> ExecutionResult<&'a mut AnyValue> { + let source = S::resolve_from_mut( + source, + ResolutionContext::new(&ctx.property.span_range(), "The property access source"), + )?; + f(ctx, source, auto_create) +} + +pub(crate) fn apply_property_owned>( + f: fn(PropertyAccessCallContext, S) -> ExecutionResult, + ctx: PropertyAccessCallContext, + source: AnyValue, +) -> ExecutionResult { + let source = S::resolve_from_value( + source, + ResolutionContext::new(&ctx.property.span_range(), "The property access source"), + )?; + f(ctx, source) +} + +// ============================================================================ +// Index Access Wrapper Functions +// ============================================================================ + +pub(crate) fn apply_index_shared<'a, S: ResolvableShared + ?Sized + 'a>( + f: for<'b> fn( + IndexAccessCallContext, + &'b S, + Spanned, + ) -> ExecutionResult<&'b AnyValue>, + ctx: IndexAccessCallContext, + source: &'a AnyValue, + index: Spanned, +) -> ExecutionResult<&'a AnyValue> { + let source = S::resolve_from_ref( + source, + ResolutionContext::new(&ctx.access.span_range(), "The index access source"), + )?; + f(ctx, source, index) +} + +pub(crate) fn apply_index_mutable<'a, S: ResolvableMutable + ?Sized + 'a>( + f: for<'b> fn( + IndexAccessCallContext, + &'b mut S, + Spanned, + bool, + ) -> ExecutionResult<&'b mut AnyValue>, + ctx: IndexAccessCallContext, + source: &'a mut AnyValue, + index: Spanned, + auto_create: bool, +) -> ExecutionResult<&'a mut AnyValue> { + let source = S::resolve_from_mut( + source, + ResolutionContext::new(&ctx.access.span_range(), "The index access source"), + )?; + f(ctx, source, index, auto_create) +} + +pub(crate) fn apply_index_owned>( + f: fn(IndexAccessCallContext, S, Spanned) -> ExecutionResult, + ctx: IndexAccessCallContext, + source: AnyValue, + index: Spanned, +) -> ExecutionResult { + let source = S::resolve_from_value( + source, + ResolutionContext::new(&ctx.access.span_range(), "The index access source"), + )?; + f(ctx, source, index) +} + +pub(crate) struct MethodCallContext<'a> { + pub interpreter: &'a mut Interpreter, + pub output_span_range: SpanRange, +} + +impl<'a> HasSpanRange for MethodCallContext<'a> { + fn span_range(&self) -> SpanRange { + self.output_span_range + } +} + +#[derive(Clone, Copy)] +pub(crate) struct UnaryOperationCallContext<'a> { + pub operation: &'a UnaryOperation, +} + +#[derive(Clone, Copy)] +pub(crate) struct BinaryOperationCallContext<'a> { + pub operation: &'a BinaryOperation, +} + +impl<'a> BinaryOperationCallContext<'a> { + #[allow(unused)] + pub(crate) fn err(&self, message: impl std::fmt::Display) -> ExecutionResult { + self.operation.value_err(message) + } + + pub(crate) fn error(&self, message: impl std::fmt::Display) -> ExecutionInterrupt { + self.operation.value_error(message) + } +} + +macro_rules! define_type_features { + ( + impl $type_def:ident, + $mod_vis:vis mod $mod_name:ident { + $mod_methods_vis:vis mod methods { + $( + $([$method_context:ident])? fn $method_name:ident($($method_args:tt)*) $(-> $method_output_ty:ty)? $([ignore_type_assertion $method_ignore_type_assertion:tt])? $method_body:block + )* + } + $mod_unary_operations_vis:vis mod unary_operations { + $( + $([$unary_context:ident])? fn $unary_name:ident($($unary_args:tt)*) $(-> $unary_output_ty:ty)? $([ignore_type_assertion $unary_ignore_type_assertion:tt])? $unary_body:block + )* + } + $mod_binary_operations_vis:vis mod binary_operations { + $( + $([$binary_context:ident])? fn $binary_name:ident($($binary_args:tt)*) $(-> $binary_output_ty:ty)? $([ignore_type_assertion $binary_ignore_type_assertion:tt])? $binary_body:block + )* + } + $(property_access($property_source_ty:ty) { + $([$property_shared_context:ident])? fn shared($($property_shared_args:tt)*) $property_shared_body:block + $([$property_mutable_context:ident])? fn mutable($($property_mutable_args:tt)*) $property_mutable_body:block + $([$property_owned_context:ident])? fn owned($($property_owned_args:tt)*) $property_owned_body:block + })? + $(index_access($index_source_ty:ty) { + $([$index_shared_context:ident])? fn shared($($index_shared_args:tt)*) $index_shared_body:block + $([$index_mutable_context:ident])? fn mutable($($index_mutable_args:tt)*) $index_mutable_body:block + $([$index_owned_context:ident])? fn owned($($index_owned_args:tt)*) $index_owned_body:block + })? + interface_items { + $($items:item)* + } + } + ) => { + $mod_vis mod $mod_name { + use super::*; + + #[allow(unused)] + #[cfg(test)] + fn asserts() { + fn assert_first_argument>() {} + fn assert_output_type() {} + $( + assert_first_argument::(); + if_exists! { + {$($method_ignore_type_assertion)?} + {} + {$(assert_output_type::<$method_output_ty>();)?} + } + )* + $( + assert_first_argument::(); + if_exists! { + {$($unary_ignore_type_assertion)?} + {} + {$(assert_output_type::<$unary_output_ty>();)?} + } + )* + $( + assert_first_argument::(); + if_exists! { + {$($binary_ignore_type_assertion)?} + {} + {$(assert_output_type::<$binary_output_ty>();)?} + } + )* + // Note: property_access and index_access source types are verified + // at compile time through the apply_* wrapper functions + } + + $mod_methods_vis mod methods { + #[allow(unused)] + use super::*; + $( + pub(crate) fn $method_name(if_empty!([$($method_context)?][_context]): &mut MethodCallContext, $($method_args)*) $(-> $method_output_ty)? { + $method_body + } + )* + } + + $mod_methods_vis mod method_definitions { + #[allow(unused)] + use super::*; + $( + $mod_methods_vis fn $method_name() -> MethodInterface { + parse_arg_types!([CALLBACK: generate_method_interface![methods::$method_name]] $($method_args)*) + } + )* + } + + $mod_unary_operations_vis mod unary_operations { + #[allow(unused)] + use super::*; + $( + $mod_unary_operations_vis fn $unary_name(if_empty!([$($unary_context)?][_context]): UnaryOperationCallContext, $($unary_args)*) $(-> $unary_output_ty)? { + $unary_body + } + )* + } + + $mod_unary_operations_vis mod unary_definitions { + #[allow(unused)] + use super::*; + $( + $mod_unary_operations_vis fn $unary_name() -> UnaryOperationInterface { + parse_arg_types!([CALLBACK: generate_unary_interface![unary_operations::$unary_name]] $($unary_args)*) + } + )* + } + + $mod_binary_operations_vis mod binary_operations { + #[allow(unused)] + use super::*; + $( + $mod_binary_operations_vis fn $binary_name(if_empty!([$($binary_context)?][_context]): BinaryOperationCallContext, $($binary_args)*) $(-> $binary_output_ty)? { + $binary_body + } + )* + } + + $mod_binary_operations_vis mod binary_definitions { + #[allow(unused)] + use super::*; + $( + $mod_binary_operations_vis fn $binary_name() -> BinaryOperationInterface { + parse_arg_types!([CALLBACK: generate_binary_interface![binary_operations::$binary_name]] $($binary_args)*) + } + )* + } + + $( + pub(crate) mod property_access { + #[allow(unused)] + use super::*; + + pub(crate) fn shared<'a>(if_empty!([$($property_shared_context)?][_ctx]): PropertyAccessCallContext, $($property_shared_args)*) -> ExecutionResult<&'a AnyValue> $property_shared_body + + pub(crate) fn mutable<'a>(if_empty!([$($property_mutable_context)?][_ctx]): PropertyAccessCallContext, $($property_mutable_args)*) -> ExecutionResult<&'a mut AnyValue> $property_mutable_body + + pub(crate) fn owned(if_empty!([$($property_owned_context)?][_ctx]): PropertyAccessCallContext, $($property_owned_args)*) -> ExecutionResult $property_owned_body + } + + pub(crate) fn property_access_interface() -> PropertyAccessInterface { + PropertyAccessInterface { + shared_access: |ctx, source| apply_property_shared::<$property_source_ty>(property_access::shared, ctx, source), + mutable_access: |ctx, source, auto_create| apply_property_mutable::<$property_source_ty>(property_access::mutable, ctx, source, auto_create), + owned_access: |ctx, source| apply_property_owned::<$property_source_ty>(property_access::owned, ctx, source), + } + } + )? + + $( + pub(crate) mod index_access { + #[allow(unused)] + use super::*; + + pub(crate) fn shared<'a>(if_empty!([$($index_shared_context)?][_ctx]): IndexAccessCallContext, $($index_shared_args)*) -> ExecutionResult<&'a AnyValue> $index_shared_body + + pub(crate) fn mutable<'a>(if_empty!([$($index_mutable_context)?][_ctx]): IndexAccessCallContext, $($index_mutable_args)*) -> ExecutionResult<&'a mut AnyValue> $index_mutable_body + + pub(crate) fn owned(if_empty!([$($index_owned_context)?][_ctx]): IndexAccessCallContext, $($index_owned_args)*) -> ExecutionResult $index_owned_body + } + + pub(crate) fn index_access_interface() -> IndexAccessInterface { + IndexAccessInterface { + index_ownership: ArgumentOwnership::Shared, + shared_access: |ctx, source, index| apply_index_shared::<$index_source_ty>(index_access::shared, ctx, source, index), + mutable_access: |ctx, source, index, auto_create| apply_index_mutable::<$index_source_ty>(index_access::mutable, ctx, source, index, auto_create), + owned_access: |ctx, source, index| apply_index_owned::<$index_source_ty>(index_access::owned, ctx, source, index), + } + } + )? + + impl TypeData for $type_def { + #[allow(unreachable_code)] + fn resolve_own_method(method_name: &str) -> Option { + Some(match method_name { + $( + stringify!($method_name) => method_definitions::$method_name(), + )* + _ => return None, + }) + } + + define_type_features!(@property_access_impl $($property_source_ty)?); + define_type_features!(@index_access_impl $($index_source_ty)?); + + // Pass through resolve_own_unary_operation and resolve_own_binary_operation + // until there's a better way to define them + $($items)* + } + } + }; + + // Helper rules for generating resolve_own_property_access when property_access is defined + (@property_access_impl $source_ty:ty) => { + fn resolve_own_property_access() -> Option { + Some(property_access_interface()) + } + }; + (@property_access_impl) => {}; + + // Helper rules for generating resolve_own_index_access when index_access is defined + (@index_access_impl $source_ty:ty) => { + fn resolve_own_index_access() -> Option { + Some(index_access_interface()) + } + }; + (@index_access_impl) => {}; +} + +#[cfg(test)] +pub(crate) use handle_first_arg_type; +pub(crate) use { + define_type_features, generate_binary_interface, generate_method_interface, + generate_unary_interface, if_empty, ignore_all, parse_arg_types, +}; diff --git a/src/expressions/type_resolution/mod.rs b/src/expressions/type_resolution/mod.rs new file mode 100644 index 00000000..3e86854f --- /dev/null +++ b/src/expressions/type_resolution/mod.rs @@ -0,0 +1,13 @@ +use super::*; + +mod arguments; +mod interface_macros; +mod outputs; +mod type_data; +mod type_kinds; + +pub(crate) use arguments::*; +pub(crate) use interface_macros::*; +pub(crate) use outputs::*; +pub(crate) use type_data::*; +pub(crate) use type_kinds::*; diff --git a/src/expressions/type_resolution/outputs.rs b/src/expressions/type_resolution/outputs.rs new file mode 100644 index 00000000..cd823619 --- /dev/null +++ b/src/expressions/type_resolution/outputs.rs @@ -0,0 +1,74 @@ +use super::*; + +/// A [`ReturnedValue`] represents an arbitrary output from a method / expression. +/// It will then be mapped into a [`RequestedValue`] according to the requested ownership. +/// +/// See also [`RequestedValue`] for values that have been fully evaluated. +pub(crate) enum ReturnedValue { + Owned(AnyValueOwned), + CopyOnWrite(CopyOnWriteValue), + Mutable(AnyValueMutable), + Shared(AnyValueShared), +} + +// TODO: Find some way to selectively enable only on MSRV (e.g. following the build.rs feature flag pattern) +// #[diagnostic::on_unimplemented( +// message = "`ResolvableOutput` is not implemented for `{Self}`", +// note = "`ResolvableOutput` is not implemented for `Shared` or `Mutable` unless `X` is `Value`. If we wish to change this, we'd need to have some way to represent some kind of `ExpressionReference`, i.e. a `Typed>` rather than a `Shared>`" +// )] +pub(crate) trait IsReturnable { + fn to_returned_value(self) -> ExecutionResult; +} + +impl IsReturnable for ReturnedValue { + fn to_returned_value(self) -> ExecutionResult { + Ok(self) + } +} + +impl IsReturnable for AnyValueShared { + fn to_returned_value(self) -> ExecutionResult { + Ok(ReturnedValue::Shared(self)) + } +} + +impl IsReturnable for AnyValueMutable { + fn to_returned_value(self) -> ExecutionResult { + Ok(ReturnedValue::Mutable(self)) + } +} + +impl IsReturnable for ExecutionResult { + fn to_returned_value(self) -> ExecutionResult { + self?.to_returned_value() + } +} + +pub(crate) trait StreamAppender { + fn append(self, output: &mut OutputStream) -> ExecutionResult<()>; +} + +impl ExecutionResult<()>> StreamAppender for F { + fn append(self, output: &mut OutputStream) -> ExecutionResult<()> { + self(output) + } +} + +pub(crate) struct StreamOutput(T); +impl ExecutionResult<()>> StreamOutput { + pub fn new(appender: F) -> Self { + Self(appender) + } +} +impl From for StreamOutput { + fn from(value: T) -> Self { + Self(value) + } +} +impl IsReturnable for StreamOutput { + fn to_returned_value(self) -> ExecutionResult { + let mut output = OutputStream::new(); + self.0.append(&mut output)?; + output.to_returned_value() + } +} diff --git a/src/expressions/type_resolution/type_data.rs b/src/expressions/type_resolution/type_data.rs new file mode 100644 index 00000000..c7877739 --- /dev/null +++ b/src/expressions/type_resolution/type_data.rs @@ -0,0 +1,380 @@ +#![allow(clippy::type_complexity)] +use super::*; + +pub(crate) trait TypeFeatureResolver { + /// Resolves a unary operation as a method interface for this type. + fn resolve_method(&self, method_name: &str) -> Option; + + /// Resolves a unary operation as a method interface for this type. + fn resolve_unary_operation( + &self, + operation: &UnaryOperation, + ) -> Option; + + /// Resolves a binary operation as a method interface for this type. + fn resolve_binary_operation( + &self, + operation: &BinaryOperation, + ) -> Option; + + /// Resolves a property of this type. + fn resolve_type_property(&self, _property_name: &str) -> Option; + + /// Resolves property access capability for this type (e.g., `obj.field`). + /// Returns Some if this type supports property access, None otherwise. + fn resolve_property_access(&self) -> Option { + None + } + + /// Resolves index access capability for this type (e.g., `arr[0]`). + /// Returns Some if this type supports indexing, None otherwise. + fn resolve_index_access(&self) -> Option { + None + } +} + +pub(crate) trait TypeData { + /// Returns None if the method is not supported on this type itself. + /// The method may still be supported on a type further up the resolution chain. + fn resolve_own_method(_method_name: &str) -> Option { + None + } + + /// Returns None if the operation is not supported on this type itself. + /// The operation may still be supported on a type further up the resolution chain. + fn resolve_own_unary_operation(_operation: &UnaryOperation) -> Option { + None + } + + /// Returns None if the operation is not supported on this type itself. + /// The operation may still be supported on a type further up the resolution chain. + fn resolve_own_binary_operation( + _operation: &BinaryOperation, + ) -> Option { + None + } + + /// Returns a property on the type. + /// Properties are *not* currently resolved up the resolution chain... but maybe they should be? + /// ... similarly, maybe functions should be too, when they are added? + fn resolve_type_property(_property_name: &str) -> Option { + None + } + + /// Returns the property access interface for this type, if supported. + fn resolve_own_property_access() -> Option { + None + } + + /// Returns the index access interface for this type, if supported. + fn resolve_own_index_access() -> Option { + None + } +} + +#[allow(unused)] +pub(crate) enum MethodInterface { + Arity0 { + method: fn(&mut MethodCallContext) -> ExecutionResult, + argument_ownership: [ArgumentOwnership; 0], + }, + Arity1 { + method: + fn(&mut MethodCallContext, Spanned) -> ExecutionResult, + argument_ownership: [ArgumentOwnership; 1], + }, + /// 1 argument, 1 optional argument + Arity1PlusOptional1 { + method: fn( + &mut MethodCallContext, + Spanned, + Option>, + ) -> ExecutionResult, + argument_ownership: [ArgumentOwnership; 2], + }, + Arity2 { + method: fn( + &mut MethodCallContext, + Spanned, + Spanned, + ) -> ExecutionResult, + argument_ownership: [ArgumentOwnership; 2], + }, + Arity2PlusOptional1 { + method: fn( + &mut MethodCallContext, + Spanned, + Spanned, + Option>, + ) -> ExecutionResult, + argument_ownership: [ArgumentOwnership; 3], + }, + Arity3 { + method: fn( + &mut MethodCallContext, + Spanned, + Spanned, + Spanned, + ) -> ExecutionResult, + argument_ownership: [ArgumentOwnership; 3], + }, + Arity3PlusOptional1 { + method: fn( + &mut MethodCallContext, + Spanned, + Spanned, + Spanned, + Option>, + ) -> ExecutionResult, + argument_ownership: [ArgumentOwnership; 4], + }, + ArityAny { + method: fn( + &mut MethodCallContext, + Vec>, + ) -> ExecutionResult, + argument_ownership: Vec, + }, +} + +impl MethodInterface { + pub(crate) fn execute( + &self, + arguments: Vec>, + context: &mut MethodCallContext, + ) -> ExecutionResult> { + let output_value = match self { + MethodInterface::Arity0 { method, .. } => { + if !arguments.is_empty() { + return context.output_span_range.type_err("Expected 0 arguments"); + } + method(context) + } + MethodInterface::Arity1 { method, .. } => { + match <[Spanned; 1]>::try_from(arguments) { + Ok([a]) => method(context, a), + Err(_) => context.output_span_range.type_err("Expected 1 argument"), + } + } + MethodInterface::Arity1PlusOptional1 { method, .. } => match arguments.len() { + 1 => { + let [a] = <[Spanned; 1]>::try_from(arguments) + .ok() + .unwrap(); + method(context, a, None) + } + 2 => { + let [a, b] = <[Spanned; 2]>::try_from(arguments) + .ok() + .unwrap(); + method(context, a, Some(b)) + } + _ => context + .output_span_range + .type_err("Expected 1 or 2 arguments"), + }, + MethodInterface::Arity2 { method, .. } => { + match <[Spanned; 2]>::try_from(arguments) { + Ok([a, b]) => method(context, a, b), + Err(_) => context.output_span_range.type_err("Expected 2 arguments"), + } + } + MethodInterface::Arity2PlusOptional1 { method, .. } => match arguments.len() { + 2 => { + let [a, b] = <[Spanned; 2]>::try_from(arguments) + .ok() + .unwrap(); + method(context, a, b, None) + } + 3 => { + let [a, b, c] = <[Spanned; 3]>::try_from(arguments) + .ok() + .unwrap(); + method(context, a, b, Some(c)) + } + _ => context + .output_span_range + .type_err("Expected 2 or 3 arguments"), + }, + MethodInterface::Arity3 { method, .. } => { + match <[Spanned; 3]>::try_from(arguments) { + Ok([a, b, c]) => method(context, a, b, c), + Err(_) => context.output_span_range.type_err("Expected 3 arguments"), + } + } + MethodInterface::Arity3PlusOptional1 { method, .. } => match arguments.len() { + 3 => { + let [a, b, c] = <[Spanned; 3]>::try_from(arguments) + .ok() + .unwrap(); + method(context, a, b, c, None) + } + 4 => { + let [a, b, c, d] = <[Spanned; 4]>::try_from(arguments) + .ok() + .unwrap(); + method(context, a, b, c, Some(d)) + } + _ => context + .output_span_range + .type_err("Expected 3 or 4 arguments"), + }, + MethodInterface::ArityAny { method, .. } => method(context, arguments), + }; + output_value.map(|v| v.spanned(context.output_span_range)) + } + + /// Returns (argument_ownerships, required_argument_count) + pub(crate) fn argument_ownerships(&self) -> (&[ArgumentOwnership], usize) { + match self { + MethodInterface::Arity0 { + argument_ownership, .. + } => (argument_ownership, 0), + MethodInterface::Arity1 { + argument_ownership, .. + } => (argument_ownership, 1), + MethodInterface::Arity1PlusOptional1 { + argument_ownership, .. + } => (argument_ownership, 1), + MethodInterface::Arity2 { + argument_ownership, .. + } => (argument_ownership, 2), + MethodInterface::Arity2PlusOptional1 { + argument_ownership, .. + } => (argument_ownership, 2), + MethodInterface::Arity3 { + argument_ownership, .. + } => (argument_ownership, 3), + MethodInterface::Arity3PlusOptional1 { + argument_ownership, .. + } => (argument_ownership, 3), + MethodInterface::ArityAny { + argument_ownership, .. + } => (argument_ownership, 0), + } + } +} + +pub(crate) struct UnaryOperationInterface { + pub method: + fn(UnaryOperationCallContext, Spanned) -> ExecutionResult, + pub argument_ownership: ArgumentOwnership, +} + +impl UnaryOperationInterface { + pub(crate) fn execute( + &self, + Spanned(input, input_span): Spanned, + operation: &UnaryOperation, + ) -> ExecutionResult> { + let output_span_range = operation.output_span_range(input_span); + Ok((self.method)( + UnaryOperationCallContext { operation }, + Spanned(input, input_span), + )? + .spanned(output_span_range)) + } + + pub(crate) fn argument_ownership(&self) -> ArgumentOwnership { + self.argument_ownership + } +} + +pub(crate) struct BinaryOperationInterface { + pub method: fn( + BinaryOperationCallContext, + Spanned, + Spanned, + ) -> ExecutionResult, + pub lhs_ownership: ArgumentOwnership, + pub rhs_ownership: ArgumentOwnership, +} + +impl BinaryOperationInterface { + pub(crate) fn execute( + &self, + Spanned(lhs, lhs_span): Spanned, + Spanned(rhs, rhs_span): Spanned, + operation: &BinaryOperation, + ) -> ExecutionResult> { + let output_span_range = SpanRange::new_between(lhs_span, rhs_span); + Ok((self.method)( + BinaryOperationCallContext { operation }, + Spanned(lhs, lhs_span), + Spanned(rhs, rhs_span), + )? + .spanned(output_span_range)) + } + + pub(crate) fn lhs_ownership(&self) -> ArgumentOwnership { + self.lhs_ownership + } + + pub(crate) fn rhs_ownership(&self) -> ArgumentOwnership { + self.rhs_ownership + } +} + +// ============================================================================ +// Property Access Interface +// ============================================================================ + +/// Context provided to property access methods. +#[derive(Clone, Copy)] +pub(crate) struct PropertyAccessCallContext<'a> { + pub property: &'a PropertyAccess, +} + +/// Interface for property access on a type (e.g., `obj.field`). +/// +/// Unlike unary/binary operations which return owned values, property access +/// returns references into the source value. This requires three separate +/// access methods for shared, mutable, and owned access patterns. +pub(crate) struct PropertyAccessInterface { + /// Access a property by shared reference. + pub shared_access: + for<'a> fn(PropertyAccessCallContext, &'a AnyValue) -> ExecutionResult<&'a AnyValue>, + /// Access a property by mutable reference, optionally auto-creating if missing. + pub mutable_access: for<'a> fn( + PropertyAccessCallContext, + &'a mut AnyValue, + bool, + ) -> ExecutionResult<&'a mut AnyValue>, + /// Extract a property from an owned value. + pub owned_access: fn(PropertyAccessCallContext, AnyValue) -> ExecutionResult, +} + +// ============================================================================ +// Index Access Interface +// ============================================================================ + +/// Context provided to index access methods. +#[derive(Clone, Copy)] +pub(crate) struct IndexAccessCallContext<'a> { + pub access: &'a IndexAccess, +} + +/// Interface for index access on a type (e.g., `arr[0]` or `obj["key"]`). +/// +/// Similar to property access, but the index is an evaluated expression +/// rather than a static identifier. +pub(crate) struct IndexAccessInterface { + /// The ownership requirement for the index value. + pub index_ownership: ArgumentOwnership, + /// Access an element by shared reference. + pub shared_access: for<'a> fn( + IndexAccessCallContext, + &'a AnyValue, + Spanned, + ) -> ExecutionResult<&'a AnyValue>, + /// Access an element by mutable reference, optionally auto-creating if missing. + pub mutable_access: for<'a> fn( + IndexAccessCallContext, + &'a mut AnyValue, + Spanned, + bool, + ) -> ExecutionResult<&'a mut AnyValue>, + /// Extract an element from an owned value. + pub owned_access: + fn(IndexAccessCallContext, AnyValue, Spanned) -> ExecutionResult, +} diff --git a/src/expressions/type_resolution/type_kinds.rs b/src/expressions/type_resolution/type_kinds.rs new file mode 100644 index 00000000..7c27e743 --- /dev/null +++ b/src/expressions/type_resolution/type_kinds.rs @@ -0,0 +1,260 @@ +use super::*; + +/// A trait for specific value kinds that can provide a display name. +/// This is implemented by `ValueLeafKind`, `IntegerKind`, `FloatKind`, etc. +pub(crate) trait IsLeafKind: Copy + Into { + fn source_type_name(&self) -> &'static str; + fn articled_display_name(&self) -> &'static str; + fn feature_resolver(&self) -> &'static dyn TypeFeatureResolver; +} + +impl AnyValueLeafKind { + /// This should be true for types which users expect to have value + /// semantics, but false for types which are expensive to clone or + /// are expected to have reference semantics. + /// + /// This indicates if an &x can be converted to an x via cloning + /// when doing method resolution. + pub(crate) fn supports_transparent_cloning(&self) -> bool { + match self { + AnyValueLeafKind::None(_) => true, + AnyValueLeafKind::Integer(_) => true, + AnyValueLeafKind::Float(_) => true, + AnyValueLeafKind::Bool(_) => true, + // Strings are value-like, so it makes sense to transparently clone them + AnyValueLeafKind::String(_) => true, + AnyValueLeafKind::Char(_) => true, + AnyValueLeafKind::UnsupportedLiteral(_) => false, + AnyValueLeafKind::Array(_) => false, + AnyValueLeafKind::Object(_) => false, + AnyValueLeafKind::Stream(_) => false, + AnyValueLeafKind::Range(_) => true, + AnyValueLeafKind::Iterator(_) => false, + // A parser is a handle, so can be cloned transparently. + // It may fail to be able to be used to parse if the underlying stream is out of scope of course. + AnyValueLeafKind::Parser(_) => true, + } + } +} + +// A AnyValueLeafKind represents a kind of leaf value. +// But a TypeKind represents a type in the hierarchy, which points at a type data. +pub(crate) enum TypeKind { + Leaf(AnyValueLeafKind), + Parent(ParentTypeKind), + Dyn(DynTypeKind), +} + +impl TypeKind { + /// This should be true for types which users expect to have value + /// semantics, but false for types which are expensive to clone or + /// are expected to have reference semantics. + /// + /// This indicates if an &x can be converted to an x via cloning + /// when doing method resolution. + pub(crate) fn supports_transparent_cloning(&self) -> bool { + match self { + TypeKind::Leaf(leaf_kind) => leaf_kind.supports_transparent_cloning(), + TypeKind::Parent(_) => false, + TypeKind::Dyn(_) => false, + } + } + + pub(crate) fn articled_display_name(&self) -> &'static str { + match self { + TypeKind::Leaf(leaf_kind) => leaf_kind.articled_display_name(), + TypeKind::Parent(parent_kind) => parent_kind.articled_display_name(), + TypeKind::Dyn(dyn_kind) => dyn_kind.articled_display_name(), + } + } + + pub(crate) fn from_source_name(name: &str) -> Option { + // Parse Leaf and Parent TypeKinds + if let Some(tk) = ::type_kind_from_source_name(name) { + return Some(tk); + } + // Parse Dyn TypeKinds + if let Some(dyn_type_kind) = DynTypeKind::from_source_name(name) { + return Some(TypeKind::Dyn(dyn_type_kind)); + } + None + } + + pub(crate) fn source_name(&self) -> &'static str { + match self { + TypeKind::Leaf(leaf_kind) => leaf_kind.source_type_name(), + TypeKind::Parent(parent_kind) => parent_kind.source_name(), + TypeKind::Dyn(dyn_kind) => dyn_kind.source_name(), + } + } +} + +pub(crate) enum ParentTypeKind { + Value(AnyValueTypeKind), + Integer(IntegerTypeKind), + Float(FloatTypeKind), +} + +impl ParentTypeKind { + pub(crate) fn articled_display_name(&self) -> &'static str { + match self { + ParentTypeKind::Value(x) => x.articled_display_name(), + ParentTypeKind::Integer(x) => x.articled_display_name(), + ParentTypeKind::Float(x) => x.articled_display_name(), + } + } + + pub(crate) fn source_name(&self) -> &'static str { + match self { + ParentTypeKind::Value(x) => x.source_type_name(), + ParentTypeKind::Integer(x) => x.source_type_name(), + ParentTypeKind::Float(x) => x.source_type_name(), + } + } + + pub(crate) fn feature_resolver(&self) -> &'static dyn TypeFeatureResolver { + match self { + ParentTypeKind::Value(x) => x.feature_resolver(), + ParentTypeKind::Integer(x) => x.feature_resolver(), + ParentTypeKind::Float(x) => x.feature_resolver(), + } + } +} + +pub(crate) enum DynTypeKind { + Iterable, +} + +impl DynTypeKind { + pub(crate) fn articled_display_name(&self) -> &'static str { + match self { + DynTypeKind::Iterable => IterableType::ARTICLED_DISPLAY_NAME, + } + } + + pub(crate) fn from_source_name(name: &str) -> Option { + match name { + IterableType::SOURCE_TYPE_NAME => Some(DynTypeKind::Iterable), + _ => None, + } + } + + pub(crate) fn source_name(&self) -> &'static str { + match self { + DynTypeKind::Iterable => IterableType::SOURCE_TYPE_NAME, + } + } + + pub(crate) fn feature_resolver(&self) -> &'static dyn TypeFeatureResolver { + match self { + DynTypeKind::Iterable => &IterableType, + } + } +} + +pub(crate) struct TypeIdent { + span: Span, + pub(crate) kind: TypeKind, +} + +impl TypeIdent { + pub(crate) fn from_ident(ident: &Ident) -> ParseResult { + let span = ident.span(); + let name = ident.to_string(); + let kind = match TypeKind::from_source_name(name.as_str()) { + Some(kind) => kind, + None => { + let lower_case_name = name.to_lowercase(); + let error_message = match TypeKind::from_source_name(&lower_case_name) { + Some(_) => format!("Expected '{}'", lower_case_name), + None => match lower_case_name.as_str() { + "integer" => format!("Expected '{}'", IntegerType::SOURCE_TYPE_NAME), + "str" => format!("Expected '{}'", StringType::SOURCE_TYPE_NAME), + "character" => format!("Expected '{}'", CharType::SOURCE_TYPE_NAME), + "list" | "vec" | "tuple" => { + format!("Expected '{}'", ArrayType::SOURCE_TYPE_NAME) + } + "obj" => format!("Expected '{}'", ObjectType::SOURCE_TYPE_NAME), + _ => format!("Unknown type '{}'", name), + }, + }; + return span.parse_err(error_message); + } + }; + Ok(Self { span, kind }) + } +} + +impl HasSpan for TypeIdent { + fn span(&self) -> Span { + self.span + } +} + +impl ParseSource for TypeIdent { + fn parse(input: SourceParser) -> ParseResult { + Self::from_ident(&input.parse()?) + } + + fn control_flow_pass(&mut self, _context: FlowCapturer) -> ParseResult<()> { + Ok(()) + } +} + +impl TypeKind { + pub(in crate::expressions) fn feature_resolver(&self) -> &'static dyn TypeFeatureResolver { + match self { + TypeKind::Leaf(leaf_kind) => leaf_kind.feature_resolver(), + TypeKind::Parent(parent_kind) => parent_kind.feature_resolver(), + TypeKind::Dyn(dyn_kind) => dyn_kind.feature_resolver(), + } + } +} + +pub(crate) struct TypeProperty { + pub(crate) source_type: TypeIdent, + _colons: Unused, + pub(crate) property: Ident, +} + +impl ParseSource for TypeProperty { + fn parse(input: SourceParser) -> ParseResult { + Ok(Self { + source_type: input.parse()?, + _colons: input.parse()?, + property: input.parse()?, + }) + } + + fn control_flow_pass(&mut self, _context: FlowCapturer) -> ParseResult<()> { + Ok(()) + } +} + +impl TypeProperty { + pub(crate) fn resolve_spanned( + &self, + ownership: RequestedOwnership, + ) -> ExecutionResult> { + let resolver = self.source_type.kind.feature_resolver(); + // TODO[performance] - lazily initialize properties as Shared + let resolved_property = resolver.resolve_type_property(&self.property.to_string()); + match resolved_property { + Some(value) => ownership.map_from_shared(Spanned( + SharedValue::new_from_owned(value.into_any_value()), + self.span_range(), + )), + None => self.type_err(format!( + "Type '{}' has no property named '{}'", + self.source_type.kind.source_name(), + self.property, + )), + } + } +} + +impl HasSpanRange for TypeProperty { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.source_type.span, self.property.span()) + } +} diff --git a/src/expressions/values/any_value.rs b/src/expressions/values/any_value.rs new file mode 100644 index 00000000..3c1a6f96 --- /dev/null +++ b/src/expressions/values/any_value.rs @@ -0,0 +1,544 @@ +use super::*; + +pub(crate) type AnyValue = AnyValueContent<'static, BeOwned>; +/// For symmetry +pub(crate) type AnyValueOwned = AnyValue; +pub(crate) type AnyValueRef<'a> = AnyValueContent<'a, BeRef>; +pub(crate) type AnyValueAnyRef<'a> = AnyValueContent<'a, BeAnyRef>; +pub(crate) type AnyValueShared = Shared; +pub(crate) type AnyValueMutable = Mutable; +pub(crate) type AnyValueAssignee = Assignee; +// pub(crate) type AnyValueShared = AnyValueContent<'static, BeShared>; +// pub(crate) type AnyValueMutable = AnyValueContent<'static, BeMutable>; +// pub(crate) type AnyValueAssignee = AnyValueContent<'static, BeAssignee>; + +define_parent_type! { + pub(crate) AnyType, + content: pub(crate) AnyValueContent, + leaf_kind: pub(crate) AnyValueLeafKind, + type_kind: ParentTypeKind::Value(pub(crate) AnyValueTypeKind), + variants: { + None => NoneType, + Integer => IntegerType, + Float => FloatType, + Bool => BoolType, + String => StringType, + Char => CharType, + // Unsupported literal is a type here so that we can parse such a token + // as a value rather than a stream, and give it better error messages + UnsupportedLiteral => UnsupportedLiteralType, + Array => ArrayType, + Object => ObjectType, + Stream => StreamType, + Range => RangeType, + Iterator => IteratorType, + Parser => ParserType, + }, + type_name: "value", + articled_display_name: "any value", +} + +define_type_features! { + impl AnyType, + pub(crate) mod value_interface { + pub(crate) mod methods { + fn clone(this: CopyOnWriteValue) -> AnyValue { + this.clone_to_owned_infallible() + } + + fn as_mut(Spanned(this, span): Spanned) -> ExecutionResult { + Ok(match this { + ArgumentValue::Owned(owned) => Mutable::new_from_owned(owned), + ArgumentValue::CopyOnWrite(copy_on_write) => ArgumentOwnership::Mutable + .map_from_copy_on_write(Spanned(copy_on_write, span))? + .expect_mutable(), + ArgumentValue::Mutable(mutable) => mutable, + ArgumentValue::Assignee(assignee) => assignee.0, + ArgumentValue::Shared(shared) => ArgumentOwnership::Mutable + .map_from_shared(Spanned(shared, span))? + .expect_mutable(), + }) + } + + // NOTE: + // All value types can be coerced into SharedValue as an input, so this method does actually do something + fn as_ref(this: SharedValue) -> SharedValue { + this + } + + fn swap(mut a: AssigneeValue, mut b: AssigneeValue) -> () { + core::mem::swap(a.0.deref_mut(), b.0.deref_mut()); + } + + fn replace(mut a: AssigneeValue, b: AnyValue) -> AnyValue { + core::mem::replace(a.0.deref_mut(), b) + } + + fn debug(Spanned(this, span_range): Spanned) -> ExecutionResult<()> { + let message = this.as_ref_value().concat_recursive(&ConcatBehaviour::debug(span_range))?; + span_range.debug_err(message) + } + + fn to_debug_string(Spanned(this, span_range): Spanned) -> ExecutionResult { + this.as_ref_value().concat_recursive(&ConcatBehaviour::debug(span_range)) + } + + fn to_stream(Spanned(input, span_range): Spanned) -> ExecutionResult { + input.map_into( + |shared| shared.as_ref_value().output_to_new_stream(Grouping::Flattened, span_range), + |owned| owned.into_stream(Grouping::Flattened, span_range), + ) + } + + fn to_group(Spanned(input, span_range): Spanned) -> ExecutionResult { + input.map_into( + |shared| shared.as_ref_value().output_to_new_stream(Grouping::Grouped, span_range), + |owned| owned.into_stream(Grouping::Grouped, span_range), + ) + } + + fn to_string(Spanned(input, span_range): Spanned) -> ExecutionResult { + input.as_ref_value().concat_recursive(&ConcatBehaviour::standard(span_range)) + } + + [context] fn with_span(value: Spanned, spans: AnyRef) -> ExecutionResult { + let mut this = to_stream(context, value)?; + let span_to_use = match spans.resolve_content_span_range() { + Some(span_range) => span_range.span_from_join_else_start(), + None => Span::call_site(), + }; + this.replace_first_level_spans(span_to_use); + Ok(this) + } + + // TYPE CHECKING + // =============================== + fn is_none(this: SharedValue) -> bool { + this.is_none() + } + + // EQUALITY METHODS + // =============================== + // Compare values with strict type checking - errors on value kind mismatch. + [context] fn typed_eq(this: AnyValueAnyRef, other: AnyValueAnyRef) -> ExecutionResult { + this.as_ref_value().typed_eq(&other.as_ref_value(), context.span_range()) + } + + // STRING-BASED CONVERSION METHODS + // =============================== + + [context] fn to_ident(this: Spanned) -> ExecutionResult { + let stream = this.into_stream()?; + let spanned = stream.into_spanned_ref(context.output_span_range); + stream_interface::methods::to_ident(context, spanned) + } + + [context] fn to_ident_camel(this: Spanned) -> ExecutionResult { + let stream = this.into_stream()?; + let spanned = stream.into_spanned_ref(context.output_span_range); + stream_interface::methods::to_ident_camel(context, spanned) + } + + [context] fn to_ident_snake(this: Spanned) -> ExecutionResult { + let stream = this.into_stream()?; + let spanned = stream.into_spanned_ref(context.output_span_range); + stream_interface::methods::to_ident_snake(context, spanned) + } + + [context] fn to_ident_upper_snake(this: Spanned) -> ExecutionResult { + let stream = this.into_stream()?; + let spanned = stream.into_spanned_ref(context.output_span_range); + stream_interface::methods::to_ident_upper_snake(context, spanned) + } + + // Some literals become Value::UnsupportedLiteral but can still be round-tripped back to a stream + [context] fn to_literal(this: Spanned) -> ExecutionResult { + let stream = this.into_stream()?; + let spanned = stream.into_spanned_ref(context.output_span_range); + stream_interface::methods::to_literal(context, spanned) + } + } + pub(crate) mod unary_operations { + fn cast_to_string(Spanned(input, span_range): Spanned) -> ExecutionResult { + input.as_ref_value().concat_recursive(&ConcatBehaviour::standard(span_range)) + } + + fn cast_to_stream(input: Spanned) -> ExecutionResult { + input.into_stream() + } + } + pub(crate) mod binary_operations { + fn eq(lhs: AnyValueAnyRef, rhs: AnyValueAnyRef) -> bool { + AnyValue::values_equal(lhs.as_ref_value(), rhs.as_ref_value()) + } + + fn ne(lhs: AnyValueAnyRef, rhs: AnyValueAnyRef) -> bool { + !AnyValue::values_equal(lhs.as_ref_value(), rhs.as_ref_value()) + } + } + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Cast { target: CastTarget(kind), .. } => match kind { + AnyValueLeafKind::String(_) => unary_definitions::cast_to_string(), + AnyValueLeafKind::Stream(_) => unary_definitions::cast_to_stream(), + _ => return None, + }, + _ => return None, + }) + } + + fn resolve_own_binary_operation( + operation: &BinaryOperation, + ) -> Option { + Some(match operation { + BinaryOperation::Equal { .. } => binary_definitions::eq(), + BinaryOperation::NotEqual { .. } => binary_definitions::ne(), + _ => return None, + }) + } + } + } +} + +pub(crate) trait IntoAnyValue: Sized { + fn into_any_value(self) -> AnyValue; +} + +impl<'a, F: IsHierarchicalForm> AnyValueContent<'a, F> { + pub(crate) fn is_none(&self) -> bool { + matches!(&self, AnyValueContent::None(_)) + } +} + +impl AnyValue { + pub(crate) fn for_literal(literal: Literal) -> AnyValue { + // The unwrap should be safe because all Literal should be parsable + // as syn::Lit; falling back to syn::Lit::Verbatim if necessary. + Self::for_syn_lit( + literal + .to_token_stream() + .interpreted_parse_with(|input| input.parse()) + .unwrap(), + ) + } + + pub(crate) fn for_syn_lit(lit: syn::Lit) -> AnyValue { + // https://docs.rs/syn/latest/syn/enum.Lit.html + let matched = match &lit { + Lit::Int(lit) => match IntegerValue::for_litint(lit) { + Ok(int) => Some(int.into_any_value()), + Err(_) => None, + }, + Lit::Float(lit) => match FloatValue::for_litfloat(lit) { + Ok(float) => Some(float.into_any_value()), + Err(_) => None, + }, + Lit::Bool(lit) => Some(lit.value.into_any_value()), + Lit::Str(lit) => Some(lit.value().into_any_value()), + Lit::Char(lit) => Some(lit.value().into_any_value()), + _ => None, + }; + match matched { + Some(value) => value, + None => UnsupportedLiteral(lit).into_any_value(), + } + } + + // TODO[concepts]: Remove from here + pub(crate) fn try_transparent_clone( + &self, + error_span_range: SpanRange, + ) -> ExecutionResult { + if !self.value_kind().supports_transparent_cloning() { + return error_span_range.ownership_err(format!( + "An owned value is required, but a reference was received, and {} does not support transparent cloning. You may wish to use .clone() explicitly.", + self.articled_kind() + )); + } + Ok(self.clone()) + } + + pub(crate) fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + self.as_ref_value() + .test_equality(&other.as_ref_value(), ctx) + } + + /// Recursively compares two values for equality using `ValuesEqual` semantics. + pub(crate) fn values_equal<'a>(lhs: AnyValueRef<'a>, rhs: AnyValueRef<'a>) -> bool { + lhs.lenient_eq(&rhs) + } +} + +impl<'a> ValuesEqual for AnyValueRef<'a> { + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + // Each variant has two lines: same-type comparison, then type-mismatch fallback. + // This ensures adding a new variant only requires adding two lines at the bottom. + match (self, other) { + (AnyValueContent::None(_), AnyValueContent::None(_)) => ctx.values_equal(), + (AnyValueContent::None(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Bool(l), AnyValueContent::Bool(r)) => l.test_equality(r, ctx), + (AnyValueContent::Bool(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Char(l), AnyValueContent::Char(r)) => l.test_equality(r, ctx), + (AnyValueContent::Char(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::String(l), AnyValueContent::String(r)) => l.test_equality(r, ctx), + (AnyValueContent::String(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Integer(l), AnyValueContent::Integer(r)) => l.test_equality(r, ctx), + (AnyValueContent::Integer(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Float(l), AnyValueContent::Float(r)) => l.test_equality(r, ctx), + (AnyValueContent::Float(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Array(l), AnyValueContent::Array(r)) => l.test_equality(r, ctx), + (AnyValueContent::Array(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Object(l), AnyValueContent::Object(r)) => l.test_equality(r, ctx), + (AnyValueContent::Object(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Stream(l), AnyValueContent::Stream(r)) => l.test_equality(r, ctx), + (AnyValueContent::Stream(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Range(l), AnyValueContent::Range(r)) => l.test_equality(r, ctx), + (AnyValueContent::Range(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::UnsupportedLiteral(l), AnyValueContent::UnsupportedLiteral(r)) => { + l.test_equality(r, ctx) + } + (AnyValueContent::UnsupportedLiteral(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Parser(l), AnyValueContent::Parser(r)) => l.test_equality(r, ctx), + (AnyValueContent::Parser(_), _) => ctx.kind_mismatch(self, other), + (AnyValueContent::Iterator(l), AnyValueContent::Iterator(r)) => l.test_equality(r, ctx), + (AnyValueContent::Iterator(_), _) => ctx.kind_mismatch(self, other), + } + } +} + +impl AnyValue { + pub(crate) fn into_stream( + self, + grouping: Grouping, + error_span_range: SpanRange, + ) -> ExecutionResult { + match (self, grouping) { + (AnyValueContent::Stream(value), Grouping::Flattened) => Ok(value), + (AnyValueContent::Stream(value), Grouping::Grouped) => { + let mut output: OutputStream = OutputStream::new(); + let span = ToStreamContext::new(&mut output, error_span_range).new_token_span(); + output.push_new_group(value, Delimiter::None, span); + Ok(output) + } + (other, grouping) => other + .as_ref_value() + .output_to_new_stream(grouping, error_span_range), + } + } +} + +impl<'a> AnyValueRef<'a> { + pub(crate) fn output_to_new_stream( + self, + grouping: Grouping, + error_span_range: SpanRange, + ) -> ExecutionResult { + let mut output = OutputStream::new(); + self.output_to( + grouping, + &mut ToStreamContext::new(&mut output, error_span_range), + )?; + Ok(output) + } + + pub(crate) fn output_to( + self, + grouping: Grouping, + output: &mut ToStreamContext, + ) -> ExecutionResult<()> { + match grouping { + Grouping::Grouped => { + // Grouping can be important for different values, to ensure they're read atomically + // when the output stream is viewed as an array/iterable, e.g. in a for loop. + // * Grouping means -1 is interpreted atomically, rather than as a punct then a number + // * Grouping means that a stream is interpreted atomically + output.push_grouped(|inner| self.output_flattened_to(inner), Delimiter::None)?; + } + Grouping::Flattened => { + self.output_flattened_to(output)?; + } + } + Ok(()) + } + + fn output_flattened_to(self, output: &mut ToStreamContext) -> ExecutionResult<()> { + match self { + AnyValueContent::None(_) => {} + AnyValueContent::Integer(value) => { + let literal = value + .clone_to_owned_infallible() + .to_literal(output.new_token_span()); + output.push_literal(literal); + } + AnyValueContent::Float(value) => { + value.clone_to_owned_infallible().output_to(output); + } + AnyValueContent::Bool(value) => { + let ident = Ident::new_bool(*value, output.new_token_span()); + output.push_ident(ident); + } + AnyValueContent::String(value) => { + let literal = Literal::string(value).with_span(output.new_token_span()); + output.push_literal(literal); + } + AnyValueContent::Char(value) => { + let literal = Literal::character(*value).with_span(output.new_token_span()); + output.push_literal(literal); + } + AnyValueContent::UnsupportedLiteral(literal) => { + output.extend_raw_tokens(literal.0.to_token_stream()) + } + AnyValueContent::Object(_) => { + return output.type_err("Objects cannot be output to a stream"); + } + AnyValueContent::Array(array) => array.output_items_to(output, Grouping::Flattened)?, + AnyValueContent::Stream(value) => value.append_cloned_into(output.output_stream), + AnyValueContent::Iterator(iterator) => iterator + .clone() + .output_items_to(output, Grouping::Flattened)?, + AnyValueContent::Range(value) => { + let iterator = IteratorValue::new_for_range(value.clone())?; + iterator.output_items_to(output, Grouping::Flattened)? + } + AnyValueContent::Parser(_) => { + return output.type_err("Parsers cannot be output to a stream"); + } + }; + Ok(()) + } + + pub(crate) fn concat_recursive(self, behaviour: &ConcatBehaviour) -> ExecutionResult { + let mut output = String::new(); + self.concat_recursive_into(&mut output, behaviour)?; + Ok(output) + } + + pub(crate) fn concat_recursive_into( + self, + output: &mut String, + behaviour: &ConcatBehaviour, + ) -> ExecutionResult<()> { + match self { + AnyValueContent::None(_) => { + if behaviour.show_none_values { + output.push_str("None"); + } + } + AnyValueContent::Stream(stream) => { + stream.concat_as_literal_into(output, behaviour); + } + AnyValueContent::Array(array) => { + array.concat_recursive_into(output, behaviour)?; + } + AnyValueContent::Object(object) => { + object.concat_recursive_into(output, behaviour)?; + } + AnyValueContent::Iterator(iterator) => { + iterator.concat_recursive_into(output, behaviour)?; + } + AnyValueContent::Range(range) => { + range.concat_recursive_into(output, behaviour)?; + } + AnyValueContent::Parser(_) => { + return behaviour + .error_span_range + .type_err("Parsers cannot be output to a string"); + } + AnyValueContent::Integer(_) + | AnyValueContent::Float(_) + | AnyValueContent::Char(_) + | AnyValueContent::Bool(_) + | AnyValueContent::UnsupportedLiteral(_) + | AnyValueContent::String(_) => { + // This isn't the most efficient, but it's less code and debug doesn't need to be super efficient. + let stream = self + .output_to_new_stream(Grouping::Flattened, behaviour.error_span_range) + .expect("Non-composite values should all be able to be outputted to a stream"); + stream.concat_content_into(output, behaviour); + } + } + Ok(()) + } +} + +pub(crate) struct ToStreamContext<'a> { + output_stream: &'a mut OutputStream, + error_span_range: SpanRange, +} + +impl<'a> ToStreamContext<'a> { + pub(crate) fn new(output_stream: &'a mut OutputStream, error_span_range: SpanRange) -> Self { + Self { + output_stream, + error_span_range, + } + } + + pub(crate) fn push_grouped( + &mut self, + f: impl FnOnce(&mut ToStreamContext) -> ExecutionResult<()>, + delimiter: Delimiter, + ) -> ExecutionResult<()> { + let span = self.new_token_span(); + self.output_stream.push_grouped( + |inner| f(&mut ToStreamContext::new(inner, self.error_span_range)), + delimiter, + span, + ) + } + + pub(crate) fn new_token_span(&self) -> Span { + // By default, we use call_site span for generated tokens + Span::call_site() + } +} + +impl Deref for ToStreamContext<'_> { + type Target = OutputStream; + + fn deref(&self) -> &Self::Target { + self.output_stream + } +} + +impl DerefMut for ToStreamContext<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.output_stream + } +} + +impl HasSpanRange for ToStreamContext<'_> { + fn span_range(&self) -> SpanRange { + self.error_span_range + } +} + +impl Spanned { + pub(crate) fn into_statement_result(self) -> ExecutionResult<()> { + let Spanned(value, span_range) = self; + match value { + AnyValueContent::None(_) => Ok(()), + _ => span_range.control_flow_err("A non-returning statement must not return a value. If you wish to explicitly discard the expression's result, use `let _ = ...;`. Alternatively, If you wish to output the value into the parent token stream, use `emit ...;`"), + } + } + + pub(crate) fn into_stream(self) -> ExecutionResult { + let Spanned(value, span_range) = self; + value.into_stream(Grouping::Flattened, span_range) + } + + pub(crate) fn resolve_any_iterator( + self, + resolution_target: &str, + ) -> ExecutionResult { + self.dyn_resolve::(resolution_target)? + .into_iterator() + } +} + +#[derive(Copy, Clone)] +pub(crate) enum Grouping { + Grouped, + Flattened, +} diff --git a/src/expressions/values/array.rs b/src/expressions/values/array.rs new file mode 100644 index 00000000..eaa02b03 --- /dev/null +++ b/src/expressions/values/array.rs @@ -0,0 +1,265 @@ +use super::*; + +define_leaf_type! { + pub(crate) ArrayType => AnyType(AnyValueContent::Array), + content: ArrayValue, + kind: pub(crate) ArrayKind, + type_name: "array", + articled_display_name: "an array", + dyn_impls: { + IterableType: impl IsIterable { + fn into_iterator(self: Box) -> ExecutionResult { + Ok(IteratorValue::new_for_array(*self)) + } + + fn len(&self, _error_span_range: SpanRange) -> ExecutionResult { + Ok(self.items.len()) + } + } + }, +} + +#[derive(Clone)] +pub(crate) struct ArrayValue { + pub(crate) items: Vec, +} + +impl ArrayValue { + pub(crate) fn new(items: Vec) -> Self { + Self { items } + } + + pub(crate) fn output_items_to( + &self, + output: &mut ToStreamContext, + grouping: Grouping, + ) -> ExecutionResult<()> { + for item in &self.items { + item.as_ref_value().output_to(grouping, output)?; + } + Ok(()) + } + + pub(super) fn into_indexed( + mut self, + Spanned(index, span_range): Spanned, + ) -> ExecutionResult { + Ok(match index { + AnyValueContent::Integer(integer) => { + let index = + self.resolve_valid_index_from_integer(Spanned(integer, span_range), false)?; + std::mem::replace(&mut self.items[index], ().into_any_value()) + } + AnyValueContent::Range(range) => { + let range = Spanned(range, span_range).resolve_to_index_range(&self)?; + let new_items: Vec<_> = self.items.drain(range).collect(); + new_items.into_any_value() + } + _ => return span_range.type_err("The index must be an integer or a range"), + }) + } + + pub(super) fn index_mut( + &mut self, + Spanned(index, span_range): Spanned, + ) -> ExecutionResult<&mut AnyValue> { + Ok(match index { + AnyValueContent::Integer(integer) => { + let index = + self.resolve_valid_index_from_integer(Spanned(integer, span_range), false)?; + &mut self.items[index] + } + AnyValueContent::Range(..) => { + // TODO[slice-support] Temporary until we add slice types - we error here + return span_range.ownership_err("Currently, a range-indexed array must be owned. Use `.take()` or `.clone()` before indexing [..]"); + } + _ => return span_range.type_err("The index must be an integer or a range"), + }) + } + + pub(super) fn index_ref( + &self, + Spanned(index, span_range): Spanned, + ) -> ExecutionResult<&AnyValue> { + Ok(match index { + AnyValueContent::Integer(integer) => { + let index = + self.resolve_valid_index_from_integer(Spanned(integer, span_range), false)?; + &self.items[index] + } + AnyValueContent::Range(..) => { + // TODO[slice-support] Temporary until we add slice types - we error here + return span_range.ownership_err("Currently, a range-indexed array must be owned. Use `.take()` or `.clone()` before indexing [..]"); + } + _ => return span_range.type_err("The index must be an integer or a range"), + }) + } + + pub(super) fn resolve_valid_index( + &self, + Spanned(index, span_range): Spanned, + is_exclusive: bool, + ) -> ExecutionResult { + match index { + AnyValueContent::Integer(int) => { + self.resolve_valid_index_from_integer(Spanned(int, span_range), is_exclusive) + } + _ => span_range.type_err("The index must be an integer"), + } + } + + fn resolve_valid_index_from_integer( + &self, + Spanned(integer, span): Spanned, + is_exclusive: bool, + ) -> ExecutionResult { + let index: OptionalSuffix = + Spanned(integer.clone_to_owned_infallible(), span).resolve_as("An array index")?; + let index = index.0; + if is_exclusive { + if index <= self.items.len() { + Ok(index) + } else { + span.value_err(format!( + "Exclusive index of {} must be less than or equal to the array length of {}", + index, + self.items.len() + )) + } + } else if index < self.items.len() { + Ok(index) + } else { + span.value_err(format!( + "Inclusive index of {} must be less than the array length of {}", + index, + self.items.len() + )) + } + } + + pub(crate) fn concat_recursive_into( + &self, + output: &mut String, + behaviour: &ConcatBehaviour, + ) -> ExecutionResult<()> { + IteratorValue::any_iterator_to_string( + self.items.iter(), + output, + behaviour, + "[]", + "[", + "]", + false, // Output all the vec because it's already in memory + ) + } +} + +impl ValuesEqual for ArrayValue { + /// Recursively compares two arrays element-by-element. + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + if self.items.len() != other.items.len() { + return ctx.lengths_unequal(Some(self.items.len()), Some(other.items.len())); + } + for (i, (l, r)) in self.items.iter().zip(other.items.iter()).enumerate() { + let result = ctx.with_array_index(i, |ctx| l.test_equality(r, ctx)); + if ctx.should_short_circuit(&result) { + return result; + } + } + ctx.values_equal() + } +} + +impl IsValueContent for Vec { + type Type = ArrayType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for Vec { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + ArrayValue { items: self } + } +} + +impl_resolvable_argument_for! { + ArrayType, + (value, context) -> ArrayValue { + match value { + AnyValueContent::Array(value) => Ok(value), + _ => context.err("an array", value), + } + } +} + +define_type_features! { + impl ArrayType, + pub(crate) mod array_interface { + pub(crate) mod methods { + fn push(mut this: Mutable, item: AnyValue) -> ExecutionResult<()> { + this.items.push(item); + Ok(()) + } + + [context] fn to_stream_grouped(this: ArrayValue) -> StreamOutput [ignore_type_assertion!] { + let error_span_range = context.span_range(); + StreamOutput::new(move |stream| this.output_items_to(&mut ToStreamContext::new(stream, error_span_range), Grouping::Grouped)) + } + } + pub(crate) mod unary_operations { + [context] fn cast_singleton_to_value(Spanned(mut this, span): Spanned) -> ExecutionResult { + let length = this.items.len(); + if length == 1 { + Ok(context.operation.evaluate(this.items.pop().unwrap().spanned(span))?.0) + } else { + context.operation.value_err(format!( + "Only a singleton array can be cast to this value but the array has {} elements", + length, + )) + } + } + } + pub(crate) mod binary_operations { + fn add(mut lhs: ArrayValue, rhs: ArrayValue) -> ArrayValue { + lhs.items.extend(rhs.items); + lhs + } + + fn add_assign(mut lhs: Assignee, rhs: ArrayValue) { + lhs.items.extend(rhs.items); + } + } + index_access(ArrayValue) { + fn shared(source: &'a ArrayValue, index: Spanned) { + source.index_ref(index) + } + fn mutable(source: &'a mut ArrayValue, index: Spanned, _auto_create: bool) { + source.index_mut(index) + } + fn owned(source: ArrayValue, index: Spanned) { + source.into_indexed(index) + } + } + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => return None, + UnaryOperation::Cast { target, .. } => if target.is_singleton_target() { + unary_definitions::cast_singleton_to_value() + } else { + return None; + } + }) + } + + fn resolve_own_binary_operation( + operation: &BinaryOperation, + ) -> Option { + Some(match operation { + BinaryOperation::Addition { .. } => binary_definitions::add(), + BinaryOperation::AddAssign { .. } => binary_definitions::add_assign(), + _ => return None, + }) + } + } + } +} diff --git a/src/expressions/values/boolean.rs b/src/expressions/values/boolean.rs new file mode 100644 index 00000000..adce8579 --- /dev/null +++ b/src/expressions/values/boolean.rs @@ -0,0 +1,194 @@ +#![allow(clippy::bool_comparison)] + +use super::*; + +define_leaf_type! { + pub(crate) BoolType => AnyType(AnyValueContent::Bool), + content: bool, + kind: pub(crate) BoolKind, + type_name: "bool", + articled_display_name: "a bool", + dyn_impls: {}, +} + +impl ValuesEqual for bool { + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + if self == other { + ctx.values_equal() + } else { + ctx.leaf_values_not_equal(self, other) + } + } +} + +define_type_features! { + impl BoolType, + pub(crate) mod boolean_interface { + pub(crate) mod methods { + } + pub(crate) mod unary_operations { + fn not(this: bool) -> bool { + !this + } + + fn cast_to_untyped_integer(input: bool) -> UntypedInteger { + UntypedInteger::from_fallback(input as FallbackInteger) + } + + fn cast_to_i8(input: bool) -> i8 { + input as i8 + } + + fn cast_to_i16(input: bool) -> i16 { + input as i16 + } + + fn cast_to_i32(input: bool) -> i32 { + input as i32 + } + + fn cast_to_i64(input: bool) -> i64 { + input as i64 + } + + fn cast_to_i128(input: bool) -> i128 { + input as i128 + } + + fn cast_to_isize(input: bool) -> isize { + input as isize + } + + fn cast_to_u8(input: bool) -> u8 { + input as u8 + } + + fn cast_to_u16(input: bool) -> u16 { + input as u16 + } + + fn cast_to_u32(input: bool) -> u32 { + input as u32 + } + + fn cast_to_u64(input: bool) -> u64 { + input as u64 + } + + fn cast_to_u128(input: bool) -> u128 { + input as u128 + } + + fn cast_to_usize(input: bool) -> usize { + input as usize + } + + fn cast_to_boolean(input: bool) -> bool { + input + } + + fn cast_to_string(input: bool) -> String { + input.to_string() + } + } + pub(crate) mod binary_operations { + fn and(lhs: bool, rhs: bool) -> bool { + lhs && rhs + } + + fn or(lhs: bool, rhs: bool) -> bool { + lhs || rhs + } + + fn bitxor(lhs: bool, rhs: bool) -> bool { + lhs ^ rhs + } + + fn bitand(lhs: bool, rhs: bool) -> bool { + lhs & rhs + } + + fn bitor(lhs: bool, rhs: bool) -> bool { + lhs | rhs + } + + fn eq(lhs: bool, rhs: bool) -> bool { + lhs == rhs + } + + fn ne(lhs: bool, rhs: bool) -> bool { + lhs != rhs + } + + fn lt(lhs: bool, rhs: bool) -> bool { + lhs < rhs + } + + fn le(lhs: bool, rhs: bool) -> bool { + lhs <= rhs + } + + fn ge(lhs: bool, rhs: bool) -> bool { + lhs >= rhs + } + + fn gt(lhs: bool, rhs: bool) -> bool { + lhs > rhs + } + } + interface_items { + fn resolve_own_binary_operation( + operation: &BinaryOperation, + ) -> Option { + Some(match operation { + BinaryOperation::LogicalAnd { .. } => binary_definitions::and(), + BinaryOperation::LogicalOr { .. } => binary_definitions::or(), + BinaryOperation::BitXor { .. } => binary_definitions::bitxor(), + BinaryOperation::BitAnd { .. } => binary_definitions::bitand(), + BinaryOperation::BitOr { .. } => binary_definitions::bitor(), + BinaryOperation::Equal { .. } => binary_definitions::eq(), + BinaryOperation::NotEqual { .. } => binary_definitions::ne(), + BinaryOperation::LessThan { .. } => binary_definitions::lt(), + BinaryOperation::LessThanOrEqual { .. } => binary_definitions::le(), + BinaryOperation::GreaterThanOrEqual { .. } => binary_definitions::ge(), + BinaryOperation::GreaterThan { .. } => binary_definitions::gt(), + _ => return None, + }) + } + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Not { .. } => unary_definitions::not(), + UnaryOperation::Cast { target: CastTarget(kind), .. } => match kind { + AnyValueLeafKind::Integer(IntegerLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_integer(), + AnyValueLeafKind::Integer(IntegerLeafKind::I8(_)) => unary_definitions::cast_to_i8(), + AnyValueLeafKind::Integer(IntegerLeafKind::I16(_)) => unary_definitions::cast_to_i16(), + AnyValueLeafKind::Integer(IntegerLeafKind::I32(_)) => unary_definitions::cast_to_i32(), + AnyValueLeafKind::Integer(IntegerLeafKind::I64(_)) => unary_definitions::cast_to_i64(), + AnyValueLeafKind::Integer(IntegerLeafKind::I128(_)) => unary_definitions::cast_to_i128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Isize(_)) => unary_definitions::cast_to_isize(), + AnyValueLeafKind::Integer(IntegerLeafKind::U8(_)) => unary_definitions::cast_to_u8(), + AnyValueLeafKind::Integer(IntegerLeafKind::U16(_)) => unary_definitions::cast_to_u16(), + AnyValueLeafKind::Integer(IntegerLeafKind::U32(_)) => unary_definitions::cast_to_u32(), + AnyValueLeafKind::Integer(IntegerLeafKind::U64(_)) => unary_definitions::cast_to_u64(), + AnyValueLeafKind::Integer(IntegerLeafKind::U128(_)) => unary_definitions::cast_to_u128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Usize(_)) => unary_definitions::cast_to_usize(), + AnyValueLeafKind::Bool(_) => unary_definitions::cast_to_boolean(), + AnyValueLeafKind::String(_) => unary_definitions::cast_to_string(), + _ => return None, + }, + _ => return None, + }) + } + } + } +} + +impl_resolvable_argument_for! { + BoolType, + (value, context) -> bool { + match value { + AnyValueContent::Bool(value) => Ok(value), + other => context.err("a bool", other), + } + } +} diff --git a/src/expressions/values/character.rs b/src/expressions/values/character.rs new file mode 100644 index 00000000..af15e2e8 --- /dev/null +++ b/src/expressions/values/character.rs @@ -0,0 +1,163 @@ +use super::*; + +define_leaf_type! { + pub(crate) CharType => AnyType(AnyValueContent::Char), + content: char, + kind: pub(crate) CharKind, + type_name: "char", + articled_display_name: "a char", + dyn_impls: {}, +} + +impl ValuesEqual for char { + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + if self == other { + ctx.values_equal() + } else { + ctx.leaf_values_not_equal(self, other) + } + } +} + +define_type_features! { + impl CharType, + pub(crate) mod char_interface { + pub(crate) mod methods { + } + pub(crate) mod unary_operations { + fn cast_to_untyped_integer(input: char) -> UntypedInteger { + UntypedInteger::from_fallback(input as FallbackInteger) + } + + fn cast_to_i8(input: char) -> i8 { + input as i8 + } + + fn cast_to_i16(input: char) -> i16 { + input as i16 + } + + fn cast_to_i32(input: char) -> i32 { + input as i32 + } + + fn cast_to_i64(input: char) -> i64 { + input as i64 + } + + fn cast_to_i128(input: char) -> i128 { + input as i128 + } + + fn cast_to_isize(input: char) -> isize { + input as isize + } + + fn cast_to_u8(input: char) -> u8 { + input as u8 + } + + fn cast_to_u16(input: char) -> u16 { + input as u16 + } + + fn cast_to_u32(input: char) -> u32 { + input as u32 + } + + fn cast_to_u64(input: char) -> u64 { + input as u64 + } + + fn cast_to_u128(input: char) -> u128 { + input as u128 + } + + fn cast_to_usize(input: char) -> usize { + input as usize + } + + fn cast_to_char(input: char) -> char { + input + } + + fn cast_to_string(input: char) -> String { + input.to_string() + } + } + pub(crate) mod binary_operations { + fn eq(lhs: char, rhs: char) -> bool { + lhs == rhs + } + + fn ne(lhs: char, rhs: char) -> bool { + lhs != rhs + } + + fn lt(lhs: char, rhs: char) -> bool { + lhs < rhs + } + + fn le(lhs: char, rhs: char) -> bool { + lhs <= rhs + } + + fn ge(lhs: char, rhs: char) -> bool { + lhs >= rhs + } + + fn gt(lhs: char, rhs: char) -> bool { + lhs > rhs + } + } + interface_items { + fn resolve_own_binary_operation( + operation: &BinaryOperation, + ) -> Option { + Some(match operation { + BinaryOperation::Equal { .. } => binary_definitions::eq(), + BinaryOperation::NotEqual { .. } => binary_definitions::ne(), + BinaryOperation::LessThan { .. } => binary_definitions::lt(), + BinaryOperation::LessThanOrEqual { .. } => binary_definitions::le(), + BinaryOperation::GreaterThanOrEqual { .. } => binary_definitions::ge(), + BinaryOperation::GreaterThan { .. } => binary_definitions::gt(), + _ => return None, + }) + } + + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Cast { target: CastTarget(kind), .. } => match kind { + AnyValueLeafKind::Integer(IntegerLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_integer(), + AnyValueLeafKind::Integer(IntegerLeafKind::I8(_)) => unary_definitions::cast_to_i8(), + AnyValueLeafKind::Integer(IntegerLeafKind::I16(_)) => unary_definitions::cast_to_i16(), + AnyValueLeafKind::Integer(IntegerLeafKind::I32(_)) => unary_definitions::cast_to_i32(), + AnyValueLeafKind::Integer(IntegerLeafKind::I64(_)) => unary_definitions::cast_to_i64(), + AnyValueLeafKind::Integer(IntegerLeafKind::I128(_)) => unary_definitions::cast_to_i128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Isize(_)) => unary_definitions::cast_to_isize(), + AnyValueLeafKind::Integer(IntegerLeafKind::U8(_)) => unary_definitions::cast_to_u8(), + AnyValueLeafKind::Integer(IntegerLeafKind::U16(_)) => unary_definitions::cast_to_u16(), + AnyValueLeafKind::Integer(IntegerLeafKind::U32(_)) => unary_definitions::cast_to_u32(), + AnyValueLeafKind::Integer(IntegerLeafKind::U64(_)) => unary_definitions::cast_to_u64(), + AnyValueLeafKind::Integer(IntegerLeafKind::U128(_)) => unary_definitions::cast_to_u128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Usize(_)) => unary_definitions::cast_to_usize(), + AnyValueLeafKind::Char(_) => unary_definitions::cast_to_char(), + AnyValueLeafKind::String(_) => unary_definitions::cast_to_string(), + _ => return None, + }, + _ => return None, + }) + } + } + } +} + +impl_resolvable_argument_for! { + CharType, + (value, context) -> char { + match value { + AnyValueContent::Char(char) => Ok(char), + _ => context.err("a char", value), + } + } +} diff --git a/src/expressions/values/float.rs b/src/expressions/values/float.rs new file mode 100644 index 00000000..3f80c925 --- /dev/null +++ b/src/expressions/values/float.rs @@ -0,0 +1,364 @@ +use super::*; + +define_parent_type! { + pub(crate) FloatType => AnyType(AnyValueContent::Float), + content: pub(crate) FloatContent, + leaf_kind: pub(crate) FloatLeafKind, + type_kind: ParentTypeKind::Float(pub(crate) FloatTypeKind), + variants: { + Untyped => UntypedFloatType, + F32 => F32Type, + F64 => F64Type, + }, + type_name: "float", + articled_display_name: "a float", +} + +pub(crate) type FloatValue = FloatContent<'static, BeOwned>; +pub(crate) type FloatValueRef<'a> = FloatContent<'a, BeRef>; + +impl FloatValue { + pub(super) fn for_litfloat(lit: &syn::LitFloat) -> ParseResult { + Ok(match lit.suffix() { + "" => FloatContent::Untyped(UntypedFloat::new_from_lit_float(lit)?), + "f32" => FloatContent::F32(lit.base10_parse()?), + "f64" => FloatContent::F64(lit.base10_parse()?), + suffix => { + return lit.span().parse_err(format!( + "The literal suffix {suffix} is not supported in preinterpret expressions" + )); + } + }) + } + + pub(crate) fn resolve_untyped_to_match(self, target: FloatValueRef) -> FloatValue { + match self { + FloatContent::Untyped(this) => this.into_kind(target.kind()), + other => other, + } + } + + #[allow(dead_code)] + pub(super) fn to_literal(self, span: Span) -> Literal { + self.to_unspanned_literal().with_span(span) + } + + fn to_unspanned_literal(self) -> Literal { + match self { + FloatContent::Untyped(float) => float.to_unspanned_literal(), + FloatContent::F32(float) => Literal::f32_suffixed(float), + FloatContent::F64(float) => Literal::f64_suffixed(float), + } + } +} + +// TODO[concepts]: Move to FloatRef<'a> when we can +impl FloatValue { + /// Outputs this float value to a token stream. + /// For finite values, outputs a literal. For non-finite values (infinity, NaN), + /// outputs the equivalent constant path like `f32::INFINITY`. + pub(super) fn output_to(&self, output: &mut ToStreamContext) { + let span = output.new_token_span(); + match self { + FloatContent::Untyped(float) => { + let f = float.into_fallback(); + if f.is_finite() { + output.push_literal(Literal::f64_unsuffixed(f).with_span(span)); + } else if f.is_nan() { + // For untyped NaN, we output f64::NAN since FallbackFloat is f64 + output.extend_raw_tokens(quote::quote_spanned!(span=> f64::NAN)); + } else if f.is_sign_positive() { + output.extend_raw_tokens(quote::quote_spanned!(span=> f64::INFINITY)); + } else { + output.extend_raw_tokens(quote::quote_spanned!(span=> f64::NEG_INFINITY)); + } + } + FloatContent::F32(f) => { + if f.is_finite() { + output.push_literal(Literal::f32_suffixed(*f).with_span(span)); + } else if f.is_nan() { + output.extend_raw_tokens(quote::quote_spanned!(span=> f32::NAN)); + } else if f.is_sign_positive() { + output.extend_raw_tokens(quote::quote_spanned!(span=> f32::INFINITY)); + } else { + output.extend_raw_tokens(quote::quote_spanned!(span=> f32::NEG_INFINITY)); + } + } + FloatContent::F64(f) => { + if f.is_finite() { + output.push_literal(Literal::f64_suffixed(*f).with_span(span)); + } else if f.is_nan() { + output.extend_raw_tokens(quote::quote_spanned!(span=> f64::NAN)); + } else if f.is_sign_positive() { + output.extend_raw_tokens(quote::quote_spanned!(span=> f64::INFINITY)); + } else { + output.extend_raw_tokens(quote::quote_spanned!(span=> f64::NEG_INFINITY)); + } + } + } + } +} + +fn assign_op( + mut left: Assignee, + right: R, + context: BinaryOperationCallContext, + op: fn(BinaryOperationCallContext, FloatValue, R) -> ExecutionResult, +) -> ExecutionResult<()> { + let left_value = core::mem::replace(&mut *left, FloatContent::F32(0.0)); + let result = op(context, left_value, right)?; + *left = result; + Ok(()) +} + +impl Debug for FloatValueRef<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FloatContent::Untyped(v) => write!(f, "{}", v.into_fallback()), + FloatContent::F32(v) => write!(f, "{:?}", v), + FloatContent::F64(v) => write!(f, "{:?}", v), + } + } +} + +/// Aligns types for comparison - converts untyped to match the other's type. +/// Unlike integers, float conversion never fails (may lose precision). +fn align_types(mut lhs: FloatValue, mut rhs: FloatValue) -> (FloatValue, FloatValue) { + match (&lhs, &rhs) { + (FloatContent::Untyped(l), typed) if !matches!(typed, FloatContent::Untyped(_)) => { + lhs = l.into_kind(typed.kind()); + } + (typed, FloatContent::Untyped(r)) if !matches!(typed, FloatContent::Untyped(_)) => { + rhs = r.into_kind(lhs.kind()); + } + _ => {} // Both same type or both untyped - no conversion needed + } + (lhs, rhs) +} + +impl<'a> ValuesEqual for FloatValueRef<'a> { + /// Handles type coercion between typed and untyped floats. + /// Uses Rust's float `==`, so `NaN != NaN`. + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + // Align types (untyped -> typed conversion) + let lhs = self.clone_to_owned_infallible(); + let rhs = other.clone_to_owned_infallible(); + let (lhs, rhs) = align_types(lhs, rhs); + + // After alignment, compare directly. + // Each variant has two lines: same-type comparison, then type-mismatch fallback. + // This ensures adding a new variant causes a compiler error. + let equal = match (lhs, rhs) { + (FloatContent::Untyped(l), FloatContent::Untyped(r)) => { + l.into_fallback() == r.into_fallback() + } + (FloatContent::Untyped(_), _) => return ctx.leaf_values_not_equal(self, other), + (FloatContent::F32(l), FloatContent::F32(r)) => l == r, + (FloatContent::F32(_), _) => return ctx.leaf_values_not_equal(self, other), + (FloatContent::F64(l), FloatContent::F64(r)) => l == r, + (FloatContent::F64(_), _) => return ctx.leaf_values_not_equal(self, other), + }; + + if equal { + ctx.values_equal() + } else { + ctx.leaf_values_not_equal(self, other) + } + } +} + +define_type_features! { + impl FloatType, + pub(crate) mod float_interface { + pub(crate) mod methods { + fn is_nan(this: FloatValue) -> bool { + match this { + FloatContent::Untyped(x) => x.into_fallback().is_nan(), + FloatContent::F32(x) => x.is_nan(), + FloatContent::F64(x) => x.is_nan(), + } + } + + fn is_infinite(this: FloatValue) -> bool { + match this { + FloatContent::Untyped(x) => x.into_fallback().is_infinite(), + FloatContent::F32(x) => x.is_infinite(), + FloatContent::F64(x) => x.is_infinite(), + } + } + + fn is_finite(this: FloatValue) -> bool { + match this { + FloatContent::Untyped(x) => x.into_fallback().is_finite(), + FloatContent::F32(x) => x.is_finite(), + FloatContent::F64(x) => x.is_finite(), + } + } + + fn is_sign_positive(this: FloatValue) -> bool { + match this { + FloatContent::Untyped(x) => x.into_fallback().is_sign_positive(), + FloatContent::F32(x) => x.is_sign_positive(), + FloatContent::F64(x) => x.is_sign_positive(), + } + } + + fn is_sign_negative(this: FloatValue) -> bool { + match this { + FloatContent::Untyped(x) => x.into_fallback().is_sign_negative(), + FloatContent::F32(x) => x.is_sign_negative(), + FloatContent::F64(x) => x.is_sign_negative(), + } + } + } + pub(crate) mod unary_operations { + } + pub(crate) mod binary_operations { + fn add(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_operation(right, |a, b| a + b), + FloatContent::F32(left) => left.paired_operation_no_overflow(right, |a, b| a + b), + FloatContent::F64(left) => left.paired_operation_no_overflow(right, |a, b| a + b), + } + } + + [context] fn add_assign(left: Assignee, right: Spanned) -> ExecutionResult<()> { + assign_op(left, right, context, add) + } + + fn sub(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_operation(right, |a, b| a - b), + FloatContent::F32(left) => left.paired_operation_no_overflow(right, |a, b| a - b), + FloatContent::F64(left) => left.paired_operation_no_overflow(right, |a, b| a - b), + } + } + + [context] fn sub_assign(left: Assignee, right: Spanned) -> ExecutionResult<()> { + assign_op(left, right, context, sub) + } + + fn mul(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_operation(right, |a, b| a * b), + FloatContent::F32(left) => left.paired_operation_no_overflow(right, |a, b| a * b), + FloatContent::F64(left) => left.paired_operation_no_overflow(right, |a, b| a * b), + } + } + + [context] fn mul_assign(left: Assignee, right: Spanned) -> ExecutionResult<()> { + assign_op(left, right, context, mul) + } + + fn div(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_operation(right, |a, b| a / b), + FloatContent::F32(left) => left.paired_operation_no_overflow(right, |a, b| a / b), + FloatContent::F64(left) => left.paired_operation_no_overflow(right, |a, b| a / b), + } + } + + [context] fn div_assign(left: Assignee, right: Spanned) -> ExecutionResult<()> { + assign_op(left, right, context, div) + } + + fn rem(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_operation(right, |a, b| a % b), + FloatContent::F32(left) => left.paired_operation_no_overflow(right, |a, b| a % b), + FloatContent::F64(left) => left.paired_operation_no_overflow(right, |a, b| a % b), + } + } + + [context] fn rem_assign(left: Assignee, right: Spanned) -> ExecutionResult<()> { + assign_op(left, right, context, rem) + } + + fn lt(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_comparison(right, |a, b| a < b), + FloatContent::F32(left) => left.paired_comparison(right, |a, b| a < b), + FloatContent::F64(left) => left.paired_comparison(right, |a, b| a < b), + } + } + + fn le(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_comparison(right, |a, b| a <= b), + FloatContent::F32(left) => left.paired_comparison(right, |a, b| a <= b), + FloatContent::F64(left) => left.paired_comparison(right, |a, b| a <= b), + } + } + + fn gt(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_comparison(right, |a, b| a > b), + FloatContent::F32(left) => left.paired_comparison(right, |a, b| a > b), + FloatContent::F64(left) => left.paired_comparison(right, |a, b| a > b), + } + } + + fn ge(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_comparison(right, |a, b| a >= b), + FloatContent::F32(left) => left.paired_comparison(right, |a, b| a >= b), + FloatContent::F64(left) => left.paired_comparison(right, |a, b| a >= b), + } + } + + fn eq(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_comparison(right, |a, b| a == b), + FloatContent::F32(left) => left.paired_comparison(right, |a, b| a == b), + FloatContent::F64(left) => left.paired_comparison(right, |a, b| a == b), + } + } + + fn ne(left: FloatValue, right: Spanned) -> ExecutionResult { + match left.resolve_untyped_to_match(right.as_ref_value()) { + FloatContent::Untyped(left) => left.paired_comparison(right, |a, b| a != b), + FloatContent::F32(left) => left.paired_comparison(right, |a, b| a != b), + FloatContent::F64(left) => left.paired_comparison(right, |a, b| a != b), + } + } + } + interface_items { + fn resolve_own_binary_operation( + operation: &BinaryOperation, + ) -> Option { + Some(match operation { + // Arithmetic operations + BinaryOperation::Addition { .. } => binary_definitions::add(), + BinaryOperation::Subtraction { .. } => binary_definitions::sub(), + BinaryOperation::Multiplication { .. } => binary_definitions::mul(), + BinaryOperation::Division { .. } => binary_definitions::div(), + BinaryOperation::Remainder { .. } => binary_definitions::rem(), + // Compound assignment operations + BinaryOperation::AddAssign { .. } => binary_definitions::add_assign(), + BinaryOperation::SubAssign { .. } => binary_definitions::sub_assign(), + BinaryOperation::MulAssign { .. } => binary_definitions::mul_assign(), + BinaryOperation::DivAssign { .. } => binary_definitions::div_assign(), + BinaryOperation::RemAssign { .. } => binary_definitions::rem_assign(), + // Comparison operations + BinaryOperation::LessThan { .. } => binary_definitions::lt(), + BinaryOperation::LessThanOrEqual { .. } => binary_definitions::le(), + BinaryOperation::GreaterThan { .. } => binary_definitions::gt(), + BinaryOperation::GreaterThanOrEqual { .. } => binary_definitions::ge(), + BinaryOperation::Equal { .. } => binary_definitions::eq(), + BinaryOperation::NotEqual { .. } => binary_definitions::ne(), + _ => return None, + }) + } + } + } +} + +impl_resolvable_argument_for! { + FloatType, + (value, context) -> FloatValue { + match value { + AnyValue::Float(value) => Ok(value), + other => context.err("a float", other), + } + } +} diff --git a/src/expressions/values/float_subtypes.rs b/src/expressions/values/float_subtypes.rs new file mode 100644 index 00000000..c3b21c33 --- /dev/null +++ b/src/expressions/values/float_subtypes.rs @@ -0,0 +1,256 @@ +use super::*; + +macro_rules! impl_float_operations { + ( + $($type_data:ident mod $mod_name:ident: $float_enum_variant:ident($float_type:ident)),* $(,)? + ) => {$( + define_type_features! { + impl $type_data, + pub(crate) mod $mod_name { + pub(crate) mod methods { + } + pub(crate) mod unary_operations { + fn neg(input: $float_type) -> $float_type { + -input + } + + fn cast_to_untyped_integer(input: $float_type) -> UntypedInteger { + UntypedInteger::from_fallback(input as FallbackInteger) + } + + fn cast_to_i8(input: $float_type) -> i8 { + input as i8 + } + + fn cast_to_i16(input: $float_type) -> i16 { + input as i16 + } + + fn cast_to_i32(input: $float_type) -> i32 { + input as i32 + } + + fn cast_to_i64(input: $float_type) -> i64 { + input as i64 + } + + fn cast_to_i128(input: $float_type) -> i128 { + input as i128 + } + + fn cast_to_isize(input: $float_type) -> isize { + input as isize + } + + fn cast_to_u8(input: $float_type) -> u8 { + input as u8 + } + + fn cast_to_u16(input: $float_type) -> u16 { + input as u16 + } + + fn cast_to_u32(input: $float_type) -> u32 { + input as u32 + } + + fn cast_to_u64(input: $float_type) -> u64 { + input as u64 + } + + fn cast_to_u128(input: $float_type) -> u128 { + input as u128 + } + + fn cast_to_usize(input: $float_type) -> usize { + input as usize + } + + fn cast_to_untyped_float(input: $float_type) -> UntypedFloat { + UntypedFloat::from_fallback(input as FallbackFloat) + } + + fn cast_to_f32(input: $float_type) -> f32 { + input as f32 + } + + fn cast_to_f64(input: $float_type) -> f64 { + input as f64 + } + + fn cast_to_string(input: $float_type) -> String { + input.to_string() + } + } + pub(crate) mod binary_operations { + } + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Neg { .. } => unary_definitions::neg(), + UnaryOperation::Cast { target: CastTarget(kind), .. } => match kind { + AnyValueLeafKind::Integer(IntegerLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_integer(), + AnyValueLeafKind::Integer(IntegerLeafKind::I8(_)) => unary_definitions::cast_to_i8(), + AnyValueLeafKind::Integer(IntegerLeafKind::I16(_)) => unary_definitions::cast_to_i16(), + AnyValueLeafKind::Integer(IntegerLeafKind::I32(_)) => unary_definitions::cast_to_i32(), + AnyValueLeafKind::Integer(IntegerLeafKind::I64(_)) => unary_definitions::cast_to_i64(), + AnyValueLeafKind::Integer(IntegerLeafKind::I128(_)) => unary_definitions::cast_to_i128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Isize(_)) => unary_definitions::cast_to_isize(), + AnyValueLeafKind::Integer(IntegerLeafKind::U8(_)) => unary_definitions::cast_to_u8(), + AnyValueLeafKind::Integer(IntegerLeafKind::U16(_)) => unary_definitions::cast_to_u16(), + AnyValueLeafKind::Integer(IntegerLeafKind::U32(_)) => unary_definitions::cast_to_u32(), + AnyValueLeafKind::Integer(IntegerLeafKind::U64(_)) => unary_definitions::cast_to_u64(), + AnyValueLeafKind::Integer(IntegerLeafKind::U128(_)) => unary_definitions::cast_to_u128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Usize(_)) => unary_definitions::cast_to_usize(), + AnyValueLeafKind::Float(FloatLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_float(), + AnyValueLeafKind::Float(FloatLeafKind::F32(_)) => unary_definitions::cast_to_f32(), + AnyValueLeafKind::Float(FloatLeafKind::F64(_)) => unary_definitions::cast_to_f64(), + AnyValueLeafKind::String(_) => unary_definitions::cast_to_string(), + _ => return None, + } + _ => return None, + }) + } + + fn resolve_own_binary_operation( + _operation: &BinaryOperation, + ) -> Option { + // All operations are defined on the parent FloatValue type + None + } + + fn resolve_type_property( + property_name: &str, + ) -> Option { + match property_name { + "MAX" => Some($float_type::MAX.into_any_value()), + "MIN" => Some($float_type::MIN.into_any_value()), + "MIN_POSITIVE" => Some($float_type::MIN_POSITIVE.into_any_value()), + "INFINITY" => Some($float_type::INFINITY.into_any_value()), + "NEG_INFINITY" => Some($float_type::NEG_INFINITY.into_any_value()), + "NAN" => Some($float_type::NAN.into_any_value()), + "EPSILON" => Some($float_type::EPSILON.into_any_value()), + _ => None, + } + } + } + } + } + + impl HandleBinaryOperation for $float_type { + fn type_name() -> &'static str { + stringify!($integer_type) + } + } + )*}; +} + +impl_float_operations!(F32Type mod f32_interface: F32(f32), F64Type mod f64_interface: F64(f64)); + +macro_rules! impl_resolvable_float_subtype { + ($type_def:ident, $kind:ident, $type:ty, $variant:ident, $type_name:literal, $articled_display_name:expr) => { + define_leaf_type! { + pub(crate) $type_def => FloatType(FloatContent::$variant) => AnyType, + content: $type, + kind: pub(crate) $kind, + type_name: $type_name, + articled_display_name: $articled_display_name, + dyn_impls: {}, + } + + impl IsArgument for OptionalSuffix<$type> { + type ValueType = FloatType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Owned; + + fn from_argument(argument: Spanned) -> ExecutionResult { + argument.expect_owned().resolve_as("This argument") + } + } + + impl ResolveAs> for Spanned { + fn resolve_as(self, resolution_target: &str) -> ExecutionResult> { + let span = self.span_range(); + let float_value: FloatValue = self.resolve_as(resolution_target)?; + Spanned(float_value, span).resolve_as(resolution_target) + } + } + + impl ResolveAs> for Spanned { + fn resolve_as(self, resolution_target: &str) -> ExecutionResult> { + let Spanned(value, span) = self; + match value { + FloatContent::Untyped(v) => Ok(OptionalSuffix(v.into_fallback() as $type)), + FloatContent::$variant(v) => Ok(OptionalSuffix(v)), + v => span.type_err(format!( + "{} is expected to be {}, but it is {}", + resolution_target, + $kind.articled_display_name(), + v.articled_kind(), + )), + } + } + } + + impl ResolvableArgumentTarget for $type { + type ValueType = $type_def; + } + + impl From<$type> for FloatValue { + fn from(value: $type) -> Self { + FloatContent::$variant(value) + } + } + + impl ResolvableOwned for $type { + fn resolve_from_value( + value: FloatValue, + context: ResolutionContext, + ) -> ExecutionResult { + match value { + FloatContent::Untyped(x) => Ok(x.into_fallback() as $type), + FloatContent::$variant(x) => Ok(x), + other => context.err($articled_display_name, other), + } + } + } + + impl ResolvableOwned for $type { + fn resolve_from_value( + value: AnyValue, + context: ResolutionContext, + ) -> ExecutionResult { + match value { + AnyValue::Float(x) => <$type>::resolve_from_value(x, context), + other => context.err($articled_display_name, other), + } + } + } + + impl ResolvableShared for $type { + fn resolve_from_ref<'a>( + value: &'a AnyValue, + context: ResolutionContext, + ) -> ExecutionResult<&'a Self> { + match value { + AnyValueContent::Float(FloatContent::$variant(x)) => Ok(x), + other => context.err($articled_display_name, other), + } + } + } + + impl ResolvableMutable for $type { + fn resolve_from_mut<'a>( + value: &'a mut AnyValue, + context: ResolutionContext, + ) -> ExecutionResult<&'a mut Self> { + match value { + AnyValueContent::Float(FloatContent::$variant(x)) => Ok(x), + other => context.err($articled_display_name, other), + } + } + } + }; +} + +impl_resolvable_float_subtype!(F32Type, F32Kind, f32, F32, "f32", "an f32"); +impl_resolvable_float_subtype!(F64Type, F64Kind, f64, F64, "f64", "an f64"); diff --git a/src/expressions/values/float_untyped.rs b/src/expressions/values/float_untyped.rs new file mode 100644 index 00000000..360feb4f --- /dev/null +++ b/src/expressions/values/float_untyped.rs @@ -0,0 +1,232 @@ +use super::*; + +define_leaf_type! { + pub(crate) UntypedFloatType => FloatType(FloatContent::Untyped) => AnyType, + content: UntypedFloat, + kind: pub(crate) UntypedFloatKind, + type_name: "untyped_float", + articled_display_name: "an untyped float", + dyn_impls: {}, +} + +#[derive(Copy, Clone)] +pub(crate) struct UntypedFloat(FallbackFloat); +pub(crate) type FallbackFloat = f64; + +impl UntypedFloat { + pub(super) fn new_from_lit_float(lit_float: &LitFloat) -> ParseResult { + Ok(Self(lit_float.base10_digits().parse().map_err(|err| { + lit_float.parse_error(format!( + "Untyped floats in preinterpret must fit inside a {}: {}", + core::any::type_name::(), + err + )) + })?)) + } + + /// Converts an untyped float to a specific float kind. + /// Unlike integers, float conversion never fails (may lose precision). + pub(crate) fn into_kind(self, kind: FloatLeafKind) -> FloatValue { + match kind { + FloatLeafKind::Untyped(_) => FloatContent::Untyped(self), + FloatLeafKind::F32(_) => FloatContent::F32(self.0 as f32), + FloatLeafKind::F64(_) => FloatContent::F64(self.0), + } + } + + pub(crate) fn paired_operation( + self, + rhs: Spanned, + perform_fn: fn(FallbackFloat, FallbackFloat) -> FallbackFloat, + ) -> ExecutionResult { + let lhs = self.0; + let rhs: UntypedFloat = rhs.downcast_resolve("This operand")?; + let rhs = rhs.0; + let output = perform_fn(lhs, rhs); + Ok(FloatContent::Untyped(UntypedFloat::from_fallback(output))) + } + + pub(crate) fn paired_comparison( + self, + rhs: Spanned, + compare_fn: fn(FallbackFloat, FallbackFloat) -> bool, + ) -> ExecutionResult { + let lhs = self.0; + let rhs: UntypedFloat = rhs.downcast_resolve("This operand")?; + let rhs = rhs.0; + Ok(compare_fn(lhs, rhs)) + } + + pub(crate) fn into_fallback(self) -> FallbackFloat { + self.0 + } + + pub(crate) fn from_fallback(value: FallbackFloat) -> Self { + Self(value) + } + + pub(super) fn to_unspanned_literal(self) -> Literal { + Literal::f64_unsuffixed(self.0) + } +} + +define_type_features! { + impl UntypedFloatType, + pub(crate) mod untyped_float_interface { + pub(crate) mod methods { + } + pub(crate) mod unary_operations { + fn neg(input: UntypedFloatFallback) -> UntypedFloat { + UntypedFloat::from_fallback(-input.0) + } + + fn cast_to_untyped_integer(input: UntypedFloatFallback) -> UntypedInteger { + UntypedInteger::from_fallback(input.0 as FallbackInteger) + } + + fn cast_to_i8(input: UntypedFloatFallback) -> i8 { + input.0 as i8 + } + + fn cast_to_i16(input: UntypedFloatFallback) -> i16 { + input.0 as i16 + } + + fn cast_to_i32(input: UntypedFloatFallback) -> i32 { + input.0 as i32 + } + + fn cast_to_i64(input: UntypedFloatFallback) -> i64 { + input.0 as i64 + } + + fn cast_to_i128(input: UntypedFloatFallback) -> i128 { + input.0 as i128 + } + + fn cast_to_isize(input: UntypedFloatFallback) -> isize { + input.0 as isize + } + + fn cast_to_u8(input: UntypedFloatFallback) -> u8 { + input.0 as u8 + } + + fn cast_to_u16(input: UntypedFloatFallback) -> u16 { + input.0 as u16 + } + + fn cast_to_u32(input: UntypedFloatFallback) -> u32 { + input.0 as u32 + } + + fn cast_to_u64(input: UntypedFloatFallback) -> u64 { + input.0 as u64 + } + + fn cast_to_u128(input: UntypedFloatFallback) -> u128 { + input.0 as u128 + } + + fn cast_to_usize(input: UntypedFloatFallback) -> usize { + input.0 as usize + } + + fn cast_to_untyped_float(input: UntypedFloatFallback) -> UntypedFloat { + UntypedFloat::from_fallback(input.0) + } + + fn cast_to_f32(input: UntypedFloatFallback) -> f32 { + input.0 as f32 + } + + fn cast_to_f64(input: UntypedFloatFallback) -> f64 { + input.0 + } + + fn cast_to_string(input: UntypedFloatFallback) -> String { + input.0.to_string() + } + } + pub(crate) mod binary_operations { + } + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Neg { .. } => unary_definitions::neg(), + UnaryOperation::Cast { target: CastTarget(kind), .. } => match kind { + AnyValueLeafKind::Integer(IntegerLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_integer(), + AnyValueLeafKind::Integer(IntegerLeafKind::I8(_)) => unary_definitions::cast_to_i8(), + AnyValueLeafKind::Integer(IntegerLeafKind::I16(_)) => unary_definitions::cast_to_i16(), + AnyValueLeafKind::Integer(IntegerLeafKind::I32(_)) => unary_definitions::cast_to_i32(), + AnyValueLeafKind::Integer(IntegerLeafKind::I64(_)) => unary_definitions::cast_to_i64(), + AnyValueLeafKind::Integer(IntegerLeafKind::I128(_)) => unary_definitions::cast_to_i128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Isize(_)) => unary_definitions::cast_to_isize(), + AnyValueLeafKind::Integer(IntegerLeafKind::U8(_)) => unary_definitions::cast_to_u8(), + AnyValueLeafKind::Integer(IntegerLeafKind::U16(_)) => unary_definitions::cast_to_u16(), + AnyValueLeafKind::Integer(IntegerLeafKind::U32(_)) => unary_definitions::cast_to_u32(), + AnyValueLeafKind::Integer(IntegerLeafKind::U64(_)) => unary_definitions::cast_to_u64(), + AnyValueLeafKind::Integer(IntegerLeafKind::U128(_)) => unary_definitions::cast_to_u128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Usize(_)) => unary_definitions::cast_to_usize(), + AnyValueLeafKind::Float(FloatLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_float(), + AnyValueLeafKind::Float(FloatLeafKind::F32(_)) => unary_definitions::cast_to_f32(), + AnyValueLeafKind::Float(FloatLeafKind::F64(_)) => unary_definitions::cast_to_f64(), + AnyValueLeafKind::String(_) => unary_definitions::cast_to_string(), + _ => return None, + }, + _ => return None, + }) + } + + fn resolve_own_binary_operation( + _operation: &BinaryOperation, + ) -> Option { + // All operations are defined on the parent FloatValue type + None + } + } + } +} + +pub(crate) struct UntypedFloatFallback(pub FallbackFloat); + +impl IsArgument for UntypedFloatFallback { + type ValueType = UntypedFloatType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Owned; + fn from_argument(value: Spanned) -> ExecutionResult { + Self::resolve_value(value.expect_owned(), "This argument") + } +} + +impl ResolvableArgumentTarget for UntypedFloatFallback { + type ValueType = UntypedFloatType; +} + +impl ResolvableOwned for UntypedFloatFallback { + fn resolve_from_value( + input_value: AnyValue, + context: ResolutionContext, + ) -> ExecutionResult { + let value = UntypedFloat::resolve_from_value(input_value, context)?; + Ok(UntypedFloatFallback(value.0)) + } +} + +impl ResolvableOwned for UntypedFloat { + fn resolve_from_value(value: FloatValue, context: ResolutionContext) -> ExecutionResult { + match value { + FloatContent::Untyped(value) => Ok(value), + _ => context.err("an untyped float", value), + } + } +} + +impl_resolvable_argument_for! { + UntypedFloatType, + (value, context) -> UntypedFloat { + match value { + AnyValueContent::Float(FloatContent::Untyped(x)) => Ok(x), + other => context.err("an untyped float", other), + } + } +} diff --git a/src/expressions/values/integer.rs b/src/expressions/values/integer.rs new file mode 100644 index 00000000..532f267b --- /dev/null +++ b/src/expressions/values/integer.rs @@ -0,0 +1,637 @@ +use super::*; + +define_parent_type! { + pub(crate) IntegerType => AnyType(AnyValueContent::Integer), + content: pub(crate) IntegerContent, + leaf_kind: pub(crate) IntegerLeafKind, + type_kind: ParentTypeKind::Integer(pub(crate) IntegerTypeKind), + variants: { + Untyped => UntypedIntegerType, + U8 => U8Type, + U16 => U16Type, + U32 => U32Type, + U64 => U64Type, + U128 => U128Type, + Usize => UsizeType, + I8 => I8Type, + I16 => I16Type, + I32 => I32Type, + I64 => I64Type, + I128 => I128Type, + Isize => IsizeType, + }, + type_name: "int", + articled_display_name: "an integer", +} + +pub(crate) type IntegerValue = IntegerContent<'static, BeOwned>; +pub(crate) type IntegerValueRef<'a> = IntegerContent<'a, BeRef>; + +impl IntegerValue { + pub(super) fn for_litint(lit: &syn::LitInt) -> ParseResult { + Ok(match lit.suffix() { + "" => IntegerContent::Untyped(UntypedInteger::new_from_lit_int(lit)?), + "u8" => IntegerContent::U8(lit.base10_parse()?), + "u16" => IntegerContent::U16(lit.base10_parse()?), + "u32" => IntegerContent::U32(lit.base10_parse()?), + "u64" => IntegerContent::U64(lit.base10_parse()?), + "u128" => IntegerContent::U128(lit.base10_parse()?), + "usize" => IntegerContent::Usize(lit.base10_parse()?), + "i8" => IntegerContent::I8(lit.base10_parse()?), + "i16" => IntegerContent::I16(lit.base10_parse()?), + "i32" => IntegerContent::I32(lit.base10_parse()?), + "i64" => IntegerContent::I64(lit.base10_parse()?), + "i128" => IntegerContent::I128(lit.base10_parse()?), + "isize" => IntegerContent::Isize(lit.base10_parse()?), + suffix => { + return lit.span().parse_err(format!( + "The literal suffix {suffix} is not supported in preinterpret expressions" + )); + } + }) + } + + pub(super) fn to_literal(self, span: Span) -> Literal { + self.to_unspanned_literal().with_span(span) + } + + pub(crate) fn resolve_untyped_to_match_other( + Spanned(value, span): Spanned, + other: &AnyValue, + ) -> ExecutionResult { + match (value, other) { + (IntegerValue::Untyped(this), AnyValue::Integer(other)) => { + this.spanned(span).into_kind(other.kind()) + } + (value, _) => Ok(value), + } + } + + pub(crate) fn resolve_untyped_to_match( + Spanned(value, span): Spanned, + target: &IntegerValue, + ) -> ExecutionResult { + match value { + IntegerValue::Untyped(this) => this.spanned(span).into_kind(target.kind()), + value => Ok(value), + } + } + + pub(crate) fn assign_op( + Spanned(mut left, left_span): Spanned>, + right: R, + context: BinaryOperationCallContext, + op: fn( + BinaryOperationCallContext, + Spanned, + R, + ) -> ExecutionResult, + ) -> ExecutionResult<()> { + let left_value = core::mem::replace(&mut *left, IntegerValue::U32(0)); + let result = op(context, Spanned(left_value, left_span), right)?; + *left = result; + Ok(()) + } + + fn to_unspanned_literal(self) -> Literal { + match self { + IntegerValue::Untyped(int) => int.to_unspanned_literal(), + IntegerValue::U8(int) => Literal::u8_suffixed(int), + IntegerValue::U16(int) => Literal::u16_suffixed(int), + IntegerValue::U32(int) => Literal::u32_suffixed(int), + IntegerValue::U64(int) => Literal::u64_suffixed(int), + IntegerValue::U128(int) => Literal::u128_suffixed(int), + IntegerValue::Usize(int) => Literal::usize_suffixed(int), + IntegerValue::I8(int) => Literal::i8_suffixed(int), + IntegerValue::I16(int) => Literal::i16_suffixed(int), + IntegerValue::I32(int) => Literal::i32_suffixed(int), + IntegerValue::I64(int) => Literal::i64_suffixed(int), + IntegerValue::I128(int) => Literal::i128_suffixed(int), + IntegerValue::Isize(int) => Literal::isize_suffixed(int), + } + } +} + +impl Debug for IntegerValueRef<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Untyped(v) => write!(f, "{}", v.into_fallback()), + Self::U8(v) => write!(f, "{:?}", v), + Self::U16(v) => write!(f, "{:?}", v), + Self::U32(v) => write!(f, "{:?}", v), + Self::U64(v) => write!(f, "{:?}", v), + Self::U128(v) => write!(f, "{:?}", v), + Self::Usize(v) => write!(f, "{:?}", v), + Self::I8(v) => write!(f, "{:?}", v), + Self::I16(v) => write!(f, "{:?}", v), + Self::I32(v) => write!(f, "{:?}", v), + Self::I64(v) => write!(f, "{:?}", v), + Self::I128(v) => write!(f, "{:?}", v), + Self::Isize(v) => write!(f, "{:?}", v), + } + } +} + +/// Aligns types for comparison - converts untyped to match the other's type. +/// Returns `None` if the untyped value doesn't fit in the target type. +fn align_types( + mut lhs: IntegerValue, + mut rhs: IntegerValue, +) -> Option<(IntegerValue, IntegerValue)> { + match (&lhs, &rhs) { + (IntegerValue::Untyped(l), typed) if !matches!(typed, IntegerValue::Untyped(_)) => { + lhs = l.try_into_kind(typed.kind())?; + } + (typed, IntegerValue::Untyped(r)) if !matches!(typed, IntegerValue::Untyped(_)) => { + rhs = r.try_into_kind(lhs.kind())?; + } + _ => {} // Both same type or both untyped - no conversion needed + } + Some((lhs, rhs)) +} + +impl<'a> ValuesEqual for IntegerValueRef<'a> { + /// Handles type coercion between typed and untyped integers. + /// E.g., `5 == 5u32` returns true. + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + // Align types (untyped -> typed conversion) + let lhs = self.clone_to_owned_infallible(); + let rhs = other.clone_to_owned_infallible(); + let Some((lhs, rhs)) = align_types(lhs, rhs) else { + return ctx.leaf_values_not_equal(self, other); + }; + + // After alignment, compare directly. + // Each variant has two lines: same-type comparison, then type-mismatch fallback. + // This ensures adding a new variant causes a compiler error. + let equal = match (lhs, rhs) { + (IntegerValue::Untyped(l), IntegerValue::Untyped(r)) => { + l.into_fallback() == r.into_fallback() + } + (IntegerValue::Untyped(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::U8(l), IntegerValue::U8(r)) => l == r, + (IntegerValue::U8(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::U16(l), IntegerValue::U16(r)) => l == r, + (IntegerValue::U16(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::U32(l), IntegerValue::U32(r)) => l == r, + (IntegerValue::U32(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::U64(l), IntegerValue::U64(r)) => l == r, + (IntegerValue::U64(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::U128(l), IntegerValue::U128(r)) => l == r, + (IntegerValue::U128(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::Usize(l), IntegerValue::Usize(r)) => l == r, + (IntegerValue::Usize(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::I8(l), IntegerValue::I8(r)) => l == r, + (IntegerValue::I8(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::I16(l), IntegerValue::I16(r)) => l == r, + (IntegerValue::I16(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::I32(l), IntegerValue::I32(r)) => l == r, + (IntegerValue::I32(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::I64(l), IntegerValue::I64(r)) => l == r, + (IntegerValue::I64(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::I128(l), IntegerValue::I128(r)) => l == r, + (IntegerValue::I128(_), _) => return ctx.leaf_values_not_equal(self, other), + (IntegerValue::Isize(l), IntegerValue::Isize(r)) => l == r, + (IntegerValue::Isize(_), _) => return ctx.leaf_values_not_equal(self, other), + }; + + if equal { + ctx.values_equal() + } else { + ctx.leaf_values_not_equal(self, other) + } + } +} + +define_type_features! { + impl IntegerType, + pub(crate) mod integer_interface { + pub(crate) mod methods { + } + pub(crate) mod unary_operations { + } + pub(crate) mod binary_operations { + [context] fn add(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_operation(right, context, FallbackInteger::checked_add), + IntegerValue::U8(left) => left.paired_operation(right, context, u8::checked_add), + IntegerValue::U16(left) => left.paired_operation(right, context, u16::checked_add), + IntegerValue::U32(left) => left.paired_operation(right, context, u32::checked_add), + IntegerValue::U64(left) => left.paired_operation(right, context, u64::checked_add), + IntegerValue::U128(left) => left.paired_operation(right, context, u128::checked_add), + IntegerValue::Usize(left) => left.paired_operation(right, context, usize::checked_add), + IntegerValue::I8(left) => left.paired_operation(right, context, i8::checked_add), + IntegerValue::I16(left) => left.paired_operation(right, context, i16::checked_add), + IntegerValue::I32(left) => left.paired_operation(right, context, i32::checked_add), + IntegerValue::I64(left) => left.paired_operation(right, context, i64::checked_add), + IntegerValue::I128(left) => left.paired_operation(right, context, i128::checked_add), + IntegerValue::Isize(left) => left.paired_operation(right, context, isize::checked_add), + } + } + + [context] fn add_assign(lhs: Spanned>, rhs: Spanned) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, add) + } + + [context] fn sub(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_operation(right, context, FallbackInteger::checked_sub), + IntegerValue::U8(left) => left.paired_operation(right, context, u8::checked_sub), + IntegerValue::U16(left) => left.paired_operation(right, context, u16::checked_sub), + IntegerValue::U32(left) => left.paired_operation(right, context, u32::checked_sub), + IntegerValue::U64(left) => left.paired_operation(right, context, u64::checked_sub), + IntegerValue::U128(left) => left.paired_operation(right, context, u128::checked_sub), + IntegerValue::Usize(left) => left.paired_operation(right, context, usize::checked_sub), + IntegerValue::I8(left) => left.paired_operation(right, context, i8::checked_sub), + IntegerValue::I16(left) => left.paired_operation(right, context, i16::checked_sub), + IntegerValue::I32(left) => left.paired_operation(right, context, i32::checked_sub), + IntegerValue::I64(left) => left.paired_operation(right, context, i64::checked_sub), + IntegerValue::I128(left) => left.paired_operation(right, context, i128::checked_sub), + IntegerValue::Isize(left) => left.paired_operation(right, context, isize::checked_sub), + } + } + + [context] fn sub_assign(lhs: Spanned>, rhs: Spanned) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, sub) + } + + [context] fn mul(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_operation(right, context, FallbackInteger::checked_mul), + IntegerValue::U8(left) => left.paired_operation(right, context, u8::checked_mul), + IntegerValue::U16(left) => left.paired_operation(right, context, u16::checked_mul), + IntegerValue::U32(left) => left.paired_operation(right, context, u32::checked_mul), + IntegerValue::U64(left) => left.paired_operation(right, context, u64::checked_mul), + IntegerValue::U128(left) => left.paired_operation(right, context, u128::checked_mul), + IntegerValue::Usize(left) => left.paired_operation(right, context, usize::checked_mul), + IntegerValue::I8(left) => left.paired_operation(right, context, i8::checked_mul), + IntegerValue::I16(left) => left.paired_operation(right, context, i16::checked_mul), + IntegerValue::I32(left) => left.paired_operation(right, context, i32::checked_mul), + IntegerValue::I64(left) => left.paired_operation(right, context, i64::checked_mul), + IntegerValue::I128(left) => left.paired_operation(right, context, i128::checked_mul), + IntegerValue::Isize(left) => left.paired_operation(right, context, isize::checked_mul), + } + } + + [context] fn mul_assign(lhs: Spanned>, rhs: Spanned) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, mul) + } + + [context] fn div(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_operation(right, context, FallbackInteger::checked_div), + IntegerValue::U8(left) => left.paired_operation(right, context, u8::checked_div), + IntegerValue::U16(left) => left.paired_operation(right, context, u16::checked_div), + IntegerValue::U32(left) => left.paired_operation(right, context, u32::checked_div), + IntegerValue::U64(left) => left.paired_operation(right, context, u64::checked_div), + IntegerValue::U128(left) => left.paired_operation(right, context, u128::checked_div), + IntegerValue::Usize(left) => left.paired_operation(right, context, usize::checked_div), + IntegerValue::I8(left) => left.paired_operation(right, context, i8::checked_div), + IntegerValue::I16(left) => left.paired_operation(right, context, i16::checked_div), + IntegerValue::I32(left) => left.paired_operation(right, context, i32::checked_div), + IntegerValue::I64(left) => left.paired_operation(right, context, i64::checked_div), + IntegerValue::I128(left) => left.paired_operation(right, context, i128::checked_div), + IntegerValue::Isize(left) => left.paired_operation(right, context, isize::checked_div), + } + } + + [context] fn div_assign(lhs: Spanned>, rhs: Spanned) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, div) + } + + [context] fn rem(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_operation(right, context, FallbackInteger::checked_rem), + IntegerValue::U8(left) => left.paired_operation(right, context, u8::checked_rem), + IntegerValue::U16(left) => left.paired_operation(right, context, u16::checked_rem), + IntegerValue::U32(left) => left.paired_operation(right, context, u32::checked_rem), + IntegerValue::U64(left) => left.paired_operation(right, context, u64::checked_rem), + IntegerValue::U128(left) => left.paired_operation(right, context, u128::checked_rem), + IntegerValue::Usize(left) => left.paired_operation(right, context, usize::checked_rem), + IntegerValue::I8(left) => left.paired_operation(right, context, i8::checked_rem), + IntegerValue::I16(left) => left.paired_operation(right, context, i16::checked_rem), + IntegerValue::I32(left) => left.paired_operation(right, context, i32::checked_rem), + IntegerValue::I64(left) => left.paired_operation(right, context, i64::checked_rem), + IntegerValue::I128(left) => left.paired_operation(right, context, i128::checked_rem), + IntegerValue::Isize(left) => left.paired_operation(right, context, isize::checked_rem), + } + } + + [context] fn rem_assign(lhs: Spanned>, rhs: Spanned) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, rem) + } + + [context] fn bitxor(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::U8(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::U16(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::U32(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::U64(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::U128(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::Usize(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::I8(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::I16(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::I32(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::I64(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::I128(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + IntegerValue::Isize(left) => left.paired_operation(right, context, |a, b| Some(a ^ b)), + } + } + + [context] fn bitxor_assign(lhs: Spanned>, rhs: Spanned) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, bitxor) + } + + [context] fn bitand(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::U8(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::U16(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::U32(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::U64(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::U128(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::Usize(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::I8(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::I16(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::I32(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::I64(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::I128(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + IntegerValue::Isize(left) => left.paired_operation(right, context, |a, b| Some(a & b)), + } + } + + [context] fn bitand_assign(lhs: Spanned>, rhs: Spanned) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, bitand) + } + + [context] fn bitor(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::U8(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::U16(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::U32(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::U64(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::U128(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::Usize(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::I8(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::I16(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::I32(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::I64(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::I128(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + IntegerValue::Isize(left) => left.paired_operation(right, context, |a, b| Some(a | b)), + } + } + + [context] fn bitor_assign(lhs: Spanned>, rhs: Spanned) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, bitor) + } + + [context] fn shift_left(lhs: Spanned, CoercedToU32(right): CoercedToU32) -> ExecutionResult { + match lhs.0 { + IntegerValue::Untyped(left) => left.shift_operation(right, context, FallbackInteger::checked_shl), + IntegerValue::U8(left) => left.shift_operation(right, context, u8::checked_shl), + IntegerValue::U16(left) => left.shift_operation(right, context, u16::checked_shl), + IntegerValue::U32(left) => left.shift_operation(right, context, u32::checked_shl), + IntegerValue::U64(left) => left.shift_operation(right, context, u64::checked_shl), + IntegerValue::U128(left) => left.shift_operation(right, context, u128::checked_shl), + IntegerValue::Usize(left) => left.shift_operation(right, context, usize::checked_shl), + IntegerValue::I8(left) => left.shift_operation(right, context, i8::checked_shl), + IntegerValue::I16(left) => left.shift_operation(right, context, i16::checked_shl), + IntegerValue::I32(left) => left.shift_operation(right, context, i32::checked_shl), + IntegerValue::I64(left) => left.shift_operation(right, context, i64::checked_shl), + IntegerValue::I128(left) => left.shift_operation(right, context, i128::checked_shl), + IntegerValue::Isize(left) => left.shift_operation(right, context, isize::checked_shl), + } + } + + [context] fn shift_left_assign(lhs: Spanned>, rhs: CoercedToU32) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, shift_left) + } + + [context] fn shift_right(lhs: Spanned, CoercedToU32(right): CoercedToU32) -> ExecutionResult { + match lhs.0 { + IntegerValue::Untyped(left) => left.shift_operation(right, context, FallbackInteger::checked_shr), + IntegerValue::U8(left) => left.shift_operation(right, context, u8::checked_shr), + IntegerValue::U16(left) => left.shift_operation(right, context, u16::checked_shr), + IntegerValue::U32(left) => left.shift_operation(right, context, u32::checked_shr), + IntegerValue::U64(left) => left.shift_operation(right, context, u64::checked_shr), + IntegerValue::U128(left) => left.shift_operation(right, context, u128::checked_shr), + IntegerValue::Usize(left) => left.shift_operation(right, context, usize::checked_shr), + IntegerValue::I8(left) => left.shift_operation(right, context, i8::checked_shr), + IntegerValue::I16(left) => left.shift_operation(right, context, i16::checked_shr), + IntegerValue::I32(left) => left.shift_operation(right, context, i32::checked_shr), + IntegerValue::I64(left) => left.shift_operation(right, context, i64::checked_shr), + IntegerValue::I128(left) => left.shift_operation(right, context, i128::checked_shr), + IntegerValue::Isize(left) => left.shift_operation(right, context, isize::checked_shr), + } + } + + [context] fn shift_right_assign(lhs: Spanned>, rhs: CoercedToU32) -> ExecutionResult<()> { + IntegerValue::assign_op(lhs, rhs, context, shift_right) + } + + fn lt(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::U8(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::U16(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::U32(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::U64(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::U128(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::Usize(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::I8(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::I16(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::I32(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::I64(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::I128(left) => left.paired_comparison(right, |a, b| a < b), + IntegerValue::Isize(left) => left.paired_comparison(right, |a, b| a < b), + } + } + + fn le(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::U8(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::U16(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::U32(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::U64(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::U128(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::Usize(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::I8(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::I16(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::I32(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::I64(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::I128(left) => left.paired_comparison(right, |a, b| a <= b), + IntegerValue::Isize(left) => left.paired_comparison(right, |a, b| a <= b), + } + } + + fn gt(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::U8(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::U16(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::U32(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::U64(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::U128(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::Usize(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::I8(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::I16(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::I32(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::I64(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::I128(left) => left.paired_comparison(right, |a, b| a > b), + IntegerValue::Isize(left) => left.paired_comparison(right, |a, b| a > b), + } + } + + fn ge(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::U8(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::U16(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::U32(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::U64(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::U128(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::Usize(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::I8(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::I16(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::I32(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::I64(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::I128(left) => left.paired_comparison(right, |a, b| a >= b), + IntegerValue::Isize(left) => left.paired_comparison(right, |a, b| a >= b), + } + } + + fn eq(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::U8(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::U16(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::U32(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::U64(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::U128(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::Usize(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::I8(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::I16(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::I32(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::I64(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::I128(left) => left.paired_comparison(right, |a, b| a == b), + IntegerValue::Isize(left) => left.paired_comparison(right, |a, b| a == b), + } + } + + fn ne(left: Spanned, right: Spanned) -> ExecutionResult { + match IntegerValue::resolve_untyped_to_match(left, &right)? { + IntegerValue::Untyped(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::U8(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::U16(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::U32(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::U64(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::U128(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::Usize(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::I8(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::I16(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::I32(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::I64(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::I128(left) => left.paired_comparison(right, |a, b| a != b), + IntegerValue::Isize(left) => left.paired_comparison(right, |a, b| a != b), + } + } + } + interface_items { + fn resolve_own_binary_operation( + operation: &BinaryOperation, + ) -> Option { + Some(match operation { + // Arithmetic operations + BinaryOperation::Addition { .. } => binary_definitions::add(), + BinaryOperation::Subtraction { .. } => binary_definitions::sub(), + BinaryOperation::Multiplication { .. } => binary_definitions::mul(), + BinaryOperation::Division { .. } => binary_definitions::div(), + BinaryOperation::Remainder { .. } => binary_definitions::rem(), + // Bitwise operations + BinaryOperation::BitXor { .. } => binary_definitions::bitxor(), + BinaryOperation::BitAnd { .. } => binary_definitions::bitand(), + BinaryOperation::BitOr { .. } => binary_definitions::bitor(), + BinaryOperation::ShiftLeft { .. } => binary_definitions::shift_left(), + BinaryOperation::ShiftRight { .. } => binary_definitions::shift_right(), + // Compound assignment operations + BinaryOperation::AddAssign { .. } => binary_definitions::add_assign(), + BinaryOperation::SubAssign { .. } => binary_definitions::sub_assign(), + BinaryOperation::MulAssign { .. } => binary_definitions::mul_assign(), + BinaryOperation::DivAssign { .. } => binary_definitions::div_assign(), + BinaryOperation::RemAssign { .. } => binary_definitions::rem_assign(), + BinaryOperation::BitXorAssign { .. } => binary_definitions::bitxor_assign(), + BinaryOperation::BitAndAssign { .. } => binary_definitions::bitand_assign(), + BinaryOperation::BitOrAssign { .. } => binary_definitions::bitor_assign(), + BinaryOperation::ShlAssign { .. } => binary_definitions::shift_left_assign(), + BinaryOperation::ShrAssign { .. } => binary_definitions::shift_right_assign(), + // Comparison operations + BinaryOperation::LessThan { .. } => binary_definitions::lt(), + BinaryOperation::LessThanOrEqual { .. } => binary_definitions::le(), + BinaryOperation::GreaterThan { .. } => binary_definitions::gt(), + BinaryOperation::GreaterThanOrEqual { .. } => binary_definitions::ge(), + BinaryOperation::Equal { .. } => binary_definitions::eq(), + BinaryOperation::NotEqual { .. } => binary_definitions::ne(), + _ => return None, + }) + } + } + } +} + +impl_resolvable_argument_for! { + IntegerType, + (value, context) -> IntegerValue { + match value { + AnyValue::Integer(value) => Ok(value), + other => context.err("an integer", other), + } + } +} + +pub(crate) struct CoercedToU32(pub(crate) u32); + +impl IsArgument for CoercedToU32 { + type ValueType = IntegerType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Owned; + fn from_argument(value: Spanned) -> ExecutionResult { + Self::resolve_value(value.expect_owned(), "This argument") + } +} + +impl ResolvableArgumentTarget for CoercedToU32 { + type ValueType = IntegerType; +} + +impl ResolvableOwned for CoercedToU32 { + fn resolve_from_value( + input_value: AnyValue, + context: ResolutionContext, + ) -> ExecutionResult { + let integer = match input_value { + AnyValue::Integer(value) => value, + other => return context.err("an integer", other), + }; + let coerced = match integer { + IntegerValue::U8(x) => Some(x as u32), + IntegerValue::U16(x) => Some(x as u32), + IntegerValue::U32(x) => Some(x), + IntegerValue::U64(x) => x.try_into().ok(), + IntegerValue::U128(x) => x.try_into().ok(), + IntegerValue::Usize(x) => x.try_into().ok(), + IntegerValue::I8(x) => x.try_into().ok(), + IntegerValue::I16(x) => x.try_into().ok(), + IntegerValue::I32(x) => x.try_into().ok(), + IntegerValue::I64(x) => x.try_into().ok(), + IntegerValue::I128(x) => x.try_into().ok(), + IntegerValue::Isize(x) => x.try_into().ok(), + IntegerValue::Untyped(x) => x.into_fallback().try_into().ok(), + }; + match coerced { + Some(value) => Ok(CoercedToU32(value)), + None => context.err("a u32-compatible integer", AnyValue::Integer(integer)), + } + } +} diff --git a/src/expressions/values/integer_subtypes.rs b/src/expressions/values/integer_subtypes.rs new file mode 100644 index 00000000..ef053838 --- /dev/null +++ b/src/expressions/values/integer_subtypes.rs @@ -0,0 +1,299 @@ +use super::*; + +// We have to use a macro because we don't have checked xx traits :( +macro_rules! impl_int_operations { + ( + $($integer_type_data:ident mod $mod_name:ident: [$(CharCast[$char_cast:ident],)?$(Signed[$signed:ident],)?] $integer_enum_variant:ident($integer_type:ident)),* $(,)? + ) => {$( + define_type_features! { + impl $integer_type_data, + pub(crate) mod $mod_name { + pub(crate) mod methods { + } + pub(crate) mod unary_operations { + $( + fn neg(Spanned(value, span): Spanned<$integer_type>) -> ExecutionResult<$integer_type> { + ignore_all!($signed); // Include only for signed types + match value.checked_neg() { + Some(negated) => Ok(negated), + None => span.value_err("Negating this value would overflow"), + } + } + )? + + $( + fn cast_to_char(input: $integer_type) -> char { + ignore_all!($char_cast); // Include only for types with CharCast + input as char + } + )? + + fn cast_to_untyped_integer(input: $integer_type) -> UntypedInteger { + UntypedInteger::from_fallback(input as FallbackInteger) + } + + fn cast_to_i8(input: $integer_type) -> i8 { + input as i8 + } + + fn cast_to_i16(input: $integer_type) -> i16 { + input as i16 + } + + fn cast_to_i32(input: $integer_type) -> i32 { + input as i32 + } + + fn cast_to_i64(input: $integer_type) -> i64 { + input as i64 + } + + fn cast_to_i128(input: $integer_type) -> i128 { + input as i128 + } + + fn cast_to_isize(input: $integer_type) -> isize { + input as isize + } + + fn cast_to_u8(input: $integer_type) -> u8 { + input as u8 + } + + fn cast_to_u16(input: $integer_type) -> u16 { + input as u16 + } + + fn cast_to_u32(input: $integer_type) -> u32 { + input as u32 + } + + fn cast_to_u64(input: $integer_type) -> u64 { + input as u64 + } + + fn cast_to_u128(input: $integer_type) -> u128 { + input as u128 + } + + fn cast_to_usize(input: $integer_type) -> usize { + input as usize + } + + fn cast_to_untyped_float(input: $integer_type) -> UntypedFloat { + UntypedFloat::from_fallback(input as FallbackFloat) + } + + fn cast_to_f32(input: $integer_type) -> f32 { + input as f32 + } + + fn cast_to_f64(input: $integer_type) -> f64 { + input as f64 + } + + fn cast_to_string(input: $integer_type) -> String { + input.to_string() + } + } + pub(crate) mod binary_operations { + } + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + $( + UnaryOperation::Neg { .. } => { + ignore_all!($signed); // Only include for signed types + unary_definitions::neg() + } + )? + UnaryOperation::Cast { target: CastTarget(kind), .. } => match kind { + $( + AnyValueLeafKind::Char(_) => { + ignore_all!($char_cast); // Only include for types with CharCast + unary_definitions::cast_to_char() + } + )? + AnyValueLeafKind::Integer(IntegerLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_integer(), + AnyValueLeafKind::Integer(IntegerLeafKind::I8(_)) => unary_definitions::cast_to_i8(), + AnyValueLeafKind::Integer(IntegerLeafKind::I16(_)) => unary_definitions::cast_to_i16(), + AnyValueLeafKind::Integer(IntegerLeafKind::I32(_)) => unary_definitions::cast_to_i32(), + AnyValueLeafKind::Integer(IntegerLeafKind::I64(_)) => unary_definitions::cast_to_i64(), + AnyValueLeafKind::Integer(IntegerLeafKind::I128(_)) => unary_definitions::cast_to_i128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Isize(_)) => unary_definitions::cast_to_isize(), + AnyValueLeafKind::Integer(IntegerLeafKind::U8(_)) => unary_definitions::cast_to_u8(), + AnyValueLeafKind::Integer(IntegerLeafKind::U16(_)) => unary_definitions::cast_to_u16(), + AnyValueLeafKind::Integer(IntegerLeafKind::U32(_)) => unary_definitions::cast_to_u32(), + AnyValueLeafKind::Integer(IntegerLeafKind::U64(_)) => unary_definitions::cast_to_u64(), + AnyValueLeafKind::Integer(IntegerLeafKind::U128(_)) => unary_definitions::cast_to_u128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Usize(_)) => unary_definitions::cast_to_usize(), + AnyValueLeafKind::Float(FloatLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_float(), + AnyValueLeafKind::Float(FloatLeafKind::F32(_)) => unary_definitions::cast_to_f32(), + AnyValueLeafKind::Float(FloatLeafKind::F64(_)) => unary_definitions::cast_to_f64(), + AnyValueLeafKind::String(_) => unary_definitions::cast_to_string(), + _ => return None, + } + _ => return None, + }) + } + + fn resolve_own_binary_operation( + _operation: &BinaryOperation, + ) -> Option { + // All operations are defined on the parent IntegerValue type + None + } + + fn resolve_type_property( + property_name: &str, + ) -> Option { + match property_name { + "MAX" => Some($integer_type::MAX.into_any_value()), + "MIN" => Some($integer_type::MIN.into_any_value()), + _ => None, + } + } + } + } + } + + impl HandleBinaryOperation for $integer_type { + fn type_name() -> &'static str { + stringify!($integer_type) + } + } + )*}; +} + +impl_int_operations!( + U8Type mod u8_interface: [CharCast[yes],] U8(u8), + U16Type mod u16_interface: [] U16(u16), + U32Type mod u32_interface: [] U32(u32), + U64Type mod u64_interface: [] U64(u64), + U128Type mod u128_interface: [] U128(u128), + UsizeType mod usize_interface: [] Usize(usize), + I8Type mod i8_interface: [Signed[yes],] I8(i8), + I16Type mod i16_interface: [Signed[yes],] I16(i16), + I32Type mod i32_interface: [Signed[yes],] I32(i32), + I64Type mod i64_interface: [Signed[yes],] I64(i64), + I128Type mod i128_interface: [Signed[yes],] I128(i128), + IsizeType mod isize_interface: [Signed[yes],] Isize(isize), +); + +macro_rules! impl_resolvable_integer_subtype { + ($type_def:ident, $kind:ident, $type:ty, $variant:ident, $type_name:literal, $articled_display_name:expr) => { + define_leaf_type! { + pub(crate) $type_def => IntegerType(IntegerContent::$variant) => AnyType, + content: $type, + kind: pub(crate) $kind, + type_name: $type_name, + articled_display_name: $articled_display_name, + dyn_impls: {}, + } + + impl IsArgument for OptionalSuffix<$type> { + type ValueType = IntegerType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Owned; + + fn from_argument(argument: Spanned) -> ExecutionResult { + argument.expect_owned().resolve_as("This argument") + } + } + + impl ResolveAs> for Spanned { + fn resolve_as(self, resolution_target: &str) -> ExecutionResult> { + let span = self.span_range(); + let integer_value: IntegerValue = self.resolve_as(resolution_target)?; + Spanned(integer_value, span).resolve_as(resolution_target) + } + } + + impl ResolveAs> for Spanned { + fn resolve_as(self, resolution_target: &str) -> ExecutionResult> { + let Spanned(value, span) = self; + match value { + IntegerValue::Untyped(v) => Ok(OptionalSuffix(v.into_fallback() as $type)), + IntegerValue::$variant(v) => Ok(OptionalSuffix(v)), + v => span.type_err(format!( + "{} is expected to be {}, but it is {}", + resolution_target, + $kind.articled_display_name(), + v.articled_kind(), + )), + } + } + } + + impl ResolvableArgumentTarget for $type { + type ValueType = $type_def; + } + + impl From<$type> for IntegerValue { + fn from(value: $type) -> Self { + IntegerValue::$variant(value) + } + } + + impl ResolvableOwned for $type { + fn resolve_from_value( + value: IntegerValue, + context: ResolutionContext, + ) -> ExecutionResult { + match value { + IntegerValue::Untyped(x) => Ok(x.into_fallback() as $type), + IntegerValue::$variant(x) => Ok(x), + other => context.err($articled_display_name, other), + } + } + } + + impl ResolvableOwned for $type { + fn resolve_from_value( + value: AnyValue, + context: ResolutionContext, + ) -> ExecutionResult { + match value { + AnyValue::Integer(x) => <$type>::resolve_from_value(x, context), + other => context.err($articled_display_name, other), + } + } + } + + impl ResolvableShared for $type { + fn resolve_from_ref<'a>( + value: &'a AnyValue, + context: ResolutionContext, + ) -> ExecutionResult<&'a Self> { + match value { + AnyValue::Integer(IntegerValue::$variant(x)) => Ok(x), + other => context.err($articled_display_name, other), + } + } + } + + impl ResolvableMutable for $type { + fn resolve_from_mut<'a>( + value: &'a mut AnyValue, + context: ResolutionContext, + ) -> ExecutionResult<&'a mut Self> { + match value { + AnyValue::Integer(IntegerValue::$variant(x)) => Ok(x), + other => context.err($articled_display_name, other), + } + } + } + }; +} + +impl_resolvable_integer_subtype!(I8Type, I8Kind, i8, I8, "i8", "an i8"); +impl_resolvable_integer_subtype!(I16Type, I16Kind, i16, I16, "i16", "an i16"); +impl_resolvable_integer_subtype!(I32Type, I32Kind, i32, I32, "i32", "an i32"); +impl_resolvable_integer_subtype!(I64Type, I64Kind, i64, I64, "i64", "an i64"); +impl_resolvable_integer_subtype!(I128Type, I128Kind, i128, I128, "i128", "an i128"); +impl_resolvable_integer_subtype!(IsizeType, IsizeKind, isize, Isize, "isize", "an isize"); +impl_resolvable_integer_subtype!(U8Type, U8Kind, u8, U8, "u8", "a u8"); +impl_resolvable_integer_subtype!(U16Type, U16Kind, u16, U16, "u16", "a u16"); +impl_resolvable_integer_subtype!(U32Type, U32Kind, u32, U32, "u32", "a u32"); +impl_resolvable_integer_subtype!(U64Type, U64Kind, u64, U64, "u64", "a u64"); +impl_resolvable_integer_subtype!(U128Type, U128Kind, u128, U128, "u128", "a u128"); +impl_resolvable_integer_subtype!(UsizeType, UsizeKind, usize, Usize, "usize", "a usize"); diff --git a/src/expressions/values/integer_untyped.rs b/src/expressions/values/integer_untyped.rs new file mode 100644 index 00000000..0a38b75a --- /dev/null +++ b/src/expressions/values/integer_untyped.rs @@ -0,0 +1,296 @@ +use super::*; + +define_leaf_type! { + pub(crate) UntypedIntegerType => IntegerType(IntegerContent::Untyped) => AnyType, + content: UntypedInteger, + kind: pub(crate) UntypedIntegerKind, + type_name: "untyped_int", + articled_display_name: "an untyped integer", + dyn_impls: {}, +} + +#[derive(Copy, Clone)] +pub(crate) struct UntypedInteger(FallbackInteger); +pub(crate) type FallbackInteger = i128; + +impl UntypedInteger { + pub(super) fn new_from_lit_int(lit_int: &LitInt) -> ParseResult { + Ok(Self(lit_int.base10_digits().parse().map_err(|err| { + lit_int.parse_error(format!( + "Untyped integers in preinterpret must fit inside a {}: {}", + core::any::type_name::(), + err + )) + })?)) + } + + fn binary_overflow_error( + context: BinaryOperationCallContext, + lhs: impl std::fmt::Display, + rhs: impl std::fmt::Display, + ) -> ExecutionInterrupt { + context.error(format!( + "The untyped integer operation {} {} {} overflowed in {} space", + lhs, + context.operation.symbolic_description(), + rhs, + core::any::type_name::(), + )) + } + + /// Tries to convert to a specific integer kind, returning None if the value doesn't fit. + pub(crate) fn try_into_kind(self, kind: IntegerLeafKind) -> Option { + let value = self.0; + Some(match kind { + IntegerLeafKind::Untyped(_) => IntegerValue::Untyped(UntypedInteger(value)), + IntegerLeafKind::I8(_) => IntegerValue::I8(value.try_into().ok()?), + IntegerLeafKind::I16(_) => IntegerValue::I16(value.try_into().ok()?), + IntegerLeafKind::I32(_) => IntegerValue::I32(value.try_into().ok()?), + IntegerLeafKind::I64(_) => IntegerValue::I64(value.try_into().ok()?), + IntegerLeafKind::I128(_) => IntegerValue::I128(value), + IntegerLeafKind::Isize(_) => IntegerValue::Isize(value.try_into().ok()?), + IntegerLeafKind::U8(_) => IntegerValue::U8(value.try_into().ok()?), + IntegerLeafKind::U16(_) => IntegerValue::U16(value.try_into().ok()?), + IntegerLeafKind::U32(_) => IntegerValue::U32(value.try_into().ok()?), + IntegerLeafKind::U64(_) => IntegerValue::U64(value.try_into().ok()?), + IntegerLeafKind::U128(_) => IntegerValue::U128(value.try_into().ok()?), + IntegerLeafKind::Usize(_) => IntegerValue::Usize(value.try_into().ok()?), + }) + } + + pub(crate) fn paired_operation( + self, + rhs: Spanned, + context: BinaryOperationCallContext, + perform_fn: fn(FallbackInteger, FallbackInteger) -> Option, + ) -> ExecutionResult { + let lhs = self.0; + let rhs: UntypedInteger = rhs.downcast_resolve("This operand")?; + let rhs = rhs.0; + let output = perform_fn(lhs, rhs) + .ok_or_else(|| UntypedInteger::binary_overflow_error(context, lhs, rhs))?; + Ok(IntegerValue::Untyped(UntypedInteger::from_fallback(output))) + } + + pub(crate) fn paired_comparison( + self, + rhs: Spanned, + compare_fn: fn(FallbackInteger, FallbackInteger) -> bool, + ) -> ExecutionResult { + let lhs = self.0; + let rhs: UntypedInteger = rhs.downcast_resolve("This operand")?; + let rhs = rhs.0; + Ok(compare_fn(lhs, rhs)) + } + + pub(crate) fn shift_operation( + self, + rhs: u32, + context: BinaryOperationCallContext, + perform_fn: fn(FallbackInteger, u32) -> Option, + ) -> ExecutionResult { + let lhs = self.0; + let output = perform_fn(lhs, rhs) + .ok_or_else(|| UntypedInteger::binary_overflow_error(context, lhs, rhs))?; + Ok(IntegerValue::Untyped(UntypedInteger::from_fallback(output))) + } + + pub(crate) fn from_fallback(value: FallbackInteger) -> Self { + Self(value) + } + + pub(super) fn into_fallback(self) -> FallbackInteger { + self.0 + } + + pub(super) fn to_unspanned_literal(self) -> Literal { + Literal::i128_unsuffixed(self.0) + } +} + +impl Spanned { + pub(crate) fn into_kind(self, kind: IntegerLeafKind) -> ExecutionResult { + let Spanned(value, span_range) = self; + value.try_into_kind(kind).ok_or_else(|| { + span_range.value_error(format!( + "The integer value {} does not fit into {}", + value.0, + kind.articled_display_name() + )) + }) + } +} + +define_type_features! { + impl UntypedIntegerType, + pub(crate) mod untyped_integer_interface { + pub(crate) mod methods { + } + pub(crate) mod unary_operations { + fn neg(Spanned(value, span): Spanned) -> ExecutionResult { + let input = value.into_fallback(); + match input.checked_neg() { + Some(negated) => Ok(UntypedInteger::from_fallback(negated)), + None => span.value_err("Negating this value would overflow in i128 space"), + } + } + + fn cast_to_untyped_integer(input: UntypedIntegerFallback) -> UntypedInteger { + UntypedInteger::from_fallback(input.0) + } + + fn cast_to_i8(input: UntypedIntegerFallback) -> i8 { + input.0 as i8 + } + + fn cast_to_i16(input: UntypedIntegerFallback) -> i16 { + input.0 as i16 + } + + fn cast_to_i32(input: UntypedIntegerFallback) -> i32 { + input.0 as i32 + } + + fn cast_to_i64(input: UntypedIntegerFallback) -> i64 { + input.0 as i64 + } + + fn cast_to_i128(input: UntypedIntegerFallback) -> i128 { + input.0 + } + + fn cast_to_isize(input: UntypedIntegerFallback) -> isize { + input.0 as isize + } + + fn cast_to_u8(input: UntypedIntegerFallback) -> u8 { + input.0 as u8 + } + + fn cast_to_u16(input: UntypedIntegerFallback) -> u16 { + input.0 as u16 + } + + fn cast_to_u32(input: UntypedIntegerFallback) -> u32 { + input.0 as u32 + } + + fn cast_to_u64(input: UntypedIntegerFallback) -> u64 { + input.0 as u64 + } + + fn cast_to_u128(input: UntypedIntegerFallback) -> u128 { + input.0 as u128 + } + + fn cast_to_usize(input: UntypedIntegerFallback) -> usize { + input.0 as usize + } + + fn cast_to_untyped_float(input: UntypedIntegerFallback) -> UntypedFloat { + UntypedFloat::from_fallback(input.0 as FallbackFloat) + } + + fn cast_to_f32(input: UntypedIntegerFallback) -> f32 { + input.0 as f32 + } + + fn cast_to_f64(input: UntypedIntegerFallback) -> f64 { + input.0 as f64 + } + + fn cast_to_string(input: UntypedIntegerFallback) -> String { + input.0.to_string() + } + } + pub(crate) mod binary_operations { + } + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Neg { .. } => unary_definitions::neg(), + UnaryOperation::Cast { target: CastTarget(kind), .. } => match kind { + AnyValueLeafKind::Integer(IntegerLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_integer(), + AnyValueLeafKind::Integer(IntegerLeafKind::I8(_)) => unary_definitions::cast_to_i8(), + AnyValueLeafKind::Integer(IntegerLeafKind::I16(_)) => unary_definitions::cast_to_i16(), + AnyValueLeafKind::Integer(IntegerLeafKind::I32(_)) => unary_definitions::cast_to_i32(), + AnyValueLeafKind::Integer(IntegerLeafKind::I64(_)) => unary_definitions::cast_to_i64(), + AnyValueLeafKind::Integer(IntegerLeafKind::I128(_)) => unary_definitions::cast_to_i128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Isize(_)) => unary_definitions::cast_to_isize(), + AnyValueLeafKind::Integer(IntegerLeafKind::U8(_)) => unary_definitions::cast_to_u8(), + AnyValueLeafKind::Integer(IntegerLeafKind::U16(_)) => unary_definitions::cast_to_u16(), + AnyValueLeafKind::Integer(IntegerLeafKind::U32(_)) => unary_definitions::cast_to_u32(), + AnyValueLeafKind::Integer(IntegerLeafKind::U64(_)) => unary_definitions::cast_to_u64(), + AnyValueLeafKind::Integer(IntegerLeafKind::U128(_)) => unary_definitions::cast_to_u128(), + AnyValueLeafKind::Integer(IntegerLeafKind::Usize(_)) => unary_definitions::cast_to_usize(), + AnyValueLeafKind::Float(FloatLeafKind::Untyped(_)) => unary_definitions::cast_to_untyped_float(), + AnyValueLeafKind::Float(FloatLeafKind::F32(_)) => unary_definitions::cast_to_f32(), + AnyValueLeafKind::Float(FloatLeafKind::F64(_)) => unary_definitions::cast_to_f64(), + AnyValueLeafKind::String(_) => unary_definitions::cast_to_string(), + _ => return None, + }, + _ => return None, + }) + } + + fn resolve_own_binary_operation( + _operation: &BinaryOperation, + ) -> Option { + // All operations are defined on the parent IntegerValue type + None + } + } + } +} + +pub(crate) struct UntypedIntegerFallback(pub(crate) FallbackInteger); + +impl IsValueContent for UntypedIntegerFallback { + type Type = IntegerType; + type Form = BeOwned; +} + +impl IsArgument for UntypedIntegerFallback { + type ValueType = UntypedIntegerType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Owned; + fn from_argument(value: Spanned) -> ExecutionResult { + Self::resolve_value(value.expect_owned(), "This argument") + } +} + +impl ResolvableArgumentTarget for UntypedIntegerFallback { + type ValueType = UntypedIntegerType; +} + +impl ResolvableOwned for UntypedIntegerFallback { + fn resolve_from_value( + input_value: AnyValue, + context: ResolutionContext, + ) -> ExecutionResult { + let value: UntypedInteger = + ResolvableOwned::::resolve_from_value(input_value, context)?; + Ok(UntypedIntegerFallback(value.into_fallback())) + } +} + +impl ResolvableOwned for UntypedInteger { + fn resolve_from_value( + value: IntegerValue, + context: ResolutionContext, + ) -> ExecutionResult { + match value { + IntegerValue::Untyped(value) => Ok(value), + _ => context.err("an untyped integer", value), + } + } +} + +impl_resolvable_argument_for! { + UntypedIntegerType, + (value, context) -> UntypedInteger { + match value { + AnyValue::Integer(IntegerValue::Untyped(x)) => Ok(x), + _ => context.err("an untyped integer", value), + } + } +} diff --git a/src/expressions/values/iterable.rs b/src/expressions/values/iterable.rs new file mode 100644 index 00000000..5a87f161 --- /dev/null +++ b/src/expressions/values/iterable.rs @@ -0,0 +1,83 @@ +use super::*; + +pub(crate) trait IsIterable: 'static { + fn into_iterator(self: Box) -> ExecutionResult; + fn len(&self, error_span_range: SpanRange) -> ExecutionResult; +} + +define_dyn_type!( + pub(crate) IterableType, + content: dyn IsIterable, + dyn_kind: DynTypeKind::Iterable, + type_name: "iterable", + articled_display_name: "an iterable (e.g. array, list, etc.)", +); + +pub(crate) type IterableValue = Box; +pub(crate) type IterableAnyRef<'a> = AnyRef<'a, dyn IsIterable>; + +define_type_features! { + impl IterableType, + pub(crate) mod iterable_interface { + pub(crate) mod methods { + fn into_iter(this: IterableValue) -> ExecutionResult { + this.into_iterator() + } + + fn len(Spanned(this, span_range): Spanned) -> ExecutionResult { + this.len(span_range) + } + + fn is_empty(Spanned(this, span_range): Spanned) -> ExecutionResult { + Ok(this.len(span_range)? == 0) + } + + [context] fn zip(this: IterableValue) -> ExecutionResult { + let iterator = this.into_iterator()?; + ZipIterators::new_from_iterator(iterator, context.span_range())?.run_zip(context.interpreter, true) + } + + [context] fn zip_truncated(this: IterableValue) -> ExecutionResult { + let iterator = this.into_iterator()?; + ZipIterators::new_from_iterator(iterator, context.span_range())?.run_zip(context.interpreter, false) + } + + fn intersperse(this: IterableValue, separator: AnyValue, settings: Option) -> ExecutionResult { + run_intersperse(this, separator, settings.unwrap_or_default()) + } + + [context] fn to_vec(this: IterableValue) -> ExecutionResult> { + let error_span_range = context.span_range(); + let mut counter = context.interpreter.start_iteration_counter(&error_span_range); + let iterator = this.into_iterator()?; + let max_hint = iterator.size_hint().1; + let mut vec = if let Some(max) = max_hint { + Vec::with_capacity(max) + } else { + Vec::new() + }; + for item in iterator { + counter.increment_and_check()?; + vec.push(item); + } + Ok(vec) + } + } + pub(crate) mod unary_operations { + fn cast_into_iterator(this: IterableValue) -> ExecutionResult { + this.into_iterator() + } + } + pub(crate) mod binary_operations {} + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Cast { target: CastTarget(AnyValueLeafKind::Iterator(_)), .. } =>{ + unary_definitions::cast_into_iterator() + } + _ => return None, + }) + } + } + } +} diff --git a/src/expressions/values/iterator.rs b/src/expressions/values/iterator.rs new file mode 100644 index 00000000..b9031076 --- /dev/null +++ b/src/expressions/values/iterator.rs @@ -0,0 +1,355 @@ +use super::*; + +define_leaf_type! { + pub(crate) IteratorType => AnyType(AnyValueContent::Iterator), + content: IteratorValue, + kind: pub(crate) IteratorKind, + type_name: "iterator", + articled_display_name: "an iterator", + dyn_impls: { + IterableType: impl IsIterable { + fn into_iterator(self: Box) -> ExecutionResult { + Ok(*self) + } + + fn len(&self, error_span_range: SpanRange) -> ExecutionResult { + self.len(error_span_range) + } + } + }, +} + +#[derive(Clone)] +pub(crate) struct IteratorValue { + iterator: IteratorValueInner, +} + +impl IteratorValue { + fn new(iterator: IteratorValueInner) -> Self { + Self { iterator } + } + + #[allow(unused)] + pub(crate) fn new_any(iterator: impl Iterator + 'static + Clone) -> Self { + Self::new_custom(Box::new(iterator)) + } + + pub(crate) fn new_for_array(array: ArrayValue) -> Self { + Self::new_vec(array.items.into_iter()) + } + + pub(crate) fn new_for_stream(stream: OutputStream) -> Self { + Self::new(IteratorValueInner::Stream(Box::new(stream.into_iter()))) + } + + pub(crate) fn new_for_range(range: RangeValue) -> ExecutionResult { + let iterator = range.inner.into_iterable()?.resolve_iterator()?; + Ok(Self::new_custom(iterator)) + } + + pub(crate) fn new_for_object(object: ObjectValue) -> Self { + // We have to collect to vec and back to make it clonable + let iterator = object + .entries + .into_iter() + .map(|(k, v)| vec![k.into_any_value(), v.value].into_any_value()) + .collect::>() + .into_iter(); + Self::new_vec(iterator) + } + + pub(crate) fn new_for_string_over_chars(string: String) -> Self { + // We have to collect to vec and back to make the iterator owned + // That's because value.chars() creates a `Chars<'_>` iterator which + // borrows from the string, which we don't allow in a Boxed iterator + // TODO: Replace with the owned iterator here to avoid this clone: + // https://internals.rust-lang.org/t/is-there-a-good-reason-why-string-has-no-into-chars/19496/5 + let iterator = string + .chars() + .map(|c| c.into_any_value()) + .collect::>() + .into_iter(); + Self::new_vec(iterator) + } + + fn new_vec(iterator: std::vec::IntoIter) -> Self { + Self::new(IteratorValueInner::Vec(Box::new(iterator))) + } + + pub(crate) fn new_custom(iterator: Box>) -> Self { + Self::new(IteratorValueInner::Other(iterator)) + } + + pub(crate) fn len(&self, error_span_range: SpanRange) -> ExecutionResult { + let (min, max) = self.size_hint(); + if max == Some(min) { + Ok(min) + } else { + error_span_range.value_err("Iterator has an inexact length") + } + } + + pub(crate) fn singleton_value(mut self) -> Option { + let first = self.next()?; + if self.next().is_none() { + Some(first) + } else { + None + } + } + + pub(super) fn output_items_to( + self, + output: &mut ToStreamContext, + grouping: Grouping, + ) -> ExecutionResult<()> { + const LIMIT: usize = 10_000; + for (i, item) in self.enumerate() { + if i > LIMIT { + return output.debug_err(format!("Only a maximum of {} items can be output to a stream from an iterator, to protect you from infinite loops. This can't currently be reconfigured with the iteration limit.", LIMIT)); + } + item.as_ref_value().output_to(grouping, output)?; + } + Ok(()) + } + + pub(crate) fn concat_recursive_into( + &self, + output: &mut String, + behaviour: &ConcatBehaviour, + ) -> ExecutionResult<()> { + Self::any_iterator_to_string( + self.clone(), + output, + behaviour, + "[]", + "[ ", + "]", + true, + ) + } + + pub(crate) fn any_iterator_to_string>( + iterator: impl Iterator, + output: &mut String, + behaviour: &ConcatBehaviour, + literal_empty: &str, + literal_start: &str, + literal_end: &str, + possibly_unbounded: bool, + ) -> ExecutionResult<()> { + let mut is_empty = true; + let max = iterator.size_hint().1; + for (i, item) in iterator.enumerate() { + if i == 0 { + if behaviour.output_literal_structure { + output.push_str(literal_start); + } + is_empty = false; + } + if possibly_unbounded && i >= behaviour.iterator_limit { + if behaviour.error_after_iterator_limit { + return behaviour.error_span_range.debug_err(format!("To protect against infinite loops, only a maximum of {} items can be output to a string from an iterator. You can use .to_vec() to avoid this limit. This can't currently be reconfigured with the iteration limit.", behaviour.iterator_limit)); + } else { + if behaviour.output_literal_structure { + match max { + Some(max) => output.push_str(&format!( + ", ..<{} further items>", + max.saturating_sub(i) + )), + None => output.push_str(", .."), + } + } + break; + } + } + let item = item.borrow(); + if i != 0 && behaviour.output_literal_structure { + output.push(','); + } + if i != 0 && behaviour.add_space_between_token_trees { + output.push(' '); + } + item.as_ref_value() + .concat_recursive_into(output, behaviour)?; + } + if behaviour.output_literal_structure { + if is_empty { + output.push_str(literal_empty); + } else { + output.push_str(literal_end); + } + } + Ok(()) + } +} + +impl IsValueContent for IteratorValueInner { + type Type = IteratorType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for IteratorValueInner { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + IteratorValue::new(self) + } +} + +impl IsValueContent for Box> { + type Type = IteratorType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for Box> { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + IteratorValue::new_custom(self) + } +} + +impl_resolvable_argument_for! { + IteratorType, + (value, context) -> IteratorValue { + match value { + AnyValue::Iterator(value) => Ok(value), + _ => context.err("an iterator", value), + } + } +} + +fn definite_size_hint(size_hint: (usize, Option)) -> Option { + let (min, max) = size_hint; + if let Some(max) = max { + if min == max { + Some(min) + } else { + None + } + } else { + None + } +} + +impl ValuesEqual for IteratorValue { + /// Compares two iterators by cloning and comparing element-by-element. + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + let mut lhs_iter = self.clone(); + let mut rhs_iter = other.clone(); + let mut index = 0; + let lhs_size = definite_size_hint(lhs_iter.size_hint()); + let rhs_size = definite_size_hint(rhs_iter.size_hint()); + const MAX_ITERATIONS: usize = 1000; + while index < MAX_ITERATIONS { + match (lhs_iter.next(), rhs_iter.next()) { + (Some(l), Some(r)) => { + let result = ctx.with_iterator_index(index, |ctx| l.test_equality(&r, ctx)); + if ctx.should_short_circuit(&result) { + return result; + } + index += 1; + } + (None, None) => return ctx.values_equal(), + _ => return ctx.lengths_unequal(lhs_size, rhs_size), + } + } + ctx.iteration_limit_exceeded(MAX_ITERATIONS) + } +} + +#[derive(Clone)] +enum IteratorValueInner { + // We Box these so that Value is smaller on the stack + Vec(Box< as IntoIterator>::IntoIter>), + Stream(Box<::IntoIter>), + Other(Box>), +} + +impl Iterator for IteratorValue { + type Item = AnyValue; + + fn next(&mut self) -> Option { + match &mut self.iterator { + IteratorValueInner::Vec(iter) => iter.next(), + IteratorValueInner::Stream(iter) => { + let item = iter.next()?; + let stream: OutputStream = item.into(); + Some(stream.coerce_into_value()) + } + IteratorValueInner::Other(iter) => iter.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.iterator { + IteratorValueInner::Vec(iter) => iter.size_hint(), + IteratorValueInner::Stream(iter) => iter.size_hint(), + IteratorValueInner::Other(iter) => iter.size_hint(), + } + } +} + +impl Iterator for Mutable { + type Item = AnyValue; + + fn next(&mut self) -> Option { + let this: &mut IteratorValue = &mut *self; + this.next() + } + + fn size_hint(&self) -> (usize, Option) { + let this: &IteratorValue = self; + this.size_hint() + } +} + +define_type_features! { + impl IteratorType, + pub(crate) mod iterator_interface { + pub(crate) mod methods { + fn next(mut this: Mutable) -> AnyValue { + match this.next() { + Some(value) => value, + None => ().into_any_value(), + } + } + + fn skip(mut this: IteratorValue, n: OptionalSuffix) -> IteratorValue { + // We make this greedy instead of lazy because the Skip iterator is not clonable. + // We return an iterator for forwards compatibility in case we change it. + for _ in 0..n.0 { + if this.next().is_none() { + break; + } + } + this + } + + fn take(this: IteratorValue, n: OptionalSuffix) -> IteratorValue { + // We collect to a vec to satisfy the clonability requirement, + // but only return an iterator for forwards compatibility in case we change it. + let taken = this.take(n.0).collect::>(); + IteratorValue::new_for_array(ArrayValue::new(taken)) + } + } + pub(crate) mod unary_operations { + [context] fn cast_singleton_to_value(Spanned(this, span): Spanned) -> ExecutionResult { + match this.singleton_value() { + Some(value) => Ok(context.operation.evaluate(Spanned(value, span))?.0), + None => span.value_err("Only an iterator with one item can be cast to this value"), + } + } + } + pub(crate) mod binary_operations {} + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => return None, + UnaryOperation::Cast { target, .. } => if target.is_singleton_target() { + unary_definitions::cast_singleton_to_value() + } else { + return None; + } + }) + } + } + } +} diff --git a/src/expressions/values/mod.rs b/src/expressions/values/mod.rs new file mode 100644 index 00000000..14fb0485 --- /dev/null +++ b/src/expressions/values/mod.rs @@ -0,0 +1,42 @@ +mod any_value; +mod array; +mod boolean; +mod character; +mod float; +mod float_subtypes; +mod float_untyped; +mod integer; +mod integer_subtypes; +mod integer_untyped; +mod iterable; +mod iterator; +mod none; +mod object; +mod parser; +mod range; +mod stream; +mod string; +mod unsupported_literal; + +pub(crate) use any_value::*; +pub(crate) use array::*; +pub(crate) use boolean::*; +pub(crate) use character::*; +pub(crate) use float::*; +pub(crate) use float_subtypes::*; +pub(crate) use float_untyped::*; +pub(crate) use integer::*; +pub(crate) use integer_subtypes::*; +pub(crate) use integer_untyped::*; +pub(crate) use iterable::*; +pub(crate) use iterator::*; +pub(crate) use none::*; +pub(crate) use object::*; +pub(crate) use parser::*; +pub(crate) use range::*; +pub(crate) use stream::*; +pub(crate) use string::*; +pub(crate) use unsupported_literal::*; + +// Marked as use for sub-modules to use with a `use super::*` statement +use super::*; diff --git a/src/expressions/values/none.rs b/src/expressions/values/none.rs new file mode 100644 index 00000000..1984a6b1 --- /dev/null +++ b/src/expressions/values/none.rs @@ -0,0 +1,46 @@ +use super::*; + +define_leaf_type! { + pub(crate) NoneType => AnyType(AnyValueContent::None), + content: (), + kind: pub(crate) NoneKind, + type_name: "none", + // Instead of saying "expected a none value", we can say "expected None" + articled_display_name: "None", + dyn_impls: {}, +} + +impl ResolvableArgumentTarget for () { + type ValueType = NoneType; +} + +impl ResolvableOwned for () { + fn resolve_from_value(value: AnyValue, context: ResolutionContext) -> ExecutionResult { + match value { + AnyValue::None(_) => Ok(()), + other => context.err("None", other), + } + } +} + +define_optional_object! { + pub(crate) struct SettingsInputs { + iteration_limit: usize => (DEFAULT_ITERATION_LIMIT_STR, "The new iteration limit"), + } +} + +define_type_features! { + impl NoneType, + pub(crate) mod none_interface { + pub(crate) mod methods { + [context] fn configure_preinterpret(_none: (), inputs: SettingsInputs) { + if let Some(limit) = inputs.iteration_limit { + context.interpreter.set_iteration_limit(Some(limit)); + } + } + } + pub(crate) mod unary_operations {} + pub(crate) mod binary_operations {} + interface_items {} + } +} diff --git a/src/expressions/values/object.rs b/src/expressions/values/object.rs new file mode 100644 index 00000000..b410cce6 --- /dev/null +++ b/src/expressions/values/object.rs @@ -0,0 +1,405 @@ +use super::*; + +define_leaf_type! { + pub(crate) ObjectType => AnyType(AnyValueContent::Object), + content: ObjectValue, + kind: pub(crate) ObjectKind, + type_name: "object", + articled_display_name: "an object", + dyn_impls: { + IterableType: impl IsIterable { + fn into_iterator(self: Box) -> ExecutionResult { + Ok(IteratorValue::new_for_object(*self)) + } + + fn len(&self, _error_span_range: SpanRange) -> ExecutionResult { + Ok(self.entries.len()) + } + } + }, +} + +#[derive(Clone)] +pub(crate) struct ObjectValue { + pub(crate) entries: BTreeMap, +} + +impl_resolvable_argument_for! { + ObjectType, + (value, context) -> ObjectValue { + match value { + AnyValue::Object(value) => Ok(value), + _ => context.err("an object", value), + } + } +} + +#[derive(Clone)] +pub(crate) struct ObjectEntry { + #[allow(unused)] + pub(crate) key_span: Span, + pub(crate) value: AnyValue, +} + +impl ObjectValue { + pub(super) fn into_indexed(mut self, index: Spanned) -> ExecutionResult { + let key = index.downcast_resolve("An object key")?; + Ok(self.remove_or_none(key)) + } + + pub(super) fn into_property(mut self, access: &PropertyAccess) -> ExecutionResult { + let key = access.property.to_string(); + Ok(self.remove_or_none(&key)) + } + + pub(crate) fn remove_or_none(&mut self, key: &str) -> AnyValue { + match self.entries.remove(key) { + Some(entry) => entry.value, + None => ().into_any_value(), + } + } + + pub(crate) fn remove_no_none(&mut self, key: &str) -> Option { + match self.entries.remove(key) { + Some(entry) => { + if entry.value.is_none() { + None + } else { + Some(entry.value) + } + } + None => None, + } + } + + pub(super) fn index_mut( + &mut self, + index: Spanned, + auto_create: bool, + ) -> ExecutionResult<&mut AnyValue> { + let index: Spanned<&str> = index.downcast_resolve("An object key")?; + self.mut_entry(index.map(|s| s.to_string()), auto_create) + } + + pub(super) fn index_ref(&self, index: Spanned) -> ExecutionResult<&AnyValue> { + let key: Spanned<&str> = index.downcast_resolve("An object key")?; + let entry = self.entries.get(*key).ok_or_else(|| { + key.value_error(format!("The object does not have a field named `{}`", *key)) + })?; + Ok(&entry.value) + } + + pub(super) fn property_mut( + &mut self, + access: &PropertyAccess, + auto_create: bool, + ) -> ExecutionResult<&mut AnyValue> { + self.mut_entry( + access.property.to_string().spanned(access.property.span()), + auto_create, + ) + } + + pub(super) fn property_ref(&self, access: &PropertyAccess) -> ExecutionResult<&AnyValue> { + let key = access.property.to_string(); + let entry = self.entries.get(&key).ok_or_else(|| { + access.value_error(format!("The object does not have a field named `{}`", key)) + })?; + Ok(&entry.value) + } + + fn mut_entry( + &mut self, + Spanned(key, key_span): Spanned, + auto_create: bool, + ) -> ExecutionResult<&mut AnyValue> { + use std::collections::btree_map::*; + Ok(match self.entries.entry(key) { + Entry::Occupied(entry) => &mut entry.into_mut().value, + Entry::Vacant(entry) => { + if auto_create { + &mut entry + .insert(ObjectEntry { + key_span: key_span.join_into_span_else_start(), + value: ().into_any_value(), + }) + .value + } else { + return key_span + .value_err(format!("No property found for key `{}`", entry.into_key())); + } + } + }) + } + + pub(crate) fn concat_recursive_into( + &self, + output: &mut String, + behaviour: &ConcatBehaviour, + ) -> ExecutionResult<()> { + if !behaviour.use_debug_literal_syntax { + return behaviour + .error_span_range + .value_err("An object can't be converted to a non-debug string"); + } + if behaviour.output_literal_structure { + if self.entries.is_empty() { + output.push_str("%{}"); + return Ok(()); + } + output.push_str("%{"); + if behaviour.add_space_between_token_trees { + output.push(' '); + } + } + let mut is_first = true; + for (key, entry) in self.entries.iter() { + if !is_first && behaviour.output_literal_structure { + output.push(','); + } + if !is_first && behaviour.add_space_between_token_trees { + output.push(' '); + } + if syn::parse_str::(key).is_ok() { + output.push_str(key); + } else { + output.push('['); + output.push_str(format!("{:?}", key).as_str()); + output.push(']'); + } + output.push(':'); + if behaviour.add_space_between_token_trees { + output.push(' '); + } + entry + .value + .as_ref_value() + .concat_recursive_into(output, behaviour)?; + is_first = false; + } + if behaviour.output_literal_structure { + if behaviour.add_space_between_token_trees { + output.push(' '); + } + output.push('}'); + } + Ok(()) + } +} + +impl ValuesEqual for ObjectValue { + /// Recursively compares two objects. + /// Objects are equal if they have the same keys and all values are equal. + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + if self.entries.len() != other.entries.len() { + return ctx.lengths_unequal(Some(self.entries.len()), Some(other.entries.len())); + } + for (key, lhs_entry) in self.entries.iter() { + match other.entries.get(key) { + Some(rhs_entry) => { + let result = ctx.with_object_key(key, |ctx| { + lhs_entry.value.test_equality(&rhs_entry.value, ctx) + }); + if ctx.should_short_circuit(&result) { + return result; + } + } + None => return ctx.missing_key(key, MissingSide::Rhs), + } + } + ctx.values_equal() + } +} + +impl Spanned<&ObjectValue> { + pub(crate) fn validate(&self, validation: &impl ObjectValidate) -> ExecutionResult<()> { + let mut missing_fields = Vec::new(); + for (field_name, _) in validation.required_fields() { + match self.entries.get(field_name) { + None + | Some(ObjectEntry { + value: AnyValue::None(_), + .. + }) => { + missing_fields.push(field_name); + } + _ => {} + } + } + let mut unexpected_fields = Vec::new(); + if !validation.allow_other_fields() { + let allowed_fields = validation.all_fields_set(); + for field_name in self.entries.keys() { + if !allowed_fields.contains(field_name) { + unexpected_fields.push(field_name.as_str()); + } + } + } + if missing_fields.is_empty() && unexpected_fields.is_empty() { + return Ok(()); + } + let mut error_message = String::new(); + error_message.push_str("Expected:\n"); + error_message.push_str(&validation.describe_object()); + + if !missing_fields.is_empty() { + error_message.push('\n'); + error_message.push_str("The following required field/s are missing: "); + error_message.push_str(&missing_fields.join(", ")); + } + + if !unexpected_fields.is_empty() { + error_message.push('\n'); + error_message.push_str("The following field/s are unexpected: "); + error_message.push_str(&unexpected_fields.join(", ")); + } + + self.value_err(error_message) + } +} + +impl IsValueContent for BTreeMap { + type Type = ObjectType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for BTreeMap { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + ObjectValue { entries: self } + } +} + +define_type_features! { + impl ObjectType, + pub(crate) mod object_interface { + pub(crate) mod methods { + [context] fn zip(this: ObjectValue) -> ExecutionResult { + ZipIterators::new_from_object(this, context.span_range())?.run_zip(context.interpreter, true) + } + + [context] fn zip_truncated(this: ObjectValue) -> ExecutionResult { + ZipIterators::new_from_object(this, context.span_range())?.run_zip(context.interpreter, false) + } + } + pub(crate) mod unary_operations { + } + pub(crate) mod binary_operations {} + property_access(ObjectValue) { + [ctx] fn shared(source: &'a ObjectValue) { + source.property_ref(ctx.property) + } + [ctx] fn mutable(source: &'a mut ObjectValue, auto_create: bool) { + source.property_mut(ctx.property, auto_create) + } + [ctx] fn owned(source: ObjectValue) { + source.into_property(ctx.property) + } + } + index_access(ObjectValue) { + fn shared(source: &'a ObjectValue, index: Spanned) { + source.index_ref(index) + } + fn mutable(source: &'a mut ObjectValue, index: Spanned, auto_create: bool) { + source.index_mut(index, auto_create) + } + fn owned(source: ObjectValue, index: Spanned) { + source.into_indexed(index) + } + } + interface_items { + } + } +} + +#[allow(unused)] // TODO[unused-clearup] +pub(crate) struct ObjectValidation { + // Should ideally be an indexmap + fields: Vec<(String, FieldDefinition)>, + allow_other_fields: bool, +} + +pub(crate) trait ObjectValidate { + fn all_fields(&self) -> Vec<(&str, &FieldDefinition)>; + fn allow_other_fields(&self) -> bool; + + fn required_fields(&self) -> Vec<(&str, &FieldDefinition)> { + self.all_fields() + .into_iter() + .filter_map(|(key, field_definition)| { + if field_definition.required { + Some((key, field_definition)) + } else { + None + } + }) + .collect() + } + + fn all_fields_set(&self) -> HashSet { + self.all_fields() + .into_iter() + .map(|(key, _)| key.to_string()) + .collect() + } + + fn describe_object(&self) -> String { + use std::fmt::Write; + let mut buffer = String::new(); + buffer.write_str("%{\n").unwrap(); + for (key, definition) in self.all_fields() { + if let Some(description) = &definition.description { + writeln!(buffer, " // {}", description).unwrap(); + } + if definition.required { + writeln!(buffer, " {}: {},", key, definition.example).unwrap(); + } else { + writeln!(buffer, " {}?: {},", key, definition.example).unwrap(); + } + } + buffer.write_str("}").unwrap(); + buffer + } +} + +impl ObjectValidate for ObjectValidation { + fn all_fields(&self) -> Vec<(&str, &FieldDefinition)> { + self.fields + .iter() + .map(|(key, value)| (key.as_str(), value)) + .collect() + } + + fn allow_other_fields(&self) -> bool { + self.allow_other_fields + } +} + +impl ObjectValidate for &[(&'static str, FieldDefinition)] { + fn all_fields(&self) -> Vec<(&str, &FieldDefinition)> { + self.iter().map(|(k, v)| (*k, v)).collect() + } + + fn allow_other_fields(&self) -> bool { + false + } +} + +pub(crate) struct FieldDefinition { + pub(crate) required: bool, + pub(crate) description: Option>, + pub(crate) example: Cow<'static, str>, +} + +impl FieldDefinition { + pub(crate) const fn new_full_static( + required: bool, + description: &'static str, + example: &'static str, + ) -> Self { + Self { + required, + description: Some(Cow::Borrowed(description)), + example: Cow::Borrowed(example), + } + } +} diff --git a/src/expressions/values/parser.rs b/src/expressions/values/parser.rs new file mode 100644 index 00000000..492c6ad5 --- /dev/null +++ b/src/expressions/values/parser.rs @@ -0,0 +1,373 @@ +use super::*; + +define_leaf_type! { + pub(crate) ParserType => AnyType(AnyValueContent::Parser), + content: ParserHandle, + kind: pub(crate) ParserKind, + type_name: "parser", + articled_display_name: "a parser", + dyn_impls: {}, +} + +struct ParserHandleValueWrapper(ParserHandle); + +impl Debug for ParserHandleValueWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Parser[{:?}]", self.0) + } +} + +fn delimiter_from_open_char(c: char) -> Option { + match c { + '(' => Some(Delimiter::Parenthesis), + '{' => Some(Delimiter::Brace), + '[' => Some(Delimiter::Bracket), + _ => None, + } +} + +fn delimiter_from_close_char(c: char) -> Option { + match c { + ')' => Some(Delimiter::Parenthesis), + '}' => Some(Delimiter::Brace), + ']' => Some(Delimiter::Bracket), + _ => None, + } +} + +impl ValuesEqual for ParserHandle { + /// Parsers are equal if they reference the same handle. + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + if self == other { + ctx.values_equal() + } else { + ctx.leaf_values_not_equal( + &ParserHandleValueWrapper(*self), + &ParserHandleValueWrapper(*other), + ) + } + } +} + +impl Spanned> { + pub(crate) fn parser<'i>( + &self, + interpreter: &'i mut Interpreter, + ) -> ExecutionResult> { + let Spanned(handle, span) = self; + interpreter.parser(**handle, *span) + } + + pub(crate) fn parse_with( + &self, + interpreter: &mut Interpreter, + f: impl FnOnce(&mut Interpreter) -> ExecutionResult, + ) -> ExecutionResult { + let Spanned(handle, _) = self; + interpreter.parse_with(**handle, f) + } +} + +fn parser<'a>( + this: Spanned>, + context: &'a mut MethodCallContext, +) -> ExecutionResult> { + this.parser(context.interpreter) +} + +define_type_features! { + impl ParserType, + pub(crate) mod parser_interface { + pub(crate) mod methods { + // GENERAL + // ======= + + [context] fn is_end(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.is_empty()) + } + + // Asserts that the parser has reached the end of input + [context] fn end(this: Spanned>) -> ExecutionResult<()> { + let parser = parser(this, context)?; + match parser.is_empty() { + true => Ok(()), + false => parser.parse_err("unexpected token")?, + } + } + + [context] fn token_tree(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.parse()?) + } + + [context] fn ident(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.parse()?) + } + + [context] fn any_ident(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.parse_any_ident()?) + } + + [context] fn punct(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.parse()?) + } + + [context] fn read(this: Spanned>, parse_template: AnyRef) -> ExecutionResult { + let this = parser(this, context)?; + let mut output = OutputStream::new(); + parse_template.parse_exact_match(this, &mut output)?; + Ok(output) + } + + [context] fn rest(this: Spanned>) -> ExecutionResult { + let input = parser(this, context)?; + let mut output = OutputStream::new(); + ParseUntil::End.handle_parse_into(input, &mut output)?; + Ok(output) + } + + [context] fn until(this: Spanned>, until: OutputStream) -> ExecutionResult { + let input = parser(this, context)?; + let until: ParseUntil = until.parse_as()?; + let mut output = OutputStream::new(); + until.handle_parse_into(input, &mut output)?; + Ok(output) + } + + [context] fn error(this: Spanned>, message: String) -> ExecutionResult<()> { + let parser = parser(this, context)?; + parser.parse_err(message).map_err(|e| e.into()) + } + + // GROUPS + // ====== + + // Opens a group with the specified delimiter character ('(', '{', or '['). + // Must be paired with `close`. + [context] fn open(this: Spanned>, Spanned(delimiter_char, char_span): Spanned) -> ExecutionResult<()> { + let delimiter = delimiter_from_open_char(delimiter_char) + .ok_or_else(|| char_span.value_error(format!( + "Invalid open delimiter '{}'. Expected '(', '{{', or '['", delimiter_char + )))?; + this.parse_with(context.interpreter, |interpreter| { + interpreter.enter_input_group(Some(delimiter))?; + Ok(()) + }) + } + + // Closes the current group. Must be paired with a prior `open`. + // The close character must match: ')' for '(', '}' for '{', ']' for '[' + [context] fn close(this: Spanned>, Spanned(delimiter_char, char_span): Spanned) -> ExecutionResult<()> { + let expected_delimiter = delimiter_from_close_char(delimiter_char) + .ok_or_else(|| char_span.value_error(format!( + "Invalid close delimiter '{}'. Expected ')', '}}', or ']'", delimiter_char + )))?; + this.parse_with(context.interpreter, |interpreter| { + // Check if there's a group to close first + if !interpreter.has_active_input_group() { + return Err(char_span.value_error(format!( + "attempting to close '{}' isn't valid, because there is no open group", + expected_delimiter.description_of_close() + ))); + } + if !interpreter.input().is_empty() { + return interpreter.input().parse_err(format!( + "expected '{}'", expected_delimiter.description_of_close() + ))?; + } + interpreter.exit_input_group(Some(expected_delimiter))?; + Ok(()) + }) + } + + // LITERALS + // ======== + + [context] fn is_literal(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.cursor().literal().is_some()) + } + + [context] fn literal(this: Spanned>) -> ExecutionResult { + let literal = parser(this, context)?.parse()?; + Ok(OutputStream::new_with(|s| s.push_literal(literal))) + } + + [context] fn inferred_literal(this: Spanned>) -> ExecutionResult { + let literal = parser(this, context)?.parse()?; + Ok(AnyValue::for_literal(literal).into_any_value()) + } + + [context] fn is_char(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.peek(syn::LitChar)) + } + + [context] fn char_literal(this: Spanned>) -> ExecutionResult { + let char: syn::LitChar = parser(this, context)?.parse()?; + Ok(OutputStream::new_with(|s| s.push_tokens(char))) + } + + [context] fn char(this: Spanned>) -> ExecutionResult { + let char: syn::LitChar = parser(this, context)?.parse()?; + Ok(char.value()) + } + + [context] fn is_string(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.peek(syn::LitStr)) + } + + [context] fn string_literal(this: Spanned>) -> ExecutionResult { + let string: syn::LitStr = parser(this, context)?.parse()?; + Ok(OutputStream::new_with(|s| s.push_tokens(string))) + } + + [context] fn string(this: Spanned>) -> ExecutionResult { + let string: syn::LitStr = parser(this, context)?.parse()?; + Ok(string.value()) + } + + [context] fn is_integer(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.peek(syn::LitInt)) + } + + [context] fn integer_literal(this: Spanned>) -> ExecutionResult { + let integer: syn::LitInt = parser(this, context)?.parse()?; + Ok(OutputStream::new_with(|s| s.push_tokens(integer))) + } + + [context] fn integer(this: Spanned>) -> ExecutionResult { + let integer: syn::LitInt = parser(this, context)?.parse()?; + Ok(IntegerValue::for_litint(&integer)?) + } + + [context] fn is_float(this: Spanned>) -> ExecutionResult { + Ok(parser(this, context)?.peek(syn::LitFloat)) + } + + [context] fn float_literal(this: Spanned>) -> ExecutionResult { + let float: syn::LitFloat = parser(this, context)?.parse()?; + Ok(OutputStream::new_with(|s| s.push_tokens(float))) + } + + [context] fn float(this: Spanned>) -> ExecutionResult { + let float: syn::LitFloat = parser(this, context)?.parse()?; + Ok(FloatValue::for_litfloat(&float)?) + } + } + pub(crate) mod unary_operations { + } + pub(crate) mod binary_operations {} + interface_items { + } + } +} + +impl_resolvable_argument_for! { + ParserType, + (value, context) -> ParserHandle { + match value { + AnyValue::Parser(value) => Ok(value), + other => context.err("a parser", other), + } + } +} + +impl IsValueContent for TokenTree { + type Type = StreamType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for TokenTree { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + OutputStream::new_with(|s| s.push_raw_token_tree(self)) + } +} + +impl IsValueContent for Ident { + type Type = StreamType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for Ident { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + OutputStream::new_with(|s| s.push_ident(self)) + } +} + +impl IsValueContent for Punct { + type Type = StreamType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for Punct { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + OutputStream::new_with(|s| s.push_punct(self)) + } +} + +impl IsValueContent for Literal { + type Type = StreamType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for Literal { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + OutputStream::new_with(|s| s.push_literal(self)) + } +} + +/// Note: This is very similar to a [`ParseTemplatePattern`], but there, the ident is a *definition*, +/// and used to capture the consumed stream into a variable. Here, the ident is a reference +/// to an existing variable, whose value is expected to be a parser. +pub(crate) struct ParseTemplateLiteral { + prefix: Token![@], + parser_reference: VariableReference, + brackets: Brackets, + content: ParseTemplateStream, +} + +impl ParseSource for ParseTemplateLiteral { + fn parse(input: SourceParser) -> ParseResult { + let prefix = input.parse()?; + let parser_reference = input.parse()?; + let (brackets, inner) = input.parse_brackets()?; + let content = ParseTemplateStream::parse_with_span(&inner, brackets.span())?; + Ok(Self { + prefix, + parser_reference, + brackets, + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.parser_reference.control_flow_pass(context)?; + self.content.control_flow_pass(context) + } +} + +impl Evaluate for ParseTemplateLiteral { + fn evaluate( + &self, + interpreter: &mut Interpreter, + ownership: RequestedOwnership, + ) -> ExecutionResult { + let parser: Shared = Spanned( + self.parser_reference.resolve_shared(interpreter)?, + self.parser_reference.span_range(), + ) + .resolve_as("The value bound by a consume literal")?; + + let parser = parser.spanned(self.parser_reference.span_range()); + + parser.parse_with(interpreter, |interpreter| self.content.consume(interpreter))?; + + ownership + .map_from_owned(Spanned(().into_any_value(), self.span_range())) + .map(|spanned| spanned.0) + } +} + +impl HasSpanRange for ParseTemplateLiteral { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.prefix.span, self.brackets.end_span()) + } +} diff --git a/src/expressions/values/range.rs b/src/expressions/values/range.rs new file mode 100644 index 00000000..d20da791 --- /dev/null +++ b/src/expressions/values/range.rs @@ -0,0 +1,560 @@ +use syn::RangeLimits; + +use super::*; + +define_leaf_type! { + pub(crate) RangeType => AnyType(AnyValueContent::Range), + content: RangeValue, + kind: pub(crate) RangeKind, + type_name: "range", + articled_display_name: "a range", + dyn_impls: { + IterableType: impl IsIterable { + fn into_iterator(self: Box) -> ExecutionResult { + IteratorValue::new_for_range(*self) + } + + fn len(&self, error_span_range: SpanRange) -> ExecutionResult { + self.len(error_span_range) + } + } + }, +} + +#[derive(Clone)] +pub(crate) struct RangeValue { + pub(crate) inner: Box, +} + +impl RangeValue { + pub(crate) fn len(&self, error_span_range: SpanRange) -> ExecutionResult { + IteratorValue::new_for_range(self.clone())?.len(error_span_range) + } + + pub(crate) fn concat_recursive_into( + &self, + output: &mut String, + behaviour: &ConcatBehaviour, + ) -> ExecutionResult<()> { + if !behaviour.use_debug_literal_syntax { + return IteratorValue::any_iterator_to_string( + self.clone().inner.into_iterable()?.resolve_iterator()?, + output, + behaviour, + "[]", + "[ ", + "]", + true, + ); + } + match &*self.inner { + RangeValueInner::Range { + start_inclusive, + end_exclusive, + .. + } => { + start_inclusive + .as_ref_value() + .concat_recursive_into(output, behaviour)?; + output.push_str(".."); + end_exclusive + .as_ref_value() + .concat_recursive_into(output, behaviour)?; + } + RangeValueInner::RangeFrom { + start_inclusive, .. + } => { + start_inclusive + .as_ref_value() + .concat_recursive_into(output, behaviour)?; + output.push_str(".."); + } + RangeValueInner::RangeTo { end_exclusive, .. } => { + output.push_str(".."); + end_exclusive + .as_ref_value() + .concat_recursive_into(output, behaviour)?; + } + RangeValueInner::RangeFull { .. } => { + output.push_str(".."); + } + RangeValueInner::RangeInclusive { + start_inclusive, + end_inclusive, + .. + } => { + start_inclusive + .as_ref_value() + .concat_recursive_into(output, behaviour)?; + output.push_str("..="); + end_inclusive + .as_ref_value() + .concat_recursive_into(output, behaviour)?; + } + RangeValueInner::RangeToInclusive { end_inclusive, .. } => { + output.push_str("..="); + end_inclusive + .as_ref_value() + .concat_recursive_into(output, behaviour)?; + } + } + Ok(()) + } +} + +impl Spanned<&RangeValue> { + pub(crate) fn resolve_to_index_range( + self, + array: &ArrayValue, + ) -> ExecutionResult> { + let Spanned(value, span_range) = self; + let mut start = 0; + let mut end = array.items.len(); + Ok(match &*value.inner { + RangeValueInner::Range { + start_inclusive, + end_exclusive, + .. + } => { + start = array.resolve_valid_index( + Spanned(start_inclusive.as_ref_value(), span_range), + false, + )?; + end = array + .resolve_valid_index(Spanned(end_exclusive.as_ref_value(), span_range), true)?; + start..end + } + RangeValueInner::RangeFrom { + start_inclusive, .. + } => { + start = array.resolve_valid_index( + Spanned(start_inclusive.as_ref_value(), span_range), + false, + )?; + start..array.items.len() + } + RangeValueInner::RangeTo { end_exclusive, .. } => { + end = array + .resolve_valid_index(Spanned(end_exclusive.as_ref_value(), span_range), true)?; + start..end + } + RangeValueInner::RangeFull { .. } => start..end, + RangeValueInner::RangeInclusive { + start_inclusive, + end_inclusive, + .. + } => { + start = array.resolve_valid_index( + Spanned(start_inclusive.as_ref_value(), span_range), + false, + )?; + // +1 is safe because it must be < array length. + end = array.resolve_valid_index( + Spanned(end_inclusive.as_ref_value(), span_range), + false, + )? + 1; + start..end + } + RangeValueInner::RangeToInclusive { end_inclusive, .. } => { + // +1 is safe because it must be < array length. + end = array.resolve_valid_index( + Spanned(end_inclusive.as_ref_value(), span_range), + false, + )? + 1; + start..end + } + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RangeStructure { + /// `start .. end` + FromTo, + /// `start ..` + From, + /// `.. end` + To, + /// `..` + Full, + /// `start ..= end` + FromToInclusive, + /// `..= end` + ToInclusive, +} + +impl RangeStructure { + pub(crate) fn articled_display_name(&self) -> &'static str { + match self { + RangeStructure::FromTo => "a range start..end", + RangeStructure::From => "a range start..", + RangeStructure::To => "a range ..end", + RangeStructure::Full => "a range ..", + RangeStructure::FromToInclusive => "a range start..=end", + RangeStructure::ToInclusive => "a range ..=end", + } + } +} + +impl ValuesEqual for RangeValue { + /// Ranges are equal if they have the same kind and the same bounds. + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + match (&*self.inner, &*other.inner) { + ( + RangeValueInner::Range { + start_inclusive: l_start, + end_exclusive: l_end, + .. + }, + RangeValueInner::Range { + start_inclusive: r_start, + end_exclusive: r_end, + .. + }, + ) => { + let result = ctx.with_range_start(|ctx| l_start.test_equality(r_start, ctx)); + if ctx.should_short_circuit(&result) { + return result; + } + ctx.with_range_end(|ctx| l_end.test_equality(r_end, ctx)) + } + ( + RangeValueInner::RangeFrom { + start_inclusive: l_start, + .. + }, + RangeValueInner::RangeFrom { + start_inclusive: r_start, + .. + }, + ) => ctx.with_range_start(|ctx| l_start.test_equality(r_start, ctx)), + ( + RangeValueInner::RangeTo { + end_exclusive: l_end, + .. + }, + RangeValueInner::RangeTo { + end_exclusive: r_end, + .. + }, + ) => ctx.with_range_end(|ctx| l_end.test_equality(r_end, ctx)), + (RangeValueInner::RangeFull { .. }, RangeValueInner::RangeFull { .. }) => { + ctx.values_equal() + } + ( + RangeValueInner::RangeInclusive { + start_inclusive: l_start, + end_inclusive: l_end, + .. + }, + RangeValueInner::RangeInclusive { + start_inclusive: r_start, + end_inclusive: r_end, + .. + }, + ) => { + let result = ctx.with_range_start(|ctx| l_start.test_equality(r_start, ctx)); + if ctx.should_short_circuit(&result) { + return result; + } + ctx.with_range_end(|ctx| l_end.test_equality(r_end, ctx)) + } + ( + RangeValueInner::RangeToInclusive { + end_inclusive: l_end, + .. + }, + RangeValueInner::RangeToInclusive { + end_inclusive: r_end, + .. + }, + ) => ctx.with_range_end(|ctx| l_end.test_equality(r_end, ctx)), + _ => ctx.range_structure_mismatch( + self.inner.structure_kind(), + other.inner.structure_kind(), + ), + } + } +} + +/// A representation of the Rust [range expression]. +/// +/// [range expression]: https://doc.rust-lang.org/reference/expressions/range-expr.html +#[derive(Clone)] +pub(crate) enum RangeValueInner { + /// `start .. end` + Range { + start_inclusive: AnyValue, + token: Token![..], + end_exclusive: AnyValue, + }, + /// `start ..` + RangeFrom { + start_inclusive: AnyValue, + token: Token![..], + }, + /// `.. end` + RangeTo { + token: Token![..], + end_exclusive: AnyValue, + }, + /// `..` (used inside arrays) + RangeFull { token: Token![..] }, + /// `start ..= end` + RangeInclusive { + start_inclusive: AnyValue, + token: Token![..=], + end_inclusive: AnyValue, + }, + /// `..= end` + RangeToInclusive { + token: Token![..=], + end_inclusive: AnyValue, + }, +} + +impl RangeValueInner { + fn structure_kind(&self) -> RangeStructure { + match self { + Self::Range { .. } => RangeStructure::FromTo, + Self::RangeFrom { .. } => RangeStructure::From, + Self::RangeTo { .. } => RangeStructure::To, + Self::RangeFull { .. } => RangeStructure::Full, + Self::RangeInclusive { .. } => RangeStructure::FromToInclusive, + Self::RangeToInclusive { .. } => RangeStructure::ToInclusive, + } + } + + pub(super) fn into_iterable(self) -> ExecutionResult> { + Ok(match self { + Self::Range { + start_inclusive, + token, + end_exclusive, + } => IterableRangeOf::RangeFromTo { + start: start_inclusive, + dots: syn::RangeLimits::HalfOpen(token), + end: end_exclusive, + }, + Self::RangeInclusive { + start_inclusive, + token, + end_inclusive, + } => IterableRangeOf::RangeFromTo { + start: start_inclusive, + dots: syn::RangeLimits::Closed(token), + end: end_inclusive, + }, + Self::RangeFrom { + start_inclusive, + token, + } => IterableRangeOf::RangeFrom { + start: start_inclusive, + dots: token, + }, + other => { + return other + .operator_span_range() + .value_err("This range has no start so is not iterable") + } + }) + } + + fn operator_span_range(&self) -> SpanRange { + match self { + Self::Range { token, .. } => token.span_range(), + Self::RangeFrom { token, .. } => token.span_range(), + Self::RangeTo { token, .. } => token.span_range(), + Self::RangeFull { token } => token.span_range(), + Self::RangeInclusive { token, .. } => token.span_range(), + Self::RangeToInclusive { token, .. } => token.span_range(), + } + } +} + +impl IsValueContent for RangeValueInner { + type Type = RangeType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for RangeValueInner { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + RangeValue { + inner: Box::new(self), + } + } +} + +impl_resolvable_argument_for! { + RangeType, + (value, context) -> RangeValue { + match value { + AnyValue::Range(value) => Ok(value), + _ => context.err("a range", value), + } + } +} + +define_type_features! { + impl RangeType, + pub(crate) mod range_interface { + pub(crate) mod methods { + } + pub(crate) mod unary_operations { + [context] fn cast_via_iterator(Spanned(this, span): Spanned) -> ExecutionResult { + let this_iterator = IteratorValue::new_for_range(this)?; + Ok(context.operation.evaluate(Spanned(this_iterator, span))?.0) + } + } + pub(crate) mod binary_operations {} + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Cast { .. } + if IteratorType::resolve_own_unary_operation(operation).is_some() => + { + unary_definitions::cast_via_iterator() + } + _ => return None, + }) + } + } + } +} + +pub(super) enum IterableRangeOf { + // start <= x < end OR start <= x <= end + RangeFromTo { + start: T, + dots: syn::RangeLimits, + end: T, + }, + // start <= x + RangeFrom { + dots: Token![..], + start: T, + }, +} + +fn resolve_range + ResolvableRange>( + start: T, + dots: syn::RangeLimits, + end: Option>, +) -> ExecutionResult>> { + let definition = match (end, dots) { + (Some(end), dots) => { + let end = end.resolve_as("The end of this range bound")?; + IterableRangeOf::RangeFromTo { start, dots, end } + } + (None, RangeLimits::HalfOpen(dots)) => IterableRangeOf::RangeFrom { start, dots }, + (None, RangeLimits::Closed(_)) => { + return dots.value_err("The range '..=' requires an end value") + } + }; + T::resolve(definition) +} + +trait ResolvableRange: Sized { + fn resolve( + definition: IterableRangeOf, + ) -> ExecutionResult>>; +} + +impl IterableRangeOf { + pub(super) fn resolve_iterator( + self, + ) -> ExecutionResult>> { + let (start, dots, end) = match self { + Self::RangeFromTo { start, dots, end } => { + (start, dots, Some(end.spanned(dots.span_range()))) + } + Self::RangeFrom { start, dots } => (start, RangeLimits::HalfOpen(dots), None), + }; + match start { + AnyValue::Integer(mut start) => { + if let Some(end) = &end { + start = IntegerValue::resolve_untyped_to_match_other( + start.spanned(dots.span_range()), + end, + )?; + } + match start { + IntegerValue::Untyped(start) => resolve_range(start, dots, end), + IntegerValue::U8(start) => resolve_range(start, dots, end), + IntegerValue::U16(start) => resolve_range(start, dots, end), + IntegerValue::U32(start) => resolve_range(start, dots, end), + IntegerValue::U64(start) => resolve_range(start, dots, end), + IntegerValue::U128(start) => resolve_range(start, dots, end), + IntegerValue::Usize(start) => resolve_range(start, dots, end), + IntegerValue::I8(start) => resolve_range(start, dots, end), + IntegerValue::I16(start) => resolve_range(start, dots, end), + IntegerValue::I32(start) => resolve_range(start, dots, end), + IntegerValue::I64(start) => resolve_range(start, dots, end), + IntegerValue::I128(start) => resolve_range(start, dots, end), + IntegerValue::Isize(start) => resolve_range(start, dots, end), + } + } + AnyValue::Char(start) => resolve_range(start, dots, end), + _ => dots.value_err("The range must be between two integers or two characters"), + } + } +} + +impl ResolvableRange for UntypedInteger { + fn resolve( + definition: IterableRangeOf, + ) -> ExecutionResult>> { + match definition { + IterableRangeOf::RangeFromTo { start, dots, end } => { + let start = start.into_fallback(); + let end = end.into_fallback(); + Ok(match dots { + syn::RangeLimits::HalfOpen { .. } => Box::new( + (start..end) + .map(move |x| UntypedInteger::from_fallback(x).into_any_value()), + ), + syn::RangeLimits::Closed { .. } => Box::new( + (start..=end) + .map(move |x| UntypedInteger::from_fallback(x).into_any_value()), + ), + }) + } + IterableRangeOf::RangeFrom { start, .. } => { + let start = start.into_fallback(); + Ok(Box::new((start..).map(move |x| { + UntypedInteger::from_fallback(x).into_any_value() + }))) + } + } + } +} + +macro_rules! define_range_resolvers { + ( + $($the_type:ident),* $(,)? + ) => {$( + impl ResolvableRange for $the_type { + fn resolve(definition: IterableRangeOf) -> ExecutionResult>> { + match definition { + IterableRangeOf::RangeFromTo { start, dots, end } => { + Ok(match dots { + syn::RangeLimits::HalfOpen { .. } => { + Box::new((start..end).map(move |x| x.into_any_value())) + } + syn::RangeLimits::Closed { .. } => { + Box::new((start..=end).map(move |x| x.into_any_value())) + } + }) + }, + IterableRangeOf::RangeFrom { start, .. } => { + Ok(Box::new((start..).map(move |x| x.into_any_value()))) + }, + } + } + } + )*}; +} + +define_range_resolvers! { + u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, char, +} diff --git a/src/expressions/values/stream.rs b/src/expressions/values/stream.rs new file mode 100644 index 00000000..fc856de3 --- /dev/null +++ b/src/expressions/values/stream.rs @@ -0,0 +1,562 @@ +use super::*; + +define_leaf_type! { + pub(crate) StreamType => AnyType(AnyValueContent::Stream), + content: OutputStream, + kind: pub(crate) StreamKind, + type_name: "stream", + articled_display_name: "a stream", + dyn_impls: { + IterableType: impl IsIterable { + fn into_iterator(self: Box) -> ExecutionResult { + Ok(IteratorValue::new_for_stream(*self)) + } + + fn len(&self, _error_span_range: SpanRange) -> ExecutionResult { + Ok(self.len()) + } + } + }, +} + +impl OutputStream { + pub(crate) fn concat_as_literal_into(&self, output: &mut String, behaviour: &ConcatBehaviour) { + if behaviour.use_stream_literal_syntax { + if self.is_empty() { + output.push_str("%[]"); + } else { + output.push_str("%["); + self.concat_content_into(output, behaviour); + output.push(']'); + } + } else { + self.concat_content_into(output, behaviour); + } + } + + pub(crate) fn resolve_content_span_range(&self) -> Option { + // Consider the case where preinterpret embeds in a declarative macro, and we have + // an error like this: + // %[$input].error("Expected 100, got " + %[$input].to_debug_string()) + // + // In cases like this, rustc wraps $input in a transparent group, which means that + // the span of that group is the span of the tokens "$input" in the definition of the + // declarative macro. This is not what we want. We want the span of the tokens which + // were fed into $input in the declarative macro. + // + // The simplest solution here is to get rid of all transparent groups, to get back to the + // source spans. + // + // Once this workstream with macro diagnostics is stabilised: + // https://github.com/rust-lang/rust/issues/54140#issuecomment-802701867 + // + // Then we can revisit this and do something better, and include all spans as separate spans + // in the error message, which will allow a user to trace an error through N different layers + // of macros. + // + // (Possibly we can try to join spans together, and if they don't join, they become separate + // spans which get printed to the error message). + // + // Coincidentally, rust analyzer currently does not properly support + // transparent groups (as of Jan 2025), so gets it right without this flattening: + // https://github.com/rust-lang/rust-analyzer/issues/18211 + + let error_span_stream = self.to_token_stream_removing_any_transparent_groups(); + if error_span_stream.is_empty() { + None + } else { + Some(error_span_stream.span_range_from_iterating_over_all_tokens()) + } + } +} + +impl Debug for OutputStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug_string = String::new(); + self.concat_as_literal_into( + &mut debug_string, + &ConcatBehaviour::debug(Span::call_site().span_range()), + ); + write!(f, "{}", debug_string) + } +} + +impl ValuesEqual for OutputStream { + /// Compares two streams by their debug string representation, ignoring spans. + /// Transparent groups (none-delimited groups) are preserved in comparison. + /// Use `remove_transparent_groups()` before comparison if you want to ignore them. + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + // Use debug concat_recursive which preserves transparent group structure + let lhs = self.concat_content(&ConcatBehaviour::debug(Span::call_site().span_range())); + let rhs = other.concat_content(&ConcatBehaviour::debug(Span::call_site().span_range())); + if lhs == rhs { + ctx.values_equal() + } else { + ctx.leaf_values_not_equal(self, other) + } + } +} + +impl IsValueContent for TokenStream { + type Type = StreamType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for TokenStream { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + OutputStream::raw(self) + } +} + +impl_resolvable_argument_for! { + StreamType, + (value, context) -> OutputStream { + match value { + AnyValueContent::Stream(value) => Ok(value), + _ => context.err("a stream", value), + } + } +} + +define_type_features! { + impl StreamType, + pub(crate) mod stream_interface { + pub(crate) mod methods { + // This is also on iterable, but is specialized here for performance + fn len(this: AnyRef) -> usize { + this.len() + } + + // This is also on iterable, but is specialized here for performance + fn is_empty(this: AnyRef) -> bool { + this.is_empty() + } + + fn flatten(this: OutputStream) -> ExecutionResult { + Ok(this.to_token_stream_removing_any_transparent_groups()) + } + + // Removes transparent (none-delimited) groups from the stream. + // Useful before equality comparison if you want to ignore them. + fn remove_transparent_groups(this: OutputStream) -> OutputStream { + OutputStream::raw(this.to_token_stream_removing_any_transparent_groups()) + } + + fn infer(this: OutputStream) -> ExecutionResult { + Ok(this.coerce_into_value()) + } + + fn split(this: OutputStream, separator: AnyRef, settings: Option) -> ExecutionResult { + handle_split(this, &separator, settings.unwrap_or_default()) + } + + // STRING-BASED CONVERSION METHODS + // =============================== + + [context] fn to_ident(this: Spanned>) -> ExecutionResult { + let string = this.concat_content(&ConcatBehaviour::standard(this.span_range())); + string_interface::methods::to_ident(context, string.into_spanned_ref(this.span_range())) + } + + [context] fn to_ident_camel(this: Spanned>) -> ExecutionResult { + let string = this.concat_content(&ConcatBehaviour::standard(this.span_range())); + string_interface::methods::to_ident_camel(context, string.into_spanned_ref(this.span_range())) + } + + [context] fn to_ident_snake(this: Spanned>) -> ExecutionResult { + let string = this.concat_content(&ConcatBehaviour::standard(this.span_range())); + string_interface::methods::to_ident_snake(context, string.into_spanned_ref(this.span_range())) + } + + [context] fn to_ident_upper_snake(this: Spanned>) -> ExecutionResult { + let string = this.concat_content(&ConcatBehaviour::standard(this.span_range())); + string_interface::methods::to_ident_upper_snake(context, string.into_spanned_ref(this.span_range())) + } + + // Some literals become Value::UnsupportedLiteral but can still be round-tripped back to a stream + [context] fn to_literal(this: Spanned>) -> ExecutionResult { + let string = this.concat_content(&ConcatBehaviour::literal(this.span_range())); + let literal = string_interface::methods::to_literal(context, string.into_spanned_ref(this.span_range()))?; + Ok(AnyValue::for_literal(literal).into_any_value()) + } + + // CORE METHODS + // ============ + + // NOTE: with_span() exists on all values, this is just a specialized mutable version for streams + fn set_span(mut this: Mutable, span_source: AnyRef) -> ExecutionResult<()> { + let span_range = span_source.resolve_content_span_range().unwrap_or(Span::call_site().span_range()); + this.replace_first_level_spans(span_range.join_into_span_else_start()); + Ok(()) + } + + fn error(this: AnyRef, message: AnyRef) -> ExecutionResult { + let error_span_range = this.resolve_content_span_range().unwrap_or(Span::call_site().span_range()); + error_span_range.assertion_err(message.as_str()) + } + + fn assert(this: AnyRef, condition: bool, message: Option>) -> ExecutionResult<()> { + if condition { + Ok(()) + } else { + let error_span_range = this.resolve_content_span_range().unwrap_or(Span::call_site().span_range()); + let message = match message { + Some(ref m) => m, + None => "Assertion failed", + }; + error_span_range.assertion_err(message) + } + } + + fn assert_eq(this: AnyRef, lhs: Spanned, rhs: Spanned, message: Option>) -> ExecutionResult<()> { + match AnyValueRef::debug_eq(&lhs.as_ref_value(), &rhs.as_ref_value()) { + Ok(()) => Ok(()), + Err(debug_error) => { + let error_span_range = this.resolve_content_span_range().unwrap_or(Span::call_site().span_range()); + let message = match message { + Some(ref m) => m.to_string(), + None => format!( + "Assertion failed: {}\n lhs = {}\n rhs = {}", + debug_error.format_message(), + lhs.as_ref_value().concat_recursive(&ConcatBehaviour::debug(lhs.span_range()))?, + rhs.as_ref_value().concat_recursive(&ConcatBehaviour::debug(rhs.span_range()))?, + ), + }; + error_span_range.assertion_err(message) + } + } + } + + [context] fn reinterpret_as_run(Spanned(this, span_range): Spanned) -> ExecutionResult { + let source = this.into_token_stream(); + let (reparsed, scope_definitions) = source.source_parse_and_analyze(ExpressionBlockContent::parse, ExpressionBlockContent::control_flow_pass)?; + let mut inner_interpreter = Interpreter::new(scope_definitions); + let return_value = reparsed.evaluate_spanned(&mut inner_interpreter, span_range, RequestedOwnership::owned())?.expect_owned(); + if !inner_interpreter.complete().is_empty() { + return context.control_flow_err("reinterpret_as_run does not allow non-empty emit output") + } + Ok(return_value.0) + } + + fn reinterpret_as_stream(Spanned(this, span_range): Spanned) -> ExecutionResult { + let source = this.into_token_stream(); + let (reparsed, scope_definitions) = source.source_parse_and_analyze( + |input| SourceStream::parse_with_span(input, span_range.span_from_join_else_start()), + SourceStream::control_flow_pass, + )?; + let mut inner_interpreter = Interpreter::new(scope_definitions); + reparsed.interpret(&mut inner_interpreter)?; + Ok(inner_interpreter.complete()) + } + } + pub(crate) mod unary_operations { + [context] fn cast_coerced_to_value(Spanned(this, span): Spanned) -> ExecutionResult { + let coerced = this.coerce_into_value(); + if let AnyValue::Stream(_) = &coerced { + return span.value_err("The stream could not be coerced into a single value"); + } + // Re-run the cast operation on the coerced value + Ok(context.operation.evaluate(Spanned(coerced, span))?.0) + } + } + pub(crate) mod binary_operations { + fn add(mut lhs: OutputStream, rhs: OutputStream) -> OutputStream { + rhs.append_into(&mut lhs); + lhs + } + + fn add_assign(mut lhs: Assignee, rhs: OutputStream) { + rhs.append_into(&mut lhs); + } + } + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Cast { target, .. } if target.is_singleton_target() => unary_definitions::cast_coerced_to_value(), + _ => return None, + }) + } + + fn resolve_own_binary_operation( + operation: &BinaryOperation, + ) -> Option { + Some(match operation { + BinaryOperation::Addition { .. } => binary_definitions::add(), + BinaryOperation::AddAssign { .. } => binary_definitions::add_assign(), + _ => return None, + }) + } + } + } +} + +pub(crate) enum StreamLiteral { + Regular(RegularStreamLiteral), + Raw(RawStreamLiteral), + Grouped(GroupedStreamLiteral), + // We're missing a grouped raw, but that can be achieved with %group[%raw[...]] + Concatenated(ConcatenatedStreamLiteral), +} + +impl ParseSource for StreamLiteral { + fn parse(input: SourceParser) -> ParseResult { + if let Some((_, next)) = input.cursor().punct_matching('%') { + if next.group_matching(Delimiter::Bracket).is_some() { + return Ok(StreamLiteral::Regular(input.parse()?)); + } + if let Some((ident, _)) = next.ident() { + match ident.to_string().as_str() { + "raw" => return Ok(StreamLiteral::Raw(input.parse()?)), + "group" => return Ok(StreamLiteral::Grouped(input.parse()?)), + "string" | "ident" | "ident_camel" | "ident_snake" | "ident_upper_snake" + | "literal" => { + return Ok(StreamLiteral::Concatenated(input.parse()?)); + } + _ => {} + } + } + } + input.parse_err("A preinterpret stream-based literal is `%[..]` or `%xxx[..]` with xxx = raw, group, string, ident, ident_camel, ident_snake, ident_upper_snake or literal.") + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + match self { + StreamLiteral::Regular(lit) => lit.control_flow_pass(context), + StreamLiteral::Raw(lit) => lit.control_flow_pass(context), + StreamLiteral::Grouped(lit) => lit.control_flow_pass(context), + StreamLiteral::Concatenated(lit) => lit.control_flow_pass(context), + } + } +} + +impl Interpret for StreamLiteral { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + match self { + StreamLiteral::Regular(lit) => lit.interpret(interpreter), + StreamLiteral::Raw(lit) => lit.interpret(interpreter), + StreamLiteral::Grouped(lit) => lit.interpret(interpreter), + StreamLiteral::Concatenated(lit) => lit.interpret(interpreter), + } + } +} + +impl HasSpanRange for StreamLiteral { + fn span_range(&self) -> SpanRange { + match self { + StreamLiteral::Regular(lit) => lit.span_range(), + StreamLiteral::Raw(lit) => lit.span_range(), + StreamLiteral::Grouped(lit) => lit.span_range(), + StreamLiteral::Concatenated(lit) => lit.span_range(), + } + } +} + +pub(crate) struct RegularStreamLiteral { + prefix: Token![%], + brackets: Brackets, + content: SourceStream, +} + +impl ParseSource for RegularStreamLiteral { + fn parse(input: SourceParser) -> ParseResult { + let prefix = input.parse()?; + let (brackets, inner) = input.parse_brackets()?; + let content = SourceStream::parse_with_span(&inner, brackets.span())?; + Ok(Self { + prefix, + brackets, + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.content.control_flow_pass(context) + } +} + +impl Interpret for RegularStreamLiteral { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + self.content.interpret(interpreter) + } +} + +impl HasSpanRange for RegularStreamLiteral { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.prefix.span, self.brackets.span()) + } +} + +pub(crate) struct RawStreamLiteral { + prefix: Token![%], + _raw: Unused, + brackets: Brackets, + content: TokenStream, +} + +impl ParseSource for RawStreamLiteral { + fn parse(input: SourceParser) -> ParseResult { + let prefix = input.parse()?; + let _raw = input.parse()?; + let (brackets, inner) = input.parse_brackets()?; + let content = inner.parse()?; + Ok(Self { + prefix, + _raw, + brackets, + content, + }) + } + + fn control_flow_pass(&mut self, _context: FlowCapturer) -> ParseResult<()> { + Ok(()) + } +} + +impl Interpret for RawStreamLiteral { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + interpreter + .output(self)? + .extend_raw_tokens(self.content.clone()); + Ok(()) + } +} + +impl HasSpanRange for RawStreamLiteral { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.prefix.span, self.brackets.span()) + } +} + +pub(crate) struct GroupedStreamLiteral { + prefix: Token![%], + _group: Unused, + brackets: Brackets, + content: SourceStream, +} + +impl ParseSource for GroupedStreamLiteral { + fn parse(input: SourceParser) -> ParseResult { + let prefix = input.parse()?; + let _group = input.parse()?; + let (brackets, inner) = input.parse_brackets()?; + let content = SourceStream::parse_with_span(&inner, brackets.span())?; + Ok(Self { + prefix, + _group, + brackets, + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.content.control_flow_pass(context) + } +} + +impl Interpret for GroupedStreamLiteral { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + interpreter.in_output_group(Delimiter::None, self.brackets.span(), |interpreter| { + self.content.interpret(interpreter) + }) + } +} + +impl HasSpanRange for GroupedStreamLiteral { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.prefix.span, self.brackets.span()) + } +} + +pub(crate) struct ConcatenatedStreamLiteral { + prefix: Token![%], + _kind_ident: Unused, + kind: ConcatenatedStreamLiteralKind, + brackets: Brackets, + content: SourceStream, +} + +#[derive(Copy, Clone)] +pub(crate) enum ConcatenatedStreamLiteralKind { + String, + Ident, + IdentCamel, + IdentSnake, + IdentUpperSnake, + Literal, +} + +impl ParseSource for ConcatenatedStreamLiteral { + fn parse(input: SourceParser) -> ParseResult { + let prefix = input.parse()?; + let kind_ident = input.parse_any_ident()?; + let kind = match kind_ident.to_string().as_str() { + "string" => ConcatenatedStreamLiteralKind::String, + "ident" => ConcatenatedStreamLiteralKind::Ident, + "ident_camel" => ConcatenatedStreamLiteralKind::IdentCamel, + "ident_snake" => ConcatenatedStreamLiteralKind::IdentSnake, + "ident_upper_snake" => ConcatenatedStreamLiteralKind::IdentUpperSnake, + "literal" => ConcatenatedStreamLiteralKind::Literal, + _ => { + return input.parse_err("Expected one of `string`, `ident`, `ident_camel`, `ident_snake` or `ident_upper_snake`"); + } + }; + let (brackets, inner) = input.parse_brackets()?; + let content = SourceStream::parse_with_span(&inner, brackets.span())?; + Ok(Self { + prefix, + _kind_ident: Unused::new(kind_ident), + kind, + brackets, + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.content.control_flow_pass(context) + } +} + +impl Interpret for ConcatenatedStreamLiteral { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + let stream = + interpreter.capture_output(|interpreter| self.content.interpret(interpreter))?; + let string = stream.concat_content(&ConcatBehaviour::standard(self.span_range())); + let ident_span = stream + .resolve_content_span_range() + .unwrap_or_else(|| self.span_range()) + .join_into_span_else_start(); + let value = match self.kind { + ConcatenatedStreamLiteralKind::String => string.into_any_value(), + ConcatenatedStreamLiteralKind::Ident => { + let str = &string; + string_to_ident(str, self, ident_span)?.into_any_value() + } + ConcatenatedStreamLiteralKind::IdentCamel => { + let str = &string_conversion::to_upper_camel_case(&string); + string_to_ident(str, self, ident_span)?.into_any_value() + } + ConcatenatedStreamLiteralKind::IdentSnake => { + let str = &string_conversion::to_lower_snake_case(&string); + string_to_ident(str, self, ident_span)?.into_any_value() + } + ConcatenatedStreamLiteralKind::IdentUpperSnake => { + let str = &string_conversion::to_upper_snake_case(&string); + string_to_ident(str, self, ident_span)?.into_any_value() + } + ConcatenatedStreamLiteralKind::Literal => { + let str = &string; + string_to_literal(str, self, ident_span)?.into_any_value() + } + }; + value.as_ref_value().output_to( + Grouping::Flattened, + &mut ToStreamContext::new(interpreter.output(self)?, self.span_range()), + ) + } +} + +impl HasSpanRange for ConcatenatedStreamLiteral { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.prefix.span, self.brackets.span()) + } +} diff --git a/src/expressions/values/string.rs b/src/expressions/values/string.rs new file mode 100644 index 00000000..99124508 --- /dev/null +++ b/src/expressions/values/string.rs @@ -0,0 +1,237 @@ +use super::*; + +define_leaf_type! { + pub(crate) StringType => AnyType(AnyValueContent::String), + content: String, + kind: pub(crate) StringKind, + type_name: "string", + articled_display_name: "a string", + dyn_impls: { + IterableType: impl IsIterable { + fn into_iterator(self: Box) -> ExecutionResult { + Ok(IteratorValue::new_for_string_over_chars(*self)) + } + + fn len(&self, _error_span_range: SpanRange) -> ExecutionResult { + // The iterator is over chars, so this must count chars. + // But contrast, string.len() counts bytes + Ok(self.chars().count()) + } + } + }, +} + +impl IsValueContent for &str { + type Type = StringType; + type Form = BeRef; +} + +impl<'a> FromValueContent<'a> for &'a str { + fn from_content(value: &'a String) -> Self { + value.as_str() + } +} + +impl ValuesEqual for String { + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + if self == other { + ctx.values_equal() + } else { + ctx.leaf_values_not_equal(self, other) + } + } +} + +pub(crate) fn string_to_ident( + str: &str, + error_source: &impl HasSpanRange, + span: Span, +) -> ExecutionResult { + let ident = parse_str::(str).map_err(|err| { + error_source.value_error(format!("`{}` is not a valid ident: {:?}", str, err)) + })?; + Ok(ident.with_span(span)) +} + +pub(crate) fn string_to_literal( + str: &str, + error_source: &impl HasSpanRange, + span: Span, +) -> ExecutionResult { + let literal = Literal::from_str(str).map_err(|err| { + error_source.value_error(format!("`{}` is not a valid literal: {:?}", str, err)) + })?; + Ok(literal.with_span(span)) +} + +define_type_features! { + impl StringType, + pub(crate) mod string_interface { + pub(crate) mod methods { + // ================== + // CONVERSION METHODS + // ================== + [context] fn to_ident(this: Spanned>) -> ExecutionResult { + string_to_ident(&this, &this, context.span_from_join_else_start()) + } + + [context] fn to_ident_camel(this: Spanned>) -> ExecutionResult { + let str = string_conversion::to_upper_camel_case(&this); + string_to_ident(&str, &this, context.span_from_join_else_start()) + } + + [context] fn to_ident_snake(this: Spanned>) -> ExecutionResult { + let str = string_conversion::to_lower_snake_case(&this); + string_to_ident(&str, &this, context.span_from_join_else_start()) + } + + [context] fn to_ident_upper_snake(this: Spanned>) -> ExecutionResult { + let str = string_conversion::to_upper_snake_case(&this); + string_to_ident(&str, &this, context.span_from_join_else_start()) + } + + [context] fn to_literal(this: Spanned>) -> ExecutionResult { + string_to_literal(&this, &this, context.span_from_join_else_start()) + } + + // ====================== + // STRING RESHAPE METHODS + // ====================== + fn to_uppercase(this: AnyRef) -> String { + string_conversion::to_uppercase(&this) + } + + fn to_lowercase(this: AnyRef) -> String { + string_conversion::to_lowercase(&this) + } + + fn to_lower_snake_case(this: AnyRef) -> String { + string_conversion::to_lower_snake_case(&this) + } + + fn to_upper_snake_case(this: AnyRef) -> String { + string_conversion::to_upper_snake_case(&this) + } + + fn to_kebab_case(this: AnyRef) -> String { + string_conversion::to_lower_kebab_case(&this) + } + + fn to_lower_camel_case(this: AnyRef) -> String { + string_conversion::to_lower_camel_case(&this) + } + + fn to_upper_camel_case(this: AnyRef) -> String { + string_conversion::to_upper_camel_case(&this) + } + + fn capitalize(this: AnyRef) -> String { + string_conversion::capitalize(&this) + } + + fn decapitalize(this: AnyRef) -> String { + string_conversion::decapitalize(&this) + } + + fn to_title_case(this: AnyRef) -> String { + string_conversion::title_case(&this) + } + + fn insert_spaces(this: AnyRef) -> String { + string_conversion::insert_spaces_between_words(&this) + } + } + pub(crate) mod unary_operations { + fn cast_to_string(this: String) -> String { + this + } + } + pub(crate) mod binary_operations { + fn add(mut lhs: String, rhs: AnyRef) -> String { + lhs.push_str(rhs.deref()); + lhs + } + + fn add_assign(mut lhs: Assignee, rhs: AnyRef) { + lhs.push_str(rhs.deref()); + } + + fn eq(lhs: AnyRef, rhs: AnyRef) -> bool { + lhs.deref() == rhs.deref() + } + + fn ne(lhs: AnyRef, rhs: AnyRef) -> bool { + lhs.deref() != rhs.deref() + } + + fn lt(lhs: AnyRef, rhs: AnyRef) -> bool { + lhs.deref() < rhs.deref() + } + + fn le(lhs: AnyRef, rhs: AnyRef) -> bool { + lhs.deref() <= rhs.deref() + } + + fn ge(lhs: AnyRef, rhs: AnyRef) -> bool { + lhs.deref() >= rhs.deref() + } + + fn gt(lhs: AnyRef, rhs: AnyRef) -> bool { + lhs.deref() > rhs.deref() + } + } + interface_items { + fn resolve_own_unary_operation(operation: &UnaryOperation) -> Option { + Some(match operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => return None, + UnaryOperation::Cast { target: CastTarget(kind), .. } => match kind { + AnyValueLeafKind::String(_) => unary_definitions::cast_to_string(), + _ => return None, + }, + }) + } + + fn resolve_own_binary_operation( + operation: &BinaryOperation, + ) -> Option { + Some(match operation { + BinaryOperation::Addition { .. } => binary_definitions::add(), + BinaryOperation::Equal { .. } => binary_definitions::eq(), + BinaryOperation::NotEqual { .. } => binary_definitions::ne(), + BinaryOperation::LessThan { .. } => binary_definitions::lt(), + BinaryOperation::LessThanOrEqual { .. } => binary_definitions::le(), + BinaryOperation::GreaterThanOrEqual { .. } => binary_definitions::ge(), + BinaryOperation::GreaterThan { .. } => binary_definitions::gt(), + BinaryOperation::AddAssign { .. } => binary_definitions::add_assign(), + _ => return None, + }) + } + } + } +} + +impl_resolvable_argument_for! { + StringType, + (value, context) -> String { + match value { + AnyValueContent::String(value) => Ok(value), + _ => context.err("a string", value), + } + } +} + +impl ResolvableArgumentTarget for str { + type ValueType = StringType; +} + +impl ResolvableShared for str { + fn resolve_from_ref<'a>( + value: &'a AnyValue, + context: ResolutionContext, + ) -> ExecutionResult<&'a Self> { + match value { + AnyValueContent::String(s) => Ok(s.as_str()), + _ => context.err("a string", value), + } + } +} diff --git a/src/expressions/values/unsupported_literal.rs b/src/expressions/values/unsupported_literal.rs new file mode 100644 index 00000000..071a4b8e --- /dev/null +++ b/src/expressions/values/unsupported_literal.rs @@ -0,0 +1,39 @@ +use super::*; + +define_leaf_type! { + pub(crate) UnsupportedLiteralType => AnyType(AnyValueContent::UnsupportedLiteral), + content: UnsupportedLiteral, + kind: pub(crate) UnsupportedLiteralKind, + type_name: "unsupported_literal", + articled_display_name: "an unsupported literal", + dyn_impls: {}, +} + +#[derive(Clone)] +pub(crate) struct UnsupportedLiteral(pub(crate) syn::Lit); + +impl ValuesEqual for UnsupportedLiteral { + fn test_equality(&self, other: &Self, ctx: &mut C) -> C::Result { + if self.0.to_token_stream().to_string() == other.0.to_token_stream().to_string() { + ctx.values_equal() + } else { + ctx.leaf_values_not_equal(self, other) + } + } +} + +impl Debug for UnsupportedLiteral { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.to_token_stream()) + } +} + +define_type_features! { + impl UnsupportedLiteralType, + pub(crate) mod unsupported_literal_interface { + pub(crate) mod methods {} + pub(crate) mod unary_operations {} + pub(crate) mod binary_operations {} + interface_items {} + } +} diff --git a/src/extensions/errors_and_spans.rs b/src/extensions/errors_and_spans.rs new file mode 100644 index 00000000..f3ce267e --- /dev/null +++ b/src/extensions/errors_and_spans.rs @@ -0,0 +1,517 @@ +use crate::internal_prelude::*; + +pub(crate) trait SynErrorExt: Sized { + fn concat(self, extra: &str) -> Self; +} + +impl SynErrorExt for syn::Error { + fn concat(self, extra: &str) -> Self { + let mut message = self.to_string(); + message.push_str(extra); + Self::new(self.span(), message) + } +} + +pub(crate) trait SpanErrorExt { + fn syn_error(&self, message: impl std::fmt::Display) -> syn::Error; + + fn parse_error(&self, message: impl std::fmt::Display) -> ParseError { + ParseError::new(self.syn_error(message)) + } + + fn parse_err(&self, message: impl std::fmt::Display) -> ParseResult { + Err(self.syn_error(message).into()) + } + + fn syntax_err(&self, message: impl std::fmt::Display) -> ExecutionResult { + Err(self.syntax_error(message)) + } + + fn syntax_error(&self, message: impl std::fmt::Display) -> ExecutionInterrupt { + ExecutionInterrupt::syntax_error(self.syn_error(message)) + } + + fn type_err(&self, message: impl std::fmt::Display) -> ExecutionResult { + Err(self.type_error(message)) + } + + fn type_error(&self, message: impl std::fmt::Display) -> ExecutionInterrupt { + ExecutionInterrupt::type_error(self.syn_error(message)) + } + + fn control_flow_err(&self, message: impl std::fmt::Display) -> ExecutionResult { + Err(self.control_flow_error(message)) + } + + fn control_flow_error(&self, message: impl std::fmt::Display) -> ExecutionInterrupt { + ExecutionInterrupt::control_flow_error(self.syn_error(message)) + } + + fn ownership_err(&self, message: impl std::fmt::Display) -> ExecutionResult { + Err(self.ownership_error(message)) + } + + fn ownership_error(&self, message: impl std::fmt::Display) -> ExecutionInterrupt { + ExecutionInterrupt::ownership_error(self.syn_error(message)) + } + + fn assertion_err(&self, message: impl std::fmt::Display) -> ExecutionResult { + Err(self.assertion_error(message)) + } + + fn debug_error(&self, message: impl std::fmt::Display) -> ExecutionInterrupt { + ExecutionInterrupt::debug_error(self.syn_error(message)) + } + + fn debug_err(&self, message: impl std::fmt::Display) -> ExecutionResult { + Err(self.debug_error(message)) + } + + fn assertion_error(&self, message: impl std::fmt::Display) -> ExecutionInterrupt { + ExecutionInterrupt::assertion_error(self.syn_error(message)) + } + + fn value_err(&self, message: impl std::fmt::Display) -> ExecutionResult { + Err(self.value_error(message)) + } + + fn value_error(&self, message: impl std::fmt::Display) -> ExecutionInterrupt { + ExecutionInterrupt::value_error(self.syn_error(message)) + } +} + +impl SpanErrorExt for T { + fn syn_error(&self, message: impl std::fmt::Display) -> syn::Error { + self.span_range().create_error(message) + } +} + +/// This is intended to be implemented only for types which have a cheap span. +/// It is cheaper than [`syn::spanned`], which requires streaming the whole type, +/// and can be very slow for e.g. large expressions. +pub(crate) trait HasSpan { + fn span(&self) -> Span; +} + +/// This is intended to be implemented only for types which have a cheap [`SpanRange`]. +/// +/// It is cheaper than [`syn::spanned`], which requires streaming the whole type, +/// and can be very slow for e.g. large expressions. [`syn::spanned`] also only returns +/// a single [`Span`], which is not sufficient for multi-token ranges on stable rust. +/// +/// See also [`SlowSpanRange`] for the equivalent of [`syn::spanned`]. +pub(crate) trait HasSpanRange { + fn span_range(&self) -> SpanRange; + + #[allow(unused)] + fn start_span(&self) -> Span { + self.span_range().start + } + + fn end_span(&self) -> Span { + self.span_range().end + } + + fn span_from_join_else_start(&self) -> Span { + self.span_range().join_into_span_else_start() + } +} + +impl HasSpanRange for T { + fn span_range(&self) -> SpanRange { + SpanRange::new_single(self.span()) + } +} + +impl HasSpanRange for &SpanRange { + fn span_range(&self) -> SpanRange { + **self + } +} + +/// In many cases, we want to be able to show an error message over several tokens. +/// +/// The "correct" solution to this is to use [`proc_macro::Span::join`], BUT +/// this is currently only available on nightly Rust. +/// +/// Instead, [`syn::Error`] uses a trick involving a start and end span. This works +/// on stable, but we need to keep around these two spans explicitly. +/// +/// Hence [`SpanRange`] was born. +/// +/// When [`proc_macro::Span::join`] is stabilised, we can swap [`SpanRange`] +/// for [`Span`] (or even remove it and [`HasSpanRange`]). +#[derive(Copy, Clone)] +pub(crate) struct SpanRange { + start: Span, + end: Span, +} + +impl SpanRange { + pub(crate) fn new_single(span: Span) -> Self { + Self { + start: span, + end: span, + } + } + + pub(crate) fn new_between(start: impl HasSpanRange, end: impl HasSpanRange) -> Self { + Self { + start: start.span_range().start, + end: end.span_range().end, + } + } + + fn create_error(&self, message: impl std::fmt::Display) -> syn::Error { + syn::Error::new_spanned(self, message) + } + + /// * On nightly, this gives a span covering the full range (the same result as `Span::join` would) + /// * On stable, this gives the span of the first token of the group (because [`proc_macro::Span::join`] is not supported) + pub(crate) fn join_into_span_else_start(&self) -> Span { + ::span(self) + } + + pub(crate) fn set_start(&mut self, start: Span) { + self.start = start; + } + + pub(crate) fn set_end(&mut self, end: Span) { + self.end = end; + } +} + +// This is implemented so we can create an error from it using `Error::new_spanned(..)` +impl ToTokens for SpanRange { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend([ + TokenTree::Punct(Punct::new('<', Spacing::Alone).with_span(self.start)), + TokenTree::Punct(Punct::new('>', Spacing::Alone).with_span(self.end)), + ]); + } +} + +impl From for SpanRange { + fn from(span: Span) -> Self { + Self::new_single(span) + } +} + +impl HasSpanRange for SpanRange { + fn span_range(&self) -> SpanRange { + *self + } +} + +impl HasSpan for Span { + fn span(&self) -> Span { + *self + } +} + +impl<'a> HasSpan for Cursor<'a> { + fn span(&self) -> Span { + Cursor::span(*self) + } +} + +impl HasSpan for TokenTree { + fn span(&self) -> Span { + TokenTree::span(self) + } +} + +impl HasSpan for Group { + fn span(&self) -> Span { + Group::span(self) + } +} + +impl HasSpan for DelimSpan { + fn span(&self) -> Span { + DelimSpan::join(self) + } +} + +impl HasSpan for Ident { + fn span(&self) -> Span { + Ident::span(self) + } +} + +impl HasSpan for Punct { + fn span(&self) -> Span { + Punct::span(self) + } +} + +impl HasSpan for Literal { + fn span(&self) -> Span { + Literal::span(self) + } +} + +/// [ ... ] +#[derive(Copy, Clone)] +pub(crate) struct Brackets { + pub(crate) delim_span: DelimSpan, +} + +impl core::ops::Deref for Brackets { + type Target = DelimSpan; + + fn deref(&self) -> &Self::Target { + &self.delim_span + } +} + +impl HasSpan for Brackets { + fn span(&self) -> Span { + self.delim_span.span() + } +} + +/// { ... } +#[derive(Copy, Clone)] +pub(crate) struct Braces { + pub(crate) delim_span: DelimSpan, +} + +impl core::ops::Deref for Braces { + type Target = DelimSpan; + + fn deref(&self) -> &Self::Target { + &self.delim_span + } +} + +impl HasSpan for Braces { + fn span(&self) -> Span { + self.delim_span.span() + } +} + +/// ( ... ) +#[derive(Copy, Clone)] +pub(crate) struct Parentheses { + pub(crate) delim_span: DelimSpan, +} + +impl core::ops::Deref for Parentheses { + type Target = DelimSpan; + + fn deref(&self) -> &Self::Target { + &self.delim_span + } +} + +impl HasSpan for Parentheses { + fn span(&self) -> Span { + self.delim_span.span() + } +} + +impl HasSpan for LitFloat { + fn span(&self) -> Span { + LitFloat::span(self) + } +} +impl HasSpan for LitInt { + fn span(&self) -> Span { + LitInt::span(self) + } +} + +impl HasSpanRange for syn::Lifetime { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.apostrophe, self.ident.span()) + } +} + +#[derive(Copy, Clone)] +pub(crate) struct TransparentDelimiters { + pub(crate) delim_span: DelimSpan, +} + +impl core::ops::Deref for TransparentDelimiters { + type Target = DelimSpan; + + fn deref(&self) -> &Self::Target { + &self.delim_span + } +} + +impl HasSpan for TransparentDelimiters { + fn span(&self) -> Span { + self.delim_span.span() + } +} + +pub(crate) trait SlowSpanRange { + /// This name is purposefully very long to discourage use, as it can cause nasty performance issues + fn span_range_from_iterating_over_all_tokens(&self) -> SpanRange; +} + +impl SlowSpanRange for T { + fn span_range_from_iterating_over_all_tokens(&self) -> SpanRange { + let mut iter = self.into_token_stream().into_iter(); + let start = iter.next().map_or_else(Span::call_site, |t| t.span()); + let end = iter.last().map_or(start, |t| t.span()); + SpanRange { start, end } + } +} + +// This should only be used for types implementing ToTokens, which have +// a small, bounded number of tokens, with sensible performance. +macro_rules! impl_auto_span_range { + ($($ty:ty),* $(,)?) => { + $( + impl HasSpanRange for $ty { + fn span_range(&self) -> SpanRange { + SlowSpanRange::span_range_from_iterating_over_all_tokens(self) + } + } + )* + }; +} + +// This should only be used for types with a bounded number of tokens +// greater than one. +// If exactly one, implement HasSpan. +// Otherwise, span_range_from_iterating_over_all_tokens() can be used +// directly with a longer name to make the performance hit clearer, so +// it's only used in error cases. +impl_auto_span_range! { + syn::BinOp, + syn::UnOp, + syn::token::Underscore, + syn::token::Dot, + syn::token::DotDot, + syn::token::DotDotEq, + syn::token::Shl, + syn::token::Shr, + syn::token::AndAnd, + syn::token::OrOr, + syn::token::EqEq, + syn::token::Lt, + syn::token::Le, + syn::token::Ne, + syn::token::Ge, + syn::token::Gt, + syn::token::Comma, + syn::token::PlusEq, + syn::token::MinusEq, + syn::token::StarEq, + syn::token::SlashEq, + syn::token::PercentEq, + syn::token::AndEq, + syn::token::OrEq, + syn::token::CaretEq, + syn::token::ShlEq, + syn::token::ShrEq, +} + +macro_rules! single_span_token { + ($(Token![$token:tt]),* $(,)?) => { + $( + impl HasSpan for Token![$token] { + fn span(&self) -> Span { + self.span + } + } + )* + }; +} + +single_span_token! { + Token![as], + Token![in], + Token![=], + Token![!], + Token![~], + Token![?], + Token![+], + Token![-], + Token![*], + Token![/], + Token![%], + Token![^], + Token![&], + Token![|], +} + +pub(crate) struct Spanned(pub(crate) T, pub(crate) SpanRange); + +impl Clone for Spanned { + fn clone(&self) -> Self { + Spanned(self.0.clone(), self.1) + } +} + +impl Copy for Spanned {} + +impl Spanned { + #[inline] + #[allow(unused)] + pub(crate) fn to_ref(&self) -> Spanned<&T> { + Spanned(&self.0, self.1) + } + + #[inline] + pub(crate) fn to_mut(&mut self) -> Spanned<&mut T> { + Spanned(&mut self.0, self.1) + } + + #[inline] + pub(crate) fn map(self, f: impl FnOnce(T) -> U) -> Spanned { + Spanned(f(self.0), self.1) + } + + #[inline] + pub(crate) fn try_map(self, f: impl FnOnce(T) -> Result) -> Result, E> { + Ok(Spanned(f(self.0)?, self.1)) + } +} + +impl HasSpanRange for Spanned { + fn span_range(&self) -> SpanRange { + self.1 + } +} + +impl Deref for Spanned { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for Spanned { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl> AsRef for Spanned { + fn as_ref(&self) -> &X { + self.0.as_ref() + } +} + +impl> AsMut for Spanned { + fn as_mut(&mut self) -> &mut X { + self.0.as_mut() + } +} + +pub(crate) trait ToSpanned: Sized { + fn spanned(self, source: impl HasSpanRange) -> Spanned; +} + +impl ToSpanned for T { + #[inline] + fn spanned(self, source: impl HasSpanRange) -> Spanned { + Spanned(self, source.span_range()) + } +} diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs new file mode 100644 index 00000000..0f3d75ef --- /dev/null +++ b/src/extensions/mod.rs @@ -0,0 +1,9 @@ +mod errors_and_spans; +mod parsing; +mod string; +mod tokens; + +pub(crate) use errors_and_spans::*; +pub(crate) use parsing::*; +pub(crate) use string::*; +pub(crate) use tokens::*; diff --git a/src/extensions/parsing.rs b/src/extensions/parsing.rs new file mode 100644 index 00000000..291c672c --- /dev/null +++ b/src/extensions/parsing.rs @@ -0,0 +1,479 @@ +use crate::internal_prelude::*; + +pub(crate) trait TokenStreamParseExt: Sized { + fn source_parse_and_analyze( + self, + parser: impl FnOnce(SourceParser) -> ParseResult, + control_flow_analysis: impl FnOnce(&mut T, FlowCapturer) -> ParseResult<()>, + ) -> ParseResult<(T, ScopeDefinitions)>; + + fn interpreted_parse_with>( + self, + parser: impl FnOnce(ParseStream) -> Result, + ) -> Result; +} + +impl TokenStreamParseExt for TokenStream { + fn source_parse_and_analyze( + self, + parser: impl FnOnce(SourceParser) -> ParseResult, + control_flow_analysis: impl FnOnce(&mut T, FlowCapturer) -> ParseResult<()>, + ) -> ParseResult<(T, ScopeDefinitions)> { + let mut parsed = parse_with(self, parse_without_analysis(parser))?; + let definitions = ControlFlowContext::analyze(&mut parsed, control_flow_analysis)?; + Ok((parsed, definitions)) + } + + fn interpreted_parse_with>( + self, + parser: impl FnOnce(ParseStream) -> Result, + ) -> Result { + parse_with(self, parser) + } +} + +pub(crate) fn parse_with>( + stream: TokenStream, + parser: impl FnOnce(ParseStream) -> Result, +) -> Result { + let mut result = None; + let parse_result = (|input: SynParseStream| -> SynResult<()> { + result = Some(parser(input.into())); + match &result { + // Some fallback error to ensure that we don't go down the unexpected branch inside parse2 + Some(Err(_)) => Err(SynError::new(Span::call_site(), "")), + _ => Ok(()), + } + }) + .parse2(stream); + + match (result, parse_result) { + (Some(Ok(value)), Ok(())) => Ok(value), + (Some(Err(error)), _) => Err(error), + // If the inner result was Ok, but the parse result was an error, this indicates that the parse2 + // hit the "unexpected" path, indicating that some parse buffer (i.e. group) wasn't fully consumed. + // So we propagate this error. + (Some(Ok(_)), Err(error)) => Err(E::from(ParseError::new(error))), + (None, _) => unreachable!(), + } +} + +pub(crate) trait CursorExt: Sized { + /// Because syn doesn't parse ' as a punct (not aligned with the TokenTree abstraction) + fn any_punct(self) -> Option<(Punct, Self)>; + fn ident_matching(self, content: &str) -> Option<(Ident, Self)>; + fn punct_matching(self, char: char) -> Option<(Punct, Self)>; + fn literal_matching(self, content: &str) -> Option<(Literal, Self)>; + fn group_matching(self, expected_delimiter: Delimiter) -> Option<(DelimSpan, Self, Self)>; +} + +impl CursorExt for Cursor<'_> { + fn any_punct(self) -> Option<(Punct, Self)> { + match self.token_tree() { + Some((TokenTree::Punct(punct), next)) => Some((punct, next)), + _ => None, + } + } + + fn ident_matching(self, content: &str) -> Option<(Ident, Self)> { + match self.ident() { + Some((ident, next)) if ident == content => Some((ident, next)), + _ => None, + } + } + + fn punct_matching(self, char: char) -> Option<(Punct, Self)> { + // self.punct() is a little more efficient, but can't match ' + let matcher = if char == '\'' { + self.any_punct() + } else { + self.punct() + }; + match matcher { + Some((punct, next)) if punct.as_char() == char => Some((punct, next)), + _ => None, + } + } + + fn literal_matching(self, content: &str) -> Option<(Literal, Self)> { + match self.literal() { + Some((literal, next)) if literal.to_string() == content => Some((literal, next)), + _ => None, + } + } + + fn group_matching(self, expected_delimiter: Delimiter) -> Option<(DelimSpan, Self, Self)> { + match self.any_group() { + Some((inner_cursor, delimiter, delim_span, next_outer_cursor)) + if delimiter == expected_delimiter => + { + Some((delim_span, inner_cursor, next_outer_cursor)) + } + _ => None, + } + } +} + +pub(crate) trait DelimiterExt { + fn description_of_open(&self) -> &'static str; + fn description_of_close(&self) -> &'static str; + #[allow(unused)] + fn description_of_group(&self) -> &'static str; +} + +impl DelimiterExt for Delimiter { + fn description_of_open(&self) -> &'static str { + match self { + Delimiter::Parenthesis => "(", + Delimiter::Brace => "{", + Delimiter::Bracket => "[", + Delimiter::None => "start of transparent group, from a grouped macro $variable substitution or preinterpret %group[...] literal", + } + } + + fn description_of_close(&self) -> &'static str { + match self { + Delimiter::Parenthesis => ")", + Delimiter::Brace => "}", + Delimiter::Bracket => "]", + Delimiter::None => "end of transparent group, from a grouped macro $variable substitution or preinterpret %group[...] literal", + } + } + + fn description_of_group(&self) -> &'static str { + match self { + Delimiter::Parenthesis => "(...)", + Delimiter::Brace => "{ ... }", + Delimiter::Bracket => "[...]", + Delimiter::None => "transparent group, from a grouped macro $variable substitution or preinterpret %group[...] literal", + } + } +} + +/// Allows storing a stack of parse buffers for certain parse strategies which require +/// handling multiple groups in parallel. +/// +/// Supports nested forking for attempt blocks: each fork level adds a new buffer to each +/// active level's stack. On commit, buffers are advanced; on rollback, fork buffers are discarded. +pub(crate) struct ParseStack<'a, K> { + /// The base level (root stream + its forks) + base: BaseLevel<'a, K>, + /// Group levels, each with their own fork stacks + groups: Vec>, +} + +/// The base (root) level of the parse stack. +struct BaseLevel<'a, K> { + /// The original parse stream (a reference) + root: ParseStream<'a, K>, + /// Forks at each depth: forks[i] is the fork at depth i+1 + forks: Vec>, +} + +/// A group level in the parse stack. +struct GroupLevel<'a, K> { + /// The delimiter used to enter this group. + delimiter: Delimiter, + /// Stack of buffers for this group. + /// - buffers[0] is at depth `fork_depth - buffers.len() + 1` + /// - buffers.last() is at the current fork_depth + /// + /// When a level's buffers becomes empty after commit/rollback, the level is removed. + buffers: Vec>, +} + +/// State of a group buffer at a particular fork depth. +enum GroupBuffer<'a, K> { + /// Group is active at this depth + Active(ParseBuffer<'a, K>), + /// Group was exited at this depth + Ended, +} + +impl<'a, K> ParseStack<'a, K> { + pub(crate) fn new(base: ParseStream<'a, K>) -> Self { + Self { + base: BaseLevel { + root: base, + forks: Vec::new(), + }, + groups: Vec::new(), + } + } + + /// Returns the current fork depth (0 = not forked). + pub(crate) fn fork_depth(&self) -> usize { + self.base.forks.len() + } + + /// Start a fork. From this point, parsing operations work on forked buffers. + /// + /// # Safety + /// Must be paired with either `commit_fork` or `rollback_fork`. + pub(crate) unsafe fn start_fork(&mut self) { + // Fork the base level + let base_current = self + .base + .forks + .last() + .map(|f| f as &_) + .unwrap_or(self.base.root); + self.base.forks.push(base_current.fork()); + + // Fork each group level + for group in &mut self.groups { + match group.buffers.last() { + Some(GroupBuffer::Active(buffer)) => { + group.buffers.push(GroupBuffer::Active(buffer.fork())); + } + Some(GroupBuffer::Ended) => { + // Group was ended at previous depth; carry forward the Ended status + group.buffers.push(GroupBuffer::Ended); + } + None => unreachable!("Group should always have at least one buffer"), + } + } + } + + /// Commit the fork: advance all original buffers to their forked positions. + /// + /// # Safety + /// Must be called after `start_fork`. + pub(crate) unsafe fn commit_fork(&mut self) { + assert!( + self.fork_depth() > 0, + "commit_fork called without active fork" + ); + + // Commit base level + let committed_fork = self.base.forks.pop().unwrap(); + if let Some(previous) = self.base.forks.last() { + previous.advance_to(&committed_fork); + } else { + self.base.root.advance_to(&committed_fork); + } + + // Commit each group level + for group in &mut self.groups { + // If buffers.len() == 1, group was created at this depth. + // On commit, it survives at the new (lower) depth - don't pop. + if group.buffers.len() == 1 { + continue; + } + + // Pop the buffer at the current depth + let popped = group.buffers.pop().unwrap(); + + match popped { + GroupBuffer::Active(buffer) => { + // Advance the previous depth to this position + if let Some(GroupBuffer::Active(previous)) = group.buffers.last() { + previous.advance_to(&buffer); + } + // If previous was Ended, nothing to advance + } + GroupBuffer::Ended => { + // Group was exited at this depth; propagate to previous depth + if let Some(previous) = group.buffers.last_mut() { + *previous = GroupBuffer::Ended; + } + } + } + } + + // Clean up groups that are ended at depth 0 + if self.fork_depth() == 0 { + self.groups + .retain(|g| matches!(g.buffers.last(), Some(GroupBuffer::Active(_)))); + } + } + + /// Rollback the fork: discard fork state, leaving originals unchanged. + /// + /// # Safety + /// Must be called after `start_fork`. + pub(crate) unsafe fn rollback_fork(&mut self) { + assert!( + self.fork_depth() > 0, + "rollback_fork called without active fork" + ); + + // Rollback base level + self.base.forks.pop(); + + // Rollback each group level + self.groups.retain_mut(|group| { + group.buffers.pop(); + // If group has no more buffers, it was entered during this fork; remove it + !group.buffers.is_empty() + }); + } + + pub(crate) fn is_forked(&self) -> bool { + self.fork_depth() > 0 + } + + pub(crate) fn current(&self) -> ParseStream<'_, K> { + // Check groups from top to bottom for an active buffer + for group in self.groups.iter().rev() { + if let Some(GroupBuffer::Active(buffer)) = group.buffers.last() { + return buffer; + } + // If Ended, continue to next group (or base) + } + // Fall back to base level + self.base + .forks + .last() + .map(|f| f as &_) + .unwrap_or(self.base.root) + } + + pub(crate) fn cursor(&self) -> Cursor<'_> { + self.current().cursor() + } + + pub(crate) fn parse_err(&self, message: impl std::fmt::Display) -> ParseResult { + self.current().parse_err(message) + } + + pub(crate) fn is_current_empty(&self) -> bool { + self.current().is_empty() + } + + #[allow(unused)] + pub(crate) fn peek(&mut self, token: T) -> bool { + self.current().peek(token) + } + + #[allow(unused)] + pub(crate) fn peek2(&mut self, token: T) -> bool { + self.current().peek2(token) + } + + pub(crate) fn parse_any_ident(&mut self) -> ParseResult { + self.current().parse_any_ident() + } + + pub(crate) fn parse_and_enter_group( + &mut self, + delimiter: Option, + ) -> ParseResult<(Delimiter, DelimSpan)> { + let (delimiter, delim_span, inner) = self.current().parse_group(delimiter)?; + let inner = unsafe { + // SAFETY: This is safe because the lifetime is there for two reasons: + // (A) Prevent mixing up different buffers from e.g. different groups, + // (B) Ensure the buffers are dropped in the correct order so that the unexpected drop glue triggers + // in the correct order. + // + // This invariant is maintained by this `ParseStack` struct: + // (A) Is enforced by the fact we're parsing the group from the top parse buffer current(). + // (B) Is enforced by a combination of: + // ==> exit_group() ensures the parse buffers are dropped in the correct order + // ==> If a user forgets to do it (or e.g. an error path or panic causes exit_group not to be called) + // Then the drop glue ensures the groups are dropped in the correct order. + std::mem::transmute::, ParseBuffer<'a, K>>(inner) + }; + + // Create a new group level with one buffer at the current fork depth + self.groups.push(GroupLevel { + delimiter, + buffers: vec![GroupBuffer::Active(inner)], + }); + Ok((delimiter, delim_span)) + } + + /// Returns true if there is an active group that can be exited. + pub(crate) fn has_active_group(&self) -> bool { + self.groups + .iter() + .any(|g| matches!(g.buffers.last(), Some(GroupBuffer::Active(_)))) + } + + /// Returns the delimiter of the innermost active group, if any. + pub(crate) fn innermost_active_group_delimiter(&self) -> Option { + self.groups + .iter() + .rev() + .find(|g| matches!(g.buffers.last(), Some(GroupBuffer::Active(_)))) + .map(|g| g.delimiter) + } + + /// Should be paired with `parse_and_enter_group`. + /// + /// If the group is not finished, the next attempt to read from the parent will trigger an error, + /// in accordance with the drop glue on `ParseBuffer`. + /// + /// If `expected_delimiter` is provided, it will be validated against the actual delimiter + /// used when entering the group. + /// + /// ### Returns + /// Returns an error if there is no group to exit or if the expected delimiter doesn't match. + pub(crate) fn exit_group(&mut self, expected_delimiter: Option) -> ParseResult<()> { + // Find the innermost active group (what current() would return) + let group_index = self + .groups + .iter() + .rposition(|g| matches!(g.buffers.last(), Some(GroupBuffer::Active(_)))) + .ok_or_else(|| self.current().parse_error("no group to close"))?; + + // Validate delimiter if expected + if let Some(expected) = expected_delimiter { + let actual = self.groups[group_index].delimiter; + if actual != expected { + return self.parse_err(format!( + "attempting to close '{}' isn't valid, because the currently open group would end with '{}'", + expected.description_of_close(), + actual.description_of_close() + )); + } + } + + if self.fork_depth() == 0 { + // Not forked: remove the group entirely + self.groups.remove(group_index); + } else { + // Forked: mark as ended + let group = &mut self.groups[group_index]; + if let Some(buffer) = group.buffers.last_mut() { + *buffer = GroupBuffer::Ended; + } + } + Ok(()) + } +} + +impl<'a> ParseStack<'a, Source> { + pub(crate) fn parse(&mut self) -> ParseResult { + self.current().parse() + } + + pub(crate) fn peek_grammar(&mut self) -> SourcePeekMatch { + self.current().peek_grammar() + } + + pub(crate) fn try_parse_or_revert(&mut self) -> ParseResult { + let current = self.current(); + let fork = current.fork(); + match fork.parse::() { + Ok(output) => { + current.advance_to(&fork); + Ok(output) + } + Err(err) => Err(err), + } + } +} + +impl Drop for ParseStack<'_, K> { + fn drop(&mut self) { + // Drop groups in reverse order (innermost first) + // Each group's buffers are also dropped in reverse order + while let Some(mut group) = self.groups.pop() { + while group.buffers.pop().is_some() {} + } + + // Drop base forks in reverse order + while self.base.forks.pop().is_some() {} + } +} diff --git a/src/extensions/string.rs b/src/extensions/string.rs new file mode 100644 index 00000000..20a6e51c --- /dev/null +++ b/src/extensions/string.rs @@ -0,0 +1,30 @@ +pub(crate) trait StringExtensions { + /// NOTE: This isn't foolproof - it is actually pretty rubbish with + /// type names, where we say things like "an f32" and "a u8" due to + /// sounding out the first letter of the acronym. + /// We may wish to just replace this with hardcoded articled names. + fn indefinite_articled(&self, upper_case_article: bool) -> String; +} + +impl> StringExtensions for T { + fn indefinite_articled(&self, upper_case_article: bool) -> String { + let string = self.as_ref(); + let first_char = string.chars().next().unwrap(); + match first_char { + 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U' => { + if upper_case_article { + format!("An {}", string) + } else { + format!("an {}", string) + } + } + _ => { + if upper_case_article { + format!("A {}", string) + } else { + format!("a {}", string) + } + } + } + } +} diff --git a/src/extensions/tokens.rs b/src/extensions/tokens.rs new file mode 100644 index 00000000..46a36e57 --- /dev/null +++ b/src/extensions/tokens.rs @@ -0,0 +1,115 @@ +use crate::internal_prelude::*; + +pub(crate) trait TokenStreamExt: Sized { + #[allow(unused)] + fn push(&mut self, token: TokenTree); + fn flatten_transparent_groups(self) -> Self; +} + +impl TokenStreamExt for TokenStream { + fn push(&mut self, token: TokenTree) { + self.extend(iter::once(token)); + } + + fn flatten_transparent_groups(self) -> Self { + let mut output = TokenStream::new(); + for token in self { + match token { + TokenTree::Group(group) if group.delimiter() == Delimiter::None => { + output.extend(group.stream().flatten_transparent_groups()); + } + other => output.extend(iter::once(other)), + } + } + output + } +} + +pub(crate) trait IdentExt: Sized { + fn new_bool(value: bool, span: Span) -> Self; + fn with_span(self, span: Span) -> Self; +} + +impl IdentExt for Ident { + fn new_bool(value: bool, span: Span) -> Self { + Ident::new(&value.to_string(), span) + } + + fn with_span(mut self, span: Span) -> Self { + self.set_span(span); + self + } +} + +pub(crate) trait LiteralExt: Sized { + #[allow(unused)] + fn content_if_string(&self) -> Option; + fn content_if_string_like(&self) -> Option; + fn inner_value_to_string(&self) -> String; +} + +impl LiteralExt for Literal { + fn content_if_string(&self) -> Option { + match parse_str::(&self.to_string()).unwrap() { + Lit::Str(lit_str) => Some(lit_str.value()), + _ => None, + } + } + + fn content_if_string_like(&self) -> Option { + match parse_str::(&self.to_string()).unwrap() { + Lit::Str(lit_str) => Some(lit_str.value()), + Lit::Char(lit_char) => Some(lit_char.value().to_string()), + Lit::CStr(lit_cstr) => Some(lit_cstr.value().to_string_lossy().to_string()), + _ => None, + } + } + + fn inner_value_to_string(&self) -> String { + match parse_str::(&self.to_string()).unwrap() { + Lit::Str(lit_str) => lit_str.value(), + Lit::Char(lit_char) => lit_char.value().to_string(), + Lit::CStr(lit_cstr) => lit_cstr.value().to_string_lossy().to_string(), + Lit::Byte(lit_byte) => lit_byte.value().to_string(), + Lit::Int(lit_int) => lit_int.base10_digits().to_string(), + Lit::Float(lit_float) => lit_float.base10_digits().to_string(), + Lit::Bool(lit_bool) => lit_bool.value.to_string(), + _ => self.to_string(), + } + } +} + +pub(crate) trait WithSpanRangeExt { + fn with_span_range(self, span_range: SpanRange) -> Self; +} + +impl WithSpanExt for T { + fn with_span(self, span: Span) -> Self { + self.with_span_range(SpanRange::new_single(span)) + } +} + +pub(crate) trait WithSpanExt { + fn with_span(self, span: Span) -> Self; +} + +impl WithSpanExt for Literal { + fn with_span(mut self, span: Span) -> Self { + self.set_span(span); + self + } +} + +impl WithSpanExt for Punct { + fn with_span(mut self, span: Span) -> Self { + self.set_span(span); + self + } +} + +impl WithSpanExt for Group { + fn with_span(mut self, span: Span) -> Self { + self.set_span(span); + self + } +} diff --git a/src/internal_prelude.rs b/src/internal_prelude.rs index b91ed286..c02b5ca4 100644 --- a/src/internal_prelude.rs +++ b/src/internal_prelude.rs @@ -1,54 +1,30 @@ -pub(crate) use core::iter; -pub(crate) use proc_macro2::*; -pub(crate) use std::{collections::HashMap, str::FromStr}; -pub(crate) use syn::{parse_str, Error, Lit, Result}; - -pub(crate) use crate::command::*; -pub(crate) use crate::commands::*; -pub(crate) use crate::interpreter::*; -pub(crate) use crate::parsing::*; -pub(crate) use crate::string_conversion::*; - -pub(crate) struct Tokens(iter::Peekable<::IntoIter>); - -impl Tokens { - pub(crate) fn new(tokens: TokenStream) -> Self { - Self(tokens.into_iter().peekable()) - } - - pub(crate) fn peek(&mut self) -> Option<&TokenTree> { - self.0.peek() - } - - pub(crate) fn next(&mut self) -> Option { - self.0.next() - } - - pub(crate) fn next_as_ident(&mut self) -> Option { - match self.next() { - Some(TokenTree::Ident(ident)) => Some(ident), - _ => None, - } - } - - pub(crate) fn next_as_punct_matching(&mut self, char: char) -> Option { - match self.next() { - Some(TokenTree::Punct(punct)) if punct.as_char() == char => Some(punct), - _ => None, - } - } - - pub(crate) fn into_token_stream(self) -> TokenStream { - self.0.collect() - } -} - -pub(crate) trait SpanErrorExt { - fn error(self, message: impl std::fmt::Display) -> Error; -} - -impl SpanErrorExt for Span { - fn error(self, message: impl std::fmt::Display) -> Error { - Error::new(self, message) - } -} +pub(crate) use proc_macro2::{extra::*, *}; +pub(crate) use quote::ToTokens; +pub(crate) use std::{ + borrow::Borrow, + borrow::Cow, + cell::{Ref, RefCell, RefMut}, + collections::{BTreeMap, HashMap, HashSet}, + fmt::Debug, + iter, + marker::PhantomData, + ops::{Deref, DerefMut}, + rc::Rc, + str::FromStr, +}; +pub(crate) use syn::{ + buffer::Cursor, + ext::IdentExt as SynIdentExt, + parse::{ + discouraged::Speculative, Parse as SynParse, ParseBuffer as SynParseBuffer, + ParseStream as SynParseStream, Parser as SynParser, + }, + parse_str, + punctuated::Punctuated, + Error as SynError, Lit, LitFloat, LitInt, Result as SynResult, Token, +}; + +pub(crate) use crate::expressions::*; +pub(crate) use crate::extensions::*; +pub(crate) use crate::interpretation::*; +pub(crate) use crate::misc::*; diff --git a/src/interpretation/bindings.rs b/src/interpretation/bindings.rs new file mode 100644 index 00000000..a5378fa9 --- /dev/null +++ b/src/interpretation/bindings.rs @@ -0,0 +1,678 @@ +use super::*; + +use std::borrow::{Borrow, ToOwned}; +use std::cell::*; +use std::rc::Rc; + +pub(super) enum VariableState { + Uninitialized, + Value(Rc>), + Finished, +} + +impl VariableState { + pub(crate) fn define(&mut self, value: AnyValue) { + match self { + content @ VariableState::Uninitialized => { + *content = VariableState::Value(Rc::new(RefCell::new(value))); + } + VariableState::Value(_) => panic!("Cannot define existing variable"), + VariableState::Finished => panic!("Cannot define finished variable"), + } + } + + pub(crate) fn resolve( + &mut self, + variable_span: Span, + is_final: bool, + ownership: RequestedOwnership, + blocked_from_mutation: Option, + ) -> ExecutionResult> { + const UNITIALIZED_ERR: &str = "Cannot resolve uninitialized variable. This shouldn't be possible, because all variables are set on first use."; + const FINISHED_ERR: &str = "Cannot resolve finished variable. This shouldn't be possible, because is_final should be marked correctly. If you see this error, please report a bug to preinterpret on github with a reproduction case."; + + let span_range = variable_span.span_range(); + + // If blocked from mutation, we technically could allow is_final to work and + // return a fully owned value without observable mutation, + // but it's likely confusingly inconsistent, so it's better to just block it entirely. + let value_rc = if is_final && blocked_from_mutation.is_none() { + let content = std::mem::replace(self, VariableState::Finished); + match content { + VariableState::Uninitialized => panic!("{}", UNITIALIZED_ERR), + VariableState::Value(ref_cell) => match Rc::try_unwrap(ref_cell) { + Ok(ref_cell) => { + if matches!( + ownership, + RequestedOwnership::Concrete(ArgumentOwnership::Assignee { .. }) + ) { + return variable_span.control_flow_err("The final usage of a variable cannot be assigned to. You can use `let _ = ..` to discard a value."); + } + return Ok(Spanned( + LateBoundValue::Owned(LateBoundOwnedValue { + owned: ref_cell.into_inner(), + is_from_last_use: true, + }), + span_range, + )); + } + // It's currently referenced, proceed with normal late-bound resolution. + // e.g. + // * `let x = %[]; x.assert_eq(x, %[]);` - the final `x` resolves to a shared reference + // * `let x = %[]; x.assert_eq(x + %[], %[]);` - errors because the final `x` is shared but it needs to be owned + Err(rc) => rc, + }, + VariableState::Finished => panic!("{}", FINISHED_ERR), + } + } else { + match self { + VariableState::Uninitialized => panic!("{}", UNITIALIZED_ERR), + VariableState::Value(ref_cell) => Rc::clone(ref_cell), + VariableState::Finished => panic!("{}", FINISHED_ERR), + } + }; + let binding = VariableBinding { + data: value_rc, + variable_span, + }; + let resolved = match ownership { + RequestedOwnership::LateBound => binding.into_late_bound(), + RequestedOwnership::Concrete(ownership) => match ownership { + ArgumentOwnership::Owned => binding.into_transparently_cloned().map(|owned| { + LateBoundValue::Owned(LateBoundOwnedValue { + owned, + is_from_last_use: false, + }) + }), + ArgumentOwnership::Shared => binding + .into_shared() + .map(CopyOnWrite::shared_in_place_of_shared) + .map(LateBoundValue::CopyOnWrite), + ArgumentOwnership::Assignee { .. } => { + binding.into_mut().map(LateBoundValue::Mutable) + } + ArgumentOwnership::Mutable => binding.into_mut().map(LateBoundValue::Mutable), + ArgumentOwnership::CopyOnWrite | ArgumentOwnership::AsIs => binding + .into_shared() + .map(CopyOnWrite::shared_in_place_of_shared) + .map(LateBoundValue::CopyOnWrite), + }, + }; + let late_bound = if let Some(mutation_block_reason) = blocked_from_mutation { + match resolved { + Ok(LateBoundValue::Mutable(mutable)) => { + let reason_not_mutable = variable_span + .syn_error(mutation_block_reason.error_message("mutate this variable")); + Ok(LateBoundValue::Shared(LateBoundSharedValue::new( + mutable.into_shared(), + reason_not_mutable, + ))) + } + x => x, + } + } else { + resolved + }?; + Ok(Spanned(late_bound, span_range)) + } +} + +#[derive(Clone)] +pub(crate) struct VariableBinding { + data: Rc>, + variable_span: Span, +} + +#[allow(unused)] +impl VariableBinding { + /// Gets the cloned expression value + /// This only works if the value can be transparently cloned + pub(crate) fn into_transparently_cloned(self) -> ExecutionResult { + let span_range = self.variable_span.span_range(); + let shared = self.into_shared()?; + let value = shared.as_ref().try_transparent_clone(span_range)?; + Ok(value) + } + + fn into_mut(self) -> ExecutionResult { + MutableValue::new_from_variable(self).map_err(ExecutionInterrupt::ownership_error) + } + + fn into_shared(self) -> ExecutionResult { + SharedValue::new_from_variable(self).map_err(ExecutionInterrupt::ownership_error) + } + + fn into_late_bound(self) -> ExecutionResult { + match MutableValue::new_from_variable(self.clone()) { + Ok(value) => Ok(LateBoundValue::Mutable(value)), + Err(reason_not_mutable) => { + // If we get an error with a mutable and shared reference, a mutable reference must already exist. + // We can just propagate the error from taking the shared reference, it should be good enough. + let shared = self.into_shared()?; + Ok(LateBoundValue::Shared(LateBoundSharedValue::new( + shared, + reason_not_mutable, + ))) + } + } + } +} + +pub(crate) struct LateBoundOwnedValue { + pub(crate) owned: AnyValueOwned, + pub(crate) is_from_last_use: bool, +} + +/// A shared value where mutable access failed for a specific reason +pub(crate) struct LateBoundSharedValue { + pub(crate) shared: SharedValue, + pub(crate) reason_not_mutable: syn::Error, +} + +impl LateBoundSharedValue { + pub(crate) fn new(shared: SharedValue, reason_not_mutable: syn::Error) -> Self { + Self { + shared, + reason_not_mutable, + } + } +} + +/// Universal value type that can resolve to any concrete ownership type. +/// +/// Sometimes, a value can be accessed, but we don't yet know *how* we need to access it. +/// In this case, we attempt to load it with the most powerful access we can have, and convert it later. +/// +/// ## Example of requirement +/// For example, if we have `x[a].method()`, we first need to resolve the type of `x[a]` to know +/// whether the method needs `x[a]` to be a shared reference, mutable reference or an owned value. +/// +/// So instead, we take the most powerful access we can have for `x[a]`, and convert it later. +pub(crate) enum LateBoundValue { + /// An owned value that can be converted to any ownership type + Owned(LateBoundOwnedValue), + /// A copy-on-write value that can be converted to an owned value + CopyOnWrite(CopyOnWriteValue), + /// A mutable reference + Mutable(AnyValueMutable), + /// A shared reference where mutable access failed for a specific reason + Shared(LateBoundSharedValue), +} + +impl Spanned { + pub(crate) fn resolve(self, ownership: ArgumentOwnership) -> ExecutionResult { + ownership.map_from_late_bound(self) + } +} + +impl LateBoundValue { + pub(crate) fn map_any( + self, + map_shared: impl FnOnce(AnyValueShared) -> ExecutionResult, + map_mutable: impl FnOnce(AnyValueMutable) -> ExecutionResult, + map_owned: impl FnOnce(AnyValueOwned) -> ExecutionResult, + ) -> ExecutionResult { + Ok(match self { + LateBoundValue::Owned(owned) => LateBoundValue::Owned(LateBoundOwnedValue { + owned: map_owned(owned.owned)?, + is_from_last_use: owned.is_from_last_use, + }), + LateBoundValue::CopyOnWrite(copy_on_write) => { + LateBoundValue::CopyOnWrite(copy_on_write.map(map_shared, map_owned)?) + } + LateBoundValue::Mutable(mutable) => LateBoundValue::Mutable(map_mutable(mutable)?), + LateBoundValue::Shared(LateBoundSharedValue { + shared, + reason_not_mutable, + }) => LateBoundValue::Shared(LateBoundSharedValue::new( + map_shared(shared)?, + reason_not_mutable, + )), + }) + } + + pub(crate) fn as_value(&self) -> &AnyValue { + match self { + LateBoundValue::Owned(owned) => &owned.owned, + LateBoundValue::CopyOnWrite(cow) => cow.as_ref(), + LateBoundValue::Mutable(mutable) => mutable.as_ref(), + LateBoundValue::Shared(shared) => shared.shared.as_ref(), + } + } +} + +impl Deref for LateBoundValue { + type Target = AnyValue; + + fn deref(&self) -> &Self::Target { + self.as_value() + } +} + +pub(crate) type MutableValue = AnyValueMutable; +pub(crate) type AssigneeValue = AnyValueAssignee; + +/// A binding of a unique (mutable) reference to a value. +/// See [`ArgumentOwnership::Assignee`] for more details. +/// +/// If you need span information, wrap with `Spanned>`. +pub(crate) struct Assignee(pub Mutable); + +impl AssigneeValue { + pub(crate) fn set(&mut self, content: impl IntoAnyValue) { + *self.0 .0 = content.into_any_value(); + } +} + +impl IsValueContent for Assignee { + type Type = AnyType; + type Form = BeAssignee; +} + +impl IntoValueContent<'static> for Assignee { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + self.0 + .0 + .replace(|inner, emplacer| inner.as_mut_value().into_assignee(emplacer)) + } +} + +impl Deref for Assignee { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Assignee { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// A simple wrapper for a mutable reference to a value. +/// +/// Can be destructured as: `Mutable(cell): Mutable` +/// +/// If you need span information, wrap with `Spanned>`. +pub(crate) struct Mutable(pub(crate) MutableSubRcRefCell); + +impl Mutable { + pub(crate) fn into_shared(self) -> Shared { + Shared(self.0.into_shared()) + } + + #[allow(unused)] + pub(crate) fn map( + self, + value_map: impl for<'a> FnOnce(&'a mut T) -> &'a mut V, + ) -> Mutable { + Mutable(self.0.map(value_map)) + } + + pub(crate) fn try_map( + self, + value_map: impl for<'a> FnOnce(&'a mut T) -> Result<&'a mut V, E>, + ) -> Result, E> { + Ok(Mutable(self.0.try_map(value_map)?)) + } +} + +pub(crate) static MUTABLE_ERROR_MESSAGE: &str = + "The variable cannot be modified as it is already being modified"; + +impl Spanned<&mut AnyValueMutable> { + /// SAFETY: + /// * Must be paired with a call to `enable()` before any further use of the value. + /// * Must not use the value while disabled. + pub(crate) unsafe fn disable(&mut self) { + self.0 .0.disable(); + } + + /// SAFETY: + /// * Must only be used after a call to `disable()`. + /// + /// Returns an ownership error if re-enabling fails (e.g., due to conflicting borrows). + pub(crate) unsafe fn enable(&mut self) -> ExecutionResult<()> { + self.0 + .0 + .enable() + .map_err(|_| self.1.ownership_error(MUTABLE_ERROR_MESSAGE)) + } +} + +impl Spanned { + pub(crate) fn transparent_clone(&self) -> ExecutionResult { + let value = self.0.as_ref().try_transparent_clone(self.1)?; + Ok(value) + } +} + +impl AnyValueMutable { + pub(crate) fn new_from_owned(value: AnyValue) -> Self { + // Unwrap is safe because it's a new refcell + Mutable(MutableSubRcRefCell::new(Rc::new(RefCell::new(value))).unwrap()) + } + + fn new_from_variable(reference: VariableBinding) -> syn::Result { + Ok(Mutable(MutableSubRcRefCell::new(reference.data).map_err( + |_| reference.variable_span.syn_error(MUTABLE_ERROR_MESSAGE), + )?)) + } +} + +impl IsValueContent for Mutable { + type Type = AnyType; + type Form = BeMutable; +} + +impl IntoValueContent<'static> for Mutable { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + self.0 + .replace(|inner, emplacer| inner.as_mut_value().into_mutable(emplacer)) + } +} + +impl AsMut for Mutable { + fn as_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl AsRef for Mutable { + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl Deref for Mutable { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Mutable { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub(crate) type SharedValue = AnyValueShared; + +/// A simple wrapper for a shared (immutable) reference to a value. +/// +/// Can be destructured as: `Shared(cell): Shared` +/// +/// If you need span information, wrap with `Spanned>`. +pub(crate) struct Shared(pub(crate) SharedSubRcRefCell); + +#[allow(unused)] +impl Shared { + pub(crate) fn clone(this: &Shared) -> Self { + Shared(SharedSubRcRefCell::clone(&this.0)) + } + + pub(crate) fn try_map( + self, + value_map: impl for<'a> FnOnce(&'a T) -> Result<&'a V, E>, + ) -> Result, E> { + Ok(Shared(self.0.try_map(value_map)?)) + } + + pub(crate) fn map(self, value_map: impl FnOnce(&T) -> &V) -> Shared { + Shared(self.0.map(value_map)) + } +} + +pub(crate) static SHARED_ERROR_MESSAGE: &str = + "The variable cannot be read as it is already being modified"; + +impl Spanned<&mut AnyValueShared> { + /// SAFETY: + /// * Must be paired with a call to `enable()` before any further use of the value. + /// * Must not use the value while disabled. + pub(crate) unsafe fn disable(&mut self) { + self.0 .0.disable(); + } + + /// SAFETY: + /// * Must only be used after a call to `disable()`. + /// + /// Returns an ownership error if re-enabling fails (e.g., due to conflicting borrows). + pub(crate) unsafe fn enable(&mut self) -> ExecutionResult<()> { + self.0 + .0 + .enable() + .map_err(|_| self.1.ownership_error(SHARED_ERROR_MESSAGE)) + } +} + +impl Spanned { + pub(crate) fn transparent_clone(&self) -> ExecutionResult { + let value = self.0.as_ref().try_transparent_clone(self.1)?; + Ok(value) + } +} + +impl AnyValueShared { + pub(crate) fn new_from_owned(value: AnyValue) -> Self { + // Unwrap is safe because it's a new refcell + Shared(SharedSubRcRefCell::new(Rc::new(RefCell::new(value))).unwrap()) + } + + pub(crate) fn infallible_clone(&self) -> AnyValue { + self.0.clone() + } + + fn new_from_variable(reference: VariableBinding) -> syn::Result { + Ok(Shared(SharedSubRcRefCell::new(reference.data).map_err( + |_| reference.variable_span.syn_error(SHARED_ERROR_MESSAGE), + )?)) + } +} + +impl IsValueContent for Shared { + type Type = AnyType; + type Form = BeShared; +} + +impl IntoValueContent<'static> for Shared { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + self.0 + .replace(|inner, emplacer| inner.as_ref_value().into_shared(emplacer)) + } +} + +impl AsRef for Shared { + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl Deref for Shared { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// Copy-on-write value that can be either owned or shared +pub(crate) struct CopyOnWrite { + pub(crate) inner: CopyOnWriteInner, +} + +pub(crate) enum CopyOnWriteInner { + /// An owned value that can be used directly + Owned(Owned), + /// For use when the CopyOnWrite value effectively represents the owned value (post-clone). + /// In this case, returning a Cow is just an optimization and we can always clone infallibly. + SharedWithInfallibleCloning(Shared), + /// For use when the CopyOnWrite value represents a pre-cloned read-only value. + /// A transparent clone may fail in this case at use time. + SharedWithTransparentCloning(Shared), +} + +impl CopyOnWrite { + pub(crate) fn shared_in_place_of_owned(shared: Shared) -> Self { + Self { + inner: CopyOnWriteInner::SharedWithInfallibleCloning(shared), + } + } + + pub(crate) fn shared_in_place_of_shared(shared: Shared) -> Self { + Self { + inner: CopyOnWriteInner::SharedWithTransparentCloning(shared), + } + } + + pub(crate) fn owned(owned: Owned) -> Self { + Self { + inner: CopyOnWriteInner::Owned(owned), + } + } + + #[allow(unused)] + pub(crate) fn extract_owned( + self, + map: impl FnOnce(T::Owned) -> Result, + ) -> Result { + match self.inner { + CopyOnWriteInner::Owned(value) => match map(value) { + Ok(mapped) => Ok(mapped), + Err(other) => Err(Self { + inner: CopyOnWriteInner::Owned(other), + }), + }, + other => Err(Self { inner: other }), + } + } + + pub(crate) fn acts_as_shared_reference(&self) -> bool { + match &self.inner { + CopyOnWriteInner::Owned { .. } => false, + CopyOnWriteInner::SharedWithInfallibleCloning { .. } => false, + CopyOnWriteInner::SharedWithTransparentCloning { .. } => true, + } + } + + pub(crate) fn map( + self, + map_shared: impl FnOnce(Shared) -> ExecutionResult>, + map_owned: impl FnOnce(Owned) -> ExecutionResult>, + ) -> ExecutionResult> { + let inner = match self.inner { + CopyOnWriteInner::Owned(owned) => CopyOnWriteInner::Owned(map_owned(owned)?), + CopyOnWriteInner::SharedWithInfallibleCloning(shared) => { + CopyOnWriteInner::SharedWithInfallibleCloning(map_shared(shared)?) + } + CopyOnWriteInner::SharedWithTransparentCloning(shared) => { + CopyOnWriteInner::SharedWithTransparentCloning(map_shared(shared)?) + } + }; + Ok(CopyOnWrite { inner }) + } + + pub(crate) fn map_into( + self, + map_shared: impl FnOnce(Shared) -> U, + map_owned: impl FnOnce(Owned) -> U, + ) -> U { + match self.inner { + CopyOnWriteInner::Owned(owned) => map_owned(owned), + CopyOnWriteInner::SharedWithInfallibleCloning(shared) => map_shared(shared), + CopyOnWriteInner::SharedWithTransparentCloning(shared) => map_shared(shared), + } + } +} + +impl Spanned<&mut CopyOnWrite> { + /// SAFETY: + /// * Must be paired with a call to `enable()` before any further use of the value. + /// * Must not use the value while disabled. + pub(crate) unsafe fn disable(&mut self) { + match &mut self.0.inner { + CopyOnWriteInner::Owned(_) => {} + CopyOnWriteInner::SharedWithInfallibleCloning(shared) => { + shared.spanned(self.1).disable() + } + CopyOnWriteInner::SharedWithTransparentCloning(shared) => { + shared.spanned(self.1).disable() + } + } + } + + /// SAFETY: + /// * Must only be used after a call to `disable()`. + /// + /// Returns an ownership error if re-enabling fails (e.g., due to conflicting borrows). + pub(crate) unsafe fn enable(&mut self) -> ExecutionResult<()> { + match &mut self.0.inner { + CopyOnWriteInner::Owned(_) => Ok(()), + CopyOnWriteInner::SharedWithInfallibleCloning(shared) => { + shared.spanned(self.1).enable() + } + CopyOnWriteInner::SharedWithTransparentCloning(shared) => { + shared.spanned(self.1).enable() + } + } + } +} + +impl AsRef for CopyOnWrite { + fn as_ref(&self) -> &T { + self + } +} + +impl Deref for CopyOnWrite { + type Target = T; + + fn deref(&self) -> &T { + match self.inner { + CopyOnWriteInner::Owned(ref owned) => (*owned).borrow(), + CopyOnWriteInner::SharedWithInfallibleCloning(ref shared) => shared.as_ref(), + CopyOnWriteInner::SharedWithTransparentCloning(ref shared) => shared.as_ref(), + } + } +} + +impl CopyOnWrite { + /// Converts to owned, cloning if necessary + pub(crate) fn clone_to_owned_infallible(self) -> AnyValueOwned { + match self.inner { + CopyOnWriteInner::Owned(owned) => owned, + CopyOnWriteInner::SharedWithInfallibleCloning(shared) => shared.infallible_clone(), + CopyOnWriteInner::SharedWithTransparentCloning(shared) => shared.infallible_clone(), + } + } + + /// Converts to owned, using transparent clone for shared values where cloning was not requested + pub(crate) fn clone_to_owned_transparently( + self, + span: SpanRange, + ) -> ExecutionResult { + match self.inner { + CopyOnWriteInner::Owned(owned) => Ok(owned), + CopyOnWriteInner::SharedWithInfallibleCloning(shared) => Ok(shared.infallible_clone()), + CopyOnWriteInner::SharedWithTransparentCloning(shared) => { + let value = shared.as_ref().try_transparent_clone(span)?; + Ok(value) + } + } + } + + /// Converts to shared reference + pub(crate) fn into_shared(self) -> SharedValue { + match self.inner { + CopyOnWriteInner::Owned(owned) => SharedValue::new_from_owned(owned), + CopyOnWriteInner::SharedWithInfallibleCloning(shared) => shared, + CopyOnWriteInner::SharedWithTransparentCloning(shared) => shared, + } + } +} + +pub(crate) type CopyOnWriteValue = CopyOnWrite; diff --git a/src/interpretation/control_flow_pass.rs b/src/interpretation/control_flow_pass.rs new file mode 100644 index 00000000..b6df240f --- /dev/null +++ b/src/interpretation/control_flow_pass.rs @@ -0,0 +1,246 @@ +use super::*; + +pub(super) struct ControlFlowAnalyzer<'a> { + definitions: &'a Arena, + references: &'a mut Arena, + scopes: &'a Arena, + segments: &'a Arena, +} + +impl<'a> ControlFlowAnalyzer<'a> { + pub(super) fn new( + definitions: &'a Arena, + references: &'a mut Arena, + scopes: &'a Arena, + segments: &'a Arena, + ) -> ControlFlowAnalyzer<'a> { + ControlFlowAnalyzer { + definitions, + references, + scopes, + segments, + } + } + + pub(super) fn analyze_and_update_references(&mut self) -> MarkFinalUseOutput { + self.mark_last_reference_uses() + } + + fn mark_last_reference_uses(&mut self) -> MarkFinalUseOutput { + // ALGORITHM OVERVIEW + // ================== + // + // The goal of this algorithm is to efficiently flag variable references as + // "is_final_reference" if they are definitely the last use of a variable + // in any possible execution path. + // + // This is quite subtle because of branching control flow, and loops. + // + // For each variable definition, this algorithm works in two phases: + // 1. Identify segments where references occur, and for each: + // - Add the last reference in the segment as a candidate for last use + // - Mark all previous segments/references as not final. + // This is recursive as "previous siblings" of self and each ancestor segment + // We mark nodes as Handled | NotFinal to save repeated work. + // 2. For each candidate, check if it's actually a last use: + // - Check it and all its relevant ancestor segments are: + // - Not marked NotFinal + // - Not loops + // - If all these checks pass, then mark it as a final reference. + + #[cfg(feature = "debug")] + let mut output = HashMap::new(); + + for (definition_id, definition) in self.definitions.iter() { + let ancestor_scopes = self.create_ancestor_scopes(definition.scope); + + let (last_use_candidates, markers) = self.last_reference_first_phase( + definition_id, + &definition.references, + &ancestor_scopes, + ); + + self.mark_valid_last_references( + definition_id, + &ancestor_scopes, + &markers, + &last_use_candidates, + ); + + #[cfg(feature = "debug")] + output.insert(definition_id, markers); + } + + #[cfg(feature = "debug")] + return output; + } + + fn create_ancestor_scopes(&self, scope: ScopeId) -> HashSet { + let mut scopes = HashSet::new(); + let mut current = self.scopes.get(scope).parent; + while let Some(scope) = current { + scopes.insert(scope); + current = self.scopes.get(scope).parent; + } + scopes + } + + /// Phase 1: + /// * Creates a shortlist of possible last-use references + /// * Marks segments as NotFinal + /// + /// In phase 2, we will validate each candidate. + fn last_reference_first_phase( + &mut self, + definition_id: VariableDefinitionId, + references: &[VariableReferenceId], + ancestor_scopes: &HashSet, + ) -> ( + Vec, + HashMap, + ) { + let mut last_use_candidates = Vec::new(); + let mut markers = HashMap::new(); + let mut segments_with_references = HashSet::new(); + for reference_id in references.iter() { + let reference = self.references.get(*reference_id); + segments_with_references.insert(reference.segment); + } + + for segment_id in segments_with_references { + let segment = self.segments.get(segment_id); + let mut reference_ids = match segment.children { + SegmentChildren::Sequential { ref children, .. } => children + .iter() + .filter_map(|c| match c { + ControlFlowChild::VariableReference(ref_id, def_id) + if *def_id == definition_id => + { + Some(*ref_id) + } + _ => None, + }) + .collect::>(), + SegmentChildren::PathBased { .. } => { + panic!("Segment was marked as having references, but is path based") + } + }; + // The last reference in the segment is a candidate for last use + assert!( + !reference_ids.is_empty(), + "Segment was marked as having references, but none were found" + ); + let last_reference_id = reference_ids.pop().unwrap(); + last_use_candidates.push(last_reference_id); + + // For each segment level between current up to the scope of the variable definition: + // - Mark all previous segments at its level as NotFinal + let mut parent_segment_id = Some(segment_id); + let mut own_child_id = + ControlFlowChild::VariableReference(last_reference_id, definition_id); + + while let Some(parent_seg_id) = parent_segment_id { + let parent = self.segments.get(parent_seg_id); + let marker = markers.get(&own_child_id); + match marker { + Some(SegmentMarker::AlreadyHandled | SegmentMarker::NotFinal) => { + break; + } + None => { + markers.insert(own_child_id, SegmentMarker::AlreadyHandled); + } + } + + // Mark all previous siblings as NotFinal + match parent.children { + SegmentChildren::PathBased { + ref node_previous_map, + } => { + // Walk up the tree of previous siblings, marking all as NotFinal + let own_seg_id = match own_child_id { + ControlFlowChild::Segment(id) => id, + _ => panic!("The child of a path-based segment must be a segment"), + }; + let mut previous = node_previous_map.get(&own_seg_id).unwrap(); + while let Some(prev_id) = *previous { + markers.insert( + ControlFlowChild::Segment(prev_id), + SegmentMarker::NotFinal, + ); + previous = node_previous_map.get(&prev_id).unwrap(); + } + } + SegmentChildren::Sequential { ref children } => { + let mut before_current_segment = false; + for sibling in children.iter().rev() { + if sibling == &own_child_id { + before_current_segment = true; + continue; + } + if before_current_segment { + markers.insert(*sibling, SegmentMarker::NotFinal); + } + } + } + } + if ancestor_scopes.contains(&parent.scope) { + break; + } + own_child_id = ControlFlowChild::Segment(parent_seg_id); + parent_segment_id = parent.parent; + } + } + (last_use_candidates, markers) + } + + fn mark_valid_last_references( + &mut self, + definition_id: VariableDefinitionId, + ancestor_scopes: &HashSet, + markers: &HashMap, + last_use_candidates: &[VariableReferenceId], + ) { + for candidate_id in last_use_candidates.iter() { + if let Some(SegmentMarker::NotFinal) = markers.get( + &ControlFlowChild::VariableReference(*candidate_id, definition_id), + ) { + continue; + } + let candidate = self.references.get_mut(*candidate_id); + let mut possibly_final = true; + let mut current_segment_id = Some(candidate.segment); + while let Some(seg_id) = current_segment_id { + let current_segment = self.segments.get(seg_id); + if ancestor_scopes.contains(¤t_segment.scope) { + break; + } + if current_segment.segment_kind.is_looping() { + possibly_final = false; + break; + } + if let Some(SegmentMarker::NotFinal) = + markers.get(&ControlFlowChild::Segment(seg_id)) + { + possibly_final = false; + break; + } + current_segment_id = current_segment.parent; + } + if possibly_final { + candidate.is_final_reference = true; + } + } + } +} + +#[cfg(feature = "debug")] +pub(super) type MarkFinalUseOutput = + HashMap>; +#[cfg(not(feature = "debug"))] +pub(super) type MarkFinalUseOutput = (); + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub(super) enum SegmentMarker { + AlreadyHandled, + NotFinal, +} diff --git a/src/interpretation/input_handler.rs b/src/interpretation/input_handler.rs new file mode 100644 index 00000000..372cff19 --- /dev/null +++ b/src/interpretation/input_handler.rs @@ -0,0 +1,128 @@ +use super::*; +use slotmap::{new_key_type, SlotMap}; + +new_key_type! { + pub(crate) struct ParserHandle; +} + +pub(crate) struct InputHandler { + parsers: SlotMap>, + parser_stack: Vec, +} + +impl InputHandler { + pub(crate) fn new() -> Self { + Self { + parsers: SlotMap::with_key(), + parser_stack: Vec::new(), + } + } + + /// SAFETY: + /// * Must be paired with a later `finish_parse` call, even in the face of control flow interrupts. + /// * `finish_parse` must be called whilst the input is still alive. + /// + /// TODO: Replace this with returning a ParseGuard which captures the lifetime and handles calling + /// `finish_parse` automatically when dropped, to avoid misuse. + /// EDIT: That doesn't work because the ParseGuard would need to borrow InputHandler mutably, + /// preventing further use of InputHandler while the guard is alive. + pub(super) unsafe fn start_parse(&mut self, input: ParseStream) -> ParserHandle { + let parse_stack = ParseStack::new(input); + + self.parsers.insert(std::mem::transmute::< + ParseStack<'_, Output>, + ParseStack<'static, Output>, + >(parse_stack)) + } + + /// SAFETY: + /// * Must be called after a prior `start_parse` call, and while the input is still alive. + pub(super) unsafe fn finish_parse(&mut self, handle: ParserHandle) -> Result<(), ParseError> { + let stack = self + .parsers + .get(handle) + .expect("finish_parse called with invalid handle"); + + // Check for unclosed groups before removing the parser + if let Some(delimiter) = stack.innermost_active_group_delimiter() { + return stack.parse_err(format!( + "a call to close '{}' is required, because there is an unclosed group", + delimiter.description_of_close() + )); + } + + self.parsers.remove(handle); + Ok(()) + } + + /// SAFETY: + /// * Must be paired with pop_current_handle + /// * finish_parse of the handle must not be called before pop_current_handle is called. + pub(super) unsafe fn push_current_handle(&mut self, handle: ParserHandle) { + self.parser_stack.push(handle); + } + + /// SAFETY: Must be paired with push_current_handle + pub(super) unsafe fn pop_current_handle(&mut self, handle: ParserHandle) { + let popped_handle = self.parser_stack.pop(); + assert_eq!( + popped_handle, + Some(handle), + "Popped handle does not match the provided handle" + ); + } + + pub(super) fn get(&mut self, handle: ParserHandle) -> Option<&mut ParseStack<'static, Output>> { + self.parsers.get_mut(handle) + } + + pub(super) fn current_stack(&mut self) -> &mut ParseStack<'static, Output> { + match self.parser_stack.last() { + Some(parser_handle) => self + .parsers + .get_mut(*parser_handle) + .expect("Parser handle in stack must be valid"), + None => { + panic!("There is no input stream available to read from. Consuming from an input stream should only be possible when a parser is available.") + } + } + } + + pub(super) fn current_input<'a>(&'a mut self) -> ParseStream<'a, Output> { + self.current_stack().current() + } + + /// Start a fork on all active parsers. + /// + /// # Safety + /// Must be paired with either `commit_fork` or `rollback_fork`. + pub(super) unsafe fn start_fork(&mut self) { + for (_, parser) in self.parsers.iter_mut() { + parser.start_fork(); + } + } + + /// Commit the fork on all active parsers. + /// + /// # Safety + /// Must be called after `start_fork`. + pub(super) unsafe fn commit_fork(&mut self) { + for (_, parser) in self.parsers.iter_mut() { + if parser.is_forked() { + parser.commit_fork(); + } + } + } + + /// Rollback the fork on all active parsers. + /// + /// # Safety + /// Must be called after `start_fork`. + pub(super) unsafe fn rollback_fork(&mut self) { + for (_, parser) in self.parsers.iter_mut() { + if parser.is_forked() { + parser.rollback_fork(); + } + } + } +} diff --git a/src/interpretation/interpret_traits.rs b/src/interpretation/interpret_traits.rs new file mode 100644 index 00000000..ccae002a --- /dev/null +++ b/src/interpretation/interpret_traits.rs @@ -0,0 +1,8 @@ +use crate::internal_prelude::*; + +// This trait isn't so important any more. It can probably be removed in future. +// It is typically used for things which output directly to the interpreter's output +// stream, rather than returning values. +pub(crate) trait Interpret: Sized { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()>; +} diff --git a/src/interpretation/interpreter.rs b/src/interpretation/interpreter.rs new file mode 100644 index 00000000..8416046a --- /dev/null +++ b/src/interpretation/interpreter.rs @@ -0,0 +1,460 @@ +use super::*; + +pub(crate) struct Interpreter { + config: InterpreterConfig, + scope_definitions: ScopeDefinitions, + scopes: Vec, + no_mutation_above: Vec<(ScopeId, MutationBlockReason)>, + output_handler: OutputHandler, + input_handler: InputHandler, +} + +impl Interpreter { + pub(crate) fn new(scope_definitions: ScopeDefinitions) -> Self { + let root_scope_id = scope_definitions.root_scope; + let mut interpreter = Self { + config: Default::default(), + scope_definitions, + scopes: vec![], + no_mutation_above: vec![], + output_handler: OutputHandler::new(OutputStream::new()), + input_handler: InputHandler::new(), + }; + interpreter.enter_scope_inner(root_scope_id, false); + interpreter + } + + fn scope_mut(&mut self, id: ScopeId) -> &mut RuntimeScope { + self.scopes + .iter_mut() + .rev() + .find(|s| s.id == id) + .expect("Scope data not found in stack") + } + + pub(crate) fn current_scope_id(&self) -> ScopeId { + self.scopes.last().unwrap().id + } + + pub(crate) fn enter_scope(&mut self, id: ScopeId) { + self.enter_scope_inner(id, true); + } + + pub(crate) fn enter_scope_starting_with_revertible_segment( + &mut self, + scope_id: ScopeId, + catch_location_id: CatchLocationId, + revertible_segment: impl FnOnce(&mut Self) -> ExecutionResult, + guard_clause: Option ExecutionResult>, + reason: MutationBlockReason, + ) -> ExecutionResult> { + self.enter_scope_inner(scope_id, true); + self.no_mutation_above.push((scope_id, reason)); + unsafe { + // SAFETY: This is paired with `unfreeze_existing` below, + // without any early returns in the middle + self.output_handler.freeze_existing(reason); + // SAFETY: This is paired with `commit_fork` or `rollback_fork` below + self.input_handler.start_fork(); + } + let revertible_result = revertible_segment(self); + let result = self.convert_revertible_result( + revertible_result, + guard_clause, + catch_location_id, + scope_id, + ); + unsafe { + // SAFETY: This is paired with `start_fork` above + match &result { + Ok(AttemptOutcome::Completed(_)) => self.input_handler.commit_fork(), + Ok(AttemptOutcome::Reverted) | Err(_) => self.input_handler.rollback_fork(), + } + // SAFETY: This is paired with `freeze_existing` above + self.output_handler.unfreeze_existing(); + } + self.no_mutation_above.pop(); + result + } + + // Creating a separate function makes it easier to verify safety invariants + // around early returns + fn convert_revertible_result( + &mut self, + revertible_result: ExecutionResult, + guard_clause: Option ExecutionResult>, + catch_location_id: CatchLocationId, + scope_id: ScopeId, + ) -> ExecutionResult> { + match revertible_result { + Ok(value) => { + let guard_result = if let Some(guard_clause) = guard_clause { + guard_clause(self) + } else { + Ok(true) + }; + // If a guard clause errors, we treat this as a standard error + // outside of the attempt arm catch. BUT we should still revert + // any mutations made in the arm. + match guard_result { + Ok(true) => Ok(AttemptOutcome::Completed(value)), + Ok(false) => Ok(AttemptOutcome::Reverted), + Err(err) => Err(err), + } + } + Err(err) if err.is_catchable_by_attempt_block(catch_location_id) => { + self.handle_catch(scope_id); + Ok(AttemptOutcome::Reverted) + } + Err(mut err) => { + if let Some((kind, error)) = err.error_mut() { + *error = core::mem::take(error).add_context_if_none(format!("NOTE: {} is not caught by an attempt block. If you wish to catch this, detect it before it is thrown and use the `revert` statement.", kind.as_str().indefinite_articled(true))); + } + Err(err) + } + } + } + + fn enter_scope_inner(&mut self, id: ScopeId, check_parent: bool) { + let new_scope = self.scope_definitions.scopes.get(id); + if check_parent { + assert!(new_scope.parent == Some(self.current_scope_id())); + } + let variables = { + let mut map = HashMap::new(); + for definition_id in new_scope.definitions.iter() { + map.insert(*definition_id, VariableState::Uninitialized); + } + map + }; + self.scopes.push(RuntimeScope { id, variables }); + } + + pub(crate) fn exit_scope(&mut self, scope_id: ScopeId) { + assert!( + scope_id == self.current_scope_id(), + "Attempted to exit scope {:?} but current scope is {:?}", + scope_id, + self.current_scope_id() + ); + self.scopes.pop(); + } + + pub(crate) fn catch_control_flow( + &mut self, + input: ExecutionResult>, + catch_location_id: CatchLocationId, + return_to_scope: ScopeId, + ) -> ExecutionResult> { + let output = match input { + Ok(value) => Ok(ExecutionOutcome::Value(value)), + Err(interrupt) => interrupt.into_outcome::(catch_location_id), + }; + if let Ok(ExecutionOutcome::ControlFlow(_)) = &output { + self.handle_catch(return_to_scope) + } + output + } + + fn handle_catch(&mut self, result_scope: ScopeId) { + // Note: OutputHandler safety upon error control flow is handled in that code. + while self.current_scope_id() != result_scope { + self.exit_scope(self.current_scope_id()); + } + } + + pub(crate) fn define_variable(&mut self, definition_id: VariableDefinitionId, value: AnyValue) { + let definition = self.scope_definitions.definitions.get(definition_id); + let scope_data = self.scope_mut(definition.scope); + scope_data.define_variable(definition_id, value) + } + + pub(crate) fn resolve( + &mut self, + variable: &VariableReference, + ownership: RequestedOwnership, + ) -> ExecutionResult> { + let reference = self.scope_definitions.references.get(variable.id); + let (definition, span, is_final) = ( + reference.definition, + reference.reference_name_span, + reference.is_final_reference, + ); + let blocked_from_mutation = match self.no_mutation_above.last() { + Some(&(no_mutation_above_scope, reason)) => 'result: { + for scope in self.scopes.iter().rev() { + match scope.id { + id if id == reference.definition_scope => break 'result None, + id if id == no_mutation_above_scope => break 'result Some(reason), + _ => {} + } + } + panic!("Definition scope expected in scope stack due to control flow analysis"); + } + None => None, + }; + let scope_data = self.scope_mut(reference.definition_scope); + scope_data.resolve(definition, span, is_final, ownership, blocked_from_mutation) + } + + pub(crate) fn start_iteration_counter<'s, S: HasSpanRange>( + &self, + span_source: &'s S, + ) -> IterationCounter<'s, S> { + IterationCounter { + span_source, + count: 0, + iteration_limit: self.config.iteration_limit, + } + } + + pub(crate) fn set_iteration_limit(&mut self, limit: Option) { + self.config.iteration_limit = limit; + } + + // Input + pub(crate) fn start_parse( + &mut self, + stream: OutputStream, + f: impl FnOnce(&mut Interpreter, ParserHandle) -> ExecutionResult, + ) -> ExecutionResult { + stream.parse_with(|input| { + let handle = unsafe { + // SAFETY: This is paired with `finish_parse` below, + // without any early returns in the middle + self.input_handler.start_parse(input) + }; + let result = self.parse_with(handle, |interpreter| f(interpreter, handle)); + let finish_result = unsafe { + // SAFETY: This is paired with `start_parse` above + self.input_handler.finish_parse(handle) + }; + // Combine results: if original failed, return that error; otherwise check finish_result + match (result, finish_result) { + (Ok(value), Ok(())) => Ok(value), + (Err(err), _) => Err(err), + (Ok(_), Err(err)) => Err(err.into()), + } + }) + } + + pub(crate) fn parse_with( + &mut self, + handle: ParserHandle, + f: impl FnOnce(&mut Interpreter) -> ExecutionResult, + ) -> ExecutionResult { + unsafe { + // SAFETY: This is paired with `pop_current_handle` below, + // without any early returns in the middle + self.input_handler.push_current_handle(handle); + } + let result = f(self); + unsafe { + // SAFETY: This is paired with `push_current_handle` above, + // without any early returns in the middle + self.input_handler.pop_current_handle(handle); + } + result + } + + pub(crate) fn parser( + &mut self, + handle: ParserHandle, + error_span_range: SpanRange, + ) -> ExecutionResult> { + let stack = self + .input_handler + .get(handle) + .ok_or_else(|| error_span_range.value_error("This parser is no longer available"))?; + Ok(stack.current()) + } + + pub(crate) fn parse_group( + &mut self, + required_delimiter: Option, + f: impl FnOnce(&mut Interpreter, Delimiter, DelimSpan) -> ExecutionResult, + ) -> ExecutionResult { + let (delimiter, delim_span) = self + .input_handler + .current_stack() + .parse_and_enter_group(required_delimiter)?; + let result = f(self, delimiter, delim_span); + self.input_handler + .current_stack() + .exit_group(Some(delimiter)) + .expect("exit_group can't fail since we pass the same delimiter we got from parse_and_enter_group"); + result + } + + /// Enter a group with the specified delimiter. + /// Must be paired with `exit_input_group`. + pub(crate) fn enter_input_group( + &mut self, + required_delimiter: Option, + ) -> ExecutionResult<(Delimiter, DelimSpan)> { + self.input_handler + .current_stack() + .parse_and_enter_group(required_delimiter) + .map_err(|e| e.into()) + } + + /// Exit the current input group. + /// Must be paired with a prior `enter_input_group`. + pub(crate) fn exit_input_group( + &mut self, + expected_delimiter: Option, + ) -> ExecutionResult<()> { + self.input_handler + .current_stack() + .exit_group(expected_delimiter) + .map_err(|e| e.into()) + } + + /// Returns true if there is an active input group that can be exited. + pub(crate) fn has_active_input_group(&mut self) -> bool { + self.input_handler.current_stack().has_active_group() + } + + pub(crate) fn input<'a>(&'a mut self) -> ParseStream<'a, Output> { + self.input_handler.current_input() + } + + // Output + pub(crate) fn in_output_group( + &mut self, + delimiter: Delimiter, + span: Span, + f: F, + ) -> ExecutionResult + where + F: FnOnce(&mut Interpreter) -> ExecutionResult, + { + unsafe { + // SAFETY: This is paired with `finish_inner_buffer_as_group` + self.output_handler.start_inner_buffer(); + } + let result = f(self); + unsafe { + // SAFETY: This is paired with `start_inner_buffer`, + // even if `f` returns an Err propogating a control flow interrupt. + self.output_handler + .finish_inner_buffer_as_group(delimiter, span); + } + result + } + + pub(crate) fn capture_output(&mut self, f: F) -> ExecutionResult + where + F: FnOnce(&mut Interpreter) -> ExecutionResult<()>, + { + unsafe { + // SAFETY: This is paired with `finish_inner_buffer_as_separate_stream` + self.output_handler.start_inner_buffer(); + } + let result = f(self); + let output = unsafe { + // SAFETY: This is paired with `start_inner_buffer`, + // even if `f` returns an Err propogating a control flow interrupt. + self.output_handler.finish_inner_buffer_as_separate_stream() + }; + let () = result?; + Ok(output) + } + + pub(crate) fn output( + &mut self, + span_source: &impl HasSpanRange, + ) -> ExecutionResult<&mut OutputStream> { + self.output_handler.current_output_mut(span_source) + } + + pub(crate) fn complete(self) -> OutputStream { + self.output_handler.complete() + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum MutationBlockReason { + AttemptRevertibleSegment, +} + +impl MutationBlockReason { + pub(crate) fn error_message(&self, mutation_kind: &str) -> String { + match self { + MutationBlockReason::AttemptRevertibleSegment => { + format!("It is not possible to {mutation_kind} here. An attempt arm is in two parts: {{ /* revertible */ }} => {{ /* unconditional */ }}. You may define variables in the revertible part, but all mutations of state outside the attempt block must be moved to the unconditional part") + } + } + } +} + +#[must_use] +pub(crate) enum AttemptOutcome { + Completed(T), + Reverted, +} + +struct RuntimeScope { + id: ScopeId, + variables: HashMap, +} + +impl RuntimeScope { + fn define_variable(&mut self, definition_id: VariableDefinitionId, value: AnyValue) { + self.variables + .get_mut(&definition_id) + .expect("Variable data not found in scope") + .define(value); + } + + fn resolve( + &mut self, + definition_id: VariableDefinitionId, + span: Span, + is_final: bool, + ownership: RequestedOwnership, + blocked_from_mutation: Option, + ) -> ExecutionResult> { + self.variables + .get_mut(&definition_id) + .expect("Variable data not found in scope") + .resolve(span, is_final, ownership, blocked_from_mutation) + } +} + +pub(crate) struct IterationCounter<'a, S: HasSpanRange> { + span_source: &'a S, + count: usize, + iteration_limit: Option, +} + +impl IterationCounter<'_, S> { + pub(crate) fn increment_and_check(&mut self) -> ExecutionResult<()> { + self.count += 1; + self.check() + } + + pub(crate) fn check(&self) -> ExecutionResult<()> { + if let Some(limit) = self.iteration_limit { + if self.count > limit { + return self.span_source.control_flow_err(format!("Iteration limit of {} exceeded.\nIf needed, the limit can be reconfigured with None.configure_preinterpret(%{{ iteration_limit: XXX }})", limit)); + } + } + Ok(()) + } +} + +pub(crate) struct InterpreterConfig { + iteration_limit: Option, +} + +pub(crate) const DEFAULT_ITERATION_LIMIT: usize = 1000; +pub(crate) const DEFAULT_ITERATION_LIMIT_STR: &str = "1000"; + +impl Default for InterpreterConfig { + fn default() -> Self { + Self { + iteration_limit: Some(DEFAULT_ITERATION_LIMIT), + } + } +} diff --git a/src/interpretation/mod.rs b/src/interpretation/mod.rs new file mode 100644 index 00000000..adb537e7 --- /dev/null +++ b/src/interpretation/mod.rs @@ -0,0 +1,29 @@ +mod bindings; +mod control_flow_pass; +mod input_handler; +mod interpret_traits; +mod interpreter; +mod output_handler; +mod output_parse_utilities; +mod output_stream; +mod parse_template_stream; +mod refs; +mod source_parsing; +mod source_stream; +mod variable; + +// Marked as use for expression sub-modules to use with a `use super::*` statement +use crate::internal_prelude::*; +pub(crate) use bindings::*; +use control_flow_pass::*; +pub(crate) use input_handler::*; +pub(crate) use interpret_traits::*; +pub(crate) use interpreter::*; +use output_handler::*; +pub(crate) use output_parse_utilities::*; +pub(crate) use output_stream::*; +pub(crate) use parse_template_stream::*; +pub(crate) use refs::*; +pub(crate) use source_parsing::*; +pub(crate) use source_stream::*; +pub(crate) use variable::*; diff --git a/src/interpretation/output_handler.rs b/src/interpretation/output_handler.rs new file mode 100644 index 00000000..f10cdfa2 --- /dev/null +++ b/src/interpretation/output_handler.rs @@ -0,0 +1,107 @@ +use super::*; + +#[derive(Debug)] +pub(super) enum OutputHandlerError { + FrozenOutputModification(MutationBlockReason), +} + +pub(super) struct OutputHandler { + output_stack: Vec, + freeze_stack_indices_at_or_below: Vec<(usize, MutationBlockReason)>, +} + +impl OutputHandler { + pub(super) fn new(initial_output: OutputStream) -> Self { + Self { + output_stack: vec![initial_output], + freeze_stack_indices_at_or_below: vec![], + } + } + + pub(super) fn complete(self) -> OutputStream { + let [final_output]: [OutputStream; 1] = self + .output_stack + .try_into() + .map_err(|_| ()) + .expect("Output stack should have height one at completion"); + final_output + } + + pub(super) fn current_output_mut( + &mut self, + span_source: &impl HasSpanRange, + ) -> ExecutionResult<&mut OutputStream> { + match self.current_output_mut_inner() { + Ok(output) => Ok(output), + Err(OutputHandlerError::FrozenOutputModification(reason)) => { + span_source.control_flow_err(reason.error_message("emit")) + } + } + } + + pub(super) fn current_output_mut_inner( + &mut self, + ) -> Result<&mut OutputStream, OutputHandlerError> { + let index = self.index_of_last_output(); + + self.validate_index(index)?; + + Ok(&mut self.output_stack[index]) + } + + /// SAFETY: Must be paired with a later `finish_inner_buffer_*` call, even in + /// the face of control flow interrupts. + pub(super) unsafe fn start_inner_buffer(&mut self) { + self.output_stack.push(OutputStream::new()); + } + + /// SAFETY: Must be paired with a prior `start_inner_buffer` call. + pub(super) unsafe fn finish_inner_buffer_as_group(&mut self, delimiter: Delimiter, span: Span) { + let inner_buffer = self.finish_inner_buffer_as_separate_stream(); + self.current_output_mut_inner() + .expect("Output stack should not be frozen if SAFETY conditions are met") + .push_new_group(inner_buffer, delimiter, span); + } + + /// SAFETY: Must be paired with a prior `start_inner_buffer` call. + pub(super) unsafe fn finish_inner_buffer_as_separate_stream(&mut self) -> OutputStream { + if self.output_stack.len() == 1 { + panic!("Cannot pop the last output stream from the output stack"); + } + + self.output_stack + .pop() + .expect("Output stack should never be empty") + } + + fn index_of_last_output(&self) -> usize { + // OVERFLOW: Safe as we maintain the invariant that output_stack is never empty + self.output_stack.len() - 1 + } + + /// SAFETY: Must be paired with unfreeze_existing. + pub(super) unsafe fn freeze_existing(&mut self, reason: MutationBlockReason) { + self.freeze_stack_indices_at_or_below + .push((self.index_of_last_output(), reason)); + } + + /// SAFETY: Must be paired with freeze_existing. + pub(super) unsafe fn unfreeze_existing(&mut self) { + let (popped, _reason) = self.freeze_stack_indices_at_or_below.pop().unwrap(); + assert_eq!( + popped, self.index_of_last_output(), + "Any additional output streams added during the freeze must be removed before unfreezing" + ); + } + + fn validate_index(&self, index: usize) -> Result<(), OutputHandlerError> { + if let Some(&(freeze_at_or_below_depth, reason)) = + self.freeze_stack_indices_at_or_below.last() + { + if index <= freeze_at_or_below_depth { + return Err(OutputHandlerError::FrozenOutputModification(reason)); + } + } + Ok(()) + } +} diff --git a/src/interpretation/output_parse_utilities.rs b/src/interpretation/output_parse_utilities.rs new file mode 100644 index 00000000..cc03fdba --- /dev/null +++ b/src/interpretation/output_parse_utilities.rs @@ -0,0 +1,181 @@ +use crate::internal_prelude::*; + +/// Designed to automatically discover (via peeking) the next token, to limit the extent of a +/// match such as `rest()`. +#[derive(Clone)] +pub(crate) enum ParseUntil { + End, + Group(Delimiter), + Ident(Ident), + Punct(Punct), + Literal(Literal), +} + +impl Parse for ParseUntil { + fn parse(input: ParseStream) -> ParseResult { + let next: TokenTree = input.parse()?; + let until = match next { + TokenTree::Group(group) => { + if !group.stream().is_empty() { + return group + .span() + .parse_err(format!( + "Until will only read up until the start of the group '{}'. The group must be empty '{}{}' to indicate this.", + group.delimiter().description_of_open(), + group.delimiter().description_of_open(), + group.delimiter().description_of_close(), + )); + } + ParseUntil::Group(group.delimiter()) + } + TokenTree::Ident(ident) => ParseUntil::Ident(ident), + TokenTree::Punct(punct) => ParseUntil::Punct(punct), + TokenTree::Literal(literal) => ParseUntil::Literal(literal), + }; + if !input.is_empty() { + return input.parse_err("Until only takes a single token."); + } + Ok(until) + } +} + +impl ParseUntil { + pub(crate) fn handle_parse_into( + &self, + input: OutputParseStream, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + match self { + ParseUntil::End => { + let remaining = input.parse::()?; + output.extend_raw_tokens(remaining); + } + ParseUntil::Group(delimiter) => { + while !input.is_empty() { + if input.peek_specific_group(*delimiter) { + return Ok(()); + } + let next = input.parse()?; + output.push_raw_token_tree(next); + } + } + ParseUntil::Ident(ident) => { + let content = ident.to_string(); + while !input.is_empty() { + if input.peek_ident_matching(&content) { + return Ok(()); + } + let next = input.parse()?; + output.push_raw_token_tree(next); + } + } + ParseUntil::Punct(punct) => { + let punct_char = punct.as_char(); + while !input.is_empty() { + if input.peek_punct_matching(punct_char) { + return Ok(()); + } + let next = input.parse()?; + output.push_raw_token_tree(next); + } + } + ParseUntil::Literal(literal) => { + let content = literal.to_string(); + while !input.is_empty() { + if input.peek_literal_matching(&content) { + return Ok(()); + } + let next = input.parse()?; + output.push_raw_token_tree(next); + } + } + } + Ok(()) + } +} + +pub(crate) fn handle_parsing_exact_output_match( + input: ParseStream, + expected: &OutputStream, + output: &mut OutputStream, +) -> ExecutionResult<()> { + for item in expected.iter() { + match item { + OutputTokenTreeRef::TokenTree(token_tree) => { + handle_parsing_exact_token_tree(input, token_tree, output)?; + } + OutputTokenTreeRef::OutputGroup(delimiter, _, inner_expected) => { + handle_parsing_exact_group( + input, + delimiter, + |inner_input, inner_output| { + handle_parsing_exact_output_match(inner_input, inner_expected, inner_output) + }, + output, + )?; + } + } + } + Ok(()) +} + +fn handle_parsing_exact_stream_match( + input: ParseStream, + expected: TokenStream, + output: &mut OutputStream, +) -> ExecutionResult<()> { + for item in expected.into_iter() { + handle_parsing_exact_token_tree(input, &item, output)?; + } + Ok(()) +} + +fn handle_parsing_exact_token_tree( + input: ParseStream, + expected: &TokenTree, + output: &mut OutputStream, +) -> ExecutionResult<()> { + match expected { + TokenTree::Group(group) => { + handle_parsing_exact_group( + input, + group.delimiter(), + |inner_input, inner_output| { + handle_parsing_exact_stream_match(inner_input, group.stream(), inner_output) + }, + output, + )?; + } + TokenTree::Ident(ident) => { + output.push_ident(input.parse_ident_matching(&ident.to_string())?); + } + TokenTree::Punct(punct) => { + output.push_punct(input.parse_punct_matching(punct.as_char())?); + } + TokenTree::Literal(literal) => { + output.push_literal(input.parse_literal_matching(&literal.to_string())?); + } + } + Ok(()) +} + +fn handle_parsing_exact_group( + input: ParseStream, + delimiter: Delimiter, + parse_inner: impl FnOnce(ParseStream, &mut OutputStream) -> ExecutionResult<()>, + output: &mut OutputStream, +) -> ExecutionResult<()> { + // Because `None` is ignored by Syn at parsing time, we can effectively be most permissive by ignoring them. + // This removes a bit of a footgun for users. + // If they really want to check for a None group, they can embed `@[GROUP @[EXACT ...]]` transformer. + if delimiter == Delimiter::None { + parse_inner(input, output) + } else { + let (source_span, inner_source) = input.parse_specific_group(delimiter)?; + output.push_grouped( + |output| parse_inner(&inner_source, output), + delimiter, + source_span.join(), + ) + } +} diff --git a/src/interpretation/output_stream.rs b/src/interpretation/output_stream.rs new file mode 100644 index 00000000..aedc470c --- /dev/null +++ b/src/interpretation/output_stream.rs @@ -0,0 +1,626 @@ +use crate::internal_prelude::*; + +#[derive(Clone)] +pub(crate) struct OutputStream { + /// Currently, even ~inside~ macro executions, round-tripping [`Delimiter::None`] groups to a TokenStream + /// breaks in rust-analyzer. This causes various spurious errors in the IDE. + /// See: https://github.com/rust-lang/rust-analyzer/issues/18211#issuecomment-2604547032 + /// + /// So instead of just storing a TokenStream here, we store a list of TokenStreams and interpreted groups. + /// This ensures any non-delimited groups are not round-tripped to a TokenStream. + /// + /// In future, we may wish to internally use some kind of `TokenBuffer` type, which I've started on below. + segments: Vec, + token_length: usize, +} + +impl OutputStream { + pub(crate) fn new() -> Self { + Self { + segments: vec![], + token_length: 0, + } + } + + pub(crate) fn new_with(appender: impl FnOnce(&mut Self)) -> Self { + let mut stream = Self::new(); + appender(&mut stream); + stream + } + + #[allow(unused)] + pub(crate) fn new_try_with( + appender: impl FnOnce(&mut Self) -> ExecutionResult<()>, + ) -> ExecutionResult { + let mut stream = Self::new(); + appender(&mut stream)?; + Ok(stream) + } + + pub(crate) fn raw(token_stream: TokenStream) -> Self { + let mut new = Self::new(); + new.extend_raw_tokens(token_stream); + new + } + + pub(crate) fn push_literal(&mut self, literal: Literal) { + self.push_raw_token_tree(literal.into()); + } + + pub(crate) fn push_ident(&mut self, ident: Ident) { + self.push_raw_token_tree(ident.into()); + } + + pub(crate) fn push_punct(&mut self, punct: Punct) { + self.push_raw_token_tree(punct.into()); + } + + pub(crate) fn push_tokens(&mut self, tokens: impl ToTokens) { + self.extend_raw_tokens(tokens.into_token_stream()); + } + + pub(crate) fn push_grouped( + &mut self, + appender: impl FnOnce(&mut Self) -> ExecutionResult<()>, + delimiter: Delimiter, + span: Span, + ) -> ExecutionResult<()> { + let mut inner = Self::new(); + appender(&mut inner)?; + self.push_new_group(inner, delimiter, span); + Ok(()) + } + + pub(crate) fn push_new_group( + &mut self, + inner_tokens: OutputStream, + delimiter: Delimiter, + span: Span, + ) { + self.segments + .push(OutputSegment::OutputGroup(delimiter, span, inner_tokens)); + self.token_length += 1; + } + + pub(crate) fn push_interpreted_item(&mut self, segment_item: OutputTokenTree) { + match segment_item { + OutputTokenTree::TokenTree(token_tree) => { + self.push_raw_token_tree(token_tree); + } + OutputTokenTree::OutputGroup(delimiter, span, inner_tokens) => { + self.push_new_group(inner_tokens, delimiter, span); + } + } + } + + pub(crate) fn push_raw_token_tree(&mut self, token_tree: TokenTree) { + self.extend_raw_tokens(iter::once(token_tree)); + } + + pub(crate) fn extend_raw_tokens(&mut self, tokens: impl IntoIterator) { + if !matches!(self.segments.last(), Some(OutputSegment::TokenVec(_))) { + self.segments.push(OutputSegment::TokenVec(vec![])); + } + + match self.segments.last_mut() { + Some(OutputSegment::TokenVec(token_vec)) => { + let before_length = token_vec.len(); + token_vec.extend(tokens); + self.token_length += token_vec.len() - before_length; + } + _ => unreachable!(), + } + } + + pub(crate) fn len(&self) -> usize { + self.token_length + } + + pub(crate) fn is_empty(&self) -> bool { + self.token_length == 0 + } + + pub(crate) fn coerce_into_value(self) -> AnyValue { + let parse_result = self.clone().parse_as::(); + match parse_result { + Ok(syn_lit) => AnyValue::for_syn_lit(syn_lit), + // Keep as stream otherwise + Err(_) => self.into_any_value(), + } + } + + /// WARNING: With rust-analyzer, this loses transparent groups which have been inserted. + /// Use only where that doesn't matter: https://github.com/rust-lang/rust-analyzer/issues/18211#issuecomment-2604547032 + pub(crate) fn parse_with( + self, + parser: impl FnOnce(ParseStream) -> ExecutionResult, + ) -> ExecutionResult { + self.into_token_stream().interpreted_parse_with(parser) + } + + /// WARNING: With rust-analyzer, this loses transparent groups which have been inserted. + /// Use only where that doesn't matter: https://github.com/rust-lang/rust-analyzer/issues/18211#issuecomment-2604547032 + pub(crate) fn parse_as>(self) -> ParseResult { + self.into_token_stream().interpreted_parse_with(T::parse) + } + + pub(crate) fn append_into(self, output: &mut OutputStream) { + output.segments.extend(self.segments); + output.token_length += self.token_length; + } + + pub(crate) fn append_cloned_into(&self, output: &mut OutputStream) { + self.clone().append_into(output); + } + + /// WARNING: With rust-analyzer, this loses transparent groups which have been inserted. + /// Use only where that doesn't matter: https://github.com/rust-lang/rust-analyzer/issues/18211#issuecomment-2604547032 + pub(crate) fn into_token_stream(self) -> TokenStream { + let mut output = TokenStream::new(); + self.append_to_token_stream(&mut output); + output + } + + fn append_to_token_stream(self, output: &mut TokenStream) { + for segment in self.segments { + match segment { + OutputSegment::TokenVec(vec) => { + output.extend(vec); + } + OutputSegment::OutputGroup(delimiter, span, inner) => { + output.extend(iter::once(TokenTree::Group( + Group::new(delimiter, inner.into_token_stream()).with_span(span), + ))) + } + } + } + } + + pub(crate) fn replace_first_level_spans(&mut self, new_span: Span) { + for segment in self.segments.iter_mut() { + match segment { + OutputSegment::TokenVec(vec) => { + for token in vec.iter_mut() { + match token { + TokenTree::Group(group) => group.set_span(new_span), + TokenTree::Ident(ident) => ident.set_span(new_span), + TokenTree::Punct(punct) => punct.set_span(new_span), + TokenTree::Literal(literal) => literal.set_span(new_span), + } + } + } + OutputSegment::OutputGroup(_, span, _) => { + *span = new_span; + } + } + } + } + + pub(crate) fn to_token_stream_removing_any_transparent_groups(&self) -> TokenStream { + let mut output = TokenStream::new(); + self.append_to_token_stream_without_transparent_groups(&mut output); + output + } + + fn append_to_token_stream_without_transparent_groups(&self, output: &mut TokenStream) { + for segment in self.segments.iter() { + match segment { + OutputSegment::TokenVec(vec) => { + for token in vec { + match token { + TokenTree::Group(group) if group.delimiter() == Delimiter::None => { + output.extend(group.stream().flatten_transparent_groups()); + } + other => output.extend(iter::once(other.clone())), + } + } + } + OutputSegment::OutputGroup(delimiter, span, interpreted_stream) => { + let delimiter = *delimiter; + let span = *span; + if delimiter == Delimiter::None { + interpreted_stream + .append_to_token_stream_without_transparent_groups(output); + } else { + let mut inner = TokenStream::new(); + interpreted_stream + .append_to_token_stream_without_transparent_groups(&mut inner); + output.extend(iter::once(TokenTree::Group( + Group::new(delimiter, inner).with_span(span), + ))); + } + } + } + } + } + + pub(crate) fn concat_content(&self, behaviour: &ConcatBehaviour) -> String { + let mut output = String::new(); + self.concat_content_into(&mut output, behaviour); + output + } + + pub(crate) fn concat_content_into(&self, output: &mut String, behaviour: &ConcatBehaviour) { + fn concat_recursive_interpreted_stream( + behaviour: &ConcatBehaviour, + output: &mut String, + prefix_spacing: Spacing, + stream: &OutputStream, + ) { + let mut spacing = prefix_spacing; + for segment in stream.segments.iter() { + spacing = match segment { + OutputSegment::TokenVec(vec) => { + concat_recursive_token_stream(behaviour, output, spacing, vec) + } + OutputSegment::OutputGroup(delimiter, _, interpreted_stream) => { + behaviour.before_token_tree(output, spacing); + behaviour.wrap_delimiters( + output, + *delimiter, + interpreted_stream.is_empty(), + |output| { + concat_recursive_interpreted_stream( + behaviour, + output, + Spacing::Joint, + interpreted_stream, + ); + }, + ); + Spacing::Alone + } + } + } + } + + fn concat_recursive_token_stream>( + behaviour: &ConcatBehaviour, + output: &mut String, + prefix_spacing: Spacing, + token_stream: impl IntoIterator, + ) -> Spacing { + let mut spacing = prefix_spacing; + for token_tree in token_stream.into_iter() { + let token_tree = token_tree.borrow(); + behaviour.before_token_tree(output, spacing); + spacing = match token_tree { + TokenTree::Literal(literal) => { + behaviour.handle_literal(output, literal); + Spacing::Alone + } + TokenTree::Group(group) => { + let inner = group.stream(); + behaviour.wrap_delimiters( + output, + group.delimiter(), + inner.is_empty(), + |output| { + concat_recursive_token_stream( + behaviour, + output, + Spacing::Joint, + inner, + ); + }, + ); + Spacing::Alone + } + TokenTree::Punct(punct) => { + let char = punct.as_char(); + // This conversion captures ~#% as ~%raw[#]%raw[%] rather than the + // more accurate %raw[~#%] which also captures the correct punct spacing. + // To do this properly would require lookahead which require quite a big + // logic change! So I've opted to ignore it for now... + // * Debug string isn't expected to be used for re-parsing + // * Even if it were, the specific spacing isn't used/relevant + // for any known grammar + if behaviour.use_stream_literal_syntax && (char == '#' || char == '%') { + output.push_str("%raw["); + output.push(char); + output.push(']'); + } else { + output.push(char); + } + punct.spacing() + } + TokenTree::Ident(ident) => { + output.push_str(&ident.to_string()); + Spacing::Alone + } + } + } + spacing + } + + concat_recursive_interpreted_stream(behaviour, output, Spacing::Joint, self); + } + + pub(crate) fn parse_exact_match( + &self, + input: ParseStream, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + handle_parsing_exact_output_match(input, self, output) + } + + pub(crate) fn iter(&self) -> impl Iterator> { + self.segments.iter().flat_map(|segment| match segment { + OutputSegment::TokenVec(vec) => { + EitherIterator::Left(vec.iter().map(OutputTokenTreeRef::TokenTree)) + } + OutputSegment::OutputGroup(delimiter, span, inner) => EitherIterator::Right( + core::iter::once(OutputTokenTreeRef::OutputGroup(*delimiter, *span, inner)), + ), + }) + } +} + +pub(crate) enum OutputTokenTreeRef<'a> { + TokenTree(&'a TokenTree), + #[allow(unused)] + OutputGroup(Delimiter, Span, &'a OutputStream), +} + +#[derive(Clone)] +pub(crate) struct OutputStreamIntoIter { + segments: std::vec::IntoIter, + current_segment_iter: Option, + count_remaining: usize, +} + +impl OutputStreamIntoIter { + fn new(stream: OutputStream) -> Self { + let mut segments = stream.segments.into_iter(); + let current_segment_iter = segments.next().map(OutputSegment::into_iter); + let count_remaining = stream.token_length; + Self { + segments, + current_segment_iter, + count_remaining, + } + } +} + +impl Iterator for OutputStreamIntoIter { + type Item = OutputTokenTree; + + fn next(&mut self) -> Option { + if self.count_remaining == 0 { + return None; + } + + loop { + if let Some(current_segment_iter) = &mut self.current_segment_iter { + if let Some(item) = current_segment_iter.next() { + self.count_remaining -= 1; + return Some(item); + } + } + + match self.segments.next() { + Some(next_segment) => { + self.current_segment_iter = Some(next_segment.into_iter()); + } + None => { + self.current_segment_iter = None; + return None; + } + } + } + } + + fn size_hint(&self) -> (usize, Option) { + (self.count_remaining, Some(self.count_remaining)) + } +} + +impl IntoIterator for OutputStream { + type IntoIter = OutputStreamIntoIter; + type Item = OutputTokenTree; + + fn into_iter(self) -> Self::IntoIter { + OutputStreamIntoIter::new(self) + } +} + +pub(crate) struct ConcatBehaviour { + pub(crate) use_debug_literal_syntax: bool, + pub(crate) add_space_between_token_trees: bool, + pub(crate) use_stream_literal_syntax: bool, + pub(crate) output_literal_structure: bool, + pub(crate) unwrap_contents_of_string_like_literals: bool, + pub(crate) show_none_values: bool, + pub(crate) iterator_limit: usize, + pub(crate) error_after_iterator_limit: bool, + pub(crate) error_span_range: SpanRange, +} + +impl ConcatBehaviour { + pub(crate) fn standard(error_span_range: SpanRange) -> Self { + Self { + add_space_between_token_trees: false, + use_stream_literal_syntax: false, + use_debug_literal_syntax: false, + output_literal_structure: false, + unwrap_contents_of_string_like_literals: true, + show_none_values: false, + iterator_limit: 1000, + error_after_iterator_limit: true, + error_span_range, + } + } + + pub(crate) fn literal(error_span_range: SpanRange) -> Self { + Self { + add_space_between_token_trees: false, + use_stream_literal_syntax: false, + use_debug_literal_syntax: true, + output_literal_structure: false, + unwrap_contents_of_string_like_literals: true, + show_none_values: false, + iterator_limit: 1000, + error_after_iterator_limit: true, + error_span_range, + } + } + + pub(crate) fn debug(error_span_range: SpanRange) -> Self { + Self { + add_space_between_token_trees: true, + use_stream_literal_syntax: true, + use_debug_literal_syntax: true, + output_literal_structure: true, + unwrap_contents_of_string_like_literals: false, + show_none_values: true, + iterator_limit: 20, + error_after_iterator_limit: false, + error_span_range, + } + } + + pub(crate) fn before_token_tree(&self, output: &mut String, spacing: Spacing) { + if self.add_space_between_token_trees && spacing == Spacing::Alone { + output.push(' '); + } + } + + pub(crate) fn handle_literal(&self, output: &mut String, literal: &Literal) { + match literal.content_if_string_like() { + Some(content) if self.unwrap_contents_of_string_like_literals => { + output.push_str(&content) + } + _ => { + if self.use_debug_literal_syntax { + output.push_str(&literal.to_string()) + } else { + output.push_str(&literal.inner_value_to_string()) + } + } + } + } + + pub(crate) fn wrap_delimiters( + &self, + output: &mut String, + delimiter: Delimiter, + is_empty: bool, + inner: impl FnOnce(&mut String), + ) { + match delimiter { + Delimiter::Parenthesis => { + output.push('('); + inner(output); + output.push(')'); + } + Delimiter::Brace => { + if is_empty { + output.push('{'); + inner(output); + output.push('}'); + } else { + output.push_str("{ "); + inner(output); + output.push_str(" }"); + } + } + Delimiter::Bracket => { + output.push('['); + inner(output); + output.push(']'); + } + Delimiter::None => { + if self.use_stream_literal_syntax { + output.push_str("%group["); + inner(output); + output.push(']'); + } else { + inner(output); + } + } + } + } +} + +impl From for OutputStream { + fn from(value: TokenTree) -> Self { + OutputStream::raw(value.into()) + } +} + +#[derive(Clone)] +// This was primarily implemented to avoid this issue: https://github.com/rust-lang/rust-analyzer/issues/18211#issuecomment-2604547032 +// But it doesn't actually help because the `syn::parse` mechanism only operates on a TokenStream, +// so we have to convert back into a TokenStream. +enum OutputSegment { + TokenVec(Vec), // Cheaper than a TokenStream (probably) + OutputGroup(Delimiter, Span, OutputStream), +} + +#[derive(Clone)] +enum OutputSegmentIntoIter { + Vec(std::vec::IntoIter), + Single(Option), +} + +impl Iterator for OutputSegmentIntoIter { + type Item = OutputTokenTree; + + fn next(&mut self) -> Option { + match self { + OutputSegmentIntoIter::Vec(iter) => iter.next().map(OutputTokenTree::TokenTree), + OutputSegmentIntoIter::Single(option) => option.take(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + OutputSegmentIntoIter::Vec(iter) => iter.size_hint(), + OutputSegmentIntoIter::Single(option) => { + let len = if option.is_some() { 1 } else { 0 }; + (len, Some(len)) + } + } + } +} + +impl IntoIterator for OutputSegment { + type IntoIter = OutputSegmentIntoIter; + type Item = OutputTokenTree; + + fn into_iter(self) -> Self::IntoIter { + match self { + OutputSegment::TokenVec(vec) => OutputSegmentIntoIter::Vec(vec.into_iter()), + OutputSegment::OutputGroup(delimiter, span, interpreted_stream) => { + OutputSegmentIntoIter::Single(Some(OutputTokenTree::OutputGroup( + delimiter, + span, + interpreted_stream, + ))) + } + } + } +} + +#[derive(Clone)] +pub(crate) enum OutputTokenTree { + TokenTree(TokenTree), + OutputGroup(Delimiter, Span, OutputStream), +} + +impl From for OutputStream { + fn from(value: OutputTokenTree) -> Self { + let mut new = Self::new(); + new.push_interpreted_item(value); + new + } +} + +impl HasSpan for OutputTokenTree { + fn span(&self) -> Span { + match self { + OutputTokenTree::TokenTree(token_tree) => token_tree.span(), + OutputTokenTree::OutputGroup(_, span, _) => *span, + } + } +} diff --git a/src/interpretation/parse_template_stream.rs b/src/interpretation/parse_template_stream.rs new file mode 100644 index 00000000..e90d7809 --- /dev/null +++ b/src/interpretation/parse_template_stream.rs @@ -0,0 +1,136 @@ +use crate::internal_prelude::*; + +/// A stream inside @parser[...] literals/patterns, which reads from the current parser +pub(crate) struct ParseTemplateStream { + items: Vec, + span: Span, +} + +impl ParseTemplateStream { + pub(crate) fn parse_with_span(input: SourceParser, span: Span) -> ParseResult { + let mut items = Vec::new(); + while !input.is_empty() { + items.push(input.parse()?); + } + Ok(Self { items, span }) + } + + pub(crate) fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + for item in self.items.iter_mut() { + item.control_flow_pass(context)?; + } + Ok(()) + } +} + +impl ParseTemplateStream { + pub(crate) fn consume(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + for item in self.items.iter() { + item.consume(interpreter)?; + } + Ok(()) + } +} + +impl HasSpan for ParseTemplateStream { + fn span(&self) -> Span { + self.span + } +} + +pub(crate) enum ParseTemplateItem { + EmbeddedStatements(EmbeddedStatements), + Group(ParseTemplateGroup), + Punct(char), + Ident(String), + Literal(String), +} + +impl ParseSource for ParseTemplateItem { + fn parse(input: SourceParser) -> ParseResult { + Ok(match input.peek_grammar() { + SourcePeekMatch::Group(_) => ParseTemplateItem::Group(input.parse()?), + SourcePeekMatch::EmbeddedVariable => return input.parse_err("Variables cannot be embedded into a parse template stream. If you intend to parse the content of the variable, use { parser.read(%[#variable]); }. If you intend to parse a #, use { parser.read(%[#]); }"), + SourcePeekMatch::EmbeddedExpression => return input.parse_err("Expressions cannot be embedded into a parse template stream. If you intend to parse the content of a value, use { parser.read(%[#variable]); }. If you intend to parse a #, use { parser.read(%[#]); }"), + SourcePeekMatch::EmbeddedStatements => { + ParseTemplateItem::EmbeddedStatements(input.parse()?) + } + SourcePeekMatch::Punct(_) => { + let punct = input.parse_any_punct()?; + ParseTemplateItem::Punct(punct.as_char()) + } + SourcePeekMatch::Literal(_) => { + let literal: Literal = input.parse()?; + ParseTemplateItem::Literal(literal.to_string()) + } + SourcePeekMatch::Ident(_) => { + let ident = input.parse_any_ident()?; + ParseTemplateItem::Ident(ident.to_string()) + } + SourcePeekMatch::StreamLiteral => return input.parse_err("Stream literals cannot be embedded into a parse template stream. Use { parser.read(%[...]); } to parse the content of a stream."), + SourcePeekMatch::ObjectLiteral => return input.parse_err("Object literals cannot be embedded into a parse template stream."), + SourcePeekMatch::End => return input.parse_err("Expected some item."), + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + match self { + ParseTemplateItem::EmbeddedStatements(block) => block.control_flow_pass(context), + ParseTemplateItem::Group(group) => group.control_flow_pass(context), + ParseTemplateItem::Punct { .. } => Ok(()), + ParseTemplateItem::Ident { .. } => Ok(()), + ParseTemplateItem::Literal { .. } => Ok(()), + } + } +} + +impl ParseTemplateItem { + fn consume(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + match self { + ParseTemplateItem::EmbeddedStatements(block) => { + block.consume(interpreter)?; + } + ParseTemplateItem::Group(group) => { + group.consume(interpreter)?; + } + ParseTemplateItem::Punct(char) => { + let _ = interpreter.input().parse_punct_matching(*char)?; + } + ParseTemplateItem::Ident(ident) => { + let _ = interpreter.input().parse_ident_matching(ident)?; + } + ParseTemplateItem::Literal(literal) => { + let _ = interpreter.input().parse_literal_matching(literal)?; + } + } + Ok(()) + } +} + +pub(crate) struct ParseTemplateGroup { + source_delimiter: Delimiter, + content: ParseTemplateStream, +} + +impl ParseSource for ParseTemplateGroup { + fn parse(input: SourceParser) -> ParseResult { + let (delimiter, delim_span, content) = input.parse_any_group()?; + let content = ParseTemplateStream::parse_with_span(&content, delim_span.join())?; + Ok(Self { + source_delimiter: delimiter, + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.content.control_flow_pass(context) + } +} + +impl ParseTemplateGroup { + fn consume(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + interpreter.parse_group(Some(self.source_delimiter), |interpreter, _, _| { + self.content.consume(interpreter) + }) + } +} diff --git a/src/interpretation/refs.rs b/src/interpretation/refs.rs new file mode 100644 index 00000000..216638ef --- /dev/null +++ b/src/interpretation/refs.rs @@ -0,0 +1,282 @@ +use std::mem::transmute; + +use super::*; + +/// A flexible type which can either be a reference to a value of type `T`, +/// or an emplaced reference from a [`Shared`]. +pub(crate) struct AnyRef<'a, T: ?Sized + 'static> { + inner: AnyRefInner<'a, T>, +} + +impl<'a, T: ?Sized + 'static> AnyRef<'a, T> { + #[allow(unused)] + pub(crate) fn map(self, f: impl for<'r> FnOnce(&'r T) -> &'r S) -> AnyRef<'a, S> { + match self.inner { + AnyRefInner::Direct(value) => AnyRef { + inner: AnyRefInner::Direct(f(value)), + }, + AnyRefInner::Encapsulated(shared) => AnyRef { + inner: AnyRefInner::Encapsulated(shared.map(|x| f(x))), + }, + } + } + + #[allow(unused)] + pub(crate) fn map_optional( + self, + f: impl for<'r> FnOnce(&'r T) -> Option<&'r S>, + ) -> Option> { + Some(match self.inner { + AnyRefInner::Direct(value) => AnyRef { + inner: AnyRefInner::Direct(f(value)?), + }, + AnyRefInner::Encapsulated(shared) => AnyRef { + inner: AnyRefInner::Encapsulated(shared.map_optional(f)?), + }, + }) + } + + pub(crate) fn replace( + self, + f: impl for<'e> FnOnce(&'e T, &mut AnyRefEmplacer<'a, 'e, T>) -> O, + ) -> O { + let copied_ref = self.deref() as *const T; + let mut emplacer = AnyRefEmplacer { + inner: Some(self), + encapsulation_lifetime: std::marker::PhantomData, + }; + f( + // SAFETY: The underlying reference is valid for the lifetime of self + // So we can copy it fine + unsafe { &*copied_ref }, + &mut emplacer, + ) + } +} + +pub(crate) struct AnyRefEmplacer<'a, 'e: 'a, T: 'static + ?Sized> { + inner: Option>, + encapsulation_lifetime: std::marker::PhantomData<&'e ()>, +} + +impl<'a, 'e: 'a, T: 'static + ?Sized> AnyRefEmplacer<'a, 'e, T> { + pub(crate) fn emplace(&mut self, value: &'e V) -> AnyRef<'a, V> { + unsafe { + // SAFETY: The lifetime 'e is equal to the &'e content argument in replace + // So this guarantees that the returned reference is valid as long as the AnyRef exists + self.emplace_unchecked(value) + } + } + + // SAFETY: + // * The caller must ensure that the value's lifetime is derived from the original content + pub(crate) unsafe fn emplace_unchecked( + &mut self, + value: &V, + ) -> AnyRef<'a, V> { + self.inner + .take() + .expect("You can only emplace to create a new AnyRef value once") + .map(|_| + // SAFETY: As defined in the rustdoc above + unsafe { transmute::<&V, &'static V>(value) }) + } +} + +impl<'a, T: ?Sized> From<&'a T> for AnyRef<'a, T> { + fn from(value: &'a T) -> Self { + Self { + inner: AnyRefInner::Direct(value), + } + } +} + +pub(crate) trait ToSpannedRef<'a> { + type Target: ?Sized; + fn into_ref(self) -> AnyRef<'a, Self::Target>; + fn into_spanned_ref(self, source: impl HasSpanRange) -> Spanned>; +} + +impl<'a, T: ?Sized> ToSpannedRef<'a> for &'a T { + type Target = T; + fn into_ref(self) -> AnyRef<'a, Self::Target> { + self.into() + } + fn into_spanned_ref(self, source: impl HasSpanRange) -> Spanned> { + self.into_ref().spanned(source) + } +} + +impl<'a, T: ?Sized> From> for AnyRef<'a, T> { + fn from(value: Shared) -> Self { + Self { + inner: AnyRefInner::Encapsulated(value.0), + } + } +} + +impl ToSpannedRef<'static> for Shared { + type Target = T; + fn into_ref(self) -> AnyRef<'static, Self::Target> { + self.into() + } + fn into_spanned_ref(self, source: impl HasSpanRange) -> Spanned> { + AnyRef::from(self).spanned(source) + } +} + +enum AnyRefInner<'a, T: 'static + ?Sized> { + Direct(&'a T), + Encapsulated(SharedSubRcRefCell), +} + +impl<'a, T: 'static + ?Sized> Deref for AnyRef<'a, T> { + type Target = T; + + fn deref(&self) -> &T { + match &self.inner { + AnyRefInner::Direct(value) => value, + AnyRefInner::Encapsulated(shared) => shared, + } + } +} + +/// A flexible type which can either be a mutable reference to a value of type `T`, +/// or an emplaced reference from a [`Mutable`]. +pub(crate) struct AnyMut<'a, T: 'static + ?Sized> { + inner: AnyMutInner<'a, T>, +} + +impl<'a, T: ?Sized> From<&'a mut T> for AnyMut<'a, T> { + fn from(value: &'a mut T) -> Self { + Self { + inner: AnyMutInner::Direct(value), + } + } +} + +impl<'a, T: ?Sized + 'static> AnyMut<'a, T> { + #[allow(unused)] + pub(crate) fn map( + self, + f: impl for<'r> FnOnce(&'r mut T) -> &'r mut S, + ) -> AnyMut<'a, S> { + match self.inner { + AnyMutInner::Direct(value) => AnyMut { + inner: AnyMutInner::Direct(f(value)), + }, + AnyMutInner::Encapsulated(mutable) => AnyMut { + inner: AnyMutInner::Encapsulated(mutable.map(|x| f(x))), + }, + } + } + + #[allow(unused)] + pub(crate) fn map_optional( + self, + f: impl for<'r> FnOnce(&'r mut T) -> Option<&'r mut S>, + ) -> Option> { + Some(match self.inner { + AnyMutInner::Direct(value) => AnyMut { + inner: AnyMutInner::Direct(f(value)?), + }, + AnyMutInner::Encapsulated(mutable) => AnyMut { + inner: AnyMutInner::Encapsulated(mutable.map_optional(f)?), + }, + }) + } + + pub(crate) fn replace( + mut self, + f: impl for<'e> FnOnce(&'e mut T, &mut AnyMutEmplacer<'a, 'e, T>) -> O, + ) -> O { + let copied_mut = self.deref_mut() as *mut T; + let mut emplacer = AnyMutEmplacer { + inner: Some(self), + encapsulation_lifetime: std::marker::PhantomData, + }; + f( + // SAFETY: We are cloning a mutable reference here, but it is safe because: + // - What it's pointing at still lives, inside emplacer.inner + // - No other "mutable reference" is created except at encapsulation time + unsafe { &mut *copied_mut }, + &mut emplacer, + ) + } +} + +pub(crate) struct AnyMutEmplacer<'a, 'e: 'a, T: 'static + ?Sized> { + inner: Option>, + encapsulation_lifetime: std::marker::PhantomData<&'e ()>, +} + +impl<'a, 'e: 'a, T: 'static + ?Sized> AnyMutEmplacer<'a, 'e, T> { + pub(crate) fn emplace(&mut self, value: &'e mut V) -> AnyMut<'a, V> { + unsafe { + // SAFETY: The lifetime 'e is equal to the &'e content argument in replace + // So this guarantees that the returned reference is valid as long as the AnyMut exists + self.emplace_unchecked(value) + } + } + + // SAFETY: + // * The caller must ensure that the value's lifetime is derived from the original content + pub(crate) unsafe fn emplace_unchecked( + &mut self, + value: &mut V, + ) -> AnyMut<'a, V> { + self.inner + .take() + .expect("You can only emplace to create a new AnyMut value once") + .map(|_| + // SAFETY: As defined in the rustdoc above + unsafe { transmute::<&mut V, &'static mut V>(value) }) + } +} + +#[allow(unused)] +pub(crate) trait IntoAnyMut<'a> { + type Target: ?Sized; + fn into_any_mut(self) -> AnyMut<'a, Self::Target>; +} + +impl<'a, T: ?Sized + 'static> IntoAnyMut<'a> for &'a mut T { + type Target = T; + + fn into_any_mut(self) -> AnyMut<'a, Self::Target> { + self.into() + } +} + +impl<'a, T: ?Sized> From> for AnyMut<'a, T> { + fn from(value: Mutable) -> Self { + Self { + inner: AnyMutInner::Encapsulated(value.0), + } + } +} + +enum AnyMutInner<'a, T: 'static + ?Sized> { + Direct(&'a mut T), + Encapsulated(MutableSubRcRefCell), +} + +impl<'a, T: 'static + ?Sized> Deref for AnyMut<'a, T> { + type Target = T; + + fn deref(&self) -> &T { + match &self.inner { + AnyMutInner::Direct(value) => value, + AnyMutInner::Encapsulated(shared) => shared, + } + } +} + +impl<'a, T: 'static + ?Sized> DerefMut for AnyMut<'a, T> { + fn deref_mut(&mut self) -> &mut T { + match &mut self.inner { + AnyMutInner::Direct(value) => value, + AnyMutInner::Encapsulated(shared) => &mut *shared, + } + } +} diff --git a/src/interpretation/source_parsing.rs b/src/interpretation/source_parsing.rs new file mode 100644 index 00000000..181b8f67 --- /dev/null +++ b/src/interpretation/source_parsing.rs @@ -0,0 +1,660 @@ +#![allow(unused)] +use super::*; + +new_key!(pub(crate) ScopeId); +new_key!(pub(crate) VariableDefinitionId); +new_key!(pub(crate) VariableReferenceId); +new_key!(pub(crate) ControlFlowSegmentId); +new_key!(pub(crate) CatchLocationId); + +pub(crate) enum InterruptDetails<'a> { + /// Break statement (targets loops or labeled blocks) + Break { + break_token: &'a Token![break], + label: Option<&'a InterruptLabel>, + }, + /// Continue statement (targets loops only) + Continue { + continue_token: &'a Token![continue], + label: Option<&'a InterruptLabel>, + }, + /// Revert statement (targets attempt blocks) + Revert { + revert_token: &'a RevertKeyword, + label: Option<&'a InterruptLabel>, + }, +} + +#[cfg(feature = "debug")] +#[derive(Clone, Copy, Debug)] +pub(crate) enum FinalUseAssertion { + None, + IsFinal(Span), + IsNotFinal(Span), +} + +#[derive(Debug)] +pub(crate) struct ScopeDefinitions { + // Scopes + pub(crate) root_scope: ScopeId, + pub(crate) scopes: Arena, + pub(crate) definitions: Arena, + pub(crate) references: Arena, + // Catch locations + pub(crate) catch_locations: Arena, + // Segments + #[cfg(feature = "debug")] + root_segment: ControlFlowSegmentId, + #[cfg(feature = "debug")] + segments: Arena, + #[cfg(feature = "debug")] + final_use_debug: MarkFinalUseOutput, +} + +#[allow(unused)] +pub(crate) struct FlowAnalysisState { + // SCOPE DATA + scope_id_stack: Vec, + scopes: Arena, + definitions: Arena, + references: Arena, + // CATCH LOCATION DATA + catch_locations: Arena, + catch_location_stack: Vec, + // CONTROL FLOW DATA + segments_stack: Vec, + segments: Arena, +} + +impl FlowAnalysisState { + pub(crate) fn new() -> Self { + let mut scopes = Arena::new(); + let definitions = Arena::new(); + let references = Arena::new(); + let root_scope = scopes.add(AllocatedScope::Defined(ScopeData { + parent: None, + definitions: Vec::new(), + })); + let mut segments = Arena::new(); + let root_segment = segments.add(ControlFlowSegmentData { + scope: root_scope, + parent: None, + segment_kind: SegmentKind::Sequential, + children: SegmentKind::Sequential.new_children(), + }); + Self { + scope_id_stack: vec![root_scope], + scopes, + definitions, + references, + catch_locations: Arena::new(), + catch_location_stack: Vec::new(), + segments_stack: vec![root_segment], + segments, + } + } + + pub(crate) fn finish(mut self) -> ParseResult { + let root_scope = self.scope_id_stack.pop().expect("No scope to pop"); + assert!( + self.scope_id_stack.is_empty(), + "Cannot finish - Unpopped scopes remain" + ); + + #[allow(unused)] // If debug is off + let root_segment = self.segments_stack.pop().expect("No segment to pop"); + assert!( + self.segments_stack.is_empty(), + "Cannot finish - Unpopped segments remain" + ); + + let scopes = self.scopes.map_all(AllocatedScope::into_defined); + let definitions = self + .definitions + .map_all(AllocatedVariableDefinition::into_defined); + let mut references = self + .references + .map_all(AllocatedVariableReference::into_defined); + + let mut analyzer = + ControlFlowAnalyzer::new(&definitions, &mut references, &scopes, &self.segments); + + #[cfg(feature = "debug")] // If debug is off + let final_use_debug = analyzer.analyze_and_update_references(); + #[cfg(not(feature = "debug"))] + analyzer.analyze_and_update_references(); + + #[cfg(feature = "debug")] + { + for (_, data) in references.iter() { + match data.assertion { + FinalUseAssertion::IsFinal(span) if !data.is_final_reference => { + return span.parse_err( + "Assertion failed. Reference was calculated to be non-final.", + ); + } + FinalUseAssertion::IsNotFinal(span) if data.is_final_reference => { + return span + .parse_err("Assertion failed. Reference was calculated to be final."); + } + _ => {} + } + } + } + + Ok(ScopeDefinitions { + root_scope, + scopes, + definitions, + references, + catch_locations: self.catch_locations, + #[cfg(feature = "debug")] + root_segment, + #[cfg(feature = "debug")] + segments: self.segments, + #[cfg(feature = "debug")] + final_use_debug, + }) + } + + pub(crate) fn allocate_scope(&mut self) -> ScopeId { + self.scopes.add(AllocatedScope::Allocated) + } + + pub(crate) fn allocate_variable_reference(&mut self, name: &Ident) -> VariableReferenceId { + self.references.add(AllocatedVariableReference::Allocated { + name: name.to_string(), + span: name.span(), + }) + } + + pub(crate) fn allocate_variable_definition(&mut self, name: &Ident) -> VariableDefinitionId { + self.definitions + .add(AllocatedVariableDefinition::Allocated { + name: name.to_string(), + span: name.span(), + }) + } + + fn current_scope_id(&self) -> ScopeId { + *self.scope_id_stack.last().unwrap() + } + + fn current_scope(&mut self) -> &mut ScopeData { + self.scopes.get_mut(self.current_scope_id()).defined_mut() + } + + pub(crate) fn enter_scope(&mut self, scope_id: ScopeId) { + *self.scopes.get_mut(scope_id) = AllocatedScope::Defined(ScopeData { + parent: Some(self.current_scope_id()), + definitions: Vec::new(), + }); + self.scope_id_stack.push(scope_id); + } + + pub(crate) fn define_variable(&mut self, id: VariableDefinitionId) { + let scope = self.current_scope_id(); + let segment = self.current_segment_id(); + let definition = self.definitions.get_mut(id); + let (name, definition_name_span) = definition.take_allocated(); + *definition = AllocatedVariableDefinition::Defined(VariableDefinitionData { + scope, + segment, + name, + definition_name_span, + references: Vec::new(), + }); + self.current_scope().definitions.push(id); + self.current_segment() + .children + .push(ControlFlowChild::VariableDefinition(id)); + } + + pub(crate) fn reference_variable( + &mut self, + id: VariableReferenceId, + #[cfg(feature = "debug")] assertion: FinalUseAssertion, + ) -> ParseResult<()> { + let segment = self.current_segment_id(); + let reference = self.references.get_mut(id); + let (name, reference_name_span) = reference.take_allocated(); + for scope_id in self.scope_id_stack.iter().rev() { + let scope = self.scopes.get(*scope_id).defined_ref(); + for &def_id in scope.definitions.iter().rev() { + let def = self.definitions.get_mut(def_id).defined_mut(); + if def.name == name { + *reference = AllocatedVariableReference::Defined(VariableReferenceData { + definition: def_id, + definition_scope: def.scope, + segment, + reference_name_span, + is_final_reference: false, // Some will be set to true later + #[cfg(feature = "debug")] + assertion, + }); + def.references.push(id); + self.current_segment() + .children + .push(ControlFlowChild::VariableReference(id, def_id)); + return Ok(()); + } + } + } + reference_name_span.parse_err(format!("Cannot find variable `{}` in this scope", name)) + } + + /// The scope parameter is just to help catch bugs. + pub(crate) fn exit_scope(&mut self, scope: ScopeId) { + let id = self.scope_id_stack.pop().expect("No scope to pop"); + assert_eq!(id, scope, "Popped scope is not the current scope"); + } + + // SEGMENTS + // ======== + + fn current_segment_id(&self) -> ControlFlowSegmentId { + *self.segments_stack.last().unwrap() + } + + fn current_segment(&mut self) -> &mut ControlFlowSegmentData { + self.segments.get_mut(self.current_segment_id()) + } + + pub(crate) fn enter_next_segment(&mut self, segment_kind: SegmentKind) -> ControlFlowSegmentId { + let parent_id = self.current_segment_id(); + let parent = self.segments.get(parent_id); + if !matches!(parent.children, SegmentChildren::Sequential { .. }) { + panic!("enter_next_segment can only be called with a sequential parent"); + } + self.enter_segment_with_valid_previous(parent_id, None, segment_kind) + } + + pub(crate) fn enter_path_segment( + &mut self, + previous_sibling_id: Option, + segment_kind: SegmentKind, + ) -> ControlFlowSegmentId { + let parent_id = self.current_segment_id(); + if let Some(previous_sibling_id) = previous_sibling_id { + let previous_sibling = self.segments.get(previous_sibling_id); + // It might be possible if gotos exist to have a non-local parent, + // But this may impact lots of the algorithms, so forbid it for now. + assert_eq!( + previous_sibling.parent, + Some(parent_id), + "Previous sibling is not under the current parent" + ); + } + let parent = self.segments.get(parent_id); + if !matches!(parent.children, SegmentChildren::PathBased { .. }) { + panic!("enter_path_segment can only be called with a path-based parent"); + } + self.enter_segment_with_valid_previous(parent_id, previous_sibling_id, segment_kind) + } + + pub(crate) fn exit_segment(&mut self, segment: ControlFlowSegmentId) { + let id = self.segments_stack.pop().expect("No segment to pop"); + assert_eq!(id, segment, "Popped segment is not the current segment"); + } + + fn enter_segment_with_valid_previous( + &mut self, + parent_id: ControlFlowSegmentId, + previous_sibling_id: Option, + segment_kind: SegmentKind, + ) -> ControlFlowSegmentId { + let child_id = self.segments.add(ControlFlowSegmentData { + scope: self.current_scope_id(), + parent: Some(parent_id), + children: segment_kind.new_children(), + segment_kind, + }); + self.segments_stack.push(child_id); + let parent = self.segments.get_mut(parent_id); + match &mut parent.children { + SegmentChildren::Sequential { ref mut children } => { + children.push(ControlFlowChild::Segment(child_id)); + } + SegmentChildren::PathBased { + node_previous_map: ref mut node_parent_map, + } => { + node_parent_map.insert(child_id, previous_sibling_id); + } + } + child_id + } + + pub(crate) fn register_catch_location(&mut self, data: CatchLocationData) -> CatchLocationId { + self.catch_locations.add(data) + } + + pub(crate) fn enter_catch(&mut self, catch_location_id: CatchLocationId) { + self.catch_location_stack.push(catch_location_id); + } + + pub(crate) fn exit_catch(&mut self, catch_location_id: CatchLocationId) { + let popped = self.catch_location_stack.pop().expect("No catch to pop"); + assert_eq!( + popped, catch_location_id, + "Popped catch location is not the expected catch location" + ); + } + + pub(crate) fn resolve_catch_for_interrupt( + &self, + interrupt_details: InterruptDetails, + ) -> ParseResult { + match interrupt_details { + InterruptDetails::Break { + label: Some(label), .. + } => { + let label_str = label.ident_string(); + for &catch_location_id in self.catch_location_stack.iter().rev() { + let catch_location = self.catch_locations.get(catch_location_id); + match catch_location { + CatchLocationData::Loop { + label: Some(loc_label), + } => { + if loc_label == &label_str { + return Ok(catch_location_id); + } + } + CatchLocationData::LabeledBlock { label: loc_label } => { + if loc_label == &label_str { + return Ok(catch_location_id); + } + } + _ => {} + } + } + label.parse_err( + "A labelled break must be used inside a loop or block with a matching label", + ) + } + InterruptDetails::Break { + label: None, + break_token, + } => { + for &catch_location_id in self.catch_location_stack.iter().rev() { + let catch_location = self.catch_locations.get(catch_location_id); + if let CatchLocationData::Loop { .. } = catch_location { + return Ok(catch_location_id); + } + } + break_token + .span + .parse_err("A break must be used inside a loop") + } + InterruptDetails::Continue { + label: Some(label), .. + } => { + let label_str = label.ident_string(); + for &catch_location_id in self.catch_location_stack.iter().rev() { + let catch_location = self.catch_locations.get(catch_location_id); + if let CatchLocationData::Loop { + label: Some(loc_label), + } = catch_location + { + if loc_label == &label_str { + return Ok(catch_location_id); + } + } + } + label.parse_err( + "A labelled continue must be used inside a loop with a matching label", + ) + } + InterruptDetails::Continue { + label: None, + continue_token, + } => { + for &catch_location_id in self.catch_location_stack.iter().rev() { + let catch_location = self.catch_locations.get(catch_location_id); + if let CatchLocationData::Loop { .. } = catch_location { + return Ok(catch_location_id); + } + } + continue_token + .span + .parse_err("A continue must be used inside a loop") + } + InterruptDetails::Revert { + revert_token, + label: Some(label), + } => { + let label_str = label.ident_string(); + for &catch_location_id in self.catch_location_stack.iter().rev() { + let catch_location = self.catch_locations.get(catch_location_id); + if let CatchLocationData::AttemptBlock { + label: Some(loc_label), + } = catch_location + { + if loc_label == &label_str { + return Ok(catch_location_id); + } + } + } + revert_token + .parse_err("A labelled revert must be used inside the left revertible part of an attempt arm, where the attempt has a matching label") + } + InterruptDetails::Revert { + revert_token, + label: None, + } => { + for &catch_location_id in self.catch_location_stack.iter().rev() { + let catch_location = self.catch_locations.get(catch_location_id); + if let CatchLocationData::AttemptBlock { .. } = catch_location { + return Ok(catch_location_id); + } + } + revert_token.parse_err( + "A revert must be used inside the left revertible part of an attempt arm", + ) + } + } + } +} + +#[derive(Debug)] +pub(crate) enum CatchLocationData { + /// A loop (can catch unlabeled break/continue, or labeled if this location has a label) + Loop { label: Option }, + /// A labeled block (can only catch labeled break with matching label) + LabeledBlock { label: String }, + /// An attempt block (can catch revert) + AttemptBlock { label: Option }, +} + +/// * Sequential: Children are instructions and segments, which have a fixed order +/// * Tree-based: Only has segment children; these form multiple possible execution paths. +/// +/// See `expressions/control_flow.rs` for some examples of how various segments are created. +#[derive(Debug)] +pub(crate) struct ControlFlowSegmentData { + pub(super) scope: ScopeId, + pub(super) parent: Option, + pub(super) children: SegmentChildren, + pub(super) segment_kind: SegmentKind, +} + +#[derive(Debug)] +pub(crate) enum SegmentKind { + Sequential, + PathBased, + LoopingSequential, + RevertibleSequential, +} + +impl SegmentKind { + pub(crate) fn is_looping(&self) -> bool { + match self { + SegmentKind::Sequential => false, + SegmentKind::PathBased => false, + SegmentKind::LoopingSequential => true, + SegmentKind::RevertibleSequential => false, + } + } + + fn new_children(&self) -> SegmentChildren { + match self { + SegmentKind::Sequential + | SegmentKind::LoopingSequential + | SegmentKind::RevertibleSequential => SegmentChildren::Sequential { children: vec![] }, + SegmentKind::PathBased => SegmentChildren::PathBased { + node_previous_map: HashMap::new(), + }, + } + } +} + +#[derive(Debug)] +pub(super) enum SegmentChildren { + Sequential { + children: Vec, + }, + PathBased { + node_previous_map: HashMap>, + }, +} + +impl SegmentChildren { + fn push(&mut self, child: ControlFlowChild) { + match self { + SegmentChildren::Sequential { children, .. } => children.push(child), + SegmentChildren::PathBased { .. } => panic!("Cannot push instruction under a tree-based segment. It needs a sequential segment underneath it."), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(super) enum ControlFlowChild { + Segment(ControlFlowSegmentId), + VariableDefinition(VariableDefinitionId), + VariableReference(VariableReferenceId, VariableDefinitionId), +} + +enum AllocatedScope { + Allocated, + Defined(ScopeData), +} + +impl AllocatedScope { + fn into_defined(self) -> ScopeData { + match self { + AllocatedScope::Defined(data) => data, + _ => panic!("Scope was not defined"), + } + } + + fn defined_mut(&mut self) -> &mut ScopeData { + match self { + AllocatedScope::Defined(data) => data, + _ => panic!("Scope was accessed before it was defined"), + } + } + + fn defined_ref(&self) -> &ScopeData { + match self { + AllocatedScope::Defined(data) => data, + _ => panic!("Scope was accessed before it was defined"), + } + } +} + +#[derive(Debug)] +pub(crate) struct ScopeData { + pub(crate) parent: Option, + pub(crate) definitions: Vec, +} + +enum AllocatedVariableDefinition { + Allocated { name: String, span: Span }, + Updating, + Defined(VariableDefinitionData), +} + +impl AllocatedVariableDefinition { + fn into_defined(self) -> VariableDefinitionData { + match self { + AllocatedVariableDefinition::Defined(data) => data, + _ => { + panic!("Variable definition was allocated but not instantiated during control flow") + } + } + } + + fn take_allocated(&mut self) -> (String, Span) { + match core::mem::replace(self, AllocatedVariableDefinition::Updating) { + AllocatedVariableDefinition::Allocated { name, span } => (name, span), + _ => panic!("Variable was already defined"), + } + } + + fn defined_mut(&mut self) -> &mut VariableDefinitionData { + match self { + AllocatedVariableDefinition::Defined(data) => data, + _ => panic!("Variable was not defined"), + } + } + + fn defined_ref(&self) -> &VariableDefinitionData { + match self { + AllocatedVariableDefinition::Defined(data) => data, + _ => panic!("Variable was not defined"), + } + } +} + +#[derive(Debug)] +pub(crate) struct VariableDefinitionData { + pub(crate) scope: ScopeId, + pub(crate) segment: ControlFlowSegmentId, + pub(crate) name: String, + pub(crate) definition_name_span: Span, + pub(crate) references: Vec, +} + +enum AllocatedVariableReference { + Allocated { name: String, span: Span }, + Updating, + Defined(VariableReferenceData), +} + +impl AllocatedVariableReference { + fn take_allocated(&mut self) -> (String, Span) { + match core::mem::replace(self, AllocatedVariableReference::Updating) { + AllocatedVariableReference::Allocated { name, span } => (name, span), + _ => panic!("Variable was already defined"), + } + } + + fn into_defined(self) -> VariableReferenceData { + match self { + AllocatedVariableReference::Defined(data) => data, + _ => panic!("Variable reference was allocated but not defined during control flow"), + } + } + + fn defined_mut(&mut self) -> &mut VariableReferenceData { + match self { + AllocatedVariableReference::Defined(data) => data, + _ => panic!("Variable was not defined"), + } + } +} + +#[derive(Debug)] +pub(crate) struct VariableReferenceData { + pub(crate) definition: VariableDefinitionId, + pub(crate) definition_scope: ScopeId, + pub(crate) segment: ControlFlowSegmentId, + pub(crate) reference_name_span: Span, + /// If a reference is the last use of a variable in any execution path + /// then it is able to be taken by value as Owned by the interpreter. + /// This avoids needing to prompt the code writer from lots of clones. + /// + /// This value is calculated during [ParseState::mark_final_use_of_variables]. + pub(crate) is_final_reference: bool, + #[cfg(feature = "debug")] + pub(crate) assertion: FinalUseAssertion, +} diff --git a/src/interpretation/source_stream.rs b/src/interpretation/source_stream.rs new file mode 100644 index 00000000..da680076 --- /dev/null +++ b/src/interpretation/source_stream.rs @@ -0,0 +1,164 @@ +use crate::internal_prelude::*; + +/// A parsed stream ready for interpretation +pub(crate) struct SourceStream { + items: Vec, + span: Span, +} + +impl SourceStream { + pub(crate) fn parse_with_span(input: SourceParser, span: Span) -> ParseResult { + let mut items = Vec::new(); + while !input.is_empty() { + items.push(input.parse()?); + } + Ok(Self { items, span }) + } + + pub(crate) fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + for item in self.items.iter_mut() { + item.control_flow_pass(context)?; + } + Ok(()) + } +} + +impl Interpret for SourceStream { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + for item in self.items.iter() { + item.interpret(interpreter)?; + } + Ok(()) + } +} + +impl HasSpan for SourceStream { + fn span(&self) -> Span { + self.span + } +} + +pub(crate) enum SourceItem { + Variable(EmbeddedVariable), + EmbeddedExpression(EmbeddedExpression), + EmbeddedStatements(EmbeddedStatements), + SourceGroup(SourceGroup), + Punct(Punct), + Ident(Ident), + Literal(Literal), + StreamLiteral(StreamLiteral), +} + +impl ParseSource for SourceItem { + fn parse(input: SourceParser) -> ParseResult { + Ok(match input.peek_grammar() { + SourcePeekMatch::Group(_) => SourceItem::SourceGroup(input.parse()?), + SourcePeekMatch::EmbeddedVariable => SourceItem::Variable(input.parse()?), + SourcePeekMatch::EmbeddedExpression => { + SourceItem::EmbeddedExpression(input.parse()?) + } + SourcePeekMatch::EmbeddedStatements => { + SourceItem::EmbeddedStatements(input.parse()?) + } + SourcePeekMatch::Punct(_) => SourceItem::Punct(input.parse_any_punct()?), + SourcePeekMatch::Ident(_) => SourceItem::Ident(input.parse_any_ident()?), + SourcePeekMatch::Literal(_) => SourceItem::Literal(input.parse()?), + SourcePeekMatch::StreamLiteral => SourceItem::StreamLiteral(input.parse().map_err(|err| err.add_context_if_none("If this wasn't intended to be a stream-based literal, replace % with %raw[%]."))?), + SourcePeekMatch::ObjectLiteral => return input.parse_err("Object literals are only supported in an expression context, not a stream context."), + SourcePeekMatch::End => return input.parse_err("Expected some item."), + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + match self { + SourceItem::Variable(variable) => variable.control_flow_pass(context), + SourceItem::EmbeddedExpression(expr) => expr.control_flow_pass(context), + SourceItem::EmbeddedStatements(block) => block.control_flow_pass(context), + SourceItem::SourceGroup(group) => group.control_flow_pass(context), + SourceItem::Punct(punct) => punct.control_flow_pass(context), + SourceItem::Ident(ident) => ident.control_flow_pass(context), + SourceItem::Literal(literal) => literal.control_flow_pass(context), + SourceItem::StreamLiteral(stream_literal) => stream_literal.control_flow_pass(context), + } + } +} + +impl Interpret for SourceItem { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + match self { + SourceItem::Variable(variable) => { + variable.interpret(interpreter)?; + } + SourceItem::EmbeddedExpression(block) => { + block.interpret(interpreter)?; + } + SourceItem::EmbeddedStatements(statements) => { + statements.interpret(interpreter)?; + } + SourceItem::SourceGroup(group) => { + group.interpret(interpreter)?; + } + SourceItem::Punct(punct) => interpreter.output(punct)?.push_punct(punct.clone()), + SourceItem::Ident(ident) => interpreter.output(ident)?.push_ident(ident.clone()), + SourceItem::Literal(literal) => { + interpreter.output(literal)?.push_literal(literal.clone()) + } + SourceItem::StreamLiteral(stream_literal) => stream_literal.interpret(interpreter)?, + } + Ok(()) + } +} + +impl HasSpanRange for SourceItem { + fn span_range(&self) -> SpanRange { + match self { + SourceItem::Variable(variable) => variable.span_range(), + SourceItem::EmbeddedExpression(expr) => expr.span_range(), + SourceItem::EmbeddedStatements(block) => block.span_range(), + SourceItem::SourceGroup(group) => group.span_range(), + SourceItem::Punct(punct) => punct.span_range(), + SourceItem::Ident(ident) => ident.span_range(), + SourceItem::Literal(literal) => literal.span_range(), + SourceItem::StreamLiteral(stream_literal) => stream_literal.span_range(), + } + } +} + +/// A parsed group ready for interpretation +pub(crate) struct SourceGroup { + source_delimiter: Delimiter, + source_delim_span: DelimSpan, + content: SourceStream, +} + +impl ParseSource for SourceGroup { + fn parse(input: SourceParser) -> ParseResult { + let (delimiter, delim_span, content) = input.parse_any_group()?; + let content = SourceStream::parse_with_span(&content, delim_span.join())?; + Ok(Self { + source_delimiter: delimiter, + source_delim_span: delim_span, + content, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.content.control_flow_pass(context) + } +} + +impl Interpret for SourceGroup { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + interpreter.in_output_group( + self.source_delimiter, + self.source_delim_span.join(), + |interpreter| self.content.interpret(interpreter), + ) + } +} + +impl HasSpan for SourceGroup { + fn span(&self) -> Span { + self.source_delim_span.join() + } +} diff --git a/src/interpretation/variable.rs b/src/interpretation/variable.rs new file mode 100644 index 00000000..72ee8431 --- /dev/null +++ b/src/interpretation/variable.rs @@ -0,0 +1,193 @@ +use crate::internal_prelude::*; + +#[derive(Clone)] +pub(crate) struct EmbeddedVariable { + marker: Token![#], + reference: VariableReference, +} + +impl ParseSource for EmbeddedVariable { + fn parse(input: SourceParser) -> ParseResult { + Ok(Self { + marker: input.parse()?, + reference: input.parse()?, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.reference.control_flow_pass(context) + } +} + +impl Interpret for EmbeddedVariable { + fn interpret(&self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + self.reference + .substitute_into_output(interpreter, Grouping::Flattened) + } +} + +impl HasSpanRange for EmbeddedVariable { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.marker.span, self.reference.span()) + } +} + +#[derive(Clone)] +pub(crate) struct VariableDefinition { + pub(crate) ident: Ident, + pub(crate) id: VariableDefinitionId, +} + +impl ParseSource for VariableDefinition { + fn parse(input: SourceParser) -> ParseResult { + let ident: Ident = input.parse()?; + + let ident_str = ident.to_string(); + + // Ident::parse() already errors on rust identifiers, so we only need + // to check preinterpret-exclusive keywords here. + if is_keyword(ident_str.as_str()) { + return ident.parse_err(format!( + "Cannot use preinterpret keyword `{}` as a variable name", + ident_str + )); + } + + let id = VariableDefinitionId::new_placeholder(); + Ok(Self { ident, id }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + context.register_variable_definition(&self.ident, &mut self.id); + context.define_variable(self.id); + Ok(()) + } +} + +impl VariableDefinition { + pub(crate) fn define(&self, interpreter: &mut Interpreter, value_source: impl IntoAnyValue) { + interpreter.define_variable(self.id, value_source.into_any_value()); + } +} + +#[derive(Clone)] +pub(crate) struct VariableReference { + pub(crate) ident: Ident, + #[allow(unused)] + pub(crate) id: VariableReferenceId, + #[cfg(feature = "debug")] + pub(crate) assertion: FinalUseAssertion, +} + +impl ParseSource for VariableReference { + fn parse(input: SourceParser) -> ParseResult { + let ident = input.parse()?; + #[cfg(feature = "debug")] + let assertion = { + if let Some((_, next)) = input.cursor().punct_matching(':') { + if let Some(_) = next.ident_matching("FINAL") { + let _ = input.parse_punct_matching(':')?; + let ident = input.parse_any_ident()?; + FinalUseAssertion::IsFinal(ident.span()) + } else if let Some(_) = next.ident_matching("NONFINAL") { + let _ = input.parse_punct_matching(':')?; + let ident = input.parse_any_ident()?; + FinalUseAssertion::IsNotFinal(ident.span()) + } else { + FinalUseAssertion::None + } + } else { + FinalUseAssertion::None + } + }; + Ok(Self { + ident, + id: VariableReferenceId::new_placeholder(), + #[cfg(feature = "debug")] + assertion, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + context.register_variable_reference(&self.ident, &mut self.id); + context.reference_variable( + self.id, + #[cfg(feature = "debug")] + self.assertion, + ) + } +} + +impl VariableReference { + fn substitute_into_output( + &self, + interpreter: &mut Interpreter, + grouping: Grouping, + ) -> ExecutionResult<()> { + let value = self.resolve_shared(interpreter)?; + value.as_ref_value().output_to( + grouping, + &mut ToStreamContext::new(interpreter.output(self)?, self.span_range()), + ) + } + + pub(crate) fn resolve_late_bound( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult> { + interpreter.resolve(self, RequestedOwnership::LateBound) + } + + pub(crate) fn resolve_concrete( + &self, + interpreter: &mut Interpreter, + ownership: ArgumentOwnership, + ) -> ExecutionResult { + interpreter + .resolve(self, RequestedOwnership::Concrete(ownership))? + .resolve(ownership) + } + + pub(crate) fn resolve_shared( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + Ok(self + .resolve_concrete(interpreter, ArgumentOwnership::Shared)? + .expect_shared()) + } +} + +impl HasSpan for VariableReference { + fn span(&self) -> Span { + self.ident.span() + } +} + +#[derive(Clone)] +pub(crate) struct VariablePattern { + pub(crate) definition: VariableDefinition, +} + +impl ParseSource for VariablePattern { + fn parse(input: SourceParser) -> ParseResult { + Ok(Self { + definition: input.parse()?, + }) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + self.definition.control_flow_pass(context) + } +} + +impl HandleDestructure for VariablePattern { + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: AnyValue, + ) -> ExecutionResult<()> { + self.definition.define(interpreter, value); + Ok(()) + } +} diff --git a/src/interpreter.rs b/src/interpreter.rs deleted file mode 100644 index c120b82f..00000000 --- a/src/interpreter.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::internal_prelude::*; - -pub(crate) fn interpret(token_stream: TokenStream) -> Result { - Interpreter::new().interpret_tokens(Tokens::new(token_stream)) -} - -pub(crate) struct Interpreter { - variables: HashMap, -} - -impl Interpreter { - pub(crate) fn new() -> Self { - Self { - variables: Default::default(), - } - } - - pub(crate) fn set_variable(&mut self, name: String, tokens: TokenStream) { - self.variables.insert(name, tokens); - } - - pub(crate) fn get_variable(&self, name: &str) -> Option<&TokenStream> { - self.variables.get(name) - } - - pub(crate) fn interpret_tokens(&mut self, mut source_tokens: Tokens) -> Result { - let mut expanded = TokenStream::new(); - loop { - match parse_next_item(&mut source_tokens)? { - NextItem::Leaf(token_tree) => { - expanded.extend(iter::once(token_tree)); - } - NextItem::Group(group) => { - expanded.extend(iter::once(TokenTree::Group(Group::new( - group.delimiter(), - // If it's a group, run interpret on its contents recursively. - self.interpret_tokens(Tokens::new(group.stream()))?, - )))); - } - NextItem::VariableSubstitution(variable_substitution) => { - expanded.extend(variable_substitution.execute(self)?); - } - NextItem::CommandInvocation(command_invocation) => { - expanded.extend(command_invocation.execute(self)?); - } - NextItem::EndOfStream => return Ok(expanded), - } - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 217f8837..aa43efe6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,43 +15,103 @@ //! * Run ./style-fix.sh //! --> //! -//! This crate provides the `preinterpret!` macro, a simple pre-processor of the token stream. It can be used inside the output of a declarative macro, or as a mini code generation tool all of its own. +//! Preinterpret takes the pain out of Rust code generation, providing a new paradigm to replace the clunkiness of declarative macros [[1](https://veykril.github.io/tlborm/decl-macros/patterns/callbacks.html), [2](https://github.com/rust-lang/rust/issues/96184#issue-1207293401), [3](https://veykril.github.io/tlborm/decl-macros/minutiae/metavar-and-expansion.html), [4](https://veykril.github.io/tlborm/decl-macros/patterns/push-down-acc.html)]. At its heart it is a bespoke Rust-like interpreted language, built from the ground up for code generation use cases. //! -//! It is a more powerful replacement for [paste](https://crates.io/crates/paste), and also brings functionality typically reserved for procedural macros: [quote](https://crates.io/crates/quote)-like token-stream substitution and some [syn](https://crates.io/crates/syn)-based functionality for operating on tokens and literals. +//! The [preinterpret](https://crates.io/crates/preinterpret) crate provides: +//! * The `stream!` macro, which starts in token stream output mode +//! * The `run!` macro, which starts in interpreter mode, and can `emit` or return a token stream //! -//! ```rust -//! preinterpret::preinterpret! { -//! [!set! #type_name = HelloWorld] +//! To install, add the following to your `Cargo.toml`: //! -//! struct #type_name; +//! ```toml +//! [dependencies] +//! preinterpret = "0.2" +//! ``` //! -//! #[doc = [!string! "This type is called [`" #type_name "`]"]] -//! impl #type_name { -//! fn [!ident_snake! say_ #type_name]() -> &'static str { -//! [!string! "It's time to say: " [!title! #type_name] "!"] +//! ## Use cases +//! +//! ### Simple code generation +//! +//! Sometimes you just need to generate lots of similar code, and preinterpet can be used directly: +//! +//! ```rust +//! trait TupleLength { +//! fn len(&self) -> usize; +//! } +//! +//! preinterpret::run!{ +//! for N in 0..=12 { +//! let type_params = %[]; +//! for a in ('A'..'Z').into_iter().take(N) { +//! type_params += a.to_ident() + %[,]; //! } +//! emit %[ +//! impl<#type_params> TupleLength for (#type_params) { +//! fn len(&self) -> usize { +//! #N +//! } +//! } +//! ]; //! } //! } -//! assert_eq!(HelloWorld::say_hello_world(), "It's time to say: Hello World!") +//! assert_eq!(('a', 'b', 'c').len(), 3); //! ``` //! -//! To install, add the following to your `Cargo.toml`: +//! ### Inside procedural macros //! -//! ```toml -//! [dependencies] -//! preinterpret = "0.2" +//! It can be used to simplify code generation inside procedural macro definitions: +//! * It replaces [paste](https://crates.io/crates/paste) to allow concatenated creation of idents +//! * It brings various features previously reserved for procedural macros: [quote](https://crates.io/crates/quote)-like token-stream substitution and [syn](https://crates.io/crates/syn)-based functionality for operating on tokens and literals. +//! +//! Notably, using variables to name token stream sections for clarity and reuse can make macro code a lot easier to read. +//! +//! ```rust +//! macro_rules! create_my_type { +//! ( +//! $(#[$attributes:meta])* +//! $vis:vis struct $type_name:ident { +//! $($field_name:ident: $inner_type:ident),* $(,)? +//! } +//! ) => {preinterpret::stream! { +//! #{ +//! let type_name = %[My $type_name].to_ident(); +//! } +//! +//! $(#[$attributes])* +//! $vis struct #type_name { +//! $($field_name: $inner_type,)* +//! } +//! +//! impl #type_name { +//! $( +//! fn #(%[my_ $inner_type].to_ident_snake())(&self) -> &$inner_type { +//! &self.$field_name +//! } +//! )* +//! } +//! }} +//! } +//! create_my_type! { +//! struct Struct { +//! field0: String, +//! field1: u64, +//! } +//! } +//! assert_eq!(MyStruct { field0: "Hello".into(), field1: 21 }.my_string(), "Hello") //! ``` //! -//! This README concerns `preinterpret` v0.2 which offers a simple pre-processor. A much more comprehensive rust-inspired interpreter is coming in v1.0, currently in progress on the `develop` branch. +//! ### As a replacement for procedural macros +//! +//! Coming soon... //! //! ## User Guide //! //! Preinterpret works with its own very simple language, with two pieces of syntax: //! //! * **Commands**: `[!command_name! input token stream...]` take an input token stream and output a token stream. There are a number of commands which cover a toolkit of useful functions. -//! * **Variables**: `[!set! #var_name = token stream...]` defines a variable, and `#var_name` substitutes the variable into another command or the output. +//! * **Variables**: `#(let var_name = %[token stream...];)` defines a variable, and `#var_name` substitutes the variable into another command or the output. //! -//! Commands can be nested intuitively. The input of all commands (except `[!raw! ...]`) are first interpreted before the command itself executes. +//! Commands can be nested intuitively. In general, the input of commands are first interpreted before the command itself executes. //! //! ### Declarative macro example //! @@ -64,8 +124,10 @@ //! $vis:vis struct $type_name:ident { //! $($field_name:ident: $inner_type:ident),* $(,)? //! } -//! ) => {preinterpret::preinterpret! { -//! [!set! #type_name = [!ident! My $type_name]] +//! ) => {preinterpret::stream! { +//! #{ +//! let type_name = %[My $type_name].to_ident(); +//! } //! //! $(#[$attributes])* //! $vis struct #type_name { @@ -74,7 +136,7 @@ //! //! impl #type_name { //! $( -//! fn [!ident_snake! my_ $inner_type](&self) -> &$inner_type { +//! fn #(%[my_ $inner_type].to_ident_snake())(&self) -> &$inner_type { //! &self.$field_name //! } //! )* @@ -117,15 +179,17 @@ //! For example: //! //! ```rust -//! preinterpret::preinterpret! { -//! [!set! #type_name = [!ident! HelloWorld]] +//! preinterpret::stream! { +//! #{ +//! let type_name = %[HelloWorld]; +//! } //! //! struct #type_name; //! -//! #[doc = [!string! "This type is called [`" #type_name "`]"]] +//! #[doc = #(%["This type is called [`" #type_name "`]"].to_string())] //! impl #type_name { -//! fn [!ident_snake! say_ #type_name]() -> &'static str { -//! [!string! "It's time to say: " [!title! #type_name] "!"] +//! fn #(%[say_ #type_name].to_ident_snake())() -> &'static str { +//! #(%["It's time to say: " #(type_name.to_string().to_title_case()) "!"].to_string()) //! } //! } //! } @@ -136,9 +200,9 @@ //! //! ### Special commands //! -//! * `[!set! #foo = Hello]` followed by `[!set! #foo = #bar(World)]` sets the variable `#foo` to the token stream `Hello` and `#bar` to the token stream `Hello(World)`, and outputs no tokens. Using `#foo` or `#bar` later on will output the current value in the corresponding variable. -//! * `[!raw! abc #abc [!ident! test]]` outputs its contents as-is, without any interpretation, giving the token stream `abc #abc [!ident! test]`. -//! * `[!ignore! $foo]` ignores all of its content and outputs no tokens. It is useful to make a declarative macro loop over a meta-variable without outputting it into the resulting stream. +//! * `#(let foo = %[Hello];)` followed by `#(let foo = %[#bar(World)];)` sets the variable `#foo` to the token stream `Hello` and `#bar` to the token stream `Hello(World)`, and outputs no tokens. Using `#foo` or `#bar` later on will output the current value in the corresponding variable. +//! * `%raw[abc #abc %[test]]` outputs its contents as-is, without any interpretation, giving the token stream `abc #abc %[test]`. +//! * `let _ = %raw[$foo]` ignores all content inside `[...]` and outputs no tokens. It is useful to make a declarative macro loop over a meta-variable without outputting it into the resulting stream. //! //! ### Concatenate and convert commands //! @@ -149,33 +213,33 @@ //! //! The following commands output idents: //! -//! * `[!ident! X Y "Z"]` outputs the ident `XYZ` -//! * `[!ident_camel! my hello_world]` outputs `MyHelloWorld` -//! * `[!ident_snake! my_ HelloWorld]` outputs `my_hello_world` -//! * `[!ident_upper_snake! my_ const Name]` outputs `MY_CONST_NAME` +//! * `%[X Y "Z"].to_ident()` outputs the ident `XYZ` +//! * `%[my hello_world].to_ident_camel()` outputs `MyHelloWorld` +//! * `%[my_ HelloWorld].to_ident_snake()` outputs `my_hello_world` +//! * `%[my_ const Name].to_ident_upper_snake()` outputs `MY_CONST_NAME` //! -//! The `!literal!` command outputs any kind of literal, for example: +//! The following commands output any kind of literal, for example: //! -//! * `[!literal! 31 u 32]` outputs the integer literal `31u32` -//! * `[!literal! '"' hello '"']` outputs the string literal `"hello"` +//! * `%[31 u 32].to_literal()` outputs the integer literal `31u32` +//! * `%['"' hello '"'].to_literal()` outputs the string literal `"hello"` //! //! The following commands output strings, without dropping non-alphanumeric characters: //! -//! * `[!string! X Y " " Z (Hello World)]` outputs `"XY Z(HelloWorld)"` -//! * `[!upper! foo_bar]` outputs `"FOO_BAR"` -//! * `[!lower! FooBar]` outputs `"foobar"` -//! * `[!capitalize! fooBar]` outputs `"FooBar"` -//! * `[!decapitalize! FooBar]` outputs `"fooBar"` +//! * `%[X Y " " Z (Hello World)].to_string()` outputs `"XY Z(HelloWorld)"` +//! * `"foo_bar".to_uppercase()` outputs `"FOO_BAR"` +//! * `"FooBar".to_lowercase()` outputs `"foobar"` +//! * `"fooBar".capitalize()"` outputs `"FooBar"` +//! * `"FooBar".decapitalize()` outputs `"fooBar"` //! //! The following commands output strings, whilst also dropping non-alphanumeric characters: //! -//! * `[!snake! FooBar]` and `[!lower_snake! FooBar]` are equivalent and output `"foo_bar"` -//! * `[!upper_snake! FooBar]` outputs `"FOO_BAR"` -//! * `[!camel! foo_bar]` and `[!upper_camel! foo_bar]` are equivalent and output `"FooBar"` -//! * `[!lower_camel! foo_bar]` outputs `"fooBar"` -//! * `[!kebab! fooBar]` outputs `"foo-bar"` -//! * `[!title! fooBar]` outputs `"Foo Bar"` -//! * `[!insert_spaces! fooBar]` outputs `"foo Bar"` +//! * `"FooBar".to_lower_snake_case()` outputs `"foo_bar"` +//! * `"FooBar".to_upper_snake_case()"` outputs `"FOO_BAR"` +//! * `"foo_bar".to_upper_camel_case()"` outputs `"FooBar"` +//! * `"foo_bar".to_lower_camel_case()"` outputs `"fooBar"` +//! * `"fooBar".to_kebab_case()"` outputs `"foo-bar"` +//! * `"fooBar".to_title_case()"` outputs `"Foo Bar"` +//! * `"fooBar".insert_spaces()"` outputs `"foo Bar"` //! //! > [!NOTE] //! > @@ -214,10 +278,12 @@ //! // Arbitrary (non-const) type generics //! < $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? $( = $deflt:tt)? ),+ > //! )? -//! } => {preinterpret::preinterpret!{ -//! [!set! #impl_generics = $(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?] -//! [!set! #type_generics = $(< $( $lt ),+ >)?] -//! [!set! #my_type = $type_name #type_generics] +//! } => {preinterpret::stream!{ +//! #{ +//! let impl_generics = %[$(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?]; +//! let type_generics = %[$(< $( $lt ),+ >)?]; +//! let my_type = %[$type_name #type_generics]; +//! } //! //! $( //! // Output each marker trait for the type @@ -243,7 +309,7 @@ //! macro_rules! create_struct_and_getters { //! ( //! $name:ident { $($field:ident),* $(,)? } -//! ) => {preinterpret::preinterpret!{ +//! ) => {preinterpret::stream!{ //! // Define a struct with the given fields //! pub struct $name { //! $( @@ -254,7 +320,7 @@ //! impl $name { //! $( //! // Define get_X for each field X -//! pub fn [!ident! get_ $field](&self) -> &str { +//! pub fn #(%[get_ $field].to_ident())(&self) -> &str { //! &self.$field //! } //! )* @@ -266,44 +332,6 @@ //! } //! ``` //! -//! Variable assignment works intuitively with the `* + ?` expansion operators, allowing basic procedural logic, such as creation of loop counts and indices before [meta-variables](https://github.com/rust-lang/rust/issues/83527) are stabilized. -//! -//! For example: -//! ```rust -//! macro_rules! count_idents { -//! { -//! $($item: ident),* -//! } => {preinterpret::preinterpret!{ -//! [!set! #current_index = 0usize] -//! $( -//! [!ignore! $item] // Loop over the items, but don't output them -//! [!set! #current_index = #current_index + 1] -//! )* -//! [!set! #count = #current_index] -//! #count -//! }} -//! } -//! ``` -//! -//! To quickly explain how this works, imagine we evaluate `count_idents!(a, b, c)`. As `count_idents!` is the most outer macro, it runs first, and expands into the following token stream: -//! -//! ```rust -//! let count = preinterpret::preinterpret!{ -//! [!set! #current_index = 0usize] -//! [!ignore! a] -//! [!set! #current_index = #current_index + 1] -//! [!ignore! = b] -//! [!set! #current_index = #current_index + 1] -//! [!ignore! = c] -//! [!set! #current_index = #current_index + 1] -//! [!set! #count = #current_index] -//! #count -//! }; -//! ``` -//! -//! Now the `preinterpret!` macro runs, resulting in `#count` equal to the token stream `0usize + 1 + 1 + 1`. -//! This will be improved in future releases by adding support for mathematical operations on integer literals. -//! //! ### Simplicity //! //! Using preinterpret partially mitigates some common areas of confusion when writing declarative macros. @@ -347,8 +375,8 @@ //! macro_rules! impl_new_type { //! { //! $vis:vis $my_type:ident($my_inner_type:ty) -//! } => {preinterpret::preinterpret!{ -//! #[xyz(as_type = [!string! $my_inner_type])] +//! } => {preinterpret::stream!{ +//! #[xyz(as_type = #(%[$my_inner_type].to_string()))] //! $vis struct $my_type($my_inner_type); //! }} //! } @@ -373,39 +401,221 @@ //! //! Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. //! -mod command; -mod commands; +mod expressions; +mod extensions; mod internal_prelude; -mod interpreter; -mod parsing; -mod string_conversion; +mod interpretation; +mod misc; use internal_prelude::*; -/// Runs a simple interpeter over the token stream, allowing for variable assignment and substitution, -/// and a toolkit of commands to simplify code generation. +/// Interprets its input as a preinterpret stream. /// -/// Commands look like `[!command! arguments as token stream here]` and can be nested. +/// See the [crate-level documentation](crate) for full details. +#[proc_macro] +pub fn stream(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream { + preinterpret_stream_internal(proc_macro2::TokenStream::from(token_stream)) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +fn preinterpret_stream_internal(input: TokenStream) -> SynResult { + let (stream, parse_state) = input + .source_parse_and_analyze( + |input| SourceStream::parse_with_span(input, Span::call_site()), + SourceStream::control_flow_pass, + ) + .convert_to_final_result()?; + + let mut interpreter = Interpreter::new(parse_state); + + stream + .interpret(&mut interpreter) + .convert_to_final_result()?; + + let output_stream = interpreter.complete(); + + Ok(output_stream.into_token_stream()) +} + +/// Interprets its input as a preinterpret expression block, which should return a token stream. /// -/// ## Command cheat sheet -/// * `[!set! #foo = ...]` set a variable to the provided token stream -/// * `#foo` outputs the variable's saved token stream -/// * `[!ident! ...]` outputs an ident from parsing the concatenated token stream -/// * `[!ident_camel! ...]` outputs an UpperCamelCased ident from parsing the concatenated token stream -/// * `[!ident_snake! ...]` outputs a lower_snake_cased ident from parsing the concatenated token stream -/// * `[!ident_upper_snake! ...]` outputs an UPPER_SNAKE_CASED ident from parsing the concatenated token stream -/// * `[!string! ...]` outputs the concatenated token stream -/// * `[!literal! ..]` outputs a literal from parsing the concatenated token stream -/// * `#[doc = [!string! "My documentation is for " #my_type "."]]` can be used to create documentation strings +/// See the [crate-level documentation](crate) for full details. +#[proc_macro] +pub fn run(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream { + preinterpret_run_internal(proc_macro2::TokenStream::from(token_stream)) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +fn preinterpret_run_internal(input: TokenStream) -> SynResult { + let (content, parse_state) = input + .source_parse_and_analyze( + ExpressionBlockContent::parse, + ExpressionBlockContent::control_flow_pass, + ) + .convert_to_final_result()?; + + let mut interpreter = Interpreter::new(parse_state); + + let entry_span = Span::call_site().span_range(); + let returned_stream = content + .evaluate_spanned(&mut interpreter, entry_span, RequestedOwnership::owned()) + .and_then(|x| x.expect_owned().into_stream()) + .convert_to_final_result()?; + + let mut output_stream = interpreter.complete(); + + let output = if output_stream.is_empty() { + returned_stream + } else { + returned_stream.append_into(&mut output_stream); + output_stream + }; + + Ok(output.into_token_stream()) +} + +/// Returns the scope and segment information for the given code. +#[cfg(feature = "debug")] +#[proc_macro] +pub fn scope_debug(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream { + debug::scope_debug(proc_macro2::TokenStream::from(token_stream)) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +#[cfg(feature = "debug")] +mod debug { + use super::*; + + pub(super) fn scope_debug(input: TokenStream) -> SynResult { + let (_, scopes) = input + .clone() + .source_parse_and_analyze( + ExpressionBlockContent::parse, + ExpressionBlockContent::control_flow_pass, + ) + .convert_to_final_result()?; + + let output = format!("{:#?}", scopes); + + Ok(TokenStream::from_iter([TokenTree::Literal( + Literal::string(output.as_str()), + )])) + } +} + +/// Interprets its input as a preinterpret expression block, which should return a token stream. /// /// See the [crate-level documentation](crate) for full details. +#[cfg(feature = "benchmark")] #[proc_macro] -pub fn preinterpret(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream { - interpret(proc_macro2::TokenStream::from(token_stream)) +pub fn benchmark_run(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream { + benchmarking::benchmark_run(proc_macro2::TokenStream::from(token_stream)) .unwrap_or_else(|err| err.to_compile_error()) .into() } +#[cfg(feature = "benchmark")] +mod benchmarking { + use super::*; + use std::time::{Duration, Instant}; + + struct TimingContext { + section_totals: HashMap<&'static str, (Duration, usize)>, + } + + impl TimingContext { + fn new() -> Self { + Self { + section_totals: HashMap::new(), + } + } + + fn time(&mut self, section: &'static str, f: impl FnOnce() -> T) -> T { + let start = Instant::now(); + let output = f(); + let duration = start.elapsed(); + let totals = self + .section_totals + .entry(section) + .or_insert((Duration::ZERO, 0)); + totals.0 += duration; + totals.1 += 1; + output + } + + fn average_duration(&self, section: &'static str) -> Option { + self.section_totals + .get(section) + .map(|(total, count)| *total / (*count as u32)) + } + } + + fn timed(f: impl Fn(&mut TimingContext) -> Result) -> Result { + const WARMUP: u32 = 100; + const REPEATS: u32 = 1000; + for _ in 0..WARMUP { + let _ = f(&mut TimingContext::new()); + } + let mut context = TimingContext::new(); + for _ in 0..REPEATS { + f(&mut context)?; + } + Ok(context) + } + + pub(super) fn benchmark_run(input: TokenStream) -> SynResult { + let results = timed(|context| -> SynResult<()> { + let input = input.clone(); + let mut parsed = context.time("parsing", || { + parse_with(input, parse_without_analysis(ExpressionBlockContent::parse)) + .convert_to_final_result() + })?; + + let scopes = context.time("analysis", || { + ControlFlowContext::analyze(&mut parsed, ExpressionBlockContent::control_flow_pass) + .convert_to_final_result() + })?; + + let output = context.time("evaluation", move || -> SynResult { + let mut interpreter = Interpreter::new(scopes); + let entry_span = Span::call_site().span_range(); + let returned_stream = parsed + .evaluate_spanned(&mut interpreter, entry_span, RequestedOwnership::owned()) + .and_then(|x| x.expect_owned().into_stream()) + .convert_to_final_result()?; + + let mut output_stream = interpreter.complete(); + + Ok(if output_stream.is_empty() { + returned_stream + } else { + returned_stream.append_into(&mut output_stream); + output_stream + }) + })?; + + let _ = context.time("output", move || output.into_token_stream()); + + Ok(()) + })?; + + let output = format!( + "- Parsing | {: >5}ns\n- Analysis | {: >5}ns\n- Evaluation | {: >5}ns\n- Output | {: >5}ns", + results.average_duration("parsing").unwrap().as_micros(), + results.average_duration("analysis").unwrap().as_micros(), + results.average_duration("evaluation").unwrap().as_micros(), + results.average_duration("output").unwrap().as_micros() + ); + + Ok(TokenStream::from_iter([TokenTree::Literal( + Literal::string(output.as_str()), + )])) + } +} + // This is the recommended way to run the doc tests in the readme #[doc = include_str!("../README.md")] #[cfg(doctest)] // Don't actually export this! diff --git a/src/misc/arena.rs b/src/misc/arena.rs new file mode 100644 index 00000000..cdc172c5 --- /dev/null +++ b/src/misc/arena.rs @@ -0,0 +1,131 @@ +use std::fmt::Debug; + +use super::*; + +macro_rules! new_key { + ($vis:vis $key:ident) => { + #[derive(Copy, Clone, PartialEq, Eq, Hash)] + $vis struct $key(Key<$key>); + + impl ArenaKey for $key { + fn from_inner(value: Key<$key>) -> Self { + Self(value) + } + + fn to_inner(self) -> Key { + self.0 + } + + fn as_inner(&self) -> &Key { + &self.0 + } + } + + impl std::fmt::Debug for $key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}({:?})", stringify!($key), self.0) + } + } + } +} + +pub(crate) use new_key; + +pub(crate) struct Arena { + data: Vec, + instance_marker: PhantomData, +} + +impl Arena { + pub(crate) fn new() -> Self { + Self { + data: Vec::new(), + instance_marker: PhantomData, + } + } + + pub(crate) fn add(&mut self, value: D) -> K { + let index = self.data.len(); + self.data.push(value); + K::from_inner(Key::new(index)) + } + + pub(crate) fn get(&self, key: K) -> &D { + let index = key.to_inner().index; + match self.data.get(index) { + Some(value) => value, + None => panic!("{}", invalid_key_message(index)), + } + } + + pub(crate) fn get_mut(&mut self, key: K) -> &mut D { + let index = key.to_inner().index; + match self.data.get_mut(index) { + Some(value) => value, + None => panic!("{}", invalid_key_message(index)), + } + } + + pub(crate) fn iter(&self) -> impl Iterator { + self.data + .iter() + .enumerate() + .map(|(index, v)| (K::from_inner(Key::new(index)), v)) + } + + pub(crate) fn map_all(self, f: impl Fn(D) -> D2) -> Arena { + Arena { + data: self.data.into_iter().map(f).collect(), + instance_marker: PhantomData, + } + } +} + +fn invalid_key_message(key_index: usize) -> &'static str { + if key_index == PLACEHOLDER_KEY_INDEX { + "Attempted to access an arena with a placeholder key. The key must be properly initialized before use." + } else { + "Arena key does not exist in this arena." + } +} + +impl Debug for Arena { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + +pub(crate) trait ArenaKey: Sized { + fn from_inner(value: Key) -> Self; + fn to_inner(self) -> Key; + fn as_inner(&self) -> &Key; + fn new_placeholder() -> Self { + Self::from_inner(Key::new(PLACEHOLDER_KEY_INDEX)) + } + fn is_placeholder(&self) -> bool { + self.as_inner().index == PLACEHOLDER_KEY_INDEX + } +} + +const PLACEHOLDER_KEY_INDEX: usize = usize::MAX; + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct Key { + index: usize, + instance_marker: PhantomData, +} + +impl std::fmt::Debug for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.index) + } +} + +impl Key { + fn new(index: usize) -> Self { + Self { + index, + instance_marker: PhantomData, + } + } +} diff --git a/src/misc/errors.rs b/src/misc/errors.rs new file mode 100644 index 00000000..6dd464c9 --- /dev/null +++ b/src/misc/errors.rs @@ -0,0 +1,370 @@ +use crate::internal_prelude::*; + +pub(crate) type ParseResult = core::result::Result; + +#[allow(unused)] +pub(crate) trait ParseResultExt { + /// This is not a `From` because it wants to be explicit + fn convert_to_final_result(self) -> syn::Result; + fn into_execution_result(self) -> ExecutionResult; +} + +impl ParseResultExt for ParseResult { + fn convert_to_final_result(self) -> syn::Result { + self.map_err(|error| error.convert_to_final_error()) + } + + fn into_execution_result(self) -> ExecutionResult { + self.map_err(|error| error.into()) + } +} + +#[derive(Debug)] +pub(crate) enum DetailedError { + Standard(syn::Error), + Contextual(syn::Error, String), +} + +impl Default for DetailedError { + fn default() -> Self { + DetailedError::Standard(syn::Error::new( + proc_macro2::Span::call_site(), + "An unknown error occurred", + )) + } +} + +impl HasSpan for DetailedError { + fn span(&self) -> Span { + match self { + DetailedError::Standard(e) => e.span(), + DetailedError::Contextual(e, _) => e.span(), + } + } +} + +impl DetailedError { + /// This is not a `From` because it wants to be explicit + pub(crate) fn convert_to_final_error(self) -> syn::Error { + match self { + DetailedError::Standard(e) => e, + DetailedError::Contextual(e, message) => e.concat(&format!("\n{}", message)), + } + } + + pub(crate) fn add_context_if_none(self, context: impl std::fmt::Display) -> Self { + match self { + DetailedError::Standard(e) => DetailedError::Contextual(e, context.to_string()), + other => other, + } + } +} + +#[derive(Debug)] +pub(crate) struct ParseError(DetailedError); + +impl From for ParseError { + fn from(e: syn::Error) -> Self { + ParseError(DetailedError::Standard(e)) + } +} + +impl HasSpan for ParseError { + fn span(&self) -> Span { + self.0.span() + } +} + +impl ParseError { + pub(crate) fn new(error: syn::Error) -> Self { + ParseError(DetailedError::Standard(error)) + } + + pub(crate) fn add_context_if_none(self, context: impl std::fmt::Display) -> Self { + ParseError(self.0.add_context_if_none(context)) + } + + /// This is not a `From` because it wants to be explicit + pub(crate) fn convert_to_final_error(self) -> syn::Error { + self.0.convert_to_final_error() + } +} + +// Ideally this would be our own enum with Completed / Interrupted variants, +// but we want it to work with `?` and defining custom FromResidual is not +// possible on stable (at least according to our MSRV). +pub(crate) type ExecutionResult = core::result::Result; + +pub(crate) enum ExecutionOutcome { + Value(Spanned), + ControlFlow(ControlFlowInterrupt), +} + +pub(crate) trait ExecutionResultExt { + /// This is not a `From` because it wants to be explicit + fn convert_to_final_result(self) -> syn::Result; +} + +impl ExecutionResultExt for ExecutionResult { + fn convert_to_final_result(self) -> syn::Result { + self.map_err(|error| error.convert_to_final_error()) + } +} + +#[derive(Debug)] +pub(crate) struct ExecutionInterrupt { + inner: Box, +} + +impl ExecutionInterrupt { + fn new(inner: ExecutionInterruptInner) -> Self { + ExecutionInterrupt { + inner: Box::new(inner), + } + } + + fn new_error(kind: ErrorKind, error: syn::Error) -> Self { + ExecutionInterrupt { + inner: Box::new(ExecutionInterruptInner::Error( + kind, + DetailedError::Standard(error), + )), + } + } + + pub(crate) fn into_outcome( + self, + catch_location_id: CatchLocationId, + ) -> ExecutionResult> { + match *self.inner { + ExecutionInterruptInner::ControlFlowInterrupt(interrupt) + if catch_location_id == interrupt.catch_location_id() => + { + Ok(ExecutionOutcome::ControlFlow(interrupt)) + } + _ => Err(self), + } + } + + /// Generally, coding errors should be propagated, while user-thrown errors + /// and runtime errors which are indicative of invalid values being + /// present should be caught. + /// + /// This allows the attempt block to use the first valid branch given the data + /// it encounters. + pub(crate) fn is_catchable_by_attempt_block(&self, catch_location_id: CatchLocationId) -> bool { + match self.inner.as_ref() { + ExecutionInterruptInner::Error(ErrorKind::Syntax, _) => false, + ExecutionInterruptInner::Error(ErrorKind::Type, _) => false, + ExecutionInterruptInner::Error(ErrorKind::Ownership, _) => false, + ExecutionInterruptInner::Error(ErrorKind::Debug, _) => false, + ExecutionInterruptInner::Error(ErrorKind::Assertion, _) => true, + ExecutionInterruptInner::Error(ErrorKind::Value, _) => true, + ExecutionInterruptInner::Error(ErrorKind::ControlFlow, _) => false, + ExecutionInterruptInner::Error(ErrorKind::Parse, _) => true, + ExecutionInterruptInner::ControlFlowInterrupt(interrupt) => { + interrupt.catch_location_id() == catch_location_id + } + } + } + + pub(crate) fn error_mut(&mut self) -> Option<(ErrorKind, &mut DetailedError)> { + Some(match self.inner.as_mut() { + ExecutionInterruptInner::Error(kind, error) => (*kind, error), + ExecutionInterruptInner::ControlFlowInterrupt { .. } => return None, + }) + } + + pub(crate) fn syntax_error(error: syn::Error) -> Self { + Self::new_error(ErrorKind::Syntax, error) + } + + pub(crate) fn type_error(error: syn::Error) -> Self { + Self::new_error(ErrorKind::Type, error) + } + + pub(crate) fn ownership_error(error: syn::Error) -> Self { + Self::new_error(ErrorKind::Ownership, error) + } + + pub(crate) fn debug_error(error: syn::Error) -> Self { + Self::new_error(ErrorKind::Debug, error) + } + + pub(crate) fn assertion_error(error: syn::Error) -> Self { + Self::new_error(ErrorKind::Assertion, error) + } + + pub(crate) fn parse_error(error: ParseError) -> Self { + Self::new(ExecutionInterruptInner::Error(ErrorKind::Parse, error.0)) + } + + pub(crate) fn value_error(error: syn::Error) -> Self { + Self::new_error(ErrorKind::Value, error) + } + + pub(crate) fn control_flow_error(error: syn::Error) -> Self { + Self::new_error(ErrorKind::ControlFlow, error) + } + + pub(crate) fn control_flow(control_flow: ControlFlowInterrupt) -> Self { + Self::new(ExecutionInterruptInner::ControlFlowInterrupt(control_flow)) + } +} + +impl From for ExecutionInterrupt { + fn from(e: ParseError) -> Self { + ExecutionInterrupt::parse_error(e) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum ErrorKind { + /// Some error with preinterpret syntax + Syntax, + /// Method doesn't exist on value, etc + Type, + /// Some violation of borrowing rules or unique ownership + Ownership, + /// An error from `.debug()` which shouldn't be caught + Debug, + /// User-thrown errors + Assertion, + /// An unexpected value (e.g. out-of-bounds index) + Value, + /// An error caused by invalid control flow (e.g. no matching attempt arm) + ControlFlow, + /// A parse error which occurred during runtime + /// (e.g. from parsing macro arguments in a preinterpret parser) + Parse, +} + +impl ErrorKind { + pub(crate) fn as_str(&self) -> &'static str { + match self { + ErrorKind::Syntax => "SyntaxError", + ErrorKind::Type => "TypeError", + ErrorKind::Ownership => "OwnershipError", + ErrorKind::Debug => "DebugError", + ErrorKind::Assertion => "AssertionError", + ErrorKind::Value => "ValueError", + ErrorKind::ControlFlow => "ControlFlowError", + ErrorKind::Parse => "ParseError", + } + } +} + +#[derive(Debug)] +enum ExecutionInterruptInner { + /// Some runtime error + Error(ErrorKind, DetailedError), + /// Indicates unwinding due to control flow (break/continue) + ControlFlowInterrupt(ControlFlowInterrupt), +} + +pub(crate) enum ControlFlowInterrupt { + Break(BreakInterrupt), + Continue(ContinueInterrupt), + Revert(RevertInterrupt), +} + +impl std::fmt::Debug for ControlFlowInterrupt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ControlFlowInterrupt::Break(_) => f.write_str("Break"), + ControlFlowInterrupt::Continue(_) => f.write_str("Continue"), + ControlFlowInterrupt::Revert(_) => f.write_str("Revert"), + } + } +} + +impl ControlFlowInterrupt { + pub(crate) fn new_break( + target_catch_location: CatchLocationId, + value: Option, + ) -> Self { + ControlFlowInterrupt::Break(BreakInterrupt { + target_catch_location, + value, + }) + } + + pub(crate) fn new_continue(target_catch_location: CatchLocationId) -> Self { + ControlFlowInterrupt::Continue(ContinueInterrupt { + target_catch_location, + }) + } + + pub(crate) fn new_revert(target_catch_location: CatchLocationId) -> Self { + ControlFlowInterrupt::Revert(RevertInterrupt { + target_catch_location, + }) + } + + fn catch_location_id(&self) -> CatchLocationId { + match self { + ControlFlowInterrupt::Break(break_interrupt) => break_interrupt.target_catch_location, + ControlFlowInterrupt::Continue(continue_interrupt) => { + continue_interrupt.target_catch_location + } + ControlFlowInterrupt::Revert(revert_interrupt) => { + revert_interrupt.target_catch_location + } + } + } +} + +pub(crate) struct BreakInterrupt { + target_catch_location: CatchLocationId, + value: Option, +} + +impl BreakInterrupt { + pub(crate) fn into_requested_value( + self, + span_range: SpanRange, + ownership: RequestedOwnership, + ) -> ExecutionResult { + let value = match self.value { + Some(value) => value, + None => ().into_any_value(), + }; + ownership + .map_from_owned(Spanned(value, span_range)) + .map(|spanned| spanned.0) + } +} + +pub(crate) struct ContinueInterrupt { + target_catch_location: CatchLocationId, +} + +pub(crate) struct RevertInterrupt { + target_catch_location: CatchLocationId, +} + +impl ExecutionInterrupt { + pub(crate) fn convert_to_final_error(self) -> syn::Error { + match *self.inner { + ExecutionInterruptInner::Error(_, e) => e.convert_to_final_error(), + ExecutionInterruptInner::ControlFlowInterrupt(ControlFlowInterrupt::Break(_)) => { + panic!( + "Internal error: break escaped to root (should be caught at parse time). \ + Please report this bug at https://github.com/dhedey/preinterpret/issues" + ) + } + ExecutionInterruptInner::ControlFlowInterrupt(ControlFlowInterrupt::Continue(_)) => { + panic!( + "Internal error: continue escaped to root (should be caught at parse time). \ + Please report this bug at https://github.com/dhedey/preinterpret/issues" + ) + } + ExecutionInterruptInner::ControlFlowInterrupt(ControlFlowInterrupt::Revert(_)) => { + panic!( + "Internal error: revert escaped to root (should be caught at parse time). \ + Please report this bug at https://github.com/dhedey/preinterpret/issues" + ) + } + } + } +} diff --git a/src/misc/field_inputs.rs b/src/misc/field_inputs.rs new file mode 100644 index 00000000..9d19385a --- /dev/null +++ b/src/misc/field_inputs.rs @@ -0,0 +1,169 @@ +macro_rules! count_fields { + ($head:tt $($tail:tt)*) => { 1 + count_fields!($($tail)*)}; + () => { 0 } +} + +pub(crate) use count_fields; + +macro_rules! if_exists { + ({$($something:tt)+} {$($then:tt)*} {$($else:tt)*}) => { $($then)* }; + ({} {$($then:tt)*} {$($else:tt)*}) => { $($else)* }; +} + +pub(crate) use if_exists; + +macro_rules! define_optional_object { + ( + $vis:vis struct $model:ident { + $( + $optional_field:ident: $optional_field_type:ty $( = $optional_field_default:expr)? => ($optional_example:tt, $optional_description:literal) + ),* $(,)? + } + ) => { + define_typed_object! { + $vis struct $model { + required: {}, + optional: { + $( + $optional_field: $optional_field_type $( = $optional_field_default)? => ($optional_example, $optional_description) + ),* + } + } + } + } +} + +pub(crate) use define_optional_object; + +macro_rules! define_typed_object { + ( + $vis:vis struct $model:ident { + required: { + $( + $required_field:ident: $required_field_type:ty => ($required_example:tt, $required_description:literal) + ),* $(,)? + }$(,)? + optional: { + $( + $optional_field:ident: $optional_field_type:ty $( = $optional_field_default:expr)? => ($optional_example:tt, $optional_description:literal) + ),* $(,)? + }$(,)? + } + ) => { + $vis struct $model { + $( + $required_field: $required_field_type, + )* + $( + $optional_field: if_exists!{ + { $($optional_field_default)? } + { $optional_field_type } + { Option<$optional_field_type> } + }, + )* + } + + impl IsArgument for $model { + type ValueType = ObjectType; + const OWNERSHIP: ArgumentOwnership = ArgumentOwnership::Owned; + fn from_argument(value: Spanned) -> ExecutionResult { + Self::resolve_value( + value.expect_owned(), + "This argument", + ) + } + } + + impl ResolvableArgumentTarget for $model { + type ValueType = ObjectType; + } + + impl ResolvableOwned for $model { + fn resolve_from_value(value: AnyValue, context: ResolutionContext) -> ExecutionResult { + Self::from_object_value(ObjectValue::resolve_spanned_from_value(value, context)?) + } + } + + if_exists!{ + {$($required_field)*} + {} + { + impl Default for $model { + fn default() -> Self { + Self { + $($optional_field: { + if_exists!{ + { $($optional_field_default)? } + { $($optional_field_default)? } + { None } + } + },)* + } + } + } + } + } + + impl $model { + fn from_object_value(Spanned(mut object, span_range): Spanned) -> ExecutionResult { + (&object).spanned(span_range).validate(&Self::validation())?; + Ok($model { + $( + $required_field: object.remove_or_none(stringify!($required_field)), + )* + $( + $optional_field: { + let optional = object.remove_no_none(stringify!($optional_field)); + if_exists!{ + { $($optional_field_default)? } + { + // Need to return the $optional_field_type + match optional { + Some(value) => ResolveAs::<$optional_field_type>::resolve_as(Spanned(value, span_range), stringify!($optional_field))?, + None => $($optional_field_default)?, + } + } + { + // Need to return Option<$optional_field_type> + match optional { + Some(value) => Some(ResolveAs::<$optional_field_type>::resolve_as(Spanned(value, span_range), stringify!($optional_field))?), + None => None, + } + } + } + }, + )* + }) + } + } + + impl $model { + fn validation() -> &'static [(&'static str, FieldDefinition)] { + static VALIDATION: [ + (&'static str, FieldDefinition); + count_fields!($($required_field)* $($optional_field)*) + ] = $model::VALIDATION; + &VALIDATION + } + + const VALIDATION: [ + (&'static str, FieldDefinition); + count_fields!($($required_field)* $($optional_field)*) + ] = [ + $( + ( + stringify!($required_field), + FieldDefinition::new_full_static(true, $required_description, $required_example), + ), + )* + $( + ( + stringify!($optional_field), + FieldDefinition::new_full_static(false, $optional_description, $optional_example), + ), + )* + ]; + } + }; +} +pub(crate) use define_typed_object; diff --git a/src/misc/iterators.rs b/src/misc/iterators.rs new file mode 100644 index 00000000..2925a367 --- /dev/null +++ b/src/misc/iterators.rs @@ -0,0 +1,350 @@ +use super::*; + +impl ClonableIterator for I { + fn clone_box(&self) -> Box> { + Box::new(self.clone()) + } +} + +pub(crate) trait ClonableIterator: Iterator { + fn clone_box(&self) -> Box>; +} + +impl Clone for Box> { + fn clone(&self) -> Self { + (**self).clone_box() + } +} + +#[derive(Clone)] +pub(crate) enum EitherIterator { + Left(L), + Right(R), +} + +impl Iterator for EitherIterator +where + L: Iterator, + R: Iterator, +{ + type Item = L::Item; + + fn next(&mut self) -> Option { + match self { + EitherIterator::Left(l) => l.next(), + EitherIterator::Right(r) => r.next(), + } + } +} + +pub(crate) enum ZipIterators { + Array(Vec, SpanRange), + Object(Vec<(String, Span, IteratorValue)>, SpanRange), +} + +impl ZipIterators { + pub(crate) fn new_from_object( + object: ObjectValue, + span_range: SpanRange, + ) -> ExecutionResult { + let entries = object + .entries + .into_iter() + .take(101) + .map(|(k, v)| -> ExecutionResult<_> { + Ok(( + k, + v.key_span, + v.value + .spanned(span_range) + .resolve_any_iterator("Each zip input")?, + )) + }) + .collect::, _>>()?; + if entries.len() == 101 { + return span_range.value_err("A maximum of 100 iterators are allowed"); + } + Ok(ZipIterators::Object(entries, span_range)) + } + + pub(crate) fn new_from_iterator( + iterator: IteratorValue, + span_range: SpanRange, + ) -> ExecutionResult { + let vec = iterator + .take(101) + .map(|x| x.spanned(span_range).resolve_any_iterator("Each zip input")) + .collect::, _>>()?; + if vec.len() == 101 { + return span_range.value_err("A maximum of 100 iterators are allowed"); + } + Ok(ZipIterators::Array(vec, span_range)) + } + + pub(crate) fn run_zip( + self, + interpreter: &mut Interpreter, + error_on_length_mismatch: bool, + ) -> ExecutionResult { + let mut iterators = self; + let error_span_range = match &iterators { + ZipIterators::Array(_, span_range) => *span_range, + ZipIterators::Object(_, span_range) => *span_range, + }; + let mut output = Vec::new(); + + if iterators.len() == 0 { + return Ok(ArrayValue::new(output)); + } + + let (min_iterator_min_length, max_iterator_max_length) = iterators.size_hint_range(); + + if error_on_length_mismatch && Some(min_iterator_min_length) != max_iterator_max_length { + return error_span_range.value_err(format!( + "The iterables have different lengths. The lengths vary from {} to {}. To truncate to the shortest, use `zip_truncated` instead of `zip`", + min_iterator_min_length, + match max_iterator_max_length { + Some(max_max) => max_max.to_string(), + None => "unbounded".to_string(), + }, + )); + } + + iterators.zip_into( + min_iterator_min_length, + interpreter, + error_span_range, + &mut output, + )?; + + Ok(ArrayValue::new(output)) + } + + /// Panics if called on an empty list of iterators + fn size_hint_range(&self) -> (usize, Option) { + let size_hints: Vec<_> = match self { + ZipIterators::Array(inner, _) => inner.iter().map(|x| x.size_hint()).collect(), + ZipIterators::Object(inner, _) => inner.iter().map(|x| x.2.size_hint()).collect(), + }; + let min_min = size_hints.iter().map(|s| s.0).min().unwrap(); + let max_max = size_hints + .iter() + .map(|sh| sh.1) + .max_by(|a, b| match (a, b) { + (None, None) => core::cmp::Ordering::Equal, + (None, Some(_)) => core::cmp::Ordering::Greater, + (Some(_), None) => core::cmp::Ordering::Less, + (Some(a), Some(b)) => a.cmp(b), + }) + .unwrap(); + (min_min, max_max) + } + + fn len(&self) -> usize { + match self { + ZipIterators::Array(inner, _) => inner.len(), + ZipIterators::Object(inner, _) => inner.len(), + } + } + + /// Panics if count is larger than the minimum length of any iterator + fn zip_into( + &mut self, + count: usize, + interpreter: &mut Interpreter, + error_span_range: SpanRange, + output: &mut Vec, + ) -> ExecutionResult<()> { + let mut counter = interpreter.start_iteration_counter(&error_span_range); + + match self { + ZipIterators::Array(iterators, _) => { + for _ in 0..count { + counter.increment_and_check()?; + let mut inner = Vec::with_capacity(iterators.len()); + for iter in iterators.iter_mut() { + inner.push(iter.next().unwrap()); + } + output.push(inner.into_any_value()); + } + } + ZipIterators::Object(iterators, _) => { + for _ in 0..count { + counter.increment_and_check()?; + let mut inner = BTreeMap::new(); + for (key, key_span, iter) in iterators.iter_mut() { + inner.insert( + key.clone(), + ObjectEntry { + key_span: *key_span, + value: iter.next().unwrap(), + }, + ); + } + output.push(inner.into_any_value()); + } + } + } + + Ok(()) + } +} + +define_optional_object! { + pub(crate) struct IntersperseSettings { + add_trailing: bool = false => ("false", "Whether to add the separator after the last item (default: false)"), + final_separator: AnyValue => ("%[or]", "Define a different final separator (default: same as normal separator)"), + } +} + +pub(crate) fn run_intersperse( + items: Box, + separator: AnyValue, + settings: IntersperseSettings, +) -> ExecutionResult { + let mut output = Vec::new(); + + let mut items = items.into_iterator()?.peekable(); + + let mut this_item = match items.next() { + Some(next) => next, + None => return Ok(ArrayValue { items: output }), + }; + + let mut appender = SeparatorAppender { + separator, + final_separator: settings.final_separator, + add_trailing: settings.add_trailing, + }; + + loop { + output.push(this_item); + let next_item = items.next(); + match next_item { + Some(next_item) => { + let remaining = if items.peek().is_some() { + RemainingItemCount::MoreThanOne + } else { + RemainingItemCount::ExactlyOne + }; + appender.add_separator(remaining, &mut output)?; + this_item = next_item; + } + None => { + appender.add_separator(RemainingItemCount::None, &mut output)?; + break; + } + } + } + + Ok(ArrayValue { items: output }) +} + +struct SeparatorAppender { + separator: AnyValue, + final_separator: Option, + add_trailing: bool, +} + +impl SeparatorAppender { + fn add_separator( + &mut self, + remaining: RemainingItemCount, + output: &mut Vec, + ) -> ExecutionResult<()> { + match self.separator(remaining) { + TrailingSeparator::Normal => output.push(self.separator.clone()), + TrailingSeparator::Final => match self.final_separator.take() { + Some(final_separator) => output.push(final_separator), + None => output.push(self.separator.clone()), + }, + TrailingSeparator::None => {} + } + Ok(()) + } + + fn separator(&self, remaining_item_count: RemainingItemCount) -> TrailingSeparator { + match remaining_item_count { + RemainingItemCount::None => { + if self.add_trailing { + TrailingSeparator::Final + } else { + TrailingSeparator::None + } + } + RemainingItemCount::ExactlyOne => { + if self.add_trailing { + TrailingSeparator::Normal + } else { + TrailingSeparator::Final + } + } + RemainingItemCount::MoreThanOne => TrailingSeparator::Normal, + } + } +} + +enum RemainingItemCount { + None, + ExactlyOne, + MoreThanOne, +} + +enum TrailingSeparator { + Normal, + Final, + None, +} + +define_optional_object! { + pub(crate) struct SplitSettings { + drop_empty_start: bool = false => ("false", "If true, a leading separator does not yield in an empty item at the start (default: false)"), + drop_empty_middle: bool = false => ("false", "If true, adjacent separators do not yield an empty item between them (default: false)"), + drop_empty_end: bool = true => ("true", "If true, a trailing separator does not yield an empty item at the end (default: true)"), + } +} + +pub(crate) fn handle_split( + input: OutputStream, + separator: &OutputStream, + settings: SplitSettings, +) -> ExecutionResult { + input.parse_with(move |input| { + let mut output = Vec::new(); + let mut current_item = OutputStream::new(); + + // Special case separator.len() == 0 to avoid an infinite loop + if separator.is_empty() { + while !input.is_empty() { + current_item.push_raw_token_tree(input.parse()?); + let complete_item = core::mem::replace(&mut current_item, OutputStream::new()); + output.push(complete_item.into_any_value()); + } + return Ok(ArrayValue::new(output)); + } + + let mut drop_empty_next = settings.drop_empty_start; + while !input.is_empty() { + let separator_fork = input.fork(); + let mut ignored_transformer_output = OutputStream::new(); + if separator + .parse_exact_match(&separator_fork, &mut ignored_transformer_output) + .is_err() + { + current_item.push_raw_token_tree(input.parse()?); + continue; + } + // This is guaranteed to progress the parser because the separator is non-empty + input.advance_to(&separator_fork); + if !current_item.is_empty() || !drop_empty_next { + let complete_item = core::mem::replace(&mut current_item, OutputStream::new()); + output.push(complete_item.into_any_value()); + } + drop_empty_next = settings.drop_empty_middle; + } + if !current_item.is_empty() || !settings.drop_empty_end { + output.push(current_item.into_any_value()); + } + Ok(ArrayValue::new(output)) + }) +} diff --git a/src/misc/keywords.rs b/src/misc/keywords.rs new file mode 100644 index 00000000..48c97442 --- /dev/null +++ b/src/misc/keywords.rs @@ -0,0 +1,53 @@ +use super::*; + +macro_rules! ExactIdent { + [$ident:ident as $name:ident] => { + pub(crate) struct $name(Ident); + + impl ParseSource for $name { + fn parse(input: SourceParser) -> ParseResult { + let ident = input.parse_ident_matching(stringify!($ident))?; + Ok(Self(ident)) + } + + fn control_flow_pass(&mut self, _context: FlowCapturer) -> ParseResult<()> { + Ok(()) + } + } + + impl HasSpan for $name { + fn span(&self) -> Span { + self.0.span() + } + } + } +} + +// FULL KEYWORDS +// These are keywords which have special meaning in the language +// and cannot be used as identifiers. + +pub(crate) mod keyword { + pub(crate) const REVERT: &str = "revert"; + pub(crate) const EMIT: &str = "emit"; + pub(crate) const ATTEMPT: &str = "attempt"; + pub(crate) const PARSE: &str = "parse"; +} + +pub(crate) fn is_keyword(ident: &str) -> bool { + matches!( + ident, + keyword::REVERT | keyword::EMIT | keyword::ATTEMPT | keyword::PARSE + ) +} + +ExactIdent![emit as EmitKeyword]; +ExactIdent![revert as RevertKeyword]; +ExactIdent![attempt as AttemptKeyword]; +ExactIdent![parse as ParseKeyword]; + +// CONTEXTUAL KEYWORDS +// These can still be used as identifiers in other contexts. + +ExactIdent![group as GroupKeyword]; +ExactIdent![raw as RawKeyword]; diff --git a/src/misc/mod.rs b/src/misc/mod.rs new file mode 100644 index 00000000..36aa4457 --- /dev/null +++ b/src/misc/mod.rs @@ -0,0 +1,50 @@ +mod arena; +mod errors; +mod field_inputs; +mod iterators; +mod keywords; +mod mut_rc_ref_cell; +mod parse_traits; +pub(crate) mod string_conversion; + +pub(crate) use arena::*; +pub(crate) use errors::*; +pub(crate) use field_inputs::*; +pub(crate) use iterators::*; +pub(crate) use keywords::*; +pub(crate) use mut_rc_ref_cell::*; +pub(crate) use parse_traits::*; + +use crate::internal_prelude::*; + +#[allow(unused)] +pub(crate) fn print_if_slow( + inner: impl FnOnce() -> T, + slow_threshold: std::time::Duration, + print_message: impl FnOnce(&T, std::time::Duration) -> String, +) -> T { + use std::time::*; + let before = SystemTime::now(); + let output = inner(); + let after = SystemTime::now(); + + let elapsed = after.duration_since(before).unwrap(); + if elapsed >= slow_threshold { + println!("{}", print_message(&output, elapsed)); + } + output +} + +// Equivalent to `!` but stable in our MSRV +pub(crate) enum Never {} + +impl IsValueContent for Never { + type Type = NoneType; + type Form = BeOwned; +} + +impl IntoValueContent<'static> for Never { + fn into_content(self) -> Content<'static, Self::Type, Self::Form> { + match self {} + } +} diff --git a/src/misc/mut_rc_ref_cell.rs b/src/misc/mut_rc_ref_cell.rs new file mode 100644 index 00000000..777e55d0 --- /dev/null +++ b/src/misc/mut_rc_ref_cell.rs @@ -0,0 +1,351 @@ +use crate::internal_prelude::*; +use std::cell::{BorrowError, BorrowMutError}; + +/// A mutable reference to a sub-value `U` inside a [`Rc>`]. +/// Only one [`MutableSubRcRefCell`] can exist at a time for a given [`Rc>`]. +pub(crate) struct MutableSubRcRefCell { + /// This is actually a reference to the contents of the RefCell + /// but we store it using `unsafe` as `'static`, and use unsafe blocks + /// to ensure it's dropped first. + /// + /// SAFETY: This *must* appear before `pointed_at` so that it's dropped first. + ref_mut: RefMut<'static, U>, + pointed_at: Rc>, +} + +impl MutableSubRcRefCell { + pub(crate) fn new(pointed_at: Rc>) -> Result { + let ref_mut = pointed_at.try_borrow_mut()?; + Ok(Self { + // SAFETY: We must ensure that this lifetime lives as long as the + // reference to pointed_at (i.e. the RefCell). + // This is guaranteed by the fact that the only time we drop the RefCell + // is when we drop the MutRcRefCell, and we ensure that the RefMut is dropped first. + ref_mut: unsafe { + less_buggy_transmute::, std::cell::RefMut<'static, T>>( + ref_mut, + ) + }, + pointed_at, + }) + } +} + +impl MutableSubRcRefCell { + pub(crate) fn into_shared(self) -> SharedSubRcRefCell { + let ptr = self.ref_mut.deref() as *const U; + drop(self.ref_mut); + // SAFETY: + // - The pointer was previously a reference, so it is safe to deference it here + // (the pointer is pointing into the Rc> which hasn't moved) + // - All our invariants for SharedSubRcRefCell / MutableSubRcRefCell are maintained + unsafe { + // The unwrap cannot panic because we just held a mutable borrow, we're not in Sync land, so no-one else can have a borrow. + SharedSubRcRefCell::new(self.pointed_at) + .unwrap() + .map(|_| &*ptr) + } + } + + /// SAFETY: + /// * Must be paired with a call to `enable()` before any further use of the value. + /// * Must not use the value while disabled. + pub(crate) unsafe fn disable(&mut self) { + // Ideally we'd just decrement the ref count, but RefCell doesn't expose that. + // Instead, we duplicate it, so the old value gets dropped automatically, + // decrementing the ref count. + self.ref_mut = unsafe { core::ptr::read(&self.ref_mut) }; + } + + /// SAFETY: + /// * Must only be used after a call to `disable()`. + pub(crate) unsafe fn enable(&mut self) -> Result<(), BorrowMutError> { + // Ideally we'd just increment the ref count, but RefCell doesn't expose that. + // Instead, we re-borrow it mutably, which increments the ref count, then forget + // the new borrow. + match self.pointed_at.try_borrow_mut() { + Ok(new_ref_mut) => { + std::mem::forget(new_ref_mut); + Ok(()) + } + Err(e) => Err(e), + } + } + + pub(crate) fn map( + self, + f: impl FnOnce(&mut U) -> &mut V, + ) -> MutableSubRcRefCell { + MutableSubRcRefCell { + ref_mut: RefMut::map(self.ref_mut, f), + pointed_at: self.pointed_at, + } + } + + pub(crate) fn map_optional( + self, + f: impl FnOnce(&mut U) -> Option<&mut V>, + ) -> Option> { + Some(MutableSubRcRefCell { + ref_mut: RefMut::filter_map(self.ref_mut, f).ok()?, + pointed_at: self.pointed_at, + }) + } + + pub(crate) fn try_map( + self, + f: impl FnOnce(&mut U) -> Result<&mut V, E>, + ) -> Result, E> { + let mut error = None; + let outcome = RefMut::filter_map(self.ref_mut, |inner| match f(inner) { + Ok(value) => Some(value), + Err(e) => { + error = Some(e); + None + } + }); + match outcome { + Ok(ref_mut) => Ok(MutableSubRcRefCell { + ref_mut, + pointed_at: self.pointed_at, + }), + Err(_) => Err(error.unwrap()), + } + } + + pub(crate) fn replace( + mut self, + f: impl for<'a> FnOnce(&'a mut U, &mut MutableSubEmplacer<'a, T, U>) -> O, + ) -> O { + let ref_mut = self.ref_mut.deref_mut() as *mut U; + let mut emplacer = MutableSubEmplacer { + inner: Some(self), + encapsulation_lifetime: std::marker::PhantomData, + }; + f( + // SAFETY: We are cloning a mutable reference here, but it is safe because: + // - What it's pointing at still lives, as RefMut still lives inside emplacer.inner + // - No other "mutable reference" is created from the RefMut except at encapsulation time + unsafe { &mut *ref_mut }, + &mut emplacer, + ) + } +} + +#[allow(unused)] +pub(crate) type MutableEmplacer<'e, U> = MutableSubEmplacer<'e, AnyValue, U>; + +pub(crate) struct MutableSubEmplacer<'e, T: 'static + ?Sized, U: 'static + ?Sized> { + inner: Option>, + encapsulation_lifetime: std::marker::PhantomData<&'e ()>, +} + +impl<'e, T: 'static + ?Sized, U: 'static + ?Sized> MutableSubEmplacer<'e, T, U> { + pub(crate) fn emplace( + &mut self, + value: &'e mut V, + ) -> MutableSubRcRefCell { + unsafe { + // SAFETY: The lifetime 'e is equal to the &'e content argument in replace + // So this guarantees that the returned reference is valid as long as the MutableSubRcRefCell exists + self.emplace_unchecked(value) + } + } + + // SAFETY: + // * The caller must ensure that the value's lifetime is derived from the original content + pub(crate) unsafe fn emplace_unchecked( + &mut self, + value: &mut V, + ) -> MutableSubRcRefCell { + self.inner + .take() + .expect("You can only emplace to create a new Mutable value once") + .map(|_| + // SAFETY: As defined in the rustdoc above + unsafe { less_buggy_transmute::<&mut V, &'static mut V>(value) }) + } +} + +impl DerefMut for MutableSubRcRefCell { + fn deref_mut(&mut self) -> &mut U { + &mut self.ref_mut + } +} + +impl Deref for MutableSubRcRefCell { + type Target = U; + fn deref(&self) -> &U { + &self.ref_mut + } +} + +/// A shared (immutable) reference to a sub-value `U` inside a [`Rc>`]. +/// Many [`SharedSubRcRefCell`] can exist at the same time for a given [`Rc>`], +/// but if any exist, then no [`MutableSubRcRefCell`] can exist. +pub(crate) struct SharedSubRcRefCell { + /// This is actually a reference to the contents of the RefCell + /// but we store it using `unsafe` as `'static`, and use unsafe blocks + /// to ensure it's dropped first. + /// + /// SAFETY: This *must* appear before `pointed_at` so that it's dropped first. + shared_ref: Ref<'static, U>, + pointed_at: Rc>, +} + +impl SharedSubRcRefCell { + pub(crate) fn new(pointed_at: Rc>) -> Result { + let shared_ref = pointed_at.try_borrow()?; + Ok(Self { + // SAFETY: We must ensure that this lifetime lives as long as the + // reference to pointed_at (i.e. the RefCell). + // This is guaranteed by the fact that the only time we drop the RefCell + // is when we drop the SharedSubRcRefCell, and we ensure that the Ref is dropped first. + shared_ref: unsafe { less_buggy_transmute::, Ref<'static, T>>(shared_ref) }, + pointed_at, + }) + } +} + +impl SharedSubRcRefCell { + pub(crate) fn clone(this: &SharedSubRcRefCell) -> Self { + Self { + shared_ref: Ref::clone(&this.shared_ref), + pointed_at: Rc::clone(&this.pointed_at), + } + } + + pub(crate) fn map( + self, + f: impl for<'a> FnOnce(&'a U) -> &'a V, + ) -> SharedSubRcRefCell { + SharedSubRcRefCell { + shared_ref: Ref::map(self.shared_ref, f), + pointed_at: self.pointed_at, + } + } + + pub(crate) fn map_optional( + self, + f: impl FnOnce(&U) -> Option<&V>, + ) -> Option> { + Some(SharedSubRcRefCell { + shared_ref: Ref::filter_map(self.shared_ref, f).ok()?, + pointed_at: self.pointed_at, + }) + } + + pub(crate) fn try_map( + self, + f: impl FnOnce(&U) -> Result<&V, E>, + ) -> Result, E> { + let mut error = None; + let outcome = Ref::filter_map(self.shared_ref, |inner| match f(inner) { + Ok(value) => Some(value), + Err(e) => { + error = Some(e); + None + } + }); + match outcome { + Ok(shared_ref) => Ok(SharedSubRcRefCell { + shared_ref, + pointed_at: self.pointed_at, + }), + Err(_) => Err(error.unwrap()), + } + } + + pub(crate) fn replace( + self, + f: impl for<'e> FnOnce(&'e U, &mut SharedSubEmplacer<'e, T, U>) -> O, + ) -> O { + let copied_ref = Ref::clone(&self.shared_ref); + let mut emplacer = SharedSubEmplacer { + inner: Some(self), + encapsulation_lifetime: std::marker::PhantomData, + }; + f(&*copied_ref, &mut emplacer) + } + + /// SAFETY: + /// * Must be paired with a call to `enable()` before any further use of the value. + /// * Must not use the value while disabled. + pub(crate) unsafe fn disable(&mut self) { + // Ideally we'd just decrement the ref count, but RefCell doesn't expose that. + // Instead, we duplicate it, so the old value gets dropped automatically, + // decrementing the ref count. + self.shared_ref = unsafe { core::ptr::read(&self.shared_ref) }; + } + + /// SAFETY: + /// * Must only be used after a call to `disable()`. + pub(crate) unsafe fn enable(&mut self) -> Result<(), BorrowError> { + // Ideally we'd just increment the ref count, but RefCell doesn't expose that. + // Instead, we re-borrow it mutably, which increments the ref count, then forget + // the new borrow. + match self.pointed_at.try_borrow() { + Ok(new_ref_mut) => { + std::mem::forget(new_ref_mut); + Ok(()) + } + Err(e) => Err(e), + } + } +} + +pub(crate) type SharedEmplacer<'e, U> = SharedSubEmplacer<'e, AnyValue, U>; + +pub(crate) struct SharedSubEmplacer<'e, T: ?Sized, U: 'static + ?Sized> { + inner: Option>, + encapsulation_lifetime: std::marker::PhantomData<&'e ()>, +} + +impl<'e, T: 'static + ?Sized, U: 'static + ?Sized> SharedSubEmplacer<'e, T, U> { + pub(crate) fn emplace( + &mut self, + value: &'e V, + ) -> SharedSubRcRefCell { + unsafe { + // SAFETY: The lifetime 'e is equal to the &'e content argument in replace + // So this guarantees that the returned reference is valid as long as the SharedSubRcRefCell exists + self.emplace_unchecked(value) + } + } + + // SAFETY: + // * The caller must ensure that the value's lifetime is derived from the original content + pub(crate) unsafe fn emplace_unchecked( + &mut self, + value: &V, + ) -> SharedSubRcRefCell { + self.inner + .take() + .expect("You can only emplace to create a new shared value once") + .map(|_| + // SAFETY: As defined in the rustdoc above + unsafe { less_buggy_transmute::<&V, &'static V>(value) }) + } +} + +impl Deref for SharedSubRcRefCell { + type Target = U; + + fn deref(&self) -> &U { + &self.shared_ref + } +} + +/// SAFETY: The user must ensure that the two types are transmutable, +/// and in particular are the same size +unsafe fn less_buggy_transmute(t: T) -> U { + // std::mem::transmute::, Ref<'static, T>> for T: ?Sized + // Is fine on latest Rust, but on MSRV only incorrectly flags: + // > error[E0512]: cannot transmute between types of different sizes, or dependently-sized types + // Likely on old versions of Rust, it assumes that Ref is therefore ?Sized (it's not). + // To workaround this, we use a recommendation from https://users.rust-lang.org/t/transmute-doesnt-work-on-generic-types/87272 + // using transmute_copy and manual forgetting + use std::mem::ManuallyDrop; + debug_assert!(std::mem::size_of::() == std::mem::size_of::()); + std::mem::transmute_copy::, U>(&ManuallyDrop::new(t)) +} diff --git a/src/misc/parse_traits.rs b/src/misc/parse_traits.rs new file mode 100644 index 00000000..473a4517 --- /dev/null +++ b/src/misc/parse_traits.rs @@ -0,0 +1,633 @@ +#![allow(unused)] +use crate::internal_prelude::*; + +// Parsing of source code tokens +// ============================= + +pub(crate) struct Source; + +impl<'a> ParseBuffer<'a, Source> { + pub(crate) fn peek_grammar(&self) -> SourcePeekMatch { + detect_preinterpret_grammar(self.cursor()) + } + + pub(crate) fn parse(&self) -> ParseResult { + T::parse(self) + } + + pub(crate) fn parse_optional(&self) -> ParseResult> { + T::parse_optional(self) + } + + pub fn parse_terminated( + &'a self, + ) -> ParseResult> { + Punctuated::parse_terminated_using(self, T::parse, P::parse) + } + + pub(crate) fn call ParseResult>( + &self, + f: F, + ) -> ParseResult { + f(self) + } + + pub(crate) fn parse_virtual_empty_stream( + &self, + parser: impl FnOnce(SourceParser) -> ParseResult, + ) -> ParseResult { + parse_with( + TokenStream::new(), + |stream: ParseStream| -> ParseResult { + let forked = stream.fork(); + let output = parser(&forked)?; + stream.advance_to(&forked); + Ok(output) + }, + ) + } +} + +#[allow(unused)] +pub(crate) enum SourcePeekMatch { + EmbeddedExpression, + EmbeddedStatements, + EmbeddedVariable, + Group(Delimiter), + Ident(Ident), + Punct(Punct), + Literal(Literal), + StreamLiteral, + ObjectLiteral, + End, +} + +fn detect_preinterpret_grammar(cursor: syn::buffer::Cursor) -> SourcePeekMatch { + if let Some((_, next)) = cursor.punct_matching('#') { + if next.ident().is_some() { + return SourcePeekMatch::EmbeddedVariable; + } + if next.group_matching(Delimiter::Parenthesis).is_some() { + return SourcePeekMatch::EmbeddedExpression; + } + if next.group_matching(Delimiter::Brace).is_some() { + return SourcePeekMatch::EmbeddedStatements; + } + } + + if let Some((_, next)) = cursor.punct_matching('%') { + if next.group_matching(Delimiter::Bracket).is_some() { + return SourcePeekMatch::StreamLiteral; + } + if let Some((ident, next)) = next.ident() { + if next.group_matching(Delimiter::Bracket).is_some() { + return SourcePeekMatch::StreamLiteral; + } + } + if next.group_matching(Delimiter::Brace).is_some() { + return SourcePeekMatch::ObjectLiteral; + } + } + + if let Some((next, delimiter, _, _)) = cursor.any_group() { + // Ideally we'd like to detect $($tt)* substitutions from macros and interpret them as + // a Raw (uninterpreted) group, because typically that's what a user would typically intend. + // + // You'd think mapping a Delimiter::None to a GrammarPeekMatch::RawGroup would be a good way + // of doing this, but unfortunately this behaviour is very arbitrary and not in a helpful way: + // => A $tt or $($tt)* is not grouped... + // => A $literal or $($literal)* _is_ outputted in a group... + // + // So this isn't possible. It's unlikely to matter much, and a user can always do: + // %raw[$($tt)*] anyway. + return SourcePeekMatch::Group(delimiter); + } + + match cursor.token_tree() { + Some((TokenTree::Ident(ident), _)) => SourcePeekMatch::Ident(ident), + Some((TokenTree::Punct(punct), _)) => SourcePeekMatch::Punct(punct), + Some((TokenTree::Literal(literal), _)) => SourcePeekMatch::Literal(literal), + Some((TokenTree::Group(_), _)) => unreachable!("Already covered above"), + None => SourcePeekMatch::End, + } +} + +// Parsing of already interpreted tokens +// (e.g. transforming / destructuring) +// ===================================== + +pub(crate) struct Output; + +impl<'a> ParseBuffer<'a, Output> { + pub(crate) fn parse>(&self) -> ParseResult { + self.parse_generic() + } + + pub fn parse_terminated, P: Parse>( + &'a self, + ) -> ParseResult> { + self.parse_terminated_generic() + } + + pub(crate) fn call) -> ParseResult>( + &self, + f: F, + ) -> ParseResult { + self.call_generic(f) + } +} + +// Source parsing +// =============== + +/// This trait is slightly more restrict/powerful than Parse +/// as it gives access to the parse context. +pub(crate) trait ParseSource: Sized { + fn parse(input: SourceParser) -> ParseResult; + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()>; +} + +impl ParseSource for T +where + T: Parse, +{ + fn parse(input: SourceParser) -> ParseResult { + >::parse(input) + } + + fn control_flow_pass(&mut self, context: FlowCapturer) -> ParseResult<()> { + Ok(()) + } +} + +pub(crate) trait ParseSourceOptional: ParseSource { + /// This may want to be overwritten to have a peek/commit behaviour + /// to give clearer errors. + fn parse_optional(input: SourceParser) -> ParseResult> { + let fork = input.fork(); + match Self::parse(&fork) { + Ok(value) => { + input.advance_to(&fork); + Ok(Some(value)) + } + Err(_) => Ok(None), + } + } +} + +pub(crate) fn parse_without_analysis( + parser: impl FnOnce(SourceParser) -> ParseResult, +) -> impl FnOnce(ParseStream) -> ParseResult { + move |stream: ParseStream| { + // To get access to an owned ParseBuffer we fork it... and advance later! + let forked = stream.fork(); + let output = parser(&forked)?; + stream.advance_to(&forked); + Ok(output) + } +} + +pub(crate) type SourceParser<'a> = &'a SourceParseBuffer<'a>; +pub(crate) type FlowCapturer<'a> = &'a mut ControlFlowContext; + +pub(crate) struct ControlFlowContext { + state: FlowAnalysisState, +} + +impl ControlFlowContext { + pub(crate) fn analyze( + parsed: &mut T, + inner: impl FnOnce(&mut T, FlowCapturer) -> ParseResult<()>, + ) -> ParseResult { + let mut context = Self { + state: FlowAnalysisState::new(), + }; + inner(parsed, &mut context)?; + context.state.finish() + } + + pub(crate) fn register_scope(&mut self, id: &mut ScopeId) { + assert!(id.is_placeholder()); + *id = self.state.allocate_scope(); + } + + pub(crate) fn register_variable_definition( + &mut self, + ident: &Ident, + id: &mut VariableDefinitionId, + ) { + assert!(id.is_placeholder()); + *id = self.state.allocate_variable_definition(ident); + } + + pub(crate) fn register_variable_reference( + &mut self, + ident: &Ident, + id: &mut VariableReferenceId, + ) { + assert!(id.is_placeholder()); + *id = self.state.allocate_variable_reference(ident); + } + + pub(crate) fn enter_scope(&mut self, scope: ScopeId) { + self.state.enter_scope(scope); + } + + pub(crate) fn exit_scope(&mut self, scope: ScopeId) { + self.state.exit_scope(scope); + } + + pub(crate) fn define_variable(&mut self, id: VariableDefinitionId) { + self.state.define_variable(id); + } + + pub(crate) fn reference_variable( + &mut self, + id: VariableReferenceId, + #[cfg(feature = "debug")] assertion: FinalUseAssertion, + ) -> ParseResult<()> { + self.state.reference_variable( + id, + #[cfg(feature = "debug")] + assertion, + ) + } + + pub(crate) fn enter_next_segment(&mut self, segment_kind: SegmentKind) -> ControlFlowSegmentId { + self.state.enter_next_segment(segment_kind) + } + + pub(crate) fn enter_path_segment( + &mut self, + previous_sibling_id: Option, + segment_kind: SegmentKind, + ) -> ControlFlowSegmentId { + self.state + .enter_path_segment(previous_sibling_id, segment_kind) + } + + pub(crate) fn exit_segment(&mut self, segment_id: ControlFlowSegmentId) { + self.state.exit_segment(segment_id); + } + + pub(crate) fn register_catch_location(&mut self, data: CatchLocationData) -> CatchLocationId { + self.state.register_catch_location(data) + } + + pub(crate) fn enter_catch(&mut self, catch_location_id: CatchLocationId) { + self.state.enter_catch(catch_location_id); + } + + pub(crate) fn exit_catch(&mut self, catch_location_id: CatchLocationId) { + self.state.exit_catch(catch_location_id); + } + + pub(crate) fn resolve_catch_for_interrupt( + &self, + interrupt_details: InterruptDetails, + ) -> ParseResult { + self.state.resolve_catch_for_interrupt(interrupt_details) + } +} + +pub(crate) type SourceParseBuffer<'a> = ParseBuffer<'a, Source>; + +// Generic parsing +// =============== + +pub(crate) trait Parse: Sized { + fn parse(input: ParseStream) -> ParseResult; +} + +impl Parse for T { + fn parse(input: ParseStream) -> ParseResult { + Ok(T::parse(&input.inner)?) + } +} + +pub(crate) type OutputParseStream<'a> = &'a ParseBuffer<'a, Output>; +pub(crate) type ParseStream<'a, K> = &'a ParseBuffer<'a, K>; + +// We create our own ParseBuffer mostly so we can overwrite +// parse to return ParseResult instead of syn::Result +#[repr(transparent)] +pub(crate) struct ParseBuffer<'a, K> { + inner: SynParseBuffer<'a>, + _kind: PhantomData, +} + +impl<'a, K> From> for ParseBuffer<'a, K> { + fn from(inner: syn::parse::ParseBuffer<'a>) -> Self { + Self { + inner, + _kind: PhantomData, + } + } +} + +// This is From<&'a SynParseBuffer<'a>> for &'a ParseBuffer<'a> +impl<'a, K> From> for ParseStream<'a, K> { + fn from(syn_parse_stream: SynParseStream<'a>) -> Self { + unsafe { + // SAFETY: This is safe because [Syn]ParseStream<'a> = &'a [Syn]ParseBuffer<'a> + // And ParseBuffer<'a> is marked as #[repr(transparent)] so has identical layout to SynParseBuffer<'a> + // So this is a transmute between compound types with identical layouts which is safe. + core::mem::transmute::, ParseStream<'a, K>>(syn_parse_stream) + } + } +} + +impl<'a, K> ParseBuffer<'a, K> { + pub(crate) fn fork(&self) -> ParseBuffer<'a, K> { + ParseBuffer { + inner: self.inner.fork(), + _kind: PhantomData, + } + } + + pub(crate) fn parse_generic>(&self) -> ParseResult { + T::parse(self) + } + + pub fn parse_terminated_generic, P: Parse>( + &'a self, + ) -> ParseResult> { + Punctuated::parse_terminated_using(self, T::parse, P::parse) + } + + pub(crate) fn call_generic) -> ParseResult>( + &self, + f: F, + ) -> ParseResult { + f(self) + } + + pub(crate) fn try_parse_or_error< + T, + F: FnOnce(&Self) -> ParseResult, + M: std::fmt::Display, + >( + &self, + parse: F, + message: M, + ) -> ParseResult { + let error_span = self.span(); + parse(self).map_err(|_| error_span.parse_error(message)) + } + + pub(crate) fn parse_any_punct(&self) -> ParseResult { + // Annoyingly, ' doesn't count as a `Punct` in syn + // So to make sure we can capture it, we handle it as parsing TokenTree rather than a Punct + match self.inner.parse::()? { + TokenTree::Punct(punct) => Ok(punct), + _ => self.span().parse_err("expected punctuation"), + } + } + + pub(crate) fn parse_any_ident(&self) -> ParseResult { + self.call_generic(|stream| Ok(Ident::parse_any(&stream.inner)?)) + } + + pub(crate) fn peek_ident_matching(&self, content: &str) -> bool { + self.cursor().ident_matching(content).is_some() + } + + pub(crate) fn parse_ident_matching(&self, content: &str) -> ParseResult { + Ok(self.inner.step(|cursor| { + cursor + .ident_matching(content) + .ok_or_else(|| cursor.syn_error(format!("expected {}", content))) + })?) + } + + pub(crate) fn peek_punct_matching(&self, punct: char) -> bool { + self.cursor().punct_matching(punct).is_some() + } + + pub(crate) fn parse_punct_matching(&self, punct: char) -> ParseResult { + Ok(self.inner.step(|cursor| { + cursor + .punct_matching(punct) + .ok_or_else(|| cursor.syn_error(format!("expected {}", punct))) + })?) + } + + pub(crate) fn peek_literal_matching(&self, content: &str) -> bool { + self.cursor().literal_matching(content).is_some() + } + + pub(crate) fn parse_literal_matching(&self, content: &str) -> ParseResult { + Ok(self.inner.step(|cursor| { + cursor + .literal_matching(content) + .ok_or_else(|| cursor.syn_error(format!("expected {}", content))) + })?) + } + + pub(crate) fn parse_group( + &self, + required_delimiter: Option, + ) -> ParseResult<(Delimiter, DelimSpan, ParseBuffer<'_, K>)> { + match required_delimiter { + Some(expected_delimiter) => { + let (delim_span, parse_buffer) = self.parse_specific_group(expected_delimiter)?; + Ok((expected_delimiter, delim_span, parse_buffer)) + } + None => self.parse_any_group(), + } + } + + pub(crate) fn parse_any_group( + &self, + ) -> ParseResult<(Delimiter, DelimSpan, ParseBuffer<'_, K>)> { + use syn::parse::discouraged::AnyDelimiter; + let (delimiter, delim_span, parse_buffer) = self.inner.parse_any_delimiter()?; + Ok((delimiter, delim_span, parse_buffer.into())) + } + + pub(crate) fn peek_specific_group(&self, delimiter: Delimiter) -> bool { + self.cursor().group_matching(delimiter).is_some() + } + + pub(crate) fn parse_group_matching( + &self, + matching: impl FnOnce(Delimiter) -> bool, + expected_message: impl FnOnce() -> String, + ) -> ParseResult<(DelimSpan, ParseBuffer<'_, K>)> { + let error_span = match self.parse_any_group() { + Ok((delimiter, delim_span, inner)) if matching(delimiter) => { + return Ok((delim_span, inner)); + } + Ok((_, delim_span, _)) => delim_span.open(), + Err(error) => error.span(), + }; + error_span.parse_err(expected_message()) + } + + pub(crate) fn parse_specific_group( + &self, + expected_delimiter: Delimiter, + ) -> ParseResult<(DelimSpan, ParseBuffer<'_, K>)> { + self.parse_group_matching( + |delimiter| delimiter == expected_delimiter, + || format!("Expected {}", expected_delimiter.description_of_open()), + ) + } + + pub(crate) fn parse_braces(&self) -> ParseResult<(Braces, ParseBuffer<'_, K>)> { + let (delim_span, inner) = self.parse_specific_group(Delimiter::Brace)?; + Ok((Braces { delim_span }, inner)) + } + + pub(crate) fn parse_brackets(&self) -> ParseResult<(Brackets, ParseBuffer<'_, K>)> { + let (delim_span, inner) = self.parse_specific_group(Delimiter::Bracket)?; + Ok((Brackets { delim_span }, inner)) + } + + pub(crate) fn parse_parentheses(&self) -> ParseResult<(Parentheses, ParseBuffer<'_, K>)> { + let (delim_span, inner) = self.parse_specific_group(Delimiter::Parenthesis)?; + Ok((Parentheses { delim_span }, inner)) + } + + pub(crate) fn parse_transparent_group( + &self, + ) -> ParseResult<(TransparentDelimiters, ParseBuffer<'_, K>)> { + let (delim_span, inner) = self.parse_specific_group(Delimiter::None)?; + Ok((TransparentDelimiters { delim_span }, inner)) + } + + pub(crate) fn advance_to(&self, fork: &Self) { + self.inner.advance_to(&fork.inner) + } + + // ERRORS + // ====== + + pub(crate) fn parse_err(&self, message: impl std::fmt::Display) -> ParseResult { + Err(self.parse_error(message)) + } + + pub(crate) fn parse_error(&self, message: impl std::fmt::Display) -> ParseError { + self.span().parse_error(message) + } + + // Pass-throughs to SynParseBuffer + // =============================== + + pub(crate) fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub(crate) fn cursor(&self) -> syn::buffer::Cursor<'a> { + self.inner.cursor() + } + + pub(crate) fn span(&self) -> Span { + self.inner.span() + } + + pub(crate) fn peek(&self, token: impl syn::parse::Peek) -> bool { + self.inner.peek(token) + } + + pub(crate) fn peek2(&self, token: impl syn::parse::Peek) -> bool { + self.inner.peek2(token) + } + + #[allow(unused)] + pub(crate) fn peek3(&self, token: impl syn::parse::Peek) -> bool { + self.inner.peek3(token) + } + + /// End with `Err(lookahead.error())?` + pub(crate) fn lookahead1(&self) -> syn::parse::Lookahead1<'a> { + self.inner.lookahead1() + } +} + +// Punctuated extensions +// ======================= + +pub(crate) trait PunctuatedExtensions: Sized { + fn parse_terminated_using( + input: I, + value_parser: impl Fn(I) -> ParseResult, + punct_parser: impl Fn(I) -> ParseResult

, + ) -> ParseResult; +} + +impl PunctuatedExtensions for Punctuated { + // More flexible than syn's built-in parse_terminated_with + fn parse_terminated_using( + input: I, + value_parser: impl Fn(I) -> ParseResult, + punct_parser: impl Fn(I) -> ParseResult

, + ) -> ParseResult { + let mut punctuated = Punctuated::new(); + + loop { + if input.is_empty() { + break; + } + let value = value_parser(input)?; + punctuated.push_value(value); + if input.is_empty() { + break; + } + let punct = punct_parser(input)?; + punctuated.push_punct(punct); + } + + Ok(punctuated) + } +} + +pub(crate) trait AnyParseStream: Copy { + fn is_empty(&self) -> bool; +} + +impl<'a, K> AnyParseStream for ParseStream<'a, K> { + fn is_empty(&self) -> bool { + self.inner.is_empty() + } +} + +/// This allows parsing of a type, but discarding the result. +/// +/// This is useful for keywords or syntax which we'd like to parse +/// in a uniform way, but don't need to keep around bloating the +/// size of our types. +pub(crate) struct Unused { + _marker: std::marker::PhantomData, +} + +impl ParseSource for Unused { + fn parse(input: SourceParser) -> ParseResult { + let _ = input.parse::()?; + Ok(Self { + _marker: std::marker::PhantomData, + }) + } + + fn control_flow_pass(&mut self, _context: FlowCapturer) -> ParseResult<()> { + unreachable!("An unused value should not have a control flow pass") + } +} + +impl Unused { + /// The argument is immediately discarded, so we don't need to keep it around. + pub(crate) fn new(_inner: T) -> Self { + Self { + _marker: std::marker::PhantomData, + } + } +} + +impl From for Unused { + fn from(_value: T) -> Self { + Self { + _marker: std::marker::PhantomData, + } + } +} diff --git a/src/string_conversion.rs b/src/misc/string_conversion.rs similarity index 100% rename from src/string_conversion.rs rename to src/misc/string_conversion.rs diff --git a/src/parsing.rs b/src/parsing.rs deleted file mode 100644 index 445e4e26..00000000 --- a/src/parsing.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::internal_prelude::*; - -pub(crate) enum NextItem { - CommandInvocation(CommandInvocation), - VariableSubstitution(VariableSubstitution), - Group(Group), - Leaf(TokenTree), - EndOfStream, -} - -pub(crate) fn parse_next_item(tokens: &mut Tokens) -> Result { - Ok(match tokens.next() { - Some(TokenTree::Group(group)) => { - if let Some(command_invocation) = parse_command_invocation(&group)? { - NextItem::CommandInvocation(command_invocation) - } else { - NextItem::Group(group) - } - } - Some(TokenTree::Punct(punct)) => { - if let Some(variable_substitution) = parse_only_if_variable_substitution(&punct, tokens) - { - NextItem::VariableSubstitution(variable_substitution) - } else { - NextItem::Leaf(TokenTree::Punct(punct)) - } - } - Some(leaf) => NextItem::Leaf(leaf), - None => NextItem::EndOfStream, - }) -} - -pub(crate) fn parse_variable_set(tokens: &mut Tokens) -> Option { - let variable_name = parse_variable(tokens)?; - tokens.next_as_punct_matching('=')?; - Some(variable_name) -} - -pub(crate) fn parse_variable(tokens: &mut Tokens) -> Option { - tokens.next_as_punct_matching('#')?; - tokens.next_as_ident() -} - -fn parse_command_invocation(group: &Group) -> Result> { - fn consume_command_start(group: &Group) -> Option<(Ident, Tokens)> { - if group.delimiter() != Delimiter::Bracket { - return None; - } - let mut tokens = Tokens::new(group.stream()); - tokens.next_as_punct_matching('!')?; - let ident = tokens.next_as_ident()?; - Some((ident, tokens)) - } - - fn consume_command_end(command_ident: &Ident, tokens: &mut Tokens) -> Option { - let command_kind = CommandKind::attempt_parse(command_ident)?; - tokens.next_as_punct_matching('!')?; - Some(command_kind) - } - - // Attempt to match `[!ident`, if that doesn't match, we assume it's not a command invocation, - // so return `Ok(None)` - let (command_ident, mut remaining_tokens) = match consume_command_start(group) { - Some(command_start) => command_start, - None => return Ok(None), - }; - - // We have now checked enough that we're confident the user is pretty intentionally using - // the call convention. Any issues we hit from this point will be a helpful compiler error. - match consume_command_end(&command_ident, &mut remaining_tokens) { - Some(command_kind) => Ok(Some(CommandInvocation::new(command_kind, group, remaining_tokens))), - None => Err(Error::new( - command_ident.span(), - format!( - "Expected `[!! ..]`, for one of: {}.\nIf this wasn't intended to be a preinterpret command, you can work around this with [!raw! [!{} ... ]]", - CommandKind::list_all(), - command_ident, - ), - )), - } -} - -// We ensure we don't consume any tokens unless we have a variable substitution -fn parse_only_if_variable_substitution( - punct: &Punct, - tokens: &mut Tokens, -) -> Option { - if punct.as_char() != '#' { - return None; - } - match tokens.peek() { - Some(TokenTree::Ident(_)) => {} - _ => return None, - } - match tokens.next() { - Some(TokenTree::Ident(variable_name)) => { - Some(VariableSubstitution::new(punct.clone(), variable_name)) - } - _ => unreachable!("We just peeked a token of this type"), - } -} diff --git a/style-check.sh b/style-check.sh index b5a96410..da8c7dd4 100755 --- a/style-check.sh +++ b/style-check.sh @@ -2,5 +2,7 @@ set -e +cd "$(dirname "$0")" + cargo fmt --check; cargo clippy --tests; \ No newline at end of file diff --git a/style-fix.sh b/style-fix.sh index f0fb61b8..1d1c4a84 100755 --- a/style-fix.sh +++ b/style-fix.sh @@ -2,5 +2,7 @@ set -e -cargo fmt; -cargo clippy --fix --tests --allow-dirty --allow-staged; \ No newline at end of file +cd "$(dirname "$0")" + +cargo clippy --fix --tests --allow-dirty --allow-staged; +cargo fmt; \ No newline at end of file diff --git a/tests/compilation_failures/complex/nested.rs b/tests/compilation_failures/complex/nested.rs new file mode 100644 index 00000000..183498c4 --- /dev/null +++ b/tests/compilation_failures/complex/nested.rs @@ -0,0 +1,14 @@ +use preinterpret::*; + +fn main() { + run!( + if true { + if true { + if true { + // Missing message + %[].error() + } + } + } + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/complex/nested.stderr b/tests/compilation_failures/complex/nested.stderr new file mode 100644 index 00000000..c08422b5 --- /dev/null +++ b/tests/compilation_failures/complex/nested.stderr @@ -0,0 +1,5 @@ +error: The method error expects 1 non-self argument/s, but 0 were provided + --> tests/compilation_failures/complex/nested.rs:9:25 + | +9 | %[].error() + | ^^^^^ diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_debug.rs b/tests/compilation_failures/control_flow/attempt/attempt_with_debug.rs new file mode 100644 index 00000000..9595bf18 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_debug.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + run!( + let x = "A debug call should propagate out of an attempt arm."; + attempt { + { x.debug() } => { None } + } + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_debug.stderr b/tests/compilation_failures/control_flow/attempt/attempt_with_debug.stderr new file mode 100644 index 00000000..9594aef4 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_debug.stderr @@ -0,0 +1,6 @@ +error: "A debug call should propagate out of an attempt arm." + NOTE: A DebugError is not caught by an attempt block. If you wish to catch this, detect it before it is thrown and use the `revert` statement. + --> tests/compilation_failures/control_flow/attempt/attempt_with_debug.rs:7:15 + | +7 | { x.debug() } => { None } + | ^ diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_guard_defining_variable.rs b/tests/compilation_failures/control_flow/attempt/attempt_with_guard_defining_variable.rs new file mode 100644 index 00000000..de12ba39 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_guard_defining_variable.rs @@ -0,0 +1,14 @@ + +use preinterpret::*; + +fn main() { + run! { + let output = attempt { + // NB: If takes an expression, so `{}` defines a new block/scope, + // and so `x` isn't visible outside of it. + { } if { let x = 4; true } => { x } + { } => { 2 } + }; + %[_].assert_eq(output, 4); + }; +} diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_guard_defining_variable.stderr b/tests/compilation_failures/control_flow/attempt/attempt_with_guard_defining_variable.stderr new file mode 100644 index 00000000..b10e0baf --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_guard_defining_variable.stderr @@ -0,0 +1,5 @@ +error: Cannot find variable `x` in this scope + --> tests/compilation_failures/control_flow/attempt/attempt_with_guard_defining_variable.rs:9:45 + | +9 | { } if { let x = 4; true } => { x } + | ^ diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_no_arms.rs b/tests/compilation_failures/control_flow/attempt/attempt_with_no_arms.rs new file mode 100644 index 00000000..fc75b2f8 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_no_arms.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + attempt {} + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_no_arms.stderr b/tests/compilation_failures/control_flow/attempt/attempt_with_no_arms.stderr new file mode 100644 index 00000000..36196367 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_no_arms.stderr @@ -0,0 +1,5 @@ +error: No attempt arm ran successfully. You may wish to add a fallback arm `{} => { None }` to ignore the error or to propagate a better message: `{} => { %[].error("Error message") }`. + --> tests/compilation_failures/control_flow/attempt/attempt_with_no_arms.rs:5:17 + | +5 | attempt {} + | ^^ diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_no_successful_arms.rs b/tests/compilation_failures/control_flow/attempt/attempt_with_no_successful_arms.rs new file mode 100644 index 00000000..8355beee --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_no_successful_arms.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + run!( + attempt { + { %[_].error("Throw"); } => { None } + } + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_no_successful_arms.stderr b/tests/compilation_failures/control_flow/attempt/attempt_with_no_successful_arms.stderr new file mode 100644 index 00000000..6ab400ca --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_no_successful_arms.stderr @@ -0,0 +1,8 @@ +error: No attempt arm ran successfully. You may wish to add a fallback arm `{} => { None }` to ignore the error or to propagate a better message: `{} => { %[].error("Error message") }`. + --> tests/compilation_failures/control_flow/attempt/attempt_with_no_successful_arms.rs:5:17 + | +5 | attempt { + | _________________^ +6 | | { %[_].error("Throw"); } => { None } +7 | | } + | |_________^ diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_non_block_no_trailing_comma.rs b/tests/compilation_failures/control_flow/attempt/attempt_with_non_block_no_trailing_comma.rs new file mode 100644 index 00000000..3a3d3b03 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_non_block_no_trailing_comma.rs @@ -0,0 +1,10 @@ + +use preinterpret::*; + +fn main() { + run! { + attempt { + { } if true => x + } + }; +} diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_non_block_no_trailing_comma.stderr b/tests/compilation_failures/control_flow/attempt/attempt_with_non_block_no_trailing_comma.stderr new file mode 100644 index 00000000..9c3295d1 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_non_block_no_trailing_comma.stderr @@ -0,0 +1,5 @@ +error: Expected trailing comma after previous non-block attempt arm + --> tests/compilation_failures/control_flow/attempt/attempt_with_non_block_no_trailing_comma.rs:8:9 + | +8 | } + | ^ diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_parent_emit.rs b/tests/compilation_failures/control_flow/attempt/attempt_with_parent_emit.rs new file mode 100644 index 00000000..b99fe050 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_parent_emit.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + run!( + attempt { + { emit "Hello"; revert; } => { None } + } + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_parent_emit.stderr b/tests/compilation_failures/control_flow/attempt/attempt_with_parent_emit.stderr new file mode 100644 index 00000000..785d1bff --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_parent_emit.stderr @@ -0,0 +1,6 @@ +error: It is not possible to emit here. An attempt arm is in two parts: { /* revertible */ } => { /* unconditional */ }. You may define variables in the revertible part, but all mutations of state outside the attempt block must be moved to the unconditional part + NOTE: A ControlFlowError is not caught by an attempt block. If you wish to catch this, detect it before it is thrown and use the `revert` statement. + --> tests/compilation_failures/control_flow/attempt/attempt_with_parent_emit.rs:6:15 + | +6 | { emit "Hello"; revert; } => { None } + | ^^^^ diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_parent_state_mutation.rs b/tests/compilation_failures/control_flow/attempt/attempt_with_parent_state_mutation.rs new file mode 100644 index 00000000..68bf8770 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_parent_state_mutation.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + run!( + let x = 0; + attempt { + { x += 1; } => { None } + } + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_parent_state_mutation.stderr b/tests/compilation_failures/control_flow/attempt/attempt_with_parent_state_mutation.stderr new file mode 100644 index 00000000..1ec1aff2 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_parent_state_mutation.stderr @@ -0,0 +1,6 @@ +error: It is not possible to mutate this variable here. An attempt arm is in two parts: { /* revertible */ } => { /* unconditional */ }. You may define variables in the revertible part, but all mutations of state outside the attempt block must be moved to the unconditional part + NOTE: An OwnershipError is not caught by an attempt block. If you wish to catch this, detect it before it is thrown and use the `revert` statement. + --> tests/compilation_failures/control_flow/attempt/attempt_with_parent_state_mutation.rs:7:15 + | +7 | { x += 1; } => { None } + | ^ diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_rhs_definition_not_accessible_later.rs b/tests/compilation_failures/control_flow/attempt/attempt_with_rhs_definition_not_accessible_later.rs new file mode 100644 index 00000000..3d7728a4 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_rhs_definition_not_accessible_later.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + let _ = run!( + attempt { + {} => { let x = "Hello World!"; } + } + x + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/attempt/attempt_with_rhs_definition_not_accessible_later.stderr b/tests/compilation_failures/control_flow/attempt/attempt_with_rhs_definition_not_accessible_later.stderr new file mode 100644 index 00000000..ddcd93f8 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/attempt_with_rhs_definition_not_accessible_later.stderr @@ -0,0 +1,5 @@ +error: Cannot find variable `x` in this scope + --> tests/compilation_failures/control_flow/attempt/attempt_with_rhs_definition_not_accessible_later.rs:8:9 + | +8 | x + | ^ diff --git a/tests/compilation_failures/control_flow/attempt/labeled_revert_in_guard_clause.rs b/tests/compilation_failures/control_flow/attempt/labeled_revert_in_guard_clause.rs new file mode 100644 index 00000000..30f8d30b --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/labeled_revert_in_guard_clause.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + run!( + 'outer: attempt { + { let x = 1; } if { revert 'outer; true } => { x } + { } => { 2 } + } + ); +} diff --git a/tests/compilation_failures/control_flow/attempt/labeled_revert_in_guard_clause.stderr b/tests/compilation_failures/control_flow/attempt/labeled_revert_in_guard_clause.stderr new file mode 100644 index 00000000..5b6370fc --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/labeled_revert_in_guard_clause.stderr @@ -0,0 +1,5 @@ +error: A labelled revert must be used inside the left revertible part of an attempt arm, where the attempt has a matching label + --> tests/compilation_failures/control_flow/attempt/labeled_revert_in_guard_clause.rs:6:33 + | +6 | { let x = 1; } if { revert 'outer; true } => { x } + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/attempt/labeled_revert_on_unconditional_part_of_arm.rs b/tests/compilation_failures/control_flow/attempt/labeled_revert_on_unconditional_part_of_arm.rs new file mode 100644 index 00000000..9a77d3b1 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/labeled_revert_on_unconditional_part_of_arm.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + run!( + 'outer: attempt { + { } => { revert 'outer; } + { } => { None } + } + ); +} diff --git a/tests/compilation_failures/control_flow/attempt/labeled_revert_on_unconditional_part_of_arm.stderr b/tests/compilation_failures/control_flow/attempt/labeled_revert_on_unconditional_part_of_arm.stderr new file mode 100644 index 00000000..b8643951 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/labeled_revert_on_unconditional_part_of_arm.stderr @@ -0,0 +1,5 @@ +error: A labelled revert must be used inside the left revertible part of an attempt arm, where the attempt has a matching label + --> tests/compilation_failures/control_flow/attempt/labeled_revert_on_unconditional_part_of_arm.rs:6:22 + | +6 | { } => { revert 'outer; } + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/attempt/nested_attempt_with_invalid_mutation.rs b/tests/compilation_failures/control_flow/attempt/nested_attempt_with_invalid_mutation.rs new file mode 100644 index 00000000..11b6748c --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/nested_attempt_with_invalid_mutation.rs @@ -0,0 +1,14 @@ +use preinterpret::*; + +fn main() { + run!( + let value = attempt { + { + let y = 1; + attempt { + { y += 1; } => { y } + } + } => { y } + }; + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/attempt/nested_attempt_with_invalid_mutation.stderr b/tests/compilation_failures/control_flow/attempt/nested_attempt_with_invalid_mutation.stderr new file mode 100644 index 00000000..4ad9ddde --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/nested_attempt_with_invalid_mutation.stderr @@ -0,0 +1,6 @@ +error: It is not possible to mutate this variable here. An attempt arm is in two parts: { /* revertible */ } => { /* unconditional */ }. You may define variables in the revertible part, but all mutations of state outside the attempt block must be moved to the unconditional part + NOTE: An OwnershipError is not caught by an attempt block. If you wish to catch this, detect it before it is thrown and use the `revert` statement. + --> tests/compilation_failures/control_flow/attempt/nested_attempt_with_invalid_mutation.rs:9:23 + | +9 | { y += 1; } => { y } + | ^ diff --git a/tests/compilation_failures/control_flow/attempt/revert_in_guard_clause.rs b/tests/compilation_failures/control_flow/attempt/revert_in_guard_clause.rs new file mode 100644 index 00000000..1a45e440 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/revert_in_guard_clause.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + run!( + attempt { + { let x = 1; } if { revert; true } => { x } + { } => { 2 } + } + ); +} diff --git a/tests/compilation_failures/control_flow/attempt/revert_in_guard_clause.stderr b/tests/compilation_failures/control_flow/attempt/revert_in_guard_clause.stderr new file mode 100644 index 00000000..2fa76618 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/revert_in_guard_clause.stderr @@ -0,0 +1,5 @@ +error: A revert must be used inside the left revertible part of an attempt arm + --> tests/compilation_failures/control_flow/attempt/revert_in_guard_clause.rs:6:33 + | +6 | { let x = 1; } if { revert; true } => { x } + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/attempt/revert_on_unconditional_part_of_arm.rs b/tests/compilation_failures/control_flow/attempt/revert_on_unconditional_part_of_arm.rs new file mode 100644 index 00000000..f057032f --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/revert_on_unconditional_part_of_arm.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + run!( + attempt { + { } => { revert; } + { } => { None } + } + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/attempt/revert_on_unconditional_part_of_arm.stderr b/tests/compilation_failures/control_flow/attempt/revert_on_unconditional_part_of_arm.stderr new file mode 100644 index 00000000..e02f5059 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/revert_on_unconditional_part_of_arm.stderr @@ -0,0 +1,5 @@ +error: A revert must be used inside the left revertible part of an attempt arm + --> tests/compilation_failures/control_flow/attempt/revert_on_unconditional_part_of_arm.rs:6:22 + | +6 | { } => { revert; } + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/attempt/revert_with_mismatched_label.rs b/tests/compilation_failures/control_flow/attempt/revert_with_mismatched_label.rs new file mode 100644 index 00000000..7be8b4ff --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/revert_with_mismatched_label.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + run!( + 'outer: attempt { + { revert 'inner; } => { 1 } + { } => { 2 } + } + ); +} diff --git a/tests/compilation_failures/control_flow/attempt/revert_with_mismatched_label.stderr b/tests/compilation_failures/control_flow/attempt/revert_with_mismatched_label.stderr new file mode 100644 index 00000000..03fe6f33 --- /dev/null +++ b/tests/compilation_failures/control_flow/attempt/revert_with_mismatched_label.stderr @@ -0,0 +1,5 @@ +error: A labelled revert must be used inside the left revertible part of an attempt arm, where the attempt has a matching label + --> tests/compilation_failures/control_flow/attempt/revert_with_mismatched_label.rs:6:15 + | +6 | { revert 'inner; } => { 1 } + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/break_outside_a_loop.rs b/tests/compilation_failures/control_flow/break_outside_a_loop.rs new file mode 100644 index 00000000..fe5eeecb --- /dev/null +++ b/tests/compilation_failures/control_flow/break_outside_a_loop.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(1 + break + 2); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/break_outside_a_loop.stderr b/tests/compilation_failures/control_flow/break_outside_a_loop.stderr new file mode 100644 index 00000000..53f9affa --- /dev/null +++ b/tests/compilation_failures/control_flow/break_outside_a_loop.stderr @@ -0,0 +1,5 @@ +error: expected identifier, found keyword `break` + --> tests/compilation_failures/control_flow/break_outside_a_loop.rs:4:14 + | +4 | run!(1 + break + 2); + | ^^^^^ diff --git a/tests/compilation_failures/control_flow/break_outside_a_loop_2.rs b/tests/compilation_failures/control_flow/break_outside_a_loop_2.rs new file mode 100644 index 00000000..99e6b11e --- /dev/null +++ b/tests/compilation_failures/control_flow/break_outside_a_loop_2.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(break;); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/break_outside_a_loop_2.stderr b/tests/compilation_failures/control_flow/break_outside_a_loop_2.stderr new file mode 100644 index 00000000..604c7b50 --- /dev/null +++ b/tests/compilation_failures/control_flow/break_outside_a_loop_2.stderr @@ -0,0 +1,5 @@ +error: A break must be used inside a loop + --> tests/compilation_failures/control_flow/break_outside_a_loop_2.rs:4:10 + | +4 | run!(break;); + | ^^^^^ diff --git a/tests/compilation_failures/control_flow/break_with_label_outside_block.rs b/tests/compilation_failures/control_flow/break_with_label_outside_block.rs new file mode 100644 index 00000000..741dee5c --- /dev/null +++ b/tests/compilation_failures/control_flow/break_with_label_outside_block.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!({ + break 'outer; + }); +} diff --git a/tests/compilation_failures/control_flow/break_with_label_outside_block.stderr b/tests/compilation_failures/control_flow/break_with_label_outside_block.stderr new file mode 100644 index 00000000..e470048f --- /dev/null +++ b/tests/compilation_failures/control_flow/break_with_label_outside_block.stderr @@ -0,0 +1,5 @@ +error: A labelled break must be used inside a loop or block with a matching label + --> tests/compilation_failures/control_flow/break_with_label_outside_block.rs:5:15 + | +5 | break 'outer; + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/break_with_mismatched_label.rs b/tests/compilation_failures/control_flow/break_with_mismatched_label.rs new file mode 100644 index 00000000..f0e47ad7 --- /dev/null +++ b/tests/compilation_failures/control_flow/break_with_mismatched_label.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + run!({ + loop { + break 'outer; + } + }); +} diff --git a/tests/compilation_failures/control_flow/break_with_mismatched_label.stderr b/tests/compilation_failures/control_flow/break_with_mismatched_label.stderr new file mode 100644 index 00000000..8c9090f8 --- /dev/null +++ b/tests/compilation_failures/control_flow/break_with_mismatched_label.stderr @@ -0,0 +1,5 @@ +error: A labelled break must be used inside a loop or block with a matching label + --> tests/compilation_failures/control_flow/break_with_mismatched_label.rs:6:19 + | +6 | break 'outer; + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/break_with_wrong_label_in_nested_loop.rs b/tests/compilation_failures/control_flow/break_with_wrong_label_in_nested_loop.rs new file mode 100644 index 00000000..6c862277 --- /dev/null +++ b/tests/compilation_failures/control_flow/break_with_wrong_label_in_nested_loop.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + run!({ + 'inner: loop { + break 'outer; + } + }); +} diff --git a/tests/compilation_failures/control_flow/break_with_wrong_label_in_nested_loop.stderr b/tests/compilation_failures/control_flow/break_with_wrong_label_in_nested_loop.stderr new file mode 100644 index 00000000..f9546c21 --- /dev/null +++ b/tests/compilation_failures/control_flow/break_with_wrong_label_in_nested_loop.stderr @@ -0,0 +1,5 @@ +error: A labelled break must be used inside a loop or block with a matching label + --> tests/compilation_failures/control_flow/break_with_wrong_label_in_nested_loop.rs:6:19 + | +6 | break 'outer; + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/continue_outside_a_loop.rs b/tests/compilation_failures/control_flow/continue_outside_a_loop.rs new file mode 100644 index 00000000..f91db234 --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_outside_a_loop.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(1 + continue + 2); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/continue_outside_a_loop.stderr b/tests/compilation_failures/control_flow/continue_outside_a_loop.stderr new file mode 100644 index 00000000..26101e6e --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_outside_a_loop.stderr @@ -0,0 +1,5 @@ +error: expected identifier, found keyword `continue` + --> tests/compilation_failures/control_flow/continue_outside_a_loop.rs:4:14 + | +4 | run!(1 + continue + 2); + | ^^^^^^^^ diff --git a/tests/compilation_failures/control_flow/continue_outside_a_loop_2.rs b/tests/compilation_failures/control_flow/continue_outside_a_loop_2.rs new file mode 100644 index 00000000..bcf02621 --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_outside_a_loop_2.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(continue;); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/continue_outside_a_loop_2.stderr b/tests/compilation_failures/control_flow/continue_outside_a_loop_2.stderr new file mode 100644 index 00000000..19734142 --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_outside_a_loop_2.stderr @@ -0,0 +1,5 @@ +error: A continue must be used inside a loop + --> tests/compilation_failures/control_flow/continue_outside_a_loop_2.rs:4:10 + | +4 | run!(continue;); + | ^^^^^^^^ diff --git a/tests/compilation_failures/control_flow/continue_with_mismatched_label.rs b/tests/compilation_failures/control_flow/continue_with_mismatched_label.rs new file mode 100644 index 00000000..88dd07e2 --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_with_mismatched_label.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + run!({ + loop { + continue 'outer; + } + }); +} diff --git a/tests/compilation_failures/control_flow/continue_with_mismatched_label.stderr b/tests/compilation_failures/control_flow/continue_with_mismatched_label.stderr new file mode 100644 index 00000000..e70f0ee2 --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_with_mismatched_label.stderr @@ -0,0 +1,5 @@ +error: A labelled continue must be used inside a loop with a matching label + --> tests/compilation_failures/control_flow/continue_with_mismatched_label.rs:6:22 + | +6 | continue 'outer; + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/continue_with_wrong_label_in_nested_loop.rs b/tests/compilation_failures/control_flow/continue_with_wrong_label_in_nested_loop.rs new file mode 100644 index 00000000..f3360de7 --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_with_wrong_label_in_nested_loop.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + run!({ + 'inner: loop { + continue 'outer; + } + }); +} diff --git a/tests/compilation_failures/control_flow/continue_with_wrong_label_in_nested_loop.stderr b/tests/compilation_failures/control_flow/continue_with_wrong_label_in_nested_loop.stderr new file mode 100644 index 00000000..05a120ad --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_with_wrong_label_in_nested_loop.stderr @@ -0,0 +1,5 @@ +error: A labelled continue must be used inside a loop with a matching label + --> tests/compilation_failures/control_flow/continue_with_wrong_label_in_nested_loop.rs:6:22 + | +6 | continue 'outer; + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/error_after_continue.rs b/tests/compilation_failures/control_flow/error_after_continue.rs new file mode 100644 index 00000000..fbad18ff --- /dev/null +++ b/tests/compilation_failures/control_flow/error_after_continue.rs @@ -0,0 +1,17 @@ +use preinterpret::*; + +fn main() { + run!( + let x = 0; + while true { + x += 1; + if x == 3 { + continue; + } else if x >= 3 { + // This checks that the "continue" flag is consumed, + // and future errors propagate correctly. + %[_].error("And now we error"); + } + } + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/error_after_continue.stderr b/tests/compilation_failures/control_flow/error_after_continue.stderr new file mode 100644 index 00000000..cd454e5f --- /dev/null +++ b/tests/compilation_failures/control_flow/error_after_continue.stderr @@ -0,0 +1,5 @@ +error: And now we error + --> tests/compilation_failures/control_flow/error_after_continue.rs:13:19 + | +13 | %[_].error("And now we error"); + | ^ diff --git a/tests/compilation_failures/control_flow/reserved_keyword_attempt.rs b/tests/compilation_failures/control_flow/reserved_keyword_attempt.rs new file mode 100644 index 00000000..8fb4fd27 --- /dev/null +++ b/tests/compilation_failures/control_flow/reserved_keyword_attempt.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + let attempt = 5; + }; +} diff --git a/tests/compilation_failures/control_flow/reserved_keyword_attempt.stderr b/tests/compilation_failures/control_flow/reserved_keyword_attempt.stderr new file mode 100644 index 00000000..91f5efa3 --- /dev/null +++ b/tests/compilation_failures/control_flow/reserved_keyword_attempt.stderr @@ -0,0 +1,5 @@ +error: Cannot use preinterpret keyword `attempt` as a variable name + --> tests/compilation_failures/control_flow/reserved_keyword_attempt.rs:5:13 + | +5 | let attempt = 5; + | ^^^^^^^ diff --git a/tests/compilation_failures/control_flow/reserved_keyword_emit.rs b/tests/compilation_failures/control_flow/reserved_keyword_emit.rs new file mode 100644 index 00000000..9eb28316 --- /dev/null +++ b/tests/compilation_failures/control_flow/reserved_keyword_emit.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + let emit = 5; + }; +} diff --git a/tests/compilation_failures/control_flow/reserved_keyword_emit.stderr b/tests/compilation_failures/control_flow/reserved_keyword_emit.stderr new file mode 100644 index 00000000..cea21847 --- /dev/null +++ b/tests/compilation_failures/control_flow/reserved_keyword_emit.stderr @@ -0,0 +1,5 @@ +error: Cannot use preinterpret keyword `emit` as a variable name + --> tests/compilation_failures/control_flow/reserved_keyword_emit.rs:5:13 + | +5 | let emit = 5; + | ^^^^ diff --git a/tests/compilation_failures/control_flow/reserved_keyword_revert.rs b/tests/compilation_failures/control_flow/reserved_keyword_revert.rs new file mode 100644 index 00000000..42fe2942 --- /dev/null +++ b/tests/compilation_failures/control_flow/reserved_keyword_revert.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + let revert = 5; + }; +} diff --git a/tests/compilation_failures/control_flow/reserved_keyword_revert.stderr b/tests/compilation_failures/control_flow/reserved_keyword_revert.stderr new file mode 100644 index 00000000..fde020d4 --- /dev/null +++ b/tests/compilation_failures/control_flow/reserved_keyword_revert.stderr @@ -0,0 +1,5 @@ +error: Cannot use preinterpret keyword `revert` as a variable name + --> tests/compilation_failures/control_flow/reserved_keyword_revert.rs:5:13 + | +5 | let revert = 5; + | ^^^^^^ diff --git a/tests/compilation_failures/control_flow/while_infinite_loop.rs b/tests/compilation_failures/control_flow/while_infinite_loop.rs new file mode 100644 index 00000000..a4bdb100 --- /dev/null +++ b/tests/compilation_failures/control_flow/while_infinite_loop.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(while true {}); +} \ No newline at end of file diff --git a/tests/compilation_failures/control_flow/while_infinite_loop.stderr b/tests/compilation_failures/control_flow/while_infinite_loop.stderr new file mode 100644 index 00000000..ac3ad2f2 --- /dev/null +++ b/tests/compilation_failures/control_flow/while_infinite_loop.stderr @@ -0,0 +1,6 @@ +error: Iteration limit of 1000 exceeded. + If needed, the limit can be reconfigured with None.configure_preinterpret(%{ iteration_limit: XXX }) + --> tests/compilation_failures/control_flow/while_infinite_loop.rs:4:21 + | +4 | run!(while true {}); + | ^^ diff --git a/tests/compilation_failures/core/assert_eq_array_element.rs b/tests/compilation_failures/core/assert_eq_array_element.rs new file mode 100644 index 00000000..f7f32e38 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_array_element.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq([1, 2, 3], [1, 5, 3])); +} diff --git a/tests/compilation_failures/core/assert_eq_array_element.stderr b/tests/compilation_failures/core/assert_eq_array_element.stderr new file mode 100644 index 00000000..02e03e09 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_array_element.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs[1] != rhs[1]: 2 != 5 + lhs = [1, 2, 3] + rhs = [1, 5, 3] + --> tests/compilation_failures/core/assert_eq_array_element.rs:4:12 + | +4 | run!(%[_].assert_eq([1, 2, 3], [1, 5, 3])); + | ^ diff --git a/tests/compilation_failures/core/assert_eq_array_length.rs b/tests/compilation_failures/core/assert_eq_array_length.rs new file mode 100644 index 00000000..391f89c3 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_array_length.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq([1, 2, 3], [1, 2])); +} diff --git a/tests/compilation_failures/core/assert_eq_array_length.stderr b/tests/compilation_failures/core/assert_eq_array_length.stderr new file mode 100644 index 00000000..3ca51a06 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_array_length.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs has length 3, but rhs has length 2 + lhs = [1, 2, 3] + rhs = [1, 2] + --> tests/compilation_failures/core/assert_eq_array_length.rs:4:12 + | +4 | run!(%[_].assert_eq([1, 2, 3], [1, 2])); + | ^ diff --git a/tests/compilation_failures/core/assert_eq_bool.rs b/tests/compilation_failures/core/assert_eq_bool.rs new file mode 100644 index 00000000..2c9113ac --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_bool.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq(true, false)); +} diff --git a/tests/compilation_failures/core/assert_eq_bool.stderr b/tests/compilation_failures/core/assert_eq_bool.stderr new file mode 100644 index 00000000..94cfcbe7 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_bool.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs != rhs: true != false + lhs = true + rhs = false + --> tests/compilation_failures/core/assert_eq_bool.rs:4:12 + | +4 | run!(%[_].assert_eq(true, false)); + | ^ diff --git a/tests/compilation_failures/core/assert_eq_call_site_with_message.rs b/tests/compilation_failures/core/assert_eq_call_site_with_message.rs new file mode 100644 index 00000000..cfb71d38 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_call_site_with_message.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[].assert_eq(1, 2, "Values are not equal")); +} \ No newline at end of file diff --git a/tests/compilation_failures/core/assert_eq_call_site_with_message.stderr b/tests/compilation_failures/core/assert_eq_call_site_with_message.stderr new file mode 100644 index 00000000..605ababb --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_call_site_with_message.stderr @@ -0,0 +1,7 @@ +error: Values are not equal + --> tests/compilation_failures/core/assert_eq_call_site_with_message.rs:4:5 + | +4 | run!(%[].assert_eq(1, 2, "Values are not equal")); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `run` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/compilation_failures/core/assert_eq_in_macro.rs b/tests/compilation_failures/core/assert_eq_in_macro.rs new file mode 100644 index 00000000..20c6db5e --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_in_macro.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq(1, 2)); +} \ No newline at end of file diff --git a/tests/compilation_failures/core/assert_eq_in_macro.stderr b/tests/compilation_failures/core/assert_eq_in_macro.stderr new file mode 100644 index 00000000..bdd65d2e --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_in_macro.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs != rhs: 1 != 2 + lhs = 1 + rhs = 2 + --> tests/compilation_failures/core/assert_eq_in_macro.rs:4:12 + | +4 | run!(%[_].assert_eq(1, 2)); + | ^ diff --git a/tests/compilation_failures/core/assert_eq_long_iterator.rs b/tests/compilation_failures/core/assert_eq_long_iterator.rs new file mode 100644 index 00000000..db8ca4c5 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_long_iterator.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq((0..2000).into_iter(), (0..2000).into_iter())); +} diff --git a/tests/compilation_failures/core/assert_eq_long_iterator.stderr b/tests/compilation_failures/core/assert_eq_long_iterator.stderr new file mode 100644 index 00000000..16002692 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_long_iterator.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs != rhs: "iteration limit 1000 exceeded" != "iteration limit 1000 exceeded" + lhs = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ..<1980 further items>] + rhs = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ..<1980 further items>] + --> tests/compilation_failures/core/assert_eq_long_iterator.rs:4:12 + | +4 | run!(%[_].assert_eq((0..2000).into_iter(), (0..2000).into_iter())); + | ^ diff --git a/tests/compilation_failures/core/assert_eq_nested_array.rs b/tests/compilation_failures/core/assert_eq_nested_array.rs new file mode 100644 index 00000000..2ed4a6c1 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_nested_array.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq([[1, 2], [3, 4]], [[1, 2], [3, 5]])); +} diff --git a/tests/compilation_failures/core/assert_eq_nested_array.stderr b/tests/compilation_failures/core/assert_eq_nested_array.stderr new file mode 100644 index 00000000..7faaac76 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_nested_array.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs[1][1] != rhs[1][1]: 4 != 5 + lhs = [[1, 2], [3, 4]] + rhs = [[1, 2], [3, 5]] + --> tests/compilation_failures/core/assert_eq_nested_array.rs:4:12 + | +4 | run!(%[_].assert_eq([[1, 2], [3, 4]], [[1, 2], [3, 5]])); + | ^ diff --git a/tests/compilation_failures/core/assert_eq_object_missing_key.rs b/tests/compilation_failures/core/assert_eq_object_missing_key.rs new file mode 100644 index 00000000..9eba2399 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_object_missing_key.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq(%{ a: 1, b: 2 }, %{ a: 1, c: 2 })); +} diff --git a/tests/compilation_failures/core/assert_eq_object_missing_key.stderr b/tests/compilation_failures/core/assert_eq_object_missing_key.stderr new file mode 100644 index 00000000..773e2851 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_object_missing_key.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs has extra key "b", compared to rhs + lhs = %{ a: 1, b: 2 } + rhs = %{ a: 1, c: 2 } + --> tests/compilation_failures/core/assert_eq_object_missing_key.rs:4:12 + | +4 | run!(%[_].assert_eq(%{ a: 1, b: 2 }, %{ a: 1, c: 2 })); + | ^ diff --git a/tests/compilation_failures/core/assert_eq_range_length.rs b/tests/compilation_failures/core/assert_eq_range_length.rs new file mode 100644 index 00000000..d8018801 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_range_length.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq(0.., 1..2)); +} diff --git a/tests/compilation_failures/core/assert_eq_range_length.stderr b/tests/compilation_failures/core/assert_eq_range_length.stderr new file mode 100644 index 00000000..2f213a28 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_range_length.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs is a range start.., but rhs is a range start..end + lhs = 0.. + rhs = 1..2 + --> tests/compilation_failures/core/assert_eq_range_length.rs:4:12 + | +4 | run!(%[_].assert_eq(0.., 1..2)); + | ^ diff --git a/tests/compilation_failures/core/assert_eq_string.rs b/tests/compilation_failures/core/assert_eq_string.rs new file mode 100644 index 00000000..839c2be4 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_string.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq("hello", "world")); +} diff --git a/tests/compilation_failures/core/assert_eq_string.stderr b/tests/compilation_failures/core/assert_eq_string.stderr new file mode 100644 index 00000000..af604b47 --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_string.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs != rhs: "hello" != "world" + lhs = "hello" + rhs = "world" + --> tests/compilation_failures/core/assert_eq_string.rs:4:12 + | +4 | run!(%[_].assert_eq("hello", "world")); + | ^ diff --git a/tests/compilation_failures/core/assert_eq_value_kind.rs b/tests/compilation_failures/core/assert_eq_value_kind.rs new file mode 100644 index 00000000..ccf0790d --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_value_kind.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(%[_].assert_eq(1, "hello")); +} diff --git a/tests/compilation_failures/core/assert_eq_value_kind.stderr b/tests/compilation_failures/core/assert_eq_value_kind.stderr new file mode 100644 index 00000000..489215de --- /dev/null +++ b/tests/compilation_failures/core/assert_eq_value_kind.stderr @@ -0,0 +1,7 @@ +error: Assertion failed: lhs is an untyped integer, but rhs is a string + lhs = 1 + rhs = "hello" + --> tests/compilation_failures/core/assert_eq_value_kind.rs:4:12 + | +4 | run!(%[_].assert_eq(1, "hello")); + | ^ diff --git a/tests/compilation_failures/core/error_no_fields.rs b/tests/compilation_failures/core/error_no_fields.rs new file mode 100644 index 00000000..e89d281e --- /dev/null +++ b/tests/compilation_failures/core/error_no_fields.rs @@ -0,0 +1,13 @@ +use preinterpret::*; + +macro_rules! assert_literals_eq { + ($input1:literal, $input2:literal) => {run!{ + if ($input1 != $input2) { + %[].error(%["Expected " $input1 " to equal " $input2].to_string()); + } + }}; +} + +fn main() { + assert_literals_eq!(102, 64); +} \ No newline at end of file diff --git a/tests/compilation_failures/core/error_no_fields.stderr b/tests/compilation_failures/core/error_no_fields.stderr new file mode 100644 index 00000000..5175977d --- /dev/null +++ b/tests/compilation_failures/core/error_no_fields.stderr @@ -0,0 +1,15 @@ +error: Expected 102 to equal 64 + --> tests/compilation_failures/core/error_no_fields.rs:4:44 + | + 4 | ($input1:literal, $input2:literal) => {run!{ + | ____________________________________________^ + 5 | | if ($input1 != $input2) { + 6 | | %[].error(%["Expected " $input1 " to equal " $input2].to_string()); + 7 | | } + 8 | | }}; + | |_____^ +... +12 | assert_literals_eq!(102, 64); + | ---------------------------- in this macro invocation + | + = note: this error originates in the macro `run` which comes from the expansion of the macro `assert_literals_eq` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/compilation_failures/core/error_no_span.rs b/tests/compilation_failures/core/error_no_span.rs new file mode 100644 index 00000000..01f2d4bf --- /dev/null +++ b/tests/compilation_failures/core/error_no_span.rs @@ -0,0 +1,13 @@ +use preinterpret::*; + +macro_rules! assert_literals_eq_no_spans { + ($input1:literal and $input2:literal) => {run!{ + if ($input1 != $input2) { + %[].error(%["Expected " $input1 " to equal " $input2].to_string()); + } + }}; +} + +fn main() { + assert_literals_eq_no_spans!(102 and 64); +} \ No newline at end of file diff --git a/tests/compilation_failures/core/error_no_span.stderr b/tests/compilation_failures/core/error_no_span.stderr new file mode 100644 index 00000000..c6f8de0b --- /dev/null +++ b/tests/compilation_failures/core/error_no_span.stderr @@ -0,0 +1,15 @@ +error: Expected 102 to equal 64 + --> tests/compilation_failures/core/error_no_span.rs:4:47 + | + 4 | ($input1:literal and $input2:literal) => {run!{ + | _______________________________________________^ + 5 | | if ($input1 != $input2) { + 6 | | %[].error(%["Expected " $input1 " to equal " $input2].to_string()); + 7 | | } + 8 | | }}; + | |_____^ +... +12 | assert_literals_eq_no_spans!(102 and 64); + | ---------------------------------------- in this macro invocation + | + = note: this error originates in the macro `run` which comes from the expansion of the macro `assert_literals_eq_no_spans` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/compilation_failures/core/error_span_multiple.rs b/tests/compilation_failures/core/error_span_multiple.rs new file mode 100644 index 00000000..8d47615e --- /dev/null +++ b/tests/compilation_failures/core/error_span_multiple.rs @@ -0,0 +1,13 @@ +use preinterpret::*; + +macro_rules! assert_literals_eq { + ($input1:literal and $input2:literal) => {run!{ + if ($input1 != $input2) { + %[$input1 $input2].error(%["Expected " $input1 " to equal " $input2].to_string()); + } + }}; +} + +fn main() { + assert_literals_eq!(102 and 64); +} \ No newline at end of file diff --git a/tests/compilation_failures/core/error_span_multiple.stderr b/tests/compilation_failures/core/error_span_multiple.stderr new file mode 100644 index 00000000..695de351 --- /dev/null +++ b/tests/compilation_failures/core/error_span_multiple.stderr @@ -0,0 +1,5 @@ +error: Expected 102 to equal 64 + --> tests/compilation_failures/core/error_span_multiple.rs:12:25 + | +12 | assert_literals_eq!(102 and 64); + | ^^^^^^^^^^ diff --git a/tests/compilation_failures/core/error_span_repeat.rs b/tests/compilation_failures/core/error_span_repeat.rs new file mode 100644 index 00000000..034d0677 --- /dev/null +++ b/tests/compilation_failures/core/error_span_repeat.rs @@ -0,0 +1,15 @@ +use preinterpret::*; + +macro_rules! assert_input_length_of_3 { + ($($input:literal)+) => {run!{ + let input = %raw[$($input)+]; + let input_length = input.len(); + if input_length != 3 { + input.error(%["Expected 3 inputs, got " #input_length].to_string()); + } + }}; +} + +fn main() { + assert_input_length_of_3!(42 101 666 1024); +} \ No newline at end of file diff --git a/tests/compilation_failures/core/error_span_repeat.stderr b/tests/compilation_failures/core/error_span_repeat.stderr new file mode 100644 index 00000000..5b2a2a81 --- /dev/null +++ b/tests/compilation_failures/core/error_span_repeat.stderr @@ -0,0 +1,5 @@ +error: Expected 3 inputs, got 4 + --> tests/compilation_failures/core/error_span_repeat.rs:14:31 + | +14 | assert_input_length_of_3!(42 101 666 1024); + | ^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/core/error_span_single.rs b/tests/compilation_failures/core/error_span_single.rs new file mode 100644 index 00000000..1632a67c --- /dev/null +++ b/tests/compilation_failures/core/error_span_single.rs @@ -0,0 +1,13 @@ +use preinterpret::*; + +macro_rules! assert_is_100 { + ($input:literal) => {run!{ + if ($input != 100) { + %[$input].error(%["Expected 100, got " $input].to_string()); + } + }}; +} + +fn main() { + assert_is_100!(5); +} \ No newline at end of file diff --git a/tests/compilation_failures/core/error_span_single.stderr b/tests/compilation_failures/core/error_span_single.stderr new file mode 100644 index 00000000..17d1d887 --- /dev/null +++ b/tests/compilation_failures/core/error_span_single.stderr @@ -0,0 +1,5 @@ +error: Expected 100, got 5 + --> tests/compilation_failures/core/error_span_single.rs:12:20 + | +12 | assert_is_100!(5); + | ^ diff --git a/tests/compilation_failures/core/extend_non_existing_variable.rs b/tests/compilation_failures/core/extend_non_existing_variable.rs new file mode 100644 index 00000000..3df945e8 --- /dev/null +++ b/tests/compilation_failures/core/extend_non_existing_variable.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + variable += %[2]; + } +} \ No newline at end of file diff --git a/tests/compilation_failures/core/extend_non_existing_variable.stderr b/tests/compilation_failures/core/extend_non_existing_variable.stderr new file mode 100644 index 00000000..afa04fdc --- /dev/null +++ b/tests/compilation_failures/core/extend_non_existing_variable.stderr @@ -0,0 +1,5 @@ +error: Cannot find variable `variable` in this scope + --> tests/compilation_failures/core/extend_non_existing_variable.rs:5:9 + | +5 | variable += %[2]; + | ^^^^^^^^ diff --git a/tests/compilation_failures/core/reinterpret_cannot_update_variables.rs b/tests/compilation_failures/core/reinterpret_cannot_update_variables.rs new file mode 100644 index 00000000..2c3db470 --- /dev/null +++ b/tests/compilation_failures/core/reinterpret_cannot_update_variables.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + run! { + let my_variable = "before"; + %[my_variable = "updated";].reinterpret_as_run(); + } +} diff --git a/tests/compilation_failures/core/reinterpret_cannot_update_variables.stderr b/tests/compilation_failures/core/reinterpret_cannot_update_variables.stderr new file mode 100644 index 00000000..aa01c3fd --- /dev/null +++ b/tests/compilation_failures/core/reinterpret_cannot_update_variables.stderr @@ -0,0 +1,5 @@ +error: Cannot find variable `my_variable` in this scope + --> tests/compilation_failures/core/reinterpret_cannot_update_variables.rs:6:11 + | +6 | %[my_variable = "updated";].reinterpret_as_run(); + | ^^^^^^^^^^^ diff --git a/tests/compilation_failures/core/set_span_example.rs b/tests/compilation_failures/core/set_span_example.rs new file mode 100644 index 00000000..0255e524 --- /dev/null +++ b/tests/compilation_failures/core/set_span_example.rs @@ -0,0 +1,24 @@ +use preinterpret::*; + +// Taken from core.rs +macro_rules! capitalize_variants { + ($enum_name:ident, [$($variants:ident),*]) => {run!{ + let enum_name = %raw[$enum_name]; + let variants = []; + for variant in [$(%raw[$variants]),*] { + let capitalized = variant.to_string().capitalize().to_ident(); + capitalized.set_span(variant); + variants.push(capitalized); + } + + %[ + enum #enum_name { + #(variants.intersperse(%[,])) + } + ] + }}; +} + +capitalize_variants!(MyEnum, [helloWorld, Test, HelloWorld]); + +fn main() {} diff --git a/tests/compilation_failures/core/set_span_example.stderr b/tests/compilation_failures/core/set_span_example.stderr new file mode 100644 index 00000000..fc079d23 --- /dev/null +++ b/tests/compilation_failures/core/set_span_example.stderr @@ -0,0 +1,9 @@ +error[E0428]: the name `HelloWorld` is defined multiple times + --> tests/compilation_failures/core/set_span_example.rs:22:49 + | +22 | capitalize_variants!(MyEnum, [helloWorld, Test, HelloWorld]); + | ---------- ^^^^^^^^^^ `HelloWorld` redefined here + | | + | previous definition of the type `HelloWorld` here + | + = note: `HelloWorld` must be defined only once in the type namespace of this enum diff --git a/tests/compilation_failures/core/settings_update_iteration_limit.rs b/tests/compilation_failures/core/settings_update_iteration_limit.rs new file mode 100644 index 00000000..1daa4185 --- /dev/null +++ b/tests/compilation_failures/core/settings_update_iteration_limit.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + run!{ + None.configure_preinterpret(%{ iteration_limit: 5 }); + loop {} + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/core/settings_update_iteration_limit.stderr b/tests/compilation_failures/core/settings_update_iteration_limit.stderr new file mode 100644 index 00000000..bfeead41 --- /dev/null +++ b/tests/compilation_failures/core/settings_update_iteration_limit.stderr @@ -0,0 +1,6 @@ +error: Iteration limit of 5 exceeded. + If needed, the limit can be reconfigured with None.configure_preinterpret(%{ iteration_limit: XXX }) + --> tests/compilation_failures/core/settings_update_iteration_limit.rs:6:14 + | +6 | loop {} + | ^^ diff --git a/tests/compilation_failures/core/simple_assert.rs b/tests/compilation_failures/core/simple_assert.rs new file mode 100644 index 00000000..14518e3a --- /dev/null +++ b/tests/compilation_failures/core/simple_assert.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +macro_rules! assert_literals_eq { + ($input1:literal, $input2:literal) => {run!{ + %[$input1 $input2].assert($input1 == $input2, %["Expected " $input1 " to equal " $input2].to_string()); + }}; +} + +fn main() { + assert_literals_eq!(102, 64); +} \ No newline at end of file diff --git a/tests/compilation_failures/core/simple_assert.stderr b/tests/compilation_failures/core/simple_assert.stderr new file mode 100644 index 00000000..75c288f9 --- /dev/null +++ b/tests/compilation_failures/core/simple_assert.stderr @@ -0,0 +1,5 @@ +error: Expected 102 to equal 64 + --> tests/compilation_failures/core/simple_assert.rs:10:25 + | +10 | assert_literals_eq!(102, 64); + | ^^^^^^^ diff --git a/tests/compilation_failures/core/typed_eq_nested_type_mismatch.rs b/tests/compilation_failures/core/typed_eq_nested_type_mismatch.rs new file mode 100644 index 00000000..54756129 --- /dev/null +++ b/tests/compilation_failures/core/typed_eq_nested_type_mismatch.rs @@ -0,0 +1,6 @@ +use preinterpret::*; + +fn main() { + // typed_eq errors on value kind mismatch at any depth + run!(%[_].assert([1, 2].typed_eq([1, "two"]))); +} diff --git a/tests/compilation_failures/core/typed_eq_nested_type_mismatch.stderr b/tests/compilation_failures/core/typed_eq_nested_type_mismatch.stderr new file mode 100644 index 00000000..57b155a5 --- /dev/null +++ b/tests/compilation_failures/core/typed_eq_nested_type_mismatch.stderr @@ -0,0 +1,5 @@ +error: lhs[1] is an untyped integer, but rhs[1] is a string + --> tests/compilation_failures/core/typed_eq_nested_type_mismatch.rs:5:28 + | +5 | run!(%[_].assert([1, 2].typed_eq([1, "two"]))); + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/core/typed_eq_object_value_type_mismatch.rs b/tests/compilation_failures/core/typed_eq_object_value_type_mismatch.rs new file mode 100644 index 00000000..b1f7acec --- /dev/null +++ b/tests/compilation_failures/core/typed_eq_object_value_type_mismatch.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + let result = %{ a: 1, b: 2 }.typed_eq(%{ a: 1, b: "two" }); + ); +} diff --git a/tests/compilation_failures/core/typed_eq_object_value_type_mismatch.stderr b/tests/compilation_failures/core/typed_eq_object_value_type_mismatch.stderr new file mode 100644 index 00000000..acbc1223 --- /dev/null +++ b/tests/compilation_failures/core/typed_eq_object_value_type_mismatch.stderr @@ -0,0 +1,5 @@ +error: lhs.b is an untyped integer, but rhs.b is a string + --> tests/compilation_failures/core/typed_eq_object_value_type_mismatch.rs:5:37 + | +5 | let result = %{ a: 1, b: 2 }.typed_eq(%{ a: 1, b: "two" }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/core/typed_eq_object_value_type_mismatch_2.rs b/tests/compilation_failures/core/typed_eq_object_value_type_mismatch_2.rs new file mode 100644 index 00000000..9274f3bc --- /dev/null +++ b/tests/compilation_failures/core/typed_eq_object_value_type_mismatch_2.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + let result = %{ a: 1, ["multi-word key"]: 2 }.typed_eq(%{ a: 1, ["multi-word key"]: "two" }); + ); +} diff --git a/tests/compilation_failures/core/typed_eq_object_value_type_mismatch_2.stderr b/tests/compilation_failures/core/typed_eq_object_value_type_mismatch_2.stderr new file mode 100644 index 00000000..9a7cfed9 --- /dev/null +++ b/tests/compilation_failures/core/typed_eq_object_value_type_mismatch_2.stderr @@ -0,0 +1,5 @@ +error: lhs["multi-word key"] is an untyped integer, but rhs["multi-word key"] is a string + --> tests/compilation_failures/core/typed_eq_object_value_type_mismatch_2.rs:5:54 + | +5 | let result = %{ a: 1, ["multi-word key"]: 2 }.typed_eq(%{ a: 1, ["multi-word key"]: "two" }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/core/typed_eq_type_mismatch.rs b/tests/compilation_failures/core/typed_eq_type_mismatch.rs new file mode 100644 index 00000000..7be532e7 --- /dev/null +++ b/tests/compilation_failures/core/typed_eq_type_mismatch.rs @@ -0,0 +1,6 @@ +use preinterpret::*; + +fn main() { + // typed_eq errors on value kind mismatch + run!(%[_].assert(1.typed_eq("hello"))); +} diff --git a/tests/compilation_failures/core/typed_eq_type_mismatch.stderr b/tests/compilation_failures/core/typed_eq_type_mismatch.stderr new file mode 100644 index 00000000..535cfbc8 --- /dev/null +++ b/tests/compilation_failures/core/typed_eq_type_mismatch.stderr @@ -0,0 +1,5 @@ +error: lhs is an untyped integer, but rhs is a string + --> tests/compilation_failures/core/typed_eq_type_mismatch.rs:5:23 + | +5 | run!(%[_].assert(1.typed_eq("hello"))); + | ^^^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/core/unrecognized_stream_literal_kind.rs b/tests/compilation_failures/core/unrecognized_stream_literal_kind.rs new file mode 100644 index 00000000..405ddcfb --- /dev/null +++ b/tests/compilation_failures/core/unrecognized_stream_literal_kind.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + stream!(%unknown[_]); +} diff --git a/tests/compilation_failures/core/unrecognized_stream_literal_kind.stderr b/tests/compilation_failures/core/unrecognized_stream_literal_kind.stderr new file mode 100644 index 00000000..eec94793 --- /dev/null +++ b/tests/compilation_failures/core/unrecognized_stream_literal_kind.stderr @@ -0,0 +1,6 @@ +error: A preinterpret stream-based literal is `%[..]` or `%xxx[..]` with xxx = raw, group, string, ident, ident_camel, ident_snake, ident_upper_snake or literal. + If this wasn't intended to be a stream-based literal, replace % with %raw[%]. + --> tests/compilation_failures/core/unrecognized_stream_literal_kind.rs:4:13 + | +4 | stream!(%unknown[_]); + | ^ diff --git a/tests/compilation_failures/core/with_span_example.rs b/tests/compilation_failures/core/with_span_example.rs new file mode 100644 index 00000000..a7390963 --- /dev/null +++ b/tests/compilation_failures/core/with_span_example.rs @@ -0,0 +1,23 @@ +use preinterpret::*; + +// Taken from core.rs +macro_rules! capitalize_variants { + ($enum_name:ident, [$($variants:ident),*]) => {run!{ + let enum_name = %raw[$enum_name]; + let variants = []; + for variant in [$(%raw[$variants]),*] { + let capitalized = variant.to_string().capitalize().to_ident().with_span(variant); + variants.push(capitalized); + } + + %[ + enum #enum_name { + #(variants.intersperse(%[,])) + } + ] + }}; +} + +capitalize_variants!(MyEnum, [helloWorld, Test, HelloWorld]); + +fn main() {} diff --git a/tests/compilation_failures/core/with_span_example.stderr b/tests/compilation_failures/core/with_span_example.stderr new file mode 100644 index 00000000..f77ec7b6 --- /dev/null +++ b/tests/compilation_failures/core/with_span_example.stderr @@ -0,0 +1,9 @@ +error[E0428]: the name `HelloWorld` is defined multiple times + --> tests/compilation_failures/core/with_span_example.rs:21:49 + | +21 | capitalize_variants!(MyEnum, [helloWorld, Test, HelloWorld]); + | ---------- ^^^^^^^^^^ `HelloWorld` redefined here + | | + | previous definition of the type `HelloWorld` here + | + = note: `HelloWorld` must be defined only once in the type namespace of this enum diff --git a/tests/compilation_failures/expressions/add_float_and_int.rs b/tests/compilation_failures/expressions/add_float_and_int.rs new file mode 100644 index 00000000..db68dd87 --- /dev/null +++ b/tests/compilation_failures/expressions/add_float_and_int.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #(1.2 + 1) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/add_float_and_int.stderr b/tests/compilation_failures/expressions/add_float_and_int.stderr new file mode 100644 index 00000000..96fa9320 --- /dev/null +++ b/tests/compilation_failures/expressions/add_float_and_int.stderr @@ -0,0 +1,5 @@ +error: This argument is expected to be a float, but it is an untyped integer + --> tests/compilation_failures/expressions/add_float_and_int.rs:5:17 + | +5 | #(1.2 + 1) + | ^ diff --git a/tests/compilation_failures/expressions/add_ints_of_different_types.rs b/tests/compilation_failures/expressions/add_ints_of_different_types.rs new file mode 100644 index 00000000..0ac730cb --- /dev/null +++ b/tests/compilation_failures/expressions/add_ints_of_different_types.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #(1u32 + 2u64) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/add_ints_of_different_types.stderr b/tests/compilation_failures/expressions/add_ints_of_different_types.stderr new file mode 100644 index 00000000..dc5c6424 --- /dev/null +++ b/tests/compilation_failures/expressions/add_ints_of_different_types.stderr @@ -0,0 +1,5 @@ +error: This operand is expected to be a u32, but it is a u64 + --> tests/compilation_failures/expressions/add_ints_of_different_types.rs:5:18 + | +5 | #(1u32 + 2u64) + | ^^^^ diff --git a/tests/compilation_failures/expressions/array_missing_comma.rs b/tests/compilation_failures/expressions/array_missing_comma.rs new file mode 100644 index 00000000..0338bdab --- /dev/null +++ b/tests/compilation_failures/expressions/array_missing_comma.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #([1 2]) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_missing_comma.stderr b/tests/compilation_failures/expressions/array_missing_comma.stderr new file mode 100644 index 00000000..245725c5 --- /dev/null +++ b/tests/compilation_failures/expressions/array_missing_comma.stderr @@ -0,0 +1,5 @@ +error: Expected comma, ], or operator + --> tests/compilation_failures/expressions/array_missing_comma.rs:5:14 + | +5 | #([1 2]) + | ^ diff --git a/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_1.rs b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_1.rs new file mode 100644 index 00000000..5ec34ae5 --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_1.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let [_, _, _] = [1, 2]; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_1.stderr b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_1.stderr new file mode 100644 index 00000000..74bc0e70 --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_1.stderr @@ -0,0 +1,5 @@ +error: The array has 2 items, but the pattern expected 3 + --> tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_1.rs:5:13 + | +5 | let [_, _, _] = [1, 2]; + | ^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_2.rs b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_2.rs new file mode 100644 index 00000000..b0e3ca72 --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_2.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let [_] = [1, 2]; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_2.stderr b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_2.stderr new file mode 100644 index 00000000..6e1b9cc1 --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_2.stderr @@ -0,0 +1,5 @@ +error: The array has 2 items, but the pattern expected 1 + --> tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_2.rs:5:13 + | +5 | let [_] = [1, 2]; + | ^^^ diff --git a/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_3.rs b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_3.rs new file mode 100644 index 00000000..e96ba73d --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_3.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let [_, _, .., _] = [1, 2]; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_3.stderr b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_3.stderr new file mode 100644 index 00000000..5f57ab99 --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_3.stderr @@ -0,0 +1,5 @@ +error: The array has 2 items, but the pattern expected at least 3 + --> tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_3.rs:5:13 + | +5 | let [_, _, .., _] = [1, 2]; + | ^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/array_pattern_destructure_multiple_dot_dots.rs b/tests/compilation_failures/expressions/array_pattern_destructure_multiple_dot_dots.rs new file mode 100644 index 00000000..11881298 --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_multiple_dot_dots.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let [_, .., _, .., _] = [1, 2, 3, 4]; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_pattern_destructure_multiple_dot_dots.stderr b/tests/compilation_failures/expressions/array_pattern_destructure_multiple_dot_dots.stderr new file mode 100644 index 00000000..3a7d15ca --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_multiple_dot_dots.stderr @@ -0,0 +1,5 @@ +error: Only one .. is allowed in an array pattern + --> tests/compilation_failures/expressions/array_pattern_destructure_multiple_dot_dots.rs:5:24 + | +5 | let [_, .., _, .., _] = [1, 2, 3, 4]; + | ^^ diff --git a/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_1.rs b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_1.rs new file mode 100644 index 00000000..8e30312c --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_1.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #([_, _, _] = [1, 2]) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_1.stderr b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_1.stderr new file mode 100644 index 00000000..788fe926 --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_1.stderr @@ -0,0 +1,5 @@ +error: The array has 2 items, but the assignee expected 3 + --> tests/compilation_failures/expressions/array_place_destructure_element_mismatch_1.rs:5:11 + | +5 | #([_, _, _] = [1, 2]) + | ^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_2.rs b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_2.rs new file mode 100644 index 00000000..d313762f --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_2.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #([_] = [1, 2]) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_2.stderr b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_2.stderr new file mode 100644 index 00000000..e472871e --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_2.stderr @@ -0,0 +1,5 @@ +error: The array has 2 items, but the assignee expected 1 + --> tests/compilation_failures/expressions/array_place_destructure_element_mismatch_2.rs:5:11 + | +5 | #([_] = [1, 2]) + | ^^^ diff --git a/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_3.rs b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_3.rs new file mode 100644 index 00000000..18443ea5 --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_3.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #([_, _, .., _] = [1, 2]) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_3.stderr b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_3.stderr new file mode 100644 index 00000000..5e279625 --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_3.stderr @@ -0,0 +1,5 @@ +error: The array has 2 items, but the assignee expected at least 3 + --> tests/compilation_failures/expressions/array_place_destructure_element_mismatch_3.rs:5:11 + | +5 | #([_, _, .., _] = [1, 2]) + | ^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/array_place_destructure_multiple_dot_dots.rs b/tests/compilation_failures/expressions/array_place_destructure_multiple_dot_dots.rs new file mode 100644 index 00000000..cc0ab745 --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_multiple_dot_dots.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #([_, .., _, .., _] = [1, 2, 3, 4]) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_place_destructure_multiple_dot_dots.stderr b/tests/compilation_failures/expressions/array_place_destructure_multiple_dot_dots.stderr new file mode 100644 index 00000000..6d6761be --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_multiple_dot_dots.stderr @@ -0,0 +1,5 @@ +error: Only one .. is allowed in an array assignee + --> tests/compilation_failures/expressions/array_place_destructure_multiple_dot_dots.rs:5:11 + | +5 | #([_, .., _, .., _] = [1, 2, 3, 4]) + | ^^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/array_place_destructure_multiple_muts.rs b/tests/compilation_failures/expressions/array_place_destructure_multiple_muts.rs new file mode 100644 index 00000000..27ad493d --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_multiple_muts.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let arr = [0, 1]; + arr[arr[1]] = 3; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/array_place_destructure_multiple_muts.stderr b/tests/compilation_failures/expressions/array_place_destructure_multiple_muts.stderr new file mode 100644 index 00000000..58d88c8c --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_multiple_muts.stderr @@ -0,0 +1,5 @@ +error: The variable cannot be read as it is already being modified + --> tests/compilation_failures/expressions/array_place_destructure_multiple_muts.rs:6:13 + | +6 | arr[arr[1]] = 3; + | ^^^ diff --git a/tests/compilation_failures/expressions/assign_to_last_use_of_variable.rs b/tests/compilation_failures/expressions/assign_to_last_use_of_variable.rs new file mode 100644 index 00000000..8ef9641a --- /dev/null +++ b/tests/compilation_failures/expressions/assign_to_last_use_of_variable.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + run!{ + let a = 3; + a = 4; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/assign_to_last_use_of_variable.stderr b/tests/compilation_failures/expressions/assign_to_last_use_of_variable.stderr new file mode 100644 index 00000000..e9bb0c7f --- /dev/null +++ b/tests/compilation_failures/expressions/assign_to_last_use_of_variable.stderr @@ -0,0 +1,5 @@ +error: The final usage of a variable cannot be assigned to. You can use `let _ = ..` to discard a value. + --> tests/compilation_failures/expressions/assign_to_last_use_of_variable.rs:6:9 + | +6 | a = 4; + | ^ diff --git a/tests/compilation_failures/expressions/assign_to_owned_value.rs b/tests/compilation_failures/expressions/assign_to_owned_value.rs new file mode 100644 index 00000000..306a30f9 --- /dev/null +++ b/tests/compilation_failures/expressions/assign_to_owned_value.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + (1 == 2) = 3; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/assign_to_owned_value.stderr b/tests/compilation_failures/expressions/assign_to_owned_value.stderr new file mode 100644 index 00000000..c4bcd5e1 --- /dev/null +++ b/tests/compilation_failures/expressions/assign_to_owned_value.stderr @@ -0,0 +1,5 @@ +error: An owned value cannot be assigned to. + --> tests/compilation_failures/expressions/assign_to_owned_value.rs:5:10 + | +5 | (1 == 2) = 3; + | ^^^^^^ diff --git a/tests/compilation_failures/expressions/cannot_output_object_to_stream.stderr b/tests/compilation_failures/expressions/cannot_output_object_to_stream.stderr new file mode 100644 index 00000000..6b4de618 --- /dev/null +++ b/tests/compilation_failures/expressions/cannot_output_object_to_stream.stderr @@ -0,0 +1,5 @@ +error: Objects cannot be output to a stream + --> tests/compilation_failures/expressions/cannot_output_object_to_stream.rs:5:12 + | +5 | #(%{ hello: "world" }) + | ^^^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/cast_int_to_bool.rs b/tests/compilation_failures/expressions/cast_int_to_bool.rs new file mode 100644 index 00000000..7fe2d87e --- /dev/null +++ b/tests/compilation_failures/expressions/cast_int_to_bool.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #(1 as bool) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/cast_int_to_bool.stderr b/tests/compilation_failures/expressions/cast_int_to_bool.stderr new file mode 100644 index 00000000..b8cfc15b --- /dev/null +++ b/tests/compilation_failures/expressions/cast_int_to_bool.stderr @@ -0,0 +1,5 @@ +error: The as bool operator is not supported for an untyped integer + --> tests/compilation_failures/expressions/cast_int_to_bool.rs:5:13 + | +5 | #(1 as bool) + | ^^ diff --git a/tests/compilation_failures/expressions/cast_to_float.rs b/tests/compilation_failures/expressions/cast_to_float.rs new file mode 100644 index 00000000..3342659c --- /dev/null +++ b/tests/compilation_failures/expressions/cast_to_float.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + 0u8 as float + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/cast_to_float.stderr b/tests/compilation_failures/expressions/cast_to_float.stderr new file mode 100644 index 00000000..7503700e --- /dev/null +++ b/tests/compilation_failures/expressions/cast_to_float.stderr @@ -0,0 +1,5 @@ +error: This type is not supported in cast expressions. Perhaps you want 'as untyped_float'? + --> tests/compilation_failures/expressions/cast_to_float.rs:5:16 + | +5 | 0u8 as float + | ^^^^^ diff --git a/tests/compilation_failures/expressions/cast_to_int.rs b/tests/compilation_failures/expressions/cast_to_int.rs new file mode 100644 index 00000000..c5f6ad90 --- /dev/null +++ b/tests/compilation_failures/expressions/cast_to_int.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + 0u8 as int + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/cast_to_int.stderr b/tests/compilation_failures/expressions/cast_to_int.stderr new file mode 100644 index 00000000..55810f00 --- /dev/null +++ b/tests/compilation_failures/expressions/cast_to_int.stderr @@ -0,0 +1,5 @@ +error: This type is not supported in cast expressions. Perhaps you want 'as untyped_int'? + --> tests/compilation_failures/expressions/cast_to_int.rs:5:16 + | +5 | 0u8 as int + | ^^^ diff --git a/tests/compilation_failures/expressions/cast_to_unknown_type.rs b/tests/compilation_failures/expressions/cast_to_unknown_type.rs new file mode 100644 index 00000000..523675cb --- /dev/null +++ b/tests/compilation_failures/expressions/cast_to_unknown_type.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + 0u8 as nonexistent_type + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/cast_to_unknown_type.stderr b/tests/compilation_failures/expressions/cast_to_unknown_type.stderr new file mode 100644 index 00000000..ca49fb70 --- /dev/null +++ b/tests/compilation_failures/expressions/cast_to_unknown_type.stderr @@ -0,0 +1,5 @@ +error: Unknown type 'nonexistent_type' + --> tests/compilation_failures/expressions/cast_to_unknown_type.rs:5:16 + | +5 | 0u8 as nonexistent_type + | ^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/compare_int_and_float.rs b/tests/compilation_failures/expressions/compare_int_and_float.rs new file mode 100644 index 00000000..12f3e3d4 --- /dev/null +++ b/tests/compilation_failures/expressions/compare_int_and_float.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #(5 < 6.4) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/compare_int_and_float.stderr b/tests/compilation_failures/expressions/compare_int_and_float.stderr new file mode 100644 index 00000000..1c6e4f2a --- /dev/null +++ b/tests/compilation_failures/expressions/compare_int_and_float.stderr @@ -0,0 +1,5 @@ +error: This argument is expected to be an integer, but it is an untyped float + --> tests/compilation_failures/expressions/compare_int_and_float.rs:5:15 + | +5 | #(5 < 6.4) + | ^^^ diff --git a/tests/compilation_failures/expressions/debug_method.rs b/tests/compilation_failures/expressions/debug_method.rs new file mode 100644 index 00000000..3cfe3d9f --- /dev/null +++ b/tests/compilation_failures/expressions/debug_method.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let x = [1, 2]; + x.debug() + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/debug_method.stderr b/tests/compilation_failures/expressions/debug_method.stderr new file mode 100644 index 00000000..d19b8a27 --- /dev/null +++ b/tests/compilation_failures/expressions/debug_method.stderr @@ -0,0 +1,5 @@ +error: [1, 2] + --> tests/compilation_failures/expressions/debug_method.rs:6:9 + | +6 | x.debug() + | ^ diff --git a/tests/compilation_failures/expressions/discard_in_value_position.rs b/tests/compilation_failures/expressions/discard_in_value_position.rs new file mode 100644 index 00000000..82a5b537 --- /dev/null +++ b/tests/compilation_failures/expressions/discard_in_value_position.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #(_) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/discard_in_value_position.stderr b/tests/compilation_failures/expressions/discard_in_value_position.stderr new file mode 100644 index 00000000..0a87912c --- /dev/null +++ b/tests/compilation_failures/expressions/discard_in_value_position.stderr @@ -0,0 +1,5 @@ +error: This cannot be used in a value expression. + --> tests/compilation_failures/expressions/discard_in_value_position.rs:5:11 + | +5 | #(_) + | ^ diff --git a/tests/compilation_failures/expressions/embedded_variable_in_expression.rs b/tests/compilation_failures/expressions/embedded_variable_in_expression.rs new file mode 100644 index 00000000..799ac714 --- /dev/null +++ b/tests/compilation_failures/expressions/embedded_variable_in_expression.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + let _ = run! { + let partial_sum = %[+ 2]; + 5 #partial_sum + }; +} diff --git a/tests/compilation_failures/expressions/embedded_variable_in_expression.stderr b/tests/compilation_failures/expressions/embedded_variable_in_expression.stderr new file mode 100644 index 00000000..e71be1e0 --- /dev/null +++ b/tests/compilation_failures/expressions/embedded_variable_in_expression.stderr @@ -0,0 +1,5 @@ +error: Invalid statement continuation. Possibly the previous statement is missing a semicolon? + --> tests/compilation_failures/expressions/embedded_variable_in_expression.rs:6:11 + | +6 | 5 #partial_sum + | ^ diff --git a/tests/compilation_failures/expressions/empty_object_literal_without_prefix.rs b/tests/compilation_failures/expressions/empty_object_literal_without_prefix.rs new file mode 100644 index 00000000..a0e9ae3b --- /dev/null +++ b/tests/compilation_failures/expressions/empty_object_literal_without_prefix.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + let x = {}; + } +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/empty_object_literal_without_prefix.stderr b/tests/compilation_failures/expressions/empty_object_literal_without_prefix.stderr new file mode 100644 index 00000000..61bbf734 --- /dev/null +++ b/tests/compilation_failures/expressions/empty_object_literal_without_prefix.stderr @@ -0,0 +1,5 @@ +error: An empty object literal is written `%{}` with a `%` prefix. If you intend to use an empty block here, instead use `{ None }`. + --> tests/compilation_failures/expressions/empty_object_literal_without_prefix.rs:5:17 + | +5 | let x = {}; + | ^ diff --git a/tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.rs b/tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.rs new file mode 100644 index 00000000..45c87a6b --- /dev/null +++ b/tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + 1 + 2 + 3 + 4; + "This gets returned" + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.stderr b/tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.stderr new file mode 100644 index 00000000..2c6e47b8 --- /dev/null +++ b/tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.stderr @@ -0,0 +1,5 @@ +error: A non-returning statement must not return a value. If you wish to explicitly discard the expression's result, use `let _ = ...;`. Alternatively, If you wish to output the value into the parent token stream, use `emit ...;` + --> tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.rs:5:9 + | +5 | 1 + 2 + 3 + 4; + | ^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/fix_me_negative_max_int_fails.rs b/tests/compilation_failures/expressions/fix_me_negative_max_int_fails.rs new file mode 100644 index 00000000..579201ad --- /dev/null +++ b/tests/compilation_failures/expressions/fix_me_negative_max_int_fails.rs @@ -0,0 +1,12 @@ +use preinterpret::*; + +fn main() { + let _ = stream! { + // This should not fail, and should be fixed. + // This test just records the fact it doesn't work as a known issue. + // A fix of this should remove this test and move it to a working test. + #(-128i8) + // This should also not fail according to the rules of rustc. + #(-(--128i8)) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/fix_me_negative_max_int_fails.stderr b/tests/compilation_failures/expressions/fix_me_negative_max_int_fails.stderr new file mode 100644 index 00000000..71c954bf --- /dev/null +++ b/tests/compilation_failures/expressions/fix_me_negative_max_int_fails.stderr @@ -0,0 +1,5 @@ +error: The - operator is not supported for an unsupported literal + --> tests/compilation_failures/expressions/fix_me_negative_max_int_fails.rs:8:11 + | +8 | #(-128i8) + | ^ diff --git a/tests/compilation_failures/expressions/for_can_not_return_value_in_statement.rs b/tests/compilation_failures/expressions/for_can_not_return_value_in_statement.rs new file mode 100644 index 00000000..d18749f6 --- /dev/null +++ b/tests/compilation_failures/expressions/for_can_not_return_value_in_statement.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + for i in 1..=5 { + i + }; // Semi-colon makes it a statement + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/for_can_not_return_value_in_statement.stderr b/tests/compilation_failures/expressions/for_can_not_return_value_in_statement.stderr new file mode 100644 index 00000000..0df7da70 --- /dev/null +++ b/tests/compilation_failures/expressions/for_can_not_return_value_in_statement.stderr @@ -0,0 +1,8 @@ +error: A non-returning statement must not return a value. If you wish to explicitly discard the expression's result, use `let _ = ...;`. Alternatively, If you wish to output the value into the parent token stream, use `emit ...;` + --> tests/compilation_failures/expressions/for_can_not_return_value_in_statement.rs:5:24 + | +5 | for i in 1..=5 { + | ________________________^ +6 | | i +7 | | }; // Semi-colon makes it a statement + | |_________^ diff --git a/tests/compilation_failures/expressions/for_can_not_return_value_in_statement_2.rs b/tests/compilation_failures/expressions/for_can_not_return_value_in_statement_2.rs new file mode 100644 index 00000000..543a6ebe --- /dev/null +++ b/tests/compilation_failures/expressions/for_can_not_return_value_in_statement_2.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + for i in 1..=5 { + for j in i..=5 { + j // Effectively this criteria elevates to the inner loop + } + }; // Semi-colon makes it a statement + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/for_can_not_return_value_in_statement_2.stderr b/tests/compilation_failures/expressions/for_can_not_return_value_in_statement_2.stderr new file mode 100644 index 00000000..38c278a8 --- /dev/null +++ b/tests/compilation_failures/expressions/for_can_not_return_value_in_statement_2.stderr @@ -0,0 +1,8 @@ +error: A non-returning statement must not return a value. If you wish to explicitly discard the expression's result, use `let _ = ...;`. Alternatively, If you wish to output the value into the parent token stream, use `emit ...;` + --> tests/compilation_failures/expressions/for_can_not_return_value_in_statement_2.rs:6:28 + | +6 | for j in i..=5 { + | ____________________________^ +7 | | j // Effectively this criteria elevates to the inner loop +8 | | } + | |_____________^ diff --git a/tests/compilation_failures/expressions/index_into_discarded_place.rs b/tests/compilation_failures/expressions/index_into_discarded_place.rs new file mode 100644 index 00000000..b6ea3a32 --- /dev/null +++ b/tests/compilation_failures/expressions/index_into_discarded_place.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #(_[0] = 10) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/index_into_discarded_place.stderr b/tests/compilation_failures/expressions/index_into_discarded_place.stderr new file mode 100644 index 00000000..00dfa436 --- /dev/null +++ b/tests/compilation_failures/expressions/index_into_discarded_place.stderr @@ -0,0 +1,5 @@ +error: This cannot be used in a value expression. + --> tests/compilation_failures/expressions/index_into_discarded_place.rs:5:11 + | +5 | #(_[0] = 10) + | ^ diff --git a/tests/compilation_failures/expressions/invalid_binary_operator.rs b/tests/compilation_failures/expressions/invalid_binary_operator.rs new file mode 100644 index 00000000..0c77f6ae --- /dev/null +++ b/tests/compilation_failures/expressions/invalid_binary_operator.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run! { + 10 _ 10 + }; +} diff --git a/tests/compilation_failures/expressions/invalid_binary_operator.stderr b/tests/compilation_failures/expressions/invalid_binary_operator.stderr new file mode 100644 index 00000000..87396117 --- /dev/null +++ b/tests/compilation_failures/expressions/invalid_binary_operator.stderr @@ -0,0 +1,5 @@ +error: Invalid statement continuation. Possibly the previous statement is missing a semicolon? + --> tests/compilation_failures/expressions/invalid_binary_operator.rs:5:12 + | +5 | 10 _ 10 + | ^ diff --git a/tests/compilation_failures/expressions/invalid_unary_operator.rs b/tests/compilation_failures/expressions/invalid_unary_operator.rs new file mode 100644 index 00000000..f117d8c9 --- /dev/null +++ b/tests/compilation_failures/expressions/invalid_unary_operator.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream! { + #(^10) + }; +} diff --git a/tests/compilation_failures/expressions/invalid_unary_operator.stderr b/tests/compilation_failures/expressions/invalid_unary_operator.stderr new file mode 100644 index 00000000..b72dcef7 --- /dev/null +++ b/tests/compilation_failures/expressions/invalid_unary_operator.stderr @@ -0,0 +1,5 @@ +error: Expected an expression + --> tests/compilation_failures/expressions/invalid_unary_operator.rs:5:11 + | +5 | #(^10) + | ^ diff --git a/tests/compilation_failures/expressions/large_range_to_string.rs b/tests/compilation_failures/expressions/large_range_to_string.rs new file mode 100644 index 00000000..37e36f5c --- /dev/null +++ b/tests/compilation_failures/expressions/large_range_to_string.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #((0..10000) as iterator as string) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/large_range_to_string.stderr b/tests/compilation_failures/expressions/large_range_to_string.stderr new file mode 100644 index 00000000..1e971e34 --- /dev/null +++ b/tests/compilation_failures/expressions/large_range_to_string.stderr @@ -0,0 +1,5 @@ +error: To protect against infinite loops, only a maximum of 1000 items can be output to a string from an iterator. You can use .to_vec() to avoid this limit. This can't currently be reconfigured with the iteration limit. + --> tests/compilation_failures/expressions/large_range_to_string.rs:5:11 + | +5 | #((0..10000) as iterator as string) + | ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.rs b/tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.rs new file mode 100644 index 00000000..18c6a1cc --- /dev/null +++ b/tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let a = "a"; + "b".swap(a); + a + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.stderr b/tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.stderr new file mode 100644 index 00000000..9f5a5bfa --- /dev/null +++ b/tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.stderr @@ -0,0 +1,5 @@ +error: An owned value cannot be assigned to. + --> tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.rs:6:9 + | +6 | "b".swap(a); + | ^^^ diff --git a/tests/compilation_failures/expressions/negate_min_int.rs b/tests/compilation_failures/expressions/negate_min_int.rs new file mode 100644 index 00000000..9e21e375 --- /dev/null +++ b/tests/compilation_failures/expressions/negate_min_int.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #(-(-127i8 - 1)) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/negate_min_int.stderr b/tests/compilation_failures/expressions/negate_min_int.stderr new file mode 100644 index 00000000..f60d9f61 --- /dev/null +++ b/tests/compilation_failures/expressions/negate_min_int.stderr @@ -0,0 +1,5 @@ +error: Negating this value would overflow + --> tests/compilation_failures/expressions/negate_min_int.rs:5:12 + | +5 | #(-(-127i8 - 1)) + | ^^^^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/object_block_confusion.rs b/tests/compilation_failures/expressions/object_block_confusion.rs new file mode 100644 index 00000000..08ad19fa --- /dev/null +++ b/tests/compilation_failures/expressions/object_block_confusion.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let x = %{ 1 + 1 + 1 }; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/object_block_confusion.stderr b/tests/compilation_failures/expressions/object_block_confusion.stderr new file mode 100644 index 00000000..58438c64 --- /dev/null +++ b/tests/compilation_failures/expressions/object_block_confusion.stderr @@ -0,0 +1,5 @@ +error: Expected an object entry (`field,` `field: ..,` or `["field"]: ..,`). If you meant to start a new block, use #{ ... } instead. + --> tests/compilation_failures/expressions/object_block_confusion.rs:5:20 + | +5 | let x = %{ 1 + 1 + 1 }; + | ^ diff --git a/tests/compilation_failures/expressions/object_field_duplication.rs b/tests/compilation_failures/expressions/object_field_duplication.rs new file mode 100644 index 00000000..d516abd3 --- /dev/null +++ b/tests/compilation_failures/expressions/object_field_duplication.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + #(%{ hello: "world", ["hello"]: "world_2" }) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/object_field_duplication.stderr b/tests/compilation_failures/expressions/object_field_duplication.stderr new file mode 100644 index 00000000..da51c974 --- /dev/null +++ b/tests/compilation_failures/expressions/object_field_duplication.stderr @@ -0,0 +1,5 @@ +error: The key hello has already been set + --> tests/compilation_failures/expressions/object_field_duplication.rs:5:30 + | +5 | #(%{ hello: "world", ["hello"]: "world_2" }) + | ^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/object_incorrect_comma.rs b/tests/compilation_failures/expressions/object_incorrect_comma.rs new file mode 100644 index 00000000..b8e0a7c5 --- /dev/null +++ b/tests/compilation_failures/expressions/object_incorrect_comma.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + let _ = stream!{ + let a = 0; + #(%{ a; b: 1 }) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/object_incorrect_comma.stderr b/tests/compilation_failures/expressions/object_incorrect_comma.stderr new file mode 100644 index 00000000..6ce649a9 --- /dev/null +++ b/tests/compilation_failures/expressions/object_incorrect_comma.stderr @@ -0,0 +1,5 @@ +error: Expected an object entry (`field,` `field: ..,` or `["field"]: ..,`). If you meant to start a new block, use #{ ... } instead. + --> tests/compilation_failures/expressions/object_incorrect_comma.rs:6:15 + | +6 | #(%{ a; b: 1 }) + | ^ diff --git a/tests/compilation_failures/expressions/object_literal_without_prefix.rs b/tests/compilation_failures/expressions/object_literal_without_prefix.rs new file mode 100644 index 00000000..54565a19 --- /dev/null +++ b/tests/compilation_failures/expressions/object_literal_without_prefix.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + [1, 2].intersperse("", { final_separator: "" }) + } +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/object_literal_without_prefix.stderr b/tests/compilation_failures/expressions/object_literal_without_prefix.stderr new file mode 100644 index 00000000..f9a33b79 --- /dev/null +++ b/tests/compilation_failures/expressions/object_literal_without_prefix.stderr @@ -0,0 +1,5 @@ +error: An object literal must be prefixed with %, e.g. `%{ field: 1 }`. Without such a prefix, { .. } defines a block. + --> tests/compilation_failures/expressions/object_literal_without_prefix.rs:5:32 + | +5 | [1, 2].intersperse("", { final_separator: "" }) + | ^ diff --git a/tests/compilation_failures/expressions/object_pattern_destructuring_repeated_field.rs b/tests/compilation_failures/expressions/object_pattern_destructuring_repeated_field.rs new file mode 100644 index 00000000..5740de99 --- /dev/null +++ b/tests/compilation_failures/expressions/object_pattern_destructuring_repeated_field.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let %{ x, x } = %{ x: 1 }; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/object_pattern_destructuring_repeated_field.stderr b/tests/compilation_failures/expressions/object_pattern_destructuring_repeated_field.stderr new file mode 100644 index 00000000..9d087022 --- /dev/null +++ b/tests/compilation_failures/expressions/object_pattern_destructuring_repeated_field.stderr @@ -0,0 +1,5 @@ +error: The key `x` has already used + --> tests/compilation_failures/expressions/object_pattern_destructuring_repeated_field.rs:5:19 + | +5 | let %{ x, x } = %{ x: 1 }; + | ^ diff --git a/tests/compilation_failures/expressions/object_pattern_destructuring_wrong_type.rs b/tests/compilation_failures/expressions/object_pattern_destructuring_wrong_type.rs new file mode 100644 index 00000000..59bf7a5e --- /dev/null +++ b/tests/compilation_failures/expressions/object_pattern_destructuring_wrong_type.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let %{ x } = 0; + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/object_pattern_destructuring_wrong_type.stderr b/tests/compilation_failures/expressions/object_pattern_destructuring_wrong_type.stderr new file mode 100644 index 00000000..42a85458 --- /dev/null +++ b/tests/compilation_failures/expressions/object_pattern_destructuring_wrong_type.stderr @@ -0,0 +1,5 @@ +error: The value destructured with an object pattern is expected to be an object, but it is an untyped integer + --> tests/compilation_failures/expressions/object_pattern_destructuring_wrong_type.rs:5:14 + | +5 | let %{ x } = 0; + | ^^^^^ diff --git a/tests/compilation_failures/expressions/object_place_destructuring_repeated_field.rs b/tests/compilation_failures/expressions/object_place_destructuring_repeated_field.rs new file mode 100644 index 00000000..9939b929 --- /dev/null +++ b/tests/compilation_failures/expressions/object_place_destructuring_repeated_field.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let x = 0; + %{ x, x } = %{ x: 1 }; + x + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/object_place_destructuring_repeated_field.stderr b/tests/compilation_failures/expressions/object_place_destructuring_repeated_field.stderr new file mode 100644 index 00000000..739a77bd --- /dev/null +++ b/tests/compilation_failures/expressions/object_place_destructuring_repeated_field.stderr @@ -0,0 +1,5 @@ +error: The key `x` has already used + --> tests/compilation_failures/expressions/object_place_destructuring_repeated_field.rs:6:15 + | +6 | %{ x, x } = %{ x: 1 }; + | ^ diff --git a/tests/compilation_failures/expressions/object_place_destructuring_wrong_type.rs b/tests/compilation_failures/expressions/object_place_destructuring_wrong_type.rs new file mode 100644 index 00000000..8b442483 --- /dev/null +++ b/tests/compilation_failures/expressions/object_place_destructuring_wrong_type.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let x = 0; + %{ x } = [x]; + x + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/object_place_destructuring_wrong_type.stderr b/tests/compilation_failures/expressions/object_place_destructuring_wrong_type.stderr new file mode 100644 index 00000000..6f3e89a4 --- /dev/null +++ b/tests/compilation_failures/expressions/object_place_destructuring_wrong_type.stderr @@ -0,0 +1,5 @@ +error: The value destructured as an object is expected to be an object, but it is an array + --> tests/compilation_failures/expressions/object_place_destructuring_wrong_type.rs:6:10 + | +6 | %{ x } = [x]; + | ^^^^^ diff --git a/tests/compilation_failures/expressions/owned_to_mutable.rs b/tests/compilation_failures/expressions/owned_to_mutable.rs new file mode 100644 index 00000000..f3a49587 --- /dev/null +++ b/tests/compilation_failures/expressions/owned_to_mutable.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let a = "a"; + a.swap("b"); + a + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/owned_to_mutable.stderr b/tests/compilation_failures/expressions/owned_to_mutable.stderr new file mode 100644 index 00000000..3c54d7a3 --- /dev/null +++ b/tests/compilation_failures/expressions/owned_to_mutable.stderr @@ -0,0 +1,5 @@ +error: An owned value cannot be assigned to. + --> tests/compilation_failures/expressions/owned_to_mutable.rs:6:16 + | +6 | a.swap("b"); + | ^^^ diff --git a/tests/compilation_failures/expressions/swap_itself.rs b/tests/compilation_failures/expressions/swap_itself.rs new file mode 100644 index 00000000..ff9fbb8e --- /dev/null +++ b/tests/compilation_failures/expressions/swap_itself.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let a = "a"; + a.swap(a); + a + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/swap_itself.stderr b/tests/compilation_failures/expressions/swap_itself.stderr new file mode 100644 index 00000000..29c94dab --- /dev/null +++ b/tests/compilation_failures/expressions/swap_itself.stderr @@ -0,0 +1,5 @@ +error: The variable cannot be modified as it is already being modified + --> tests/compilation_failures/expressions/swap_itself.rs:6:16 + | +6 | a.swap(a); + | ^ diff --git a/tests/compilation_failures/expressions/tuple_syntax_helpful_error.rs b/tests/compilation_failures/expressions/tuple_syntax_helpful_error.rs new file mode 100644 index 00000000..3d6b2837 --- /dev/null +++ b/tests/compilation_failures/expressions/tuple_syntax_helpful_error.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let x = (1, 2); + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/tuple_syntax_helpful_error.stderr b/tests/compilation_failures/expressions/tuple_syntax_helpful_error.stderr new file mode 100644 index 00000000..159b7f97 --- /dev/null +++ b/tests/compilation_failures/expressions/tuple_syntax_helpful_error.stderr @@ -0,0 +1,5 @@ +error: Commas are only permitted inside preinterpret arrays []. Preinterpret arrays [a, b] can be used as a drop-in replacement for rust tuples (a, b). + --> tests/compilation_failures/expressions/tuple_syntax_helpful_error.rs:5:19 + | +5 | let x = (1, 2); + | ^ diff --git a/tests/compilation_failures/expressions/untyped_integer_overflow.rs b/tests/compilation_failures/expressions/untyped_integer_overflow.rs new file mode 100644 index 00000000..48ceb487 --- /dev/null +++ b/tests/compilation_failures/expressions/untyped_integer_overflow.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(i128::MAX as untyped_int + 1); +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/untyped_integer_overflow.stderr b/tests/compilation_failures/expressions/untyped_integer_overflow.stderr new file mode 100644 index 00000000..2a1e29b4 --- /dev/null +++ b/tests/compilation_failures/expressions/untyped_integer_overflow.stderr @@ -0,0 +1,5 @@ +error: The untyped integer operation 170141183460469231731687303715884105727 + 1 overflowed in i128 space + --> tests/compilation_failures/expressions/untyped_integer_overflow.rs:4:43 + | +4 | let _ = run!(i128::MAX as untyped_int + 1); + | ^ diff --git a/tests/compilation_failures/expressions/untyped_integer_too_large_arithmetic_fails.rs b/tests/compilation_failures/expressions/untyped_integer_too_large_arithmetic_fails.rs new file mode 100644 index 00000000..23fb7364 --- /dev/null +++ b/tests/compilation_failures/expressions/untyped_integer_too_large_arithmetic_fails.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + let _ = run!( + // (i128::MAX + 1) so we're an unsupported literal + let x = 170_141_183_460_469_231_731_687_303_715_884_105_728; + // This should error because an unsupported literal doesn't support operations + x - 1 + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/untyped_integer_too_large_arithmetic_fails.stderr b/tests/compilation_failures/expressions/untyped_integer_too_large_arithmetic_fails.stderr new file mode 100644 index 00000000..a936b0ef --- /dev/null +++ b/tests/compilation_failures/expressions/untyped_integer_too_large_arithmetic_fails.stderr @@ -0,0 +1,5 @@ +error: The - operator is not supported for an unsupported literal operand + --> tests/compilation_failures/expressions/untyped_integer_too_large_arithmetic_fails.rs:8:11 + | +8 | x - 1 + | ^ diff --git a/tests/compilation_failures/expressions/variable_with_incomplete_expression.rs b/tests/compilation_failures/expressions/variable_with_incomplete_expression.rs new file mode 100644 index 00000000..1a02153c --- /dev/null +++ b/tests/compilation_failures/expressions/variable_with_incomplete_expression.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + let _ = run!{ + let x = %[+ 1]; + 1 x + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/variable_with_incomplete_expression.stderr b/tests/compilation_failures/expressions/variable_with_incomplete_expression.stderr new file mode 100644 index 00000000..24d3e28c --- /dev/null +++ b/tests/compilation_failures/expressions/variable_with_incomplete_expression.stderr @@ -0,0 +1,5 @@ +error: Invalid statement continuation. Possibly the previous statement is missing a semicolon? + --> tests/compilation_failures/expressions/variable_with_incomplete_expression.rs:6:11 + | +6 | 1 x + | ^ diff --git a/tests/compilation_failures/iteration/infinite_range_len.rs b/tests/compilation_failures/iteration/infinite_range_len.rs new file mode 100644 index 00000000..0fd7e1f0 --- /dev/null +++ b/tests/compilation_failures/iteration/infinite_range_len.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!((0..).len()); +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/infinite_range_len.stderr b/tests/compilation_failures/iteration/infinite_range_len.stderr new file mode 100644 index 00000000..7888017f --- /dev/null +++ b/tests/compilation_failures/iteration/infinite_range_len.stderr @@ -0,0 +1,5 @@ +error: Iterator has an inexact length + --> tests/compilation_failures/iteration/infinite_range_len.rs:4:18 + | +4 | let _ = run!((0..).len()); + | ^^^^^ diff --git a/tests/compilation_failures/iteration/infinite_range_to_string.rs b/tests/compilation_failures/iteration/infinite_range_to_string.rs new file mode 100644 index 00000000..feb093e2 --- /dev/null +++ b/tests/compilation_failures/iteration/infinite_range_to_string.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!((0..).to_string()); +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/infinite_range_to_string.stderr b/tests/compilation_failures/iteration/infinite_range_to_string.stderr new file mode 100644 index 00000000..9960eb71 --- /dev/null +++ b/tests/compilation_failures/iteration/infinite_range_to_string.stderr @@ -0,0 +1,5 @@ +error: To protect against infinite loops, only a maximum of 1000 items can be output to a string from an iterator. You can use .to_vec() to avoid this limit. This can't currently be reconfigured with the iteration limit. + --> tests/compilation_failures/iteration/infinite_range_to_string.rs:4:18 + | +4 | let _ = run!((0..).to_string()); + | ^^^^^ diff --git a/tests/compilation_failures/iteration/infinite_range_to_vec.rs b/tests/compilation_failures/iteration/infinite_range_to_vec.rs new file mode 100644 index 00000000..542565b5 --- /dev/null +++ b/tests/compilation_failures/iteration/infinite_range_to_vec.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!((0..).to_vec()); +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/infinite_range_to_vec.stderr b/tests/compilation_failures/iteration/infinite_range_to_vec.stderr new file mode 100644 index 00000000..ff582f02 --- /dev/null +++ b/tests/compilation_failures/iteration/infinite_range_to_vec.stderr @@ -0,0 +1,6 @@ +error: Iteration limit of 1000 exceeded. + If needed, the limit can be reconfigured with None.configure_preinterpret(%{ iteration_limit: XXX }) + --> tests/compilation_failures/iteration/infinite_range_to_vec.rs:4:23 + | +4 | let _ = run!((0..).to_vec()); + | ^^^^^^^^^ diff --git a/tests/compilation_failures/iteration/intersperse_too_few_arguments.rs b/tests/compilation_failures/iteration/intersperse_too_few_arguments.rs new file mode 100644 index 00000000..1b270b91 --- /dev/null +++ b/tests/compilation_failures/iteration/intersperse_too_few_arguments.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + [1, 2].intersperse() + } +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/intersperse_too_few_arguments.stderr b/tests/compilation_failures/iteration/intersperse_too_few_arguments.stderr new file mode 100644 index 00000000..862a971e --- /dev/null +++ b/tests/compilation_failures/iteration/intersperse_too_few_arguments.stderr @@ -0,0 +1,5 @@ +error: The method intersperse expects 1 to 2 non-self argument/s, but 0 were provided + --> tests/compilation_failures/iteration/intersperse_too_few_arguments.rs:5:16 + | +5 | [1, 2].intersperse() + | ^^^^^^^^^^^ diff --git a/tests/compilation_failures/iteration/intersperse_too_many_arguments.rs b/tests/compilation_failures/iteration/intersperse_too_many_arguments.rs new file mode 100644 index 00000000..0972a97f --- /dev/null +++ b/tests/compilation_failures/iteration/intersperse_too_many_arguments.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + [1, 2].intersperse("", %{}, %{}) + } +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/intersperse_too_many_arguments.stderr b/tests/compilation_failures/iteration/intersperse_too_many_arguments.stderr new file mode 100644 index 00000000..699bac24 --- /dev/null +++ b/tests/compilation_failures/iteration/intersperse_too_many_arguments.stderr @@ -0,0 +1,5 @@ +error: The method intersperse expects 1 to 2 non-self argument/s, but 3 were provided + --> tests/compilation_failures/iteration/intersperse_too_many_arguments.rs:5:16 + | +5 | [1, 2].intersperse("", %{}, %{}) + | ^^^^^^^^^^^ diff --git a/tests/compilation_failures/iteration/intersperse_wrong_settings.rs b/tests/compilation_failures/iteration/intersperse_wrong_settings.rs new file mode 100644 index 00000000..4a439b2d --- /dev/null +++ b/tests/compilation_failures/iteration/intersperse_wrong_settings.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + [1, 2].intersperse("", %{ finl_separator: "" }) + } +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/intersperse_wrong_settings.stderr b/tests/compilation_failures/iteration/intersperse_wrong_settings.stderr new file mode 100644 index 00000000..0f5dc239 --- /dev/null +++ b/tests/compilation_failures/iteration/intersperse_wrong_settings.stderr @@ -0,0 +1,12 @@ +error: Expected: + %{ + // Whether to add the separator after the last item (default: false) + add_trailing?: false, + // Define a different final separator (default: same as normal separator) + final_separator?: %[or], + } + The following field/s are unexpected: finl_separator + --> tests/compilation_failures/iteration/intersperse_wrong_settings.rs:5:33 + | +5 | [1, 2].intersperse("", %{ finl_separator: "" }) + | ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/iteration/long_iterable_to_string.rs b/tests/compilation_failures/iteration/long_iterable_to_string.rs new file mode 100644 index 00000000..1bb1d921 --- /dev/null +++ b/tests/compilation_failures/iteration/long_iterable_to_string.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!((0..10000).into_iter().to_string()) +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/long_iterable_to_string.stderr b/tests/compilation_failures/iteration/long_iterable_to_string.stderr new file mode 100644 index 00000000..7178f763 --- /dev/null +++ b/tests/compilation_failures/iteration/long_iterable_to_string.stderr @@ -0,0 +1,5 @@ +error: To protect against infinite loops, only a maximum of 1000 items can be output to a string from an iterator. You can use .to_vec() to avoid this limit. This can't currently be reconfigured with the iteration limit. + --> tests/compilation_failures/iteration/long_iterable_to_string.rs:4:20 + | +4 | run!((0..10000).into_iter().to_string()) + | ^^^^^^^^^^^^ diff --git a/tests/compilation_failures/iteration/long_iterable_to_vec.rs b/tests/compilation_failures/iteration/long_iterable_to_vec.rs new file mode 100644 index 00000000..da032eaf --- /dev/null +++ b/tests/compilation_failures/iteration/long_iterable_to_vec.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!((0..10000).into_iter().to_vec()) +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/long_iterable_to_vec.stderr b/tests/compilation_failures/iteration/long_iterable_to_vec.stderr new file mode 100644 index 00000000..32591675 --- /dev/null +++ b/tests/compilation_failures/iteration/long_iterable_to_vec.stderr @@ -0,0 +1,6 @@ +error: Iteration limit of 1000 exceeded. + If needed, the limit can be reconfigured with None.configure_preinterpret(%{ iteration_limit: XXX }) + --> tests/compilation_failures/iteration/long_iterable_to_vec.rs:4:32 + | +4 | run!((0..10000).into_iter().to_vec()) + | ^^^^^^^^^ diff --git a/tests/compilation_failures/iteration/long_range_to_string.rs b/tests/compilation_failures/iteration/long_range_to_string.rs new file mode 100644 index 00000000..49a6b5a3 --- /dev/null +++ b/tests/compilation_failures/iteration/long_range_to_string.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!((0..10000).to_string()) +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/long_range_to_string.stderr b/tests/compilation_failures/iteration/long_range_to_string.stderr new file mode 100644 index 00000000..a5aaf1f3 --- /dev/null +++ b/tests/compilation_failures/iteration/long_range_to_string.stderr @@ -0,0 +1,5 @@ +error: To protect against infinite loops, only a maximum of 1000 items can be output to a string from an iterator. You can use .to_vec() to avoid this limit. This can't currently be reconfigured with the iteration limit. + --> tests/compilation_failures/iteration/long_range_to_string.rs:4:10 + | +4 | run!((0..10000).to_string()) + | ^^^^^^^^^^ diff --git a/tests/compilation_failures/iteration/long_range_to_vec.rs b/tests/compilation_failures/iteration/long_range_to_vec.rs new file mode 100644 index 00000000..a08ffec2 --- /dev/null +++ b/tests/compilation_failures/iteration/long_range_to_vec.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!((0..10000).to_vec()) +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/long_range_to_vec.stderr b/tests/compilation_failures/iteration/long_range_to_vec.stderr new file mode 100644 index 00000000..9b603952 --- /dev/null +++ b/tests/compilation_failures/iteration/long_range_to_vec.stderr @@ -0,0 +1,6 @@ +error: Iteration limit of 1000 exceeded. + If needed, the limit can be reconfigured with None.configure_preinterpret(%{ iteration_limit: XXX }) + --> tests/compilation_failures/iteration/long_range_to_vec.rs:4:20 + | +4 | run!((0..10000).to_vec()) + | ^^^^^^^^^ diff --git a/tests/compilation_failures/iteration/zip_different_length_streams.rs b/tests/compilation_failures/iteration/zip_different_length_streams.rs new file mode 100644 index 00000000..10889cb9 --- /dev/null +++ b/tests/compilation_failures/iteration/zip_different_length_streams.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run! { + [["A", "B", "C"], [1, 2, 3, 4]].zip() + } +} \ No newline at end of file diff --git a/tests/compilation_failures/iteration/zip_different_length_streams.stderr b/tests/compilation_failures/iteration/zip_different_length_streams.stderr new file mode 100644 index 00000000..87093ef1 --- /dev/null +++ b/tests/compilation_failures/iteration/zip_different_length_streams.stderr @@ -0,0 +1,5 @@ +error: The iterables have different lengths. The lengths vary from 3 to 4. To truncate to the shortest, use `zip_truncated` instead of `zip` + --> tests/compilation_failures/iteration/zip_different_length_streams.rs:5:40 + | +5 | [["A", "B", "C"], [1, 2, 3, 4]].zip() + | ^^^^^^ diff --git a/tests/compilation_failures/operations/add_bool_and_int.rs b/tests/compilation_failures/operations/add_bool_and_int.rs new file mode 100644 index 00000000..1748cc28 --- /dev/null +++ b/tests/compilation_failures/operations/add_bool_and_int.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(true + 1); +} diff --git a/tests/compilation_failures/operations/add_bool_and_int.stderr b/tests/compilation_failures/operations/add_bool_and_int.stderr new file mode 100644 index 00000000..432e7a66 --- /dev/null +++ b/tests/compilation_failures/operations/add_bool_and_int.stderr @@ -0,0 +1,5 @@ +error: The + operator is not supported for a bool operand + --> tests/compilation_failures/operations/add_bool_and_int.rs:4:23 + | +4 | let _ = run!(true + 1); + | ^ diff --git a/tests/compilation_failures/operations/add_int_and_array.rs b/tests/compilation_failures/operations/add_int_and_array.rs new file mode 100644 index 00000000..d40ef8ed --- /dev/null +++ b/tests/compilation_failures/operations/add_int_and_array.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(1 + []); +} diff --git a/tests/compilation_failures/operations/add_int_and_array.stderr b/tests/compilation_failures/operations/add_int_and_array.stderr new file mode 100644 index 00000000..8e8bc881 --- /dev/null +++ b/tests/compilation_failures/operations/add_int_and_array.stderr @@ -0,0 +1,5 @@ +error: This argument is expected to be an integer, but it is an array + --> tests/compilation_failures/operations/add_int_and_array.rs:4:22 + | +4 | let _ = run!(1 + []); + | ^^ diff --git a/tests/compilation_failures/operations/bitwise_on_float.rs b/tests/compilation_failures/operations/bitwise_on_float.rs new file mode 100644 index 00000000..834b3d27 --- /dev/null +++ b/tests/compilation_failures/operations/bitwise_on_float.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(1.0 & 2.0); +} diff --git a/tests/compilation_failures/operations/bitwise_on_float.stderr b/tests/compilation_failures/operations/bitwise_on_float.stderr new file mode 100644 index 00000000..04a3b5c8 --- /dev/null +++ b/tests/compilation_failures/operations/bitwise_on_float.stderr @@ -0,0 +1,5 @@ +error: The & operator is not supported for an untyped float operand + --> tests/compilation_failures/operations/bitwise_on_float.rs:4:22 + | +4 | let _ = run!(1.0 & 2.0); + | ^ diff --git a/tests/compilation_failures/operations/bitwise_or_on_float.rs b/tests/compilation_failures/operations/bitwise_or_on_float.rs new file mode 100644 index 00000000..7694f002 --- /dev/null +++ b/tests/compilation_failures/operations/bitwise_or_on_float.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(1.0 | 2.0); +} diff --git a/tests/compilation_failures/operations/bitwise_or_on_float.stderr b/tests/compilation_failures/operations/bitwise_or_on_float.stderr new file mode 100644 index 00000000..afb1d9cc --- /dev/null +++ b/tests/compilation_failures/operations/bitwise_or_on_float.stderr @@ -0,0 +1,5 @@ +error: The | operator is not supported for an untyped float operand + --> tests/compilation_failures/operations/bitwise_or_on_float.rs:4:22 + | +4 | let _ = run!(1.0 | 2.0); + | ^ diff --git a/tests/compilation_failures/operations/compare_bool_and_int.rs b/tests/compilation_failures/operations/compare_bool_and_int.rs new file mode 100644 index 00000000..bed62165 --- /dev/null +++ b/tests/compilation_failures/operations/compare_bool_and_int.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(true == 1); +} diff --git a/tests/compilation_failures/operations/compare_bool_and_int.stderr b/tests/compilation_failures/operations/compare_bool_and_int.stderr new file mode 100644 index 00000000..cd8c5ce4 --- /dev/null +++ b/tests/compilation_failures/operations/compare_bool_and_int.stderr @@ -0,0 +1,5 @@ +error: This argument is expected to be a bool, but it is an untyped integer + --> tests/compilation_failures/operations/compare_bool_and_int.rs:4:26 + | +4 | let _ = run!(true == 1); + | ^ diff --git a/tests/compilation_failures/operations/compare_int_and_string.rs b/tests/compilation_failures/operations/compare_int_and_string.rs new file mode 100644 index 00000000..dfb5ca04 --- /dev/null +++ b/tests/compilation_failures/operations/compare_int_and_string.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(5 == "five"); +} diff --git a/tests/compilation_failures/operations/compare_int_and_string.stderr b/tests/compilation_failures/operations/compare_int_and_string.stderr new file mode 100644 index 00000000..4bb09d8d --- /dev/null +++ b/tests/compilation_failures/operations/compare_int_and_string.stderr @@ -0,0 +1,5 @@ +error: This argument is expected to be an integer, but it is a string + --> tests/compilation_failures/operations/compare_int_and_string.rs:4:23 + | +4 | let _ = run!(5 == "five"); + | ^^^^^^ diff --git a/tests/compilation_failures/operations/divide_by_zero_int.rs b/tests/compilation_failures/operations/divide_by_zero_int.rs new file mode 100644 index 00000000..d98864ca --- /dev/null +++ b/tests/compilation_failures/operations/divide_by_zero_int.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(10 / 0); +} diff --git a/tests/compilation_failures/operations/divide_by_zero_int.stderr b/tests/compilation_failures/operations/divide_by_zero_int.stderr new file mode 100644 index 00000000..90c84a4a --- /dev/null +++ b/tests/compilation_failures/operations/divide_by_zero_int.stderr @@ -0,0 +1,5 @@ +error: The untyped integer operation 10 / 0 overflowed in i128 space + --> tests/compilation_failures/operations/divide_by_zero_int.rs:4:21 + | +4 | let _ = run!(10 / 0); + | ^ diff --git a/tests/compilation_failures/operations/integer_overflow_add.rs b/tests/compilation_failures/operations/integer_overflow_add.rs new file mode 100644 index 00000000..9785450c --- /dev/null +++ b/tests/compilation_failures/operations/integer_overflow_add.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(255u8 + 1u8); +} diff --git a/tests/compilation_failures/operations/integer_overflow_add.stderr b/tests/compilation_failures/operations/integer_overflow_add.stderr new file mode 100644 index 00000000..a45f0fa5 --- /dev/null +++ b/tests/compilation_failures/operations/integer_overflow_add.stderr @@ -0,0 +1,5 @@ +error: The u8 operation 255 + 1 overflowed + --> tests/compilation_failures/operations/integer_overflow_add.rs:4:24 + | +4 | let _ = run!(255u8 + 1u8); + | ^ diff --git a/tests/compilation_failures/operations/integer_overflow_mul.rs b/tests/compilation_failures/operations/integer_overflow_mul.rs new file mode 100644 index 00000000..9b91521e --- /dev/null +++ b/tests/compilation_failures/operations/integer_overflow_mul.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(200u8 * 2u8); +} diff --git a/tests/compilation_failures/operations/integer_overflow_mul.stderr b/tests/compilation_failures/operations/integer_overflow_mul.stderr new file mode 100644 index 00000000..e9712b5d --- /dev/null +++ b/tests/compilation_failures/operations/integer_overflow_mul.stderr @@ -0,0 +1,5 @@ +error: The u8 operation 200 * 2 overflowed + --> tests/compilation_failures/operations/integer_overflow_mul.rs:4:24 + | +4 | let _ = run!(200u8 * 2u8); + | ^ diff --git a/tests/compilation_failures/operations/integer_underflow_sub.rs b/tests/compilation_failures/operations/integer_underflow_sub.rs new file mode 100644 index 00000000..9f80369a --- /dev/null +++ b/tests/compilation_failures/operations/integer_underflow_sub.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(0u8 - 1u8); +} diff --git a/tests/compilation_failures/operations/integer_underflow_sub.stderr b/tests/compilation_failures/operations/integer_underflow_sub.stderr new file mode 100644 index 00000000..fffdbecd --- /dev/null +++ b/tests/compilation_failures/operations/integer_underflow_sub.stderr @@ -0,0 +1,5 @@ +error: The u8 operation 0 - 1 overflowed + --> tests/compilation_failures/operations/integer_underflow_sub.rs:4:22 + | +4 | let _ = run!(0u8 - 1u8); + | ^ diff --git a/tests/compilation_failures/operations/logical_and_on_int.rs b/tests/compilation_failures/operations/logical_and_on_int.rs new file mode 100644 index 00000000..aa4a4666 --- /dev/null +++ b/tests/compilation_failures/operations/logical_and_on_int.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(1 && 2); +} diff --git a/tests/compilation_failures/operations/logical_and_on_int.stderr b/tests/compilation_failures/operations/logical_and_on_int.stderr new file mode 100644 index 00000000..0e3f47bb --- /dev/null +++ b/tests/compilation_failures/operations/logical_and_on_int.stderr @@ -0,0 +1,5 @@ +error: The left operand to && is expected to be a bool, but it is an untyped integer + --> tests/compilation_failures/operations/logical_and_on_int.rs:4:18 + | +4 | let _ = run!(1 && 2); + | ^ diff --git a/tests/compilation_failures/operations/logical_not_on_int.rs b/tests/compilation_failures/operations/logical_not_on_int.rs new file mode 100644 index 00000000..ba72885a --- /dev/null +++ b/tests/compilation_failures/operations/logical_not_on_int.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(!5); +} diff --git a/tests/compilation_failures/operations/logical_not_on_int.stderr b/tests/compilation_failures/operations/logical_not_on_int.stderr new file mode 100644 index 00000000..f9bf8831 --- /dev/null +++ b/tests/compilation_failures/operations/logical_not_on_int.stderr @@ -0,0 +1,5 @@ +error: The ! operator is not supported for an untyped integer + --> tests/compilation_failures/operations/logical_not_on_int.rs:4:18 + | +4 | let _ = run!(!5); + | ^ diff --git a/tests/compilation_failures/operations/logical_or_on_int.rs b/tests/compilation_failures/operations/logical_or_on_int.rs new file mode 100644 index 00000000..b0b19532 --- /dev/null +++ b/tests/compilation_failures/operations/logical_or_on_int.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(1 || 2); +} diff --git a/tests/compilation_failures/operations/logical_or_on_int.stderr b/tests/compilation_failures/operations/logical_or_on_int.stderr new file mode 100644 index 00000000..ce7a0f7e --- /dev/null +++ b/tests/compilation_failures/operations/logical_or_on_int.stderr @@ -0,0 +1,5 @@ +error: The left operand to || is expected to be a bool, but it is an untyped integer + --> tests/compilation_failures/operations/logical_or_on_int.rs:4:18 + | +4 | let _ = run!(1 || 2); + | ^ diff --git a/tests/compilation_failures/operations/multiply_strings.rs b/tests/compilation_failures/operations/multiply_strings.rs new file mode 100644 index 00000000..4b028e70 --- /dev/null +++ b/tests/compilation_failures/operations/multiply_strings.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!("hello" * 3); +} diff --git a/tests/compilation_failures/operations/multiply_strings.stderr b/tests/compilation_failures/operations/multiply_strings.stderr new file mode 100644 index 00000000..7bed64f2 --- /dev/null +++ b/tests/compilation_failures/operations/multiply_strings.stderr @@ -0,0 +1,5 @@ +error: The * operator is not supported for a string operand + --> tests/compilation_failures/operations/multiply_strings.rs:4:26 + | +4 | let _ = run!("hello" * 3); + | ^ diff --git a/tests/compilation_failures/operations/negate_unsigned.rs b/tests/compilation_failures/operations/negate_unsigned.rs new file mode 100644 index 00000000..3567ef67 --- /dev/null +++ b/tests/compilation_failures/operations/negate_unsigned.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(-5u32); +} diff --git a/tests/compilation_failures/operations/negate_unsigned.stderr b/tests/compilation_failures/operations/negate_unsigned.stderr new file mode 100644 index 00000000..5316148d --- /dev/null +++ b/tests/compilation_failures/operations/negate_unsigned.stderr @@ -0,0 +1,5 @@ +error: The - operator is not supported for a u32 + --> tests/compilation_failures/operations/negate_unsigned.rs:4:18 + | +4 | let _ = run!(-5u32); + | ^ diff --git a/tests/compilation_failures/operations/remainder_by_zero_int.rs b/tests/compilation_failures/operations/remainder_by_zero_int.rs new file mode 100644 index 00000000..73287dfc --- /dev/null +++ b/tests/compilation_failures/operations/remainder_by_zero_int.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(10 % 0); +} diff --git a/tests/compilation_failures/operations/remainder_by_zero_int.stderr b/tests/compilation_failures/operations/remainder_by_zero_int.stderr new file mode 100644 index 00000000..64689cd0 --- /dev/null +++ b/tests/compilation_failures/operations/remainder_by_zero_int.stderr @@ -0,0 +1,5 @@ +error: The untyped integer operation 10 % 0 overflowed in i128 space + --> tests/compilation_failures/operations/remainder_by_zero_int.rs:4:21 + | +4 | let _ = run!(10 % 0); + | ^ diff --git a/tests/compilation_failures/operations/shift_left_on_float.rs b/tests/compilation_failures/operations/shift_left_on_float.rs new file mode 100644 index 00000000..47df1147 --- /dev/null +++ b/tests/compilation_failures/operations/shift_left_on_float.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(1.0 << 2); +} diff --git a/tests/compilation_failures/operations/shift_left_on_float.stderr b/tests/compilation_failures/operations/shift_left_on_float.stderr new file mode 100644 index 00000000..2de6785d --- /dev/null +++ b/tests/compilation_failures/operations/shift_left_on_float.stderr @@ -0,0 +1,5 @@ +error: The << operator is not supported for an untyped float operand + --> tests/compilation_failures/operations/shift_left_on_float.rs:4:22 + | +4 | let _ = run!(1.0 << 2); + | ^^ diff --git a/tests/compilation_failures/operations/shift_overflow.rs b/tests/compilation_failures/operations/shift_overflow.rs new file mode 100644 index 00000000..5633979c --- /dev/null +++ b/tests/compilation_failures/operations/shift_overflow.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(1u8 << 8); +} diff --git a/tests/compilation_failures/operations/shift_overflow.stderr b/tests/compilation_failures/operations/shift_overflow.stderr new file mode 100644 index 00000000..24e06a2a --- /dev/null +++ b/tests/compilation_failures/operations/shift_overflow.stderr @@ -0,0 +1,5 @@ +error: The u8 operation 1 << 8 overflowed + --> tests/compilation_failures/operations/shift_overflow.rs:4:22 + | +4 | let _ = run!(1u8 << 8); + | ^^ diff --git a/tests/compilation_failures/operations/stream_subtract.rs b/tests/compilation_failures/operations/stream_subtract.rs new file mode 100644 index 00000000..a7382697 --- /dev/null +++ b/tests/compilation_failures/operations/stream_subtract.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!(%[hello] - %[world]); +} diff --git a/tests/compilation_failures/operations/stream_subtract.stderr b/tests/compilation_failures/operations/stream_subtract.stderr new file mode 100644 index 00000000..d1c99f09 --- /dev/null +++ b/tests/compilation_failures/operations/stream_subtract.stderr @@ -0,0 +1,5 @@ +error: The - operator is not supported for a stream operand + --> tests/compilation_failures/operations/stream_subtract.rs:4:27 + | +4 | let _ = run!(%[hello] - %[world]); + | ^ diff --git a/tests/compilation_failures/operations/subtract_strings.rs b/tests/compilation_failures/operations/subtract_strings.rs new file mode 100644 index 00000000..35a46bbd --- /dev/null +++ b/tests/compilation_failures/operations/subtract_strings.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + let _ = run!("hello" - "world"); +} diff --git a/tests/compilation_failures/operations/subtract_strings.stderr b/tests/compilation_failures/operations/subtract_strings.stderr new file mode 100644 index 00000000..03af4d8b --- /dev/null +++ b/tests/compilation_failures/operations/subtract_strings.stderr @@ -0,0 +1,5 @@ +error: The - operator is not supported for a string operand + --> tests/compilation_failures/operations/subtract_strings.rs:4:26 + | +4 | let _ = run!("hello" - "world"); + | ^ diff --git a/tests/compilation_failures/parsing/close_invalid_delimiter.rs b/tests/compilation_failures/parsing/close_invalid_delimiter.rs new file mode 100644 index 00000000..5711c1bb --- /dev/null +++ b/tests/compilation_failures/parsing/close_invalid_delimiter.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + run!( + let @parser[#{ + parser.close('x'); + }] = %[Hello World]; + ); +} diff --git a/tests/compilation_failures/parsing/close_invalid_delimiter.stderr b/tests/compilation_failures/parsing/close_invalid_delimiter.stderr new file mode 100644 index 00000000..976e3a45 --- /dev/null +++ b/tests/compilation_failures/parsing/close_invalid_delimiter.stderr @@ -0,0 +1,5 @@ +error: Invalid close delimiter 'x'. Expected ')', '}', or ']' + --> tests/compilation_failures/parsing/close_invalid_delimiter.rs:6:26 + | +6 | parser.close('x'); + | ^^^ diff --git a/tests/compilation_failures/parsing/close_mismatched_delimiter.rs b/tests/compilation_failures/parsing/close_mismatched_delimiter.rs new file mode 100644 index 00000000..9dfafe47 --- /dev/null +++ b/tests/compilation_failures/parsing/close_mismatched_delimiter.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +fn main() { + run!( + let @parser[#{ + parser.open('('); + let _ = parser.ident(); + parser.close(']'); + }] = %[(Hello)]; + ); +} diff --git a/tests/compilation_failures/parsing/close_mismatched_delimiter.stderr b/tests/compilation_failures/parsing/close_mismatched_delimiter.stderr new file mode 100644 index 00000000..4ded7559 --- /dev/null +++ b/tests/compilation_failures/parsing/close_mismatched_delimiter.stderr @@ -0,0 +1,5 @@ +error: attempting to close ']' isn't valid, because the currently open group would end with ')' + --> tests/compilation_failures/parsing/close_mismatched_delimiter.rs:9:16 + | +9 | }] = %[(Hello)]; + | ^^^^^^^ diff --git a/tests/compilation_failures/parsing/close_without_consuming.rs b/tests/compilation_failures/parsing/close_without_consuming.rs new file mode 100644 index 00000000..3c298252 --- /dev/null +++ b/tests/compilation_failures/parsing/close_without_consuming.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +fn main() { + run!( + let @parser[#{ + parser.open('('); + // Fail to consume the content + parser.close(')'); + }] = %[(Hello World)]; + ); +} diff --git a/tests/compilation_failures/parsing/close_without_consuming.stderr b/tests/compilation_failures/parsing/close_without_consuming.stderr new file mode 100644 index 00000000..4ffa4d1b --- /dev/null +++ b/tests/compilation_failures/parsing/close_without_consuming.stderr @@ -0,0 +1,5 @@ +error: expected ')' + --> tests/compilation_failures/parsing/close_without_consuming.rs:9:17 + | +9 | }] = %[(Hello World)]; + | ^^^^^ diff --git a/tests/compilation_failures/parsing/close_without_open.rs b/tests/compilation_failures/parsing/close_without_open.rs new file mode 100644 index 00000000..b6cef230 --- /dev/null +++ b/tests/compilation_failures/parsing/close_without_open.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + run!( + let @parser[#{ + parser.close(')'); + }] = %[Hello World]; + ); +} diff --git a/tests/compilation_failures/parsing/close_without_open.stderr b/tests/compilation_failures/parsing/close_without_open.stderr new file mode 100644 index 00000000..3413812d --- /dev/null +++ b/tests/compilation_failures/parsing/close_without_open.stderr @@ -0,0 +1,5 @@ +error: attempting to close ')' isn't valid, because there is no open group + --> tests/compilation_failures/parsing/close_without_open.rs:6:26 + | +6 | parser.close(')'); + | ^^^ diff --git a/tests/compilation_failures/parsing/destructure_with_group_stream_pattern.rs b/tests/compilation_failures/parsing/destructure_with_group_stream_pattern.rs new file mode 100644 index 00000000..5c97e3f7 --- /dev/null +++ b/tests/compilation_failures/parsing/destructure_with_group_stream_pattern.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(let %group[Output] = %group[Output];) +} diff --git a/tests/compilation_failures/parsing/destructure_with_group_stream_pattern.stderr b/tests/compilation_failures/parsing/destructure_with_group_stream_pattern.stderr new file mode 100644 index 00000000..981db14d --- /dev/null +++ b/tests/compilation_failures/parsing/destructure_with_group_stream_pattern.stderr @@ -0,0 +1,5 @@ +error: Use `@input[#{ input.read(%group[...]); }]` to match `%group[...]` stream literal content. Although this is likely not necessary, as transparent groups are typically silently ignored when parsing, unless explicitly parsing a token tree or group. + --> tests/compilation_failures/parsing/destructure_with_group_stream_pattern.rs:4:14 + | +4 | run!(let %group[Output] = %group[Output];) + | ^ diff --git a/tests/compilation_failures/parsing/destructure_with_raw_stream_pattern.rs b/tests/compilation_failures/parsing/destructure_with_raw_stream_pattern.rs new file mode 100644 index 00000000..04adb11d --- /dev/null +++ b/tests/compilation_failures/parsing/destructure_with_raw_stream_pattern.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + run!(let %raw[Output] = %raw[Output];) +} diff --git a/tests/compilation_failures/parsing/destructure_with_raw_stream_pattern.stderr b/tests/compilation_failures/parsing/destructure_with_raw_stream_pattern.stderr new file mode 100644 index 00000000..323f79e5 --- /dev/null +++ b/tests/compilation_failures/parsing/destructure_with_raw_stream_pattern.stderr @@ -0,0 +1,5 @@ +error: Use `@input[#{ input.read(%raw[...]); }]` to match `%raw[...]` stream literal content + --> tests/compilation_failures/parsing/destructure_with_raw_stream_pattern.rs:4:14 + | +4 | run!(let %raw[Output] = %raw[Output];) + | ^ diff --git a/tests/compilation_failures/parsing/invalid_content_too_long.rs b/tests/compilation_failures/parsing/invalid_content_too_long.rs new file mode 100644 index 00000000..aba8a320 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_too_long.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + let %[Hello World] = %[Hello World!!!]; + ); +} diff --git a/tests/compilation_failures/parsing/invalid_content_too_long.stderr b/tests/compilation_failures/parsing/invalid_content_too_long.stderr new file mode 100644 index 00000000..295209e8 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_too_long.stderr @@ -0,0 +1,5 @@ +error: unexpected token + --> tests/compilation_failures/parsing/invalid_content_too_long.rs:5:43 + | +5 | let %[Hello World] = %[Hello World!!!]; + | ^ diff --git a/tests/compilation_failures/parsing/invalid_content_too_short.rs b/tests/compilation_failures/parsing/invalid_content_too_short.rs new file mode 100644 index 00000000..f855c111 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_too_short.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + let %[Hello World] = %[Hello]; + ); +} diff --git a/tests/compilation_failures/parsing/invalid_content_too_short.stderr b/tests/compilation_failures/parsing/invalid_content_too_short.stderr new file mode 100644 index 00000000..2c1d9467 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_too_short.stderr @@ -0,0 +1,9 @@ +error: expected World + --> tests/compilation_failures/parsing/invalid_content_too_short.rs:4:5 + | +4 | / run!( +5 | | let %[Hello World] = %[Hello]; +6 | | ); + | |_____^ + | + = note: this error originates in the macro `run` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/compilation_failures/parsing/invalid_content_wrong_delimiter.rs b/tests/compilation_failures/parsing/invalid_content_wrong_delimiter.rs new file mode 100644 index 00000000..e5afdba4 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_wrong_delimiter.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + let %[(#{ let _ = input.rest(); })] = %[[Hello World]]; + ); +} diff --git a/tests/compilation_failures/parsing/invalid_content_wrong_delimiter.stderr b/tests/compilation_failures/parsing/invalid_content_wrong_delimiter.stderr new file mode 100644 index 00000000..db703ed3 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_wrong_delimiter.stderr @@ -0,0 +1,5 @@ +error: Cannot find variable `input` in this scope + --> tests/compilation_failures/parsing/invalid_content_wrong_delimiter.rs:5:27 + | +5 | let %[(#{ let _ = input.rest(); })] = %[[Hello World]]; + | ^^^^^ diff --git a/tests/compilation_failures/parsing/invalid_content_wrong_ident.rs b/tests/compilation_failures/parsing/invalid_content_wrong_ident.rs new file mode 100644 index 00000000..e1c4ab49 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_wrong_ident.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + let %[Hello World] = %[Hello Earth]; + ); +} diff --git a/tests/compilation_failures/parsing/invalid_content_wrong_ident.stderr b/tests/compilation_failures/parsing/invalid_content_wrong_ident.stderr new file mode 100644 index 00000000..d51997b2 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_wrong_ident.stderr @@ -0,0 +1,5 @@ +error: expected World + --> tests/compilation_failures/parsing/invalid_content_wrong_ident.rs:5:38 + | +5 | let %[Hello World] = %[Hello Earth]; + | ^^^^^ diff --git a/tests/compilation_failures/parsing/invalid_content_wrong_punct.rs b/tests/compilation_failures/parsing/invalid_content_wrong_punct.rs new file mode 100644 index 00000000..cc07cdd6 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_wrong_punct.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + let %[Hello _ World] = %[Hello World]; + ); +} diff --git a/tests/compilation_failures/parsing/invalid_content_wrong_punct.stderr b/tests/compilation_failures/parsing/invalid_content_wrong_punct.stderr new file mode 100644 index 00000000..f5ea0787 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_content_wrong_punct.stderr @@ -0,0 +1,5 @@ +error: expected _ + --> tests/compilation_failures/parsing/invalid_content_wrong_punct.rs:5:40 + | +5 | let %[Hello _ World] = %[Hello World]; + | ^^^^^ diff --git a/tests/compilation_failures/parsing/invalid_group_content_too_long.rs b/tests/compilation_failures/parsing/invalid_group_content_too_long.rs new file mode 100644 index 00000000..67c0a9fa --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_group_content_too_long.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + let %[Group: (Hello World)] = %[Group: (Hello World!!!)]; + ); +} diff --git a/tests/compilation_failures/parsing/invalid_group_content_too_long.stderr b/tests/compilation_failures/parsing/invalid_group_content_too_long.stderr new file mode 100644 index 00000000..82de2f3f --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_group_content_too_long.stderr @@ -0,0 +1,5 @@ +error: unexpected token, expected `)` + --> tests/compilation_failures/parsing/invalid_group_content_too_long.rs:5:60 + | +5 | let %[Group: (Hello World)] = %[Group: (Hello World!!!)]; + | ^ diff --git a/tests/compilation_failures/parsing/invalid_group_content_too_short.rs b/tests/compilation_failures/parsing/invalid_group_content_too_short.rs new file mode 100644 index 00000000..64f70957 --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_group_content_too_short.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + run!( + let %[Group: (Hello World!!)] = %[Group: (Hello World)]; + ); +} diff --git a/tests/compilation_failures/parsing/invalid_group_content_too_short.stderr b/tests/compilation_failures/parsing/invalid_group_content_too_short.stderr new file mode 100644 index 00000000..6e265c4e --- /dev/null +++ b/tests/compilation_failures/parsing/invalid_group_content_too_short.stderr @@ -0,0 +1,5 @@ +error: expected ! + --> tests/compilation_failures/parsing/invalid_group_content_too_short.rs:5:50 + | +5 | let %[Group: (Hello World!!)] = %[Group: (Hello World)]; + | ^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/parsing/open_invalid_delimiter.rs b/tests/compilation_failures/parsing/open_invalid_delimiter.rs new file mode 100644 index 00000000..e11295bc --- /dev/null +++ b/tests/compilation_failures/parsing/open_invalid_delimiter.rs @@ -0,0 +1,9 @@ +use preinterpret::*; + +fn main() { + run!( + let @parser[#{ + parser.open('x'); + }] = %[Hello World]; + ); +} diff --git a/tests/compilation_failures/parsing/open_invalid_delimiter.stderr b/tests/compilation_failures/parsing/open_invalid_delimiter.stderr new file mode 100644 index 00000000..4fc5f63e --- /dev/null +++ b/tests/compilation_failures/parsing/open_invalid_delimiter.stderr @@ -0,0 +1,5 @@ +error: Invalid open delimiter 'x'. Expected '(', '{', or '[' + --> tests/compilation_failures/parsing/open_invalid_delimiter.rs:6:25 + | +6 | parser.open('x'); + | ^^^ diff --git a/tests/compilation_failures/parsing/open_without_close_empty.rs b/tests/compilation_failures/parsing/open_without_close_empty.rs new file mode 100644 index 00000000..23dc51d2 --- /dev/null +++ b/tests/compilation_failures/parsing/open_without_close_empty.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +fn main() { + // Open empty group but forget to close + run!( + let @parser[#{ + parser.open('('); + // Group is empty, but we never call close() + }] = %[()]; + ); +} diff --git a/tests/compilation_failures/parsing/open_without_close_empty.stderr b/tests/compilation_failures/parsing/open_without_close_empty.stderr new file mode 100644 index 00000000..fdaae8d3 --- /dev/null +++ b/tests/compilation_failures/parsing/open_without_close_empty.stderr @@ -0,0 +1,5 @@ +error: a call to close ')' is required, because there is an unclosed group + --> tests/compilation_failures/parsing/open_without_close_empty.rs:9:16 + | +9 | }] = %[()]; + | ^^ diff --git a/tests/compilation_failures/parsing/open_without_close_unconsumed.rs b/tests/compilation_failures/parsing/open_without_close_unconsumed.rs new file mode 100644 index 00000000..b31d0cc4 --- /dev/null +++ b/tests/compilation_failures/parsing/open_without_close_unconsumed.rs @@ -0,0 +1,13 @@ +use preinterpret::*; + +fn main() { + // Open group with content we DON'T fully consume + run!( + let @parser[#{ + parser.open('('); + // Only parse one ident, leaving "World" unconsumed + let _ = parser.ident(); + // Don't close - should trigger error from syn's drop glue + }] = %[(Hello World)]; + ); +} diff --git a/tests/compilation_failures/parsing/open_without_close_unconsumed.stderr b/tests/compilation_failures/parsing/open_without_close_unconsumed.stderr new file mode 100644 index 00000000..e9f9d952 --- /dev/null +++ b/tests/compilation_failures/parsing/open_without_close_unconsumed.stderr @@ -0,0 +1,5 @@ +error: a call to close ')' is required, because there is an unclosed group + --> tests/compilation_failures/parsing/open_without_close_unconsumed.rs:11:23 + | +11 | }] = %[(Hello World)]; + | ^^^^^ diff --git a/tests/compilation_failures/parsing/parser_after_rest.rs b/tests/compilation_failures/parsing/parser_after_rest.rs new file mode 100644 index 00000000..a447a710 --- /dev/null +++ b/tests/compilation_failures/parsing/parser_after_rest.rs @@ -0,0 +1,13 @@ +use preinterpret::*; + +fn main() { + run!( + let @input[#{ + let _ = input.rest(); + // This is a _bad_ error - syn sticks it out at the end of Span::call_site(), + // Which is not helpful here. It's quite hard to fix this properly, but let's + // address some time before launch. + let _ = input.token_tree(); + }] = %[Hello World]; + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/parsing/parser_after_rest.stderr b/tests/compilation_failures/parsing/parser_after_rest.stderr new file mode 100644 index 00000000..f04fa894 --- /dev/null +++ b/tests/compilation_failures/parsing/parser_after_rest.stderr @@ -0,0 +1,12 @@ +error: unexpected end of input, expected token tree + --> tests/compilation_failures/parsing/parser_after_rest.rs:4:5 + | + 4 | / run!( + 5 | | let @input[#{ + 6 | | let _ = input.rest(); +... | +11 | | }] = %[Hello World]; +12 | | ); + | |_____^ + | + = note: this error originates in the macro `run` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/compilation_failures/parsing/smuggling_parser_out.rs b/tests/compilation_failures/parsing/smuggling_parser_out.rs new file mode 100644 index 00000000..4ae7f4f6 --- /dev/null +++ b/tests/compilation_failures/parsing/smuggling_parser_out.rs @@ -0,0 +1,13 @@ +use preinterpret::*; + +fn main() { + let _ = run! { + let smuggled_input; + parse %[Hello World] => |input| { + let _ = input.ident(); + let _ = input.ident(); + smuggled_input = input; + } + let _ = smuggled_input.ident(); + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/parsing/smuggling_parser_out.stderr b/tests/compilation_failures/parsing/smuggling_parser_out.stderr new file mode 100644 index 00000000..36e5688e --- /dev/null +++ b/tests/compilation_failures/parsing/smuggling_parser_out.stderr @@ -0,0 +1,5 @@ +error: This parser is no longer available + --> tests/compilation_failures/parsing/smuggling_parser_out.rs:11:17 + | +11 | let _ = smuggled_input.ident(); + | ^^^^^^^^^^^^^^ diff --git a/tests/complex.rs b/tests/complex.rs new file mode 100644 index 00000000..8f27fe4f --- /dev/null +++ b/tests/complex.rs @@ -0,0 +1,40 @@ +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; + +preinterpret::run! { + let bytes = 32; + let postfix = %[Hello World #bytes]; + let some_symbols = %[and some symbols such as %raw[#] and #123]; + let MyRawVar = %raw[Test no #str $(%[replacement].to_ident()) #raw[abc] #ident[def]]; + let _ = %raw[non - sensical !code :D - ignored (!)]; + %[ + struct MyStruct; + type %ident[X "Boo" #(%[Hello 1].to_string()) #postfix] = MyStruct; + const NUM: u32 = #(%[1337u #bytes].to_literal()); + const STRING: &str = #(MyRawVar.to_string()); + const SNAKE_CASE: &str = #("MyVar".to_lower_snake_case()); + ] +} + +#[test] +#[cfg_attr(miri, ignore = "incompatible with miri")] +fn test_complex_compilation_failures() { + if !should_run_ui_tests() { + // Some of the outputs are different on nightly, so don't test these + return; + } + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compilation_failures/complex/*.rs"); +} + +#[test] +fn complex_example_evaluates_correctly() { + let _x: XBooHello1HelloWorld32 = MyStruct; + assert_eq!(NUM, 1337u32); + assert_eq!( + STRING, + "Testno#str$(%[replacement].to_ident())#raw[abc]#ident[def]" + ); + assert_eq!(SNAKE_CASE, "my_var"); +} diff --git a/tests/control_flow.rs b/tests/control_flow.rs new file mode 100644 index 00000000..3679ce9f --- /dev/null +++ b/tests/control_flow.rs @@ -0,0 +1,607 @@ +#![allow(clippy::identity_op)] // https://github.com/rust-lang/rust-clippy/issues/13924 + +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; + +#[test] +#[cfg_attr(miri, ignore = "incompatible with miri")] +fn test_control_flow_compilation_failures() { + if !should_run_ui_tests() { + // Some of the outputs are different on nightly, so don't test these + return; + } + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compilation_failures/control_flow/**/*.rs"); +} + +#[test] +fn test_if() { + assert_eq!( + run! { + if (1 == 2) { "YES" } else { "NO" } + }, + "NO" + ); + assert_eq!( + run! { + let x = 1 == 2; + if x { "YES" } else { "NO" } + }, + "NO" + ); + assert_eq!( + run! { + let x = 1; + let y = 2; + if x == y { "YES" } else { "NO" } + }, + "NO" + ); + assert_eq!( + run! { + %[0 #(if true { %[+ 1] })] + }, + 1 + ); + assert_eq!( + stream! { + 0 + #(if false { %[+ 1] }) + }, + 0 + ); + assert_eq!( + run! { + if false { + 1 + } else if false { + 2 + } else if true { + 3 + } else { + 4 + } + }, + 3 + ); +} + +#[test] +fn test_while() { + assert_eq!( + run! { + let x = 0; + while x < 5 { + x += 1; + } + x + }, + 5 + ); +} + +#[test] +fn test_loop_continue_and_break() { + assert_eq!( + run! { + let x = 0; + loop { + x += 1; + if x >= 10 { + break; + } + } + x + }, + 10 + ); + assert_eq!( + run! { + let arr = []; + for x in 65..75 { + if x % 2 == 0 { + continue; + } + arr.push(x as u8 as char); + } + arr.to_string() + }, + "ACEGI" + ); +} + +#[test] +fn test_for() { + assert_eq!( + run! { + let arr = []; + for x in 65..70 { + arr.push(x as u8 as char); + } + arr.to_string() + }, + "ABCDE" + ); + assert_eq!( + run! { + // A stream is iterated token-tree by token-tree + // So we can match each value with a stream pattern matching each `(X,)` + let arr = []; + for @parser[(#{ let x = parser.ident(); },)] in %[(a,) (b,) (c,)] { + if x.to_string() == "c" { + break; + } + arr.push(x.to_string()); + } + emit arr.to_string(); + }, + "ab" + ); +} + +#[test] +fn test_attempt() { + let x = run! { + attempt { + {} => { 1 } + {} => { 2 } + } + }; + assert_eq!(x, 1); + run! { + let x = 0; + let output = attempt { + { %[_].assert_eq(x, 1) } => { 1 } + { let x = 2; } => { x } + }; + %[_].assert_eq(output, 2); + } + // Mutating a variable defined inside the arm works. + // (Not being able to mutate parent scope variables is tested as a compilation failure.) + run! { + let value = attempt { + { let x = 0; x += 1; } => { x } + }; + %[_].assert_eq(value, 1); + } + run! { + let x = 0; + attempt { + { } => { x += 1; } + }; + %[_].assert_eq(x, 1); + } + run! { + let y = 3; + let value = attempt { + { let x = y + 1; } => { x } + }; + %[_].assert_eq(value, 4); + } + run! { + let value = attempt { + { + let y = 1; + attempt { + {} => { y += 1; } + } + } => { y } + }; + %[_].assert_eq(value, 2); + } + // Control flow interrupts can be inside attempt arm LHS. + run! { + loop { + attempt { + { break; } => { None } + } + %[_].error("Should be unreachable"); + } + } + // Control flow interrupts can be inside attempt arm RHS. + run! { + loop { + attempt { + {} => { break; } + } + %[_].error("Should be unreachable"); + } + } + run! { + let value = attempt { + { revert; } => { None } + { } => 1, + }; + %[_].assert_eq(value, 1); + } + run!( + let value = attempt { + { + // This revert propagates to the LHS of the outer attempt + attempt { + { } => { revert; } + { } => { None } + } + } => { 1 } + { } => { 2 } + }; + %[_].assert_eq(value, 2); + ); +} + +#[test] +fn test_labeled_attempt_blocks() { + // Simple labeled attempt with revert to that label + run! { + let value = 'outer: attempt { + { revert 'outer; } => { 1 } + { } => { 2 } + }; + %[_].assert_eq(value, 2); + } + + // Nested attempt blocks with labeled revert to outer + run! { + let value = 'outer: attempt { + { + attempt { + { revert 'outer; } => { None } + { } => { None } + } + } => { 1 } + { } => { 2 } + }; + %[_].assert_eq(value, 2); + } + + // Multiple nested attempts with specific label targeting + run! { + let value = 'outer: attempt { + { + 'inner: attempt { + { revert 'inner; } => { None } + { } => { None } + } + } => { 1 } + { } => { 2 } + }; + %[_].assert_eq(value, 1); + } + + // Labeled attempt with successful arm + run! { + let value = 'labeled: attempt { + { let x = 10; } => { x } + { } => { 0 } + }; + %[_].assert_eq(value, 10); + } + + // Revert to specific outer attempt skipping intermediate one + run! { + let value = 'outer: attempt { + { + 'middle: attempt { + { + 'inner: attempt { + { revert 'outer; } => { None } + { } => { None } + } + } => { None } + { } => { None } + } + } => { 1 } + { } => { 2 } + }; + %[_].assert_eq(value, 2); + } + + // Unlabeled revert in labeled attempt (should revert to immediately enclosing attempt) + run! { + let value = 'outer: attempt { + { revert; } => { 1 } + { } => { 2 } + }; + %[_].assert_eq(value, 2); + } +} + +#[test] +fn test_attempt_guard_clauses() { + run! { + let output = attempt { + { } if false => 1, + { } => 2, + }; + %[_].assert_eq(output, 2); + } + run! { + let output = attempt { + { let x = 4; } if { x += 1; true } => { x } + { } => { 2 } + }; + %[_].assert_eq(output, 5); + } + run! { + let output = attempt { + { let x = 2; } if x >= 3 => x, + { let x = 5; } if x >= 3 => x, + }; + %[_].assert_eq(output, 5); + } +} + +#[test] +fn test_emit_statement() { + assert_eq!( + run! { + let s = "Hello"; + emit s; + }, + "Hello" + ); + + assert_eq!( + stream! { + [#{ + for i in 1..=5 { + emit i; + emit %[,]; + } + }] + }, + [1, 2, 3, 4, 5] + ); + + run! { + emit %[ + fn my_add(a: i32, b: i32) -> i32 { + a + b + } + ]; + emit %[ + fn my_sub(a: i32, b: i32) -> i32 { + a - b + } + ]; + // Final return is also emitted + %[ + fn my_mul(a: i32, b: i32) -> i32 { + a * b + } + ] + }; + assert!(my_add(5, 3) == 8); + assert!(my_sub(5, 3) == 2); + assert!(my_mul(5, 3) == 15); + + // Internal emits inside revertible segments are OK + assert_eq!( + run! { + attempt { + { + let x = %[#{ emit 1; }]; + } => { emit %[#x + #x]; } + } + }, + 2 + ); +} + +#[test] +fn test_break_with_value() { + // Simple break with value from loop + assert_eq!( + run! { + let result = loop { + break 42; + }; + result + }, + 42 + ); + + // Break with computed value + assert_eq!( + run! { + let result = loop { + let x = 10; + let y = 5; + break x + y; + }; + result + }, + 15 + ); + + // While loop with break value + assert_eq!( + run! { + let x = 0; + let result = while x < 10 { + x += 1; + if x == 5 { + break x * 2; + } + }; + result + }, + 10 + ); + + // For loop with break value + assert_eq!( + run! { + let result = for i in 1..=10 { + if i == 7 { + break i * i; + } + }; + result + }, + 49 + ); +} + +#[test] +fn test_labeled_loops() { + // Labeled loop with break + assert_eq!( + run! { + let result = 'outer: loop { + break 'outer 100; + }; + result + }, + 100 + ); + + // Labeled while loop + assert_eq!( + run! { + let x = 0; + 'counting: while x < 10 { + x += 1; + if x == 3 { + break 'counting x; + } + } + }, + 3 + ); + + // Labeled for loop + assert_eq!( + run! { + 'summing: for i in 1..=5 { + if i == 3 { + break 'summing i * 10; + } + } + }, + 30 + ); +} + +#[test] +fn test_nested_loops_with_labels() { + // Break outer loop from inner loop + assert_eq!( + run! { + let result = 'outer: loop { + for i in 1..=5 { + if i == 3 { + break 'outer i + 100; + } + } + break 'outer 0; + }; + result + }, + 103 + ); + + // Continue outer loop from inner loop + assert_eq!( + run! { + let count = 0; + 'outer: for i in 1..=3 { + for j in 1..=3 { + count += 1; + if j == 2 { + continue 'outer; + } + } + } + count + }, + 6 // Should count: (1,1), (1,2), (2,1), (2,2), (3,1), (3,2) + ); + + // Complex nested example + assert_eq!( + run! { + let result = 'outer: loop { + 'middle: for i in 1..=3 { + 'inner: for j in 1..=3 { + if i == 2 && j == 2 { + break 'outer i * 10 + j; + } + } + } + break 'outer 0; + }; + result + }, + 22 + ); +} + +#[test] +fn test_labeled_blocks() { + // Simple labeled block with break + assert_eq!( + run! { + let result = 'block: { + let x = 5; + if x > 3 { + break 'block x * 2; + } + x + }; + result + }, + 10 + ); + + // Labeled block with conditional break + assert_eq!( + run! { + let result = 'compute: { + let value = 10; + if value < 5 { + break 'compute 0; + } else if value < 15 { + break 'compute value * 2; + } + value * 3 + }; + result + }, + 20 + ); + + // Nested labeled blocks + assert_eq!( + run! { + let result = 'outer: { + let x = 5; + 'inner: { + if x > 3 { + break 'outer x + 100; + } + break 'inner x; + }; + x * 2 + }; + result + }, + 105 + ); + + // Labeled block with loop inside + assert_eq!( + run! { + 'block: { + for i in 1..=5 { + if i == 3 { + break 'block i * 10; + } + } + 0 + } + }, + 30 + ); +} diff --git a/tests/core.rs b/tests/core.rs new file mode 100644 index 00000000..66eb515c --- /dev/null +++ b/tests/core.rs @@ -0,0 +1,177 @@ +#![allow(clippy::assertions_on_constants)] + +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; + +#[test] +#[cfg_attr(miri, ignore = "incompatible with miri")] +fn test_core_compilation_failures() { + if !should_run_ui_tests() { + // Some of the outputs are different on nightly, so don't test these + return; + } + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compilation_failures/core/*.rs"); +} + +#[test] +fn test_simple_let() { + assert_eq!( + run! { + let output = %["Hello World!"]; + output + }, + "Hello World!" + ); + assert_eq!( + run! { + let hello = %["Hello"]; + let world = %["World"]; + let output = %[#hello " " #world "!"]; + let output = output.to_string(); + output + }, + "Hello World!" + ); +} + +#[test] +fn test_raw() { + assert_eq!( + run!(%raw[#variable and [!command!] are not interpreted or error].to_string()), + "#variableand[!command!]arenotinterpretedorerror" + ); +} + +#[test] +fn test_extend() { + assert_eq!( + run! { + let variable = %["Hello"]; + variable += %[" World!"]; + variable.to_debug_string() + }, + r#"%["Hello" " World!"]"#, + ); + assert_eq!( + run! { + let i = 1; + let output = %[]; + while i <= 4 { + output += %[#i]; + if i <= 3 { + output += %[", "]; + } + i += 1; + } + output.to_string() + }, + "1, 2, 3, 4" + ); +} + +#[test] +fn test_ignore() { + assert_eq!( + run! { + let x = %[false]; + // Using `let _ = %raw[...]` effectively acts as ignoring any tokens. + let _ = %raw[#(let x = %[true];) nothing is interpreted. Everything is ignored...]; + x + }, + false + ); +} + +#[test] +fn test_empty_set() { + assert_eq!( + run! { + let x = %[]; + x += %["hello"]; + x + }, + "hello" + ); + assert_eq!( + run! { + let x = %[]; + let y = %[]; + x += %["hello"]; + y += %["world"]; + %[#x " " #y].to_string() + }, + "hello world" + ); + assert_eq!( + run! { + let x = %[]; + let y = %[]; + let z = %[]; + x += %["hello"]; + y += %["world"]; + %[#x " " #y #z].to_string() + }, + "hello world" + ); +} + +#[test] +fn test_discard_set() { + assert!(run! { + let x = false; + let _ = %[#(x = true) things _are_ interpreted, but the result is ignored...]; + x + }); +} + +#[test] +fn test_debug() { + // It keeps the semantic punctuation spacing intact + // (e.g. it keeps 'a and >> together) + assert_eq!( + run!{ + %[impl<'a, T> MyStruct<'a, T> { + pub fn new() -> Self { + !($crate::Test::CONSTANT >> 5 > 1) + } + }].to_debug_string() + }, + "%[impl < 'a , T > MyStruct < 'a , T > { pub fn new () -> Self { ! ($ crate :: Test :: CONSTANT >> 5 > 1) } }]" + ); + // It shows transparent groups, and uses #raw when needed + assert_eq!( + run! { + let x = %[Hello (World)]; + %[#(x.to_group()) %raw[#test] "and" %raw[##] #x (3 %raw[%] 2)].to_debug_string() + }, + r###"%[%group[Hello (World)] %raw[#] test "and" %raw[#]%raw[#] Hello (World) (3 %raw[%] 2)]"### + ); +} + +macro_rules! capitalize_variants { + ($enum_name:ident, [$($variants:ident),*]) => {run!{ + let enum_name = %raw[$enum_name]; + let variants = []; + for variant in [$(%raw[$variants]),*] { + let capitalized = variant.to_string().capitalize().to_ident().with_span(variant); + variants.push(capitalized); + } + + %[ + enum #enum_name { + #(variants.intersperse(%[,])) + } + ] + }}; +} + +capitalize_variants!(CapitalizeTest, [one, two, three]); + +#[test] +fn test_capitalize_variants() { + let _ = CapitalizeTest::One; + let _ = CapitalizeTest::Two; + let _ = CapitalizeTest::Three; +} diff --git a/tests/expressions.rs b/tests/expressions.rs new file mode 100644 index 00000000..750dfff9 --- /dev/null +++ b/tests/expressions.rs @@ -0,0 +1,700 @@ +#![allow(clippy::assertions_on_constants)] +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; + +#[test] +#[cfg_attr(miri, ignore = "incompatible with miri")] +fn test_expression_compilation_failures() { + if !should_run_ui_tests() { + // Some of the outputs are different on nightly, so don't test these + return; + } + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compilation_failures/expressions/*.rs"); +} + +#[test] +fn test_basic_evaluate_works() { + assert!(run!(!!(!!(true)))); + assert_eq!(run!(1 + 5), 6u8); + assert_eq!(run!(1 + 5), 6i128); + assert_eq!(run!("Hello" + " " + "World!"), "Hello World!"); + assert_eq!(run!(1 + 5u16), 6u16); + assert_eq!(run!(127i8 + (-127i8) + (-127i8)), -127i8); + assert_eq!(run!(3.0 + 3.2), 6.2); + assert_eq!( + run!(3.6 + 3999999999999999992.0), + 3.6 + 3999999999999999992.0 + ); + assert_eq!(run!(-3.2), -3.2); + assert!(run!(true && true || false)); + assert!(run!(true || false && false)); // The && has priority + assert!(run!(true | false & false)); // The & has priority + assert_eq!(run!(true as u32 + 2), 3); + assert_eq!(run!(3.57 as untyped_int + 1), 4u32); + assert_eq!(run!(3.57 as untyped_int + 1), 4u64); + assert_eq!(run!(0b1000 & 0b1101), 0b1000); + assert_eq!(run!(0b1000 | 0b1101), 0b1101); + assert_eq!(run!(0b1000 ^ 0b1101), 0b101); + assert_eq!(run!(5 << 2), 20); + assert_eq!(run!(5 >> 1), 2); + assert!(!run!(123 == 456)); + assert!(run!(123 < 456)); + assert!(run!(123 <= 456)); + assert!(run!(123 != 456)); + assert!(!run!(123 >= 456)); + assert!(!run!(123 > 456)); + assert_eq!(run!(let six_as_sum = 3 + 3; six_as_sum * six_as_sum), 36); + assert_eq!( + run!( + let partial_sum = %[+ 2]; + %[#(%[5] + partial_sum.clone()) %[=] %raw[#](5 #partial_sum)].reinterpret_as_stream().to_debug_string() + ), + "%[5 + 2 = 7]" + ); + assert_eq!(run!(1 + (1..2) as untyped_int), 2); + assert!(!run!("hello" == "world")); + assert!(run!("hello" == "hello")); + assert!(run!('A' as u8 == 65)); + assert!(run!(65u8 as char == 'A')); + assert!(run!('A' == 'A')); + assert!(!run!('A' == 'B')); + assert!(run!('A' < 'B')); + assert!(run!("Zoo" > "Aardvark")); + assert_eq!( + run!(("Hello" as stream + "World" as stream + (1 + 1) as stream + (1 + 1).to_group() + [1 + 2].to_group() + %group[1 + 2 + 3]).to_debug_string()), + r#"%["Hello" "World" 2 %group[2] %group[3] %group[1 + 2 + 3]]"# + ); + assert_eq!(run!([1, 2, 1 + 2, 4].to_debug_string()), "[1, 2, 3, 4]"); + assert_eq!( + run!(([1 + (3 + 4), [5,] + [], [[6, 7],]] + [123]).to_debug_string()), + "[8, [5], [[6, 7]], 123]" + ); + assert_eq!( + run!( + ("Hello" as stream + "World" as stream + (1 + 1) as stream + (1 + 1).to_group()) + .to_debug_string() + ), + r#"%["Hello" "World" 2 %group[2]]"# + ); + assert_eq!(run!(let x = 1; x + 2), 3); +} + +#[test] +fn test_expression_precedence() { + // The general rules are: + // * Operators at higher precedence should group more tightly than operators at lower precedence. + // * Operators at the same precedence should left-associate. + + // 1 + -1 + ((2 + 4) * 3) - 9 => 1 + -1 + 18 - 9 => 9 + assert_eq!(run!(1 + -(1) + (2 + 4) * 3 - 9), 9); + // (true > true) > true => false > true => false + assert!(!run!(true > true > true)); + // (5 - 2) - 1 => 3 - 1 => 2 + assert_eq!(run!(5 - 2 - 1), 2); + // ((3 * 3 - 4) < (3 << 1)) && true => 5 < 6 => true + assert!(run!(3 * 3 - 4 < 3 << 1 && true)); +} + +#[test] +fn test_reinterpret() { + assert_eq!( + run!( + let method = "to_lower_camel_case".to_ident(); + %["Hello World".#method()].reinterpret_as_run() + ), + "helloWorld" + ); + assert_eq!( + run!( + let method = "to_lower_camel_case".to_ident(); + %[%raw[%][Hello World].to_string().#method()].reinterpret_as_run() + ), + "helloWorld" + ); + assert_eq!( + run!( + let method = "to_lower_camel_case".to_ident(); + %[%group[%][Hello World].to_string().#method()].reinterpret_as_run() + ), + "helloWorld" + ); + assert_eq!( + run!( + %[ + %raw[#]{ let my_variable = "the answer"; } + %raw[#]my_variable + ].reinterpret_as_stream() + ), + "the answer" + ); + // Transparent groups are transparently ignored when detecting preinterpret grammar + assert_eq!( + run!( + %[ + %raw[#]{ let my_variable = "the answer"; } + %group[#]my_variable + ].reinterpret_as_stream() + ), + "the answer" + ); + // ... but they are otherwise preserved + assert_eq!( + run!( + // * If the %group is preserved, then content has a stream length of 1 (the group) + // * If the %group is removed, then content has a stream length of 2 ("Hello" and "World") + let content = %group["Hello" "World"]; + %[{ + %raw[%][#content].len() + }].reinterpret_as_run() + ), + 1 + ); + // Reinterpreted code doesn't see parent scope variables + assert_eq!( + run!( + let my_variable = "before"; + %[let my_variable = "replaced";].reinterpret_as_run(); + my_variable + ), + "before" + ); +} + +#[test] +#[allow(clippy::zero_prefixed_literal)] +fn test_very_long_expression_works() { + assert_eq!( + run! { + None.configure_preinterpret(%{ + iteration_limit: 100000, + }); + let expression = %[]; + for _ in 0..100000 { + expression += %[1 +] + }; + (expression + %[0]).reinterpret_as_run() + }, + 100000 + ); +} + +#[test] +fn boolean_operators_short_circuit() { + // && short-circuits if first operand is false + assert!(run!( + let is_lazy = true; + let _ = false && { is_lazy = false; true }; + is_lazy + )); + // || short-circuits if first operand is true + assert!(run!( + let is_lazy = true; + let _ = true || { is_lazy = false; true }; + is_lazy + )); + // For comparison, the & operator does _not_ short-circuit + assert!(!run!( + let is_lazy = true; + let _ = false & { is_lazy = false; true }; + is_lazy + )); +} + +#[test] +fn assign_works() { + assert_eq!( + run!( + let x = 5 + 5; + x.to_debug_string() + ), + "10" + ); + assert_eq!( + run!( + let x = 10; + x /= 1 + 1; // 10 / (1 + 1) + x += 2 + x; // 5 + (2 + 5) + x + ), + 12 + ); + // Assign can reference itself in its expression, + // because the expression result is buffered. + assert_eq!( + run!( + let x = 2; + x += x; + x + ), + 4 + ); + // Check that assignments to composite places work + run!( + let x = %{ y: [3], }; + x.y[0] += 2; + %[_].assert_eq(x.y[0], 5); + ); +} + +#[test] +fn test_range() { + assert_eq!( + run!((-2..5).intersperse(" ").to_string()), + "-2 -1 0 1 2 3 4" + ); + assert_eq!( + run!((-2..=5).intersperse(" ").to_stream().to_string()), + "-2 -1 0 1 2 3 4 5" + ); + assert_eq!( + run!((2u32..=5).intersperse(" ").to_stream().to_string()), + "2 3 4 5" + ); + assert_eq!( + run!((2..=5u32).intersperse(" ").to_stream().to_string()), + "2 3 4 5" + ); + assert_eq!( + run! { let x = 2; ((x + x)..=5).intersperse(" ").to_string() }, + "4 5" + ); + assert_eq!( + run! { + (8..=5).intersperse(" ").to_string() + }, + "" + ); + assert_eq!( + run! { + ('a'..='f').to_string() + }, + "abcdef" + ); + assert_eq!(run! {(-1i8..3i8).to_debug_string()}, "-1i8..3i8"); + + // Large ranges are allowed, but are subject to limits at iteration time + assert_eq!(run! {(0..10000).to_debug_string()}, "0..10000"); + assert_eq!(run! {(..5 + 5).to_debug_string()}, "..10"); + assert_eq!(run! {(..=9).to_debug_string()}, "..=9"); + assert_eq!(run! {(..).to_debug_string()}, ".."); + assert_eq!(run! {([.., ..].to_debug_string())}, "[.., ..]"); + assert_eq!( + run! {([..[1, 2..], ..].to_debug_string())}, + "[..[1, 2..], ..]" + ); + assert_eq!(run! {((4 + 7..=10).to_debug_string())}, "11..=10"); + assert_eq!( + run! { + let output = 0; + for i in 0..10000000 { + if i == 5 { + output = i; + break; + } + } + output + }, + 5 + ); + run! { + %[_].assert_eq('A'.. .into_iter().take(5).to_string(), "ABCDE"); + } +} + +#[test] +fn test_array_indexing() { + assert_eq!(run!(let x = [1, 2, 3]; x[1]), 2); + assert_eq!(run!(let x = [1, 2, 3]; x[x[0] + x[x[1] - 1] - 1]), 3); + // And setting indices... + assert_eq!( + run!( + let x = [0, 0, 0]; + x[0] = 2; + (x[1 + 1]) = x[0]; + x.to_debug_string() + ), + "[2, 0, 2]" + ); + // And ranges in value position + assert_eq!( + run!( + let x = [1, 2, 3, 4, 5]; + x[..].to_debug_string() + ), + "[1, 2, 3, 4, 5]" + ); + assert_eq!( + run!( + let x = [1, 2, 3, 4, 5]; + x[0..0].to_debug_string() + ), + "[]" + ); + assert_eq!( + run!( + let x = [1, 2, 3, 4, 5]; + x[2..=2].to_debug_string() + ), + "[3]" + ); + assert_eq!( + run!( + let x = [1, 2, 3, 4, 5]; + x[..=2].to_debug_string() + ), + "[1, 2, 3]" + ); + assert_eq!( + run!( + let x = [1, 2, 3, 4, 5]; + x[..4].to_debug_string() + ), + "[1, 2, 3, 4]" + ); + assert_eq!( + run!( + let x = [1, 2, 3, 4, 5]; + x[2..].to_debug_string() + ), + "[3, 4, 5]" + ); +} + +#[test] +fn test_array_place_destructurings() { + // And array destructuring + assert_eq!( + run!( + let a = 0; let b = 0; let c = 0; + let x = [1, 2, 3, 4, 5]; + [a, b, _, _, c] = x; + [a, b, c].to_debug_string() + ), + "[1, 2, 5]" + ); + assert_eq!( + run!( + let a = 0; let b = 0; let c = 0; + let x = [1, 2, 3, 4, 5]; + [a, b, c, ..] = x; + [a, b, c].to_debug_string() + ), + "[1, 2, 3]" + ); + assert_eq!( + run!( + let a = 0; let b = 0; let c = 0; + let x = [1, 2, 3, 4, 5]; + [.., a, b] = x; + [a, b, c].to_debug_string() + ), + "[4, 5, 0]" + ); + assert_eq!( + run!( + let a = 0; let b = 0; let c = 0; + let x = [1, 2, 3, 4, 5]; + [a, .., b, c] = x; + [a, b, c].to_debug_string() + ), + "[1, 4, 5]" + ); + // Nested places + assert_eq!( + run!( + let out = [[0, 0], 0]; + let a = 0; let b = 0; let c = 0; + [out[1], .., out[0][0], out[0][1]] = [1, 2, 3, 4, 5]; + out.to_debug_string() + ), + "[[4, 5], 1]" + ); + // Misc + assert_eq!( + run!( + let a = [0, 0, 0, 0, 0]; + let b = 3; + let c = 0; + // Demonstrates right-associativity of = + let _ = c = [a[2], _] = [4, 5]; + let _ = a[1] += 2; + let _ = b = 2; + [a, b, c].to_debug_string() + ), + "[[0, 2, 4, 0, 0], 2, None]" + ); + assert_eq!( + run!( + let a = [0, 0]; + let b = 0; + // Unlike rust, we execute left-to-right, so: + // * a[b] is evaluated to a[0] + // * The RHS is evaluated, which increments b to 1 + // * a[0] is converted back to a mutable reference, and assigned to + // + // Rust actually executes the other way around, and ends up with: + // %{ a: [0, 5], b: 1 } + // This is likely due to borrowing rules, which we can partially + // circumvent by temporarily disabling them with enable/disable. + // + // I don't think it's frequent for people to rely on this behaviour, + // so I don't think it's an issue for us to diverge from rust here + // (in fact, I think we're more intuitive this way) + a[b] += { b += 1; 5 }; + %{ a, b }.to_debug_string() + ), + "%{ a: [5, 0], b: 1 }" + ); + // This test demonstrates that the assignee operation is executed + // incrementally, to align with the rust behaviour. + assert_eq!( + run!( + let arr = [0, 0]; + let arr2 = [0, 0]; + // The first assignment arr[0] = 1 occurs before being overwritten + // by the arr[0] = 5 in the second index. + [arr[0], arr2.as_mut()[{ arr[0] = 5; 1 }]] = [1, 1]; + arr[0] + ), + 5 + ); +} + +#[test] +fn test_array_pattern_destructurings() { + // And array destructuring + assert_eq!( + run!( + let [a, b, _, _, c] = [1, 2, 3, 4, 5]; + [a, b, c].to_debug_string() + ), + "[1, 2, 5]" + ); + assert_eq!( + run!( + let [a, b, c, ..] = [1, 2, 3, 4, 5]; + [a, b, c].to_debug_string() + ), + "[1, 2, 3]" + ); + assert_eq!( + run!( + let [.., a, b] = [1, 2, 3, 4, 5]; + [a, b].to_debug_string() + ), + "[4, 5]" + ); + assert_eq!( + run!( + let [a, .., b, c] = [1, 2, 3, 4, 5]; + [a, b, c].to_debug_string() + ), + "[1, 4, 5]" + ); + assert_eq!( + run!( + let [a, .., b, c] = [[1, "a"], 2, 3, 4, 5]; + [a, b, c].to_debug_string() + ), + r#"[[1, "a"], 4, 5]"# + ); +} + +#[test] +fn test_objects() { + assert_eq!( + run!( + let a = %{}; + let b = "Hello"; + let x = %{ a: a.clone(), hello: 1, ["world"]: 2, b }; + x["x y z"] = 4; + x["z\" test"] = %{}; + x.y = 5; + x.to_debug_string() + ), + r#"%{ a: %{}, b: "Hello", hello: 1, world: 2, ["x y z"]: 4, y: 5, ["z\" test"]: %{} }"# + ); + assert_eq!( + run!( + %{ prop1: 1 }["prop1"].to_debug_string() + ), + r#"1"# + ); + assert_eq!( + run!( + %{ prop1: 1 }["prop2"].to_debug_string() + ), + r#"None"# + ); + assert_eq!( + run!( + %{ prop1: 1 }.prop1.to_debug_string() + ), + r#"1"# + ); + assert_eq!( + run!( + let a; + let b; + let z; + %{ a, y: [_, b], z } = %{ a: 1, y: [5, 7] }; + %{ a, b, z }.to_debug_string() + ), + r#"%{ a: 1, b: 7, z: None }"# + ); + assert_eq!( + run!( + let %{ a, y: [_, b], ["c"]: c, [r#"two "words"#]: x, z } = %{ a: 1, y: [5, 7], ["two \"words"]: %{}, }; + %{ a, b, c, x: x, z }.to_debug_string() + ), + r#"%{ a: 1, b: 7, c: None, x: %{}, z: None }"# + ); +} + +#[test] +fn test_method_calls() { + assert_eq!( + run!( + let x = [1, 2, 3]; + x.len() + %["Hello" world].len() + ), + 2 + 3 + ); + assert_eq!( + run!( + let x = [1, 2, 3]; + x.push(5); + x.push(2); + x.to_debug_string() + ), + "[1, 2, 3, 5, 2]" + ); + // Push returns None + assert_eq!(run!([1, 2, 3].as_mut().push(4).to_debug_string()), "None"); + // Converting to mut and then to shared works + assert_eq!(run!([].as_mut().len().to_debug_string()), "0usize"); + assert_eq!( + run!( + let x = [1, 2, 3]; + let y = x.clone(); + x.to_debug_string() + " - " + y.to_debug_string() + ), + "[1, 2, 3] - [1, 2, 3]" + ); + assert_eq!( + run!( + let a = "a"; + let b = "b"; + a.swap(b); + %[#a " - " #b].to_string() + ), + "b - a" + ); + assert_eq!( + run!( + let obj_a = %{ value: "a" }; + let arr_b = ["b"]; + obj_a.value.swap(arr_b[0]); + %[#(obj_a.value) " - " #(arr_b[0])].to_string() + ), + "b - a" + ); + assert_eq!( + run!( + let obj_a = %{ value: "a" }; + let arr_b = ["b"]; + arr_b[0].swap(obj_a.value); + %[#(obj_a.value) " - " #(arr_b[0])].to_string() + ), + "b - a" + ); + run!( + let a = "a"; + let b = ["b"]; + let out = a.replace(b); + %[_].assert_eq(a, ["b"]); + %[_].assert_eq(out, "a"); + ); + run!( + let a = "a"; + let out = a.replace({ None }); + %[_].assert_eq(a, None); + %[_].assert_eq(out, "a"); + ); +} + +#[test] +fn stream_append_can_use_self_in_appender() { + // These tests *can* be broken if we wish, but we should + // decide which way to go before v1 and fix it + assert_eq!( + run! { + let variable = %[Hello]; + variable += %[World #variable]; + variable.to_debug_string() + }, + "%[Hello World Hello]" + ); + assert_eq!( + run! { + let variable = %[Hello]; + variable += %[World #(variable += %[!])]; + variable.to_debug_string() + }, + "%[Hello ! World]" + ); + assert_eq!( + run! { + let variable = %[Hello]; + variable += %[World #(variable = %[Hello2])]; + variable.to_debug_string() + }, + "%[Hello2 World]" + ); +} + +#[test] +fn can_assign_to_mutable_references() { + run!( + let x = 5; + x.as_mut() += 2; + %[_].assert_eq(x, 7); + ); + run!( + let y = 3; + y.as_mut() = 1; + %[_].assert_eq(y, 1); + ); +} + +#[test] +fn test_stream_equality_preserves_transparent_groups() { + // Stream equality preserves transparent groups - they are NOT ignored. + // A stream with a transparent group is different from one without. + run!( + let with_group = %[%group[Hello] world]; + let without_group = %[Hello world]; + // They're NOT equal because transparent groups are preserved + %[_].assert(with_group != without_group); + ); + // Their debug strings also show the difference + run!( + let with_group = %group[Hello World]; + let without_group = %[Hello World]; + %[_].assert(with_group.to_debug_string() != without_group.to_debug_string()); + ); + // remove_transparent_groups can be used to normalize streams for comparison + run!( + let nested = %group[%group[Hello]]; + let flat = %[Hello]; + %[_].assert(nested.remove_transparent_groups() == flat.remove_transparent_groups()); + ); + // After removing transparent groups, equality comparison matches + run!( + let with_group = %[%group[Hello] world]; + let without_group = %[Hello world]; + %[_].assert(with_group.remove_transparent_groups() == without_group.remove_transparent_groups()); + ); +} diff --git a/tests/helpers/prelude.rs b/tests/helpers/prelude.rs new file mode 100644 index 00000000..ee8ab3f8 --- /dev/null +++ b/tests/helpers/prelude.rs @@ -0,0 +1,15 @@ +// This file is imported into lots of different integration test files, each of which is considered as a separate crates / compilation unit. +// Some of these exports aren't used by all integration test files, so we need to suppress the warnings. +#![allow(unused_imports, unused_macros)] +pub(crate) use preinterpret::*; + +#[allow(dead_code)] // This is used only when ui tests are running +pub(crate) fn should_run_ui_tests() -> bool { + // Nightly has different outputs for some tests + // And as of https://github.com/rust-lang/rust/pull/144609 both beta and nightly do + // So we only run these tests on stable or in local developer environments. + match option_env!("TEST_RUST_MODE") { + Some("nightly") | Some("beta") => false, + _ => true, // Default case: run the tests + } +} diff --git a/tests/ident.rs b/tests/ident.rs index b893adf6..b8577f37 100644 --- a/tests/ident.rs +++ b/tests/ident.rs @@ -1,10 +1,8 @@ -use preinterpret::preinterpret; - -macro_rules! my_assert_ident_eq { - ($input:tt, $check:ident) => {{ +macro_rules! assert_ident { + (($($input:tt)*), $check:ident) => {{ assert_eq!( { - let preinterpret!($input) = 1; + let preinterpret::run!($($input)*) = 1; $check }, 1 @@ -15,38 +13,42 @@ macro_rules! my_assert_ident_eq { #[test] #[allow(non_snake_case)] fn test_ident() { - my_assert_ident_eq!([!ident! a B C _D E], aBC_DE); - my_assert_ident_eq!([!ident! a 12 "3"], a123); - my_assert_ident_eq!([!ident! "MyString"], MyString); - my_assert_ident_eq!([!ident! get_ [!snake! them]], get_them); + assert_ident!((%ident[a B C _D E]), aBC_DE); + assert_ident!((%[a B C _D E].to_ident()), aBC_DE); + assert_ident!((%[a 12 "3"].to_ident()), a123); + assert_ident!((%["MyString"].to_ident()), MyString); + assert_ident!((%[get_ #(%[them].to_string().to_lower_snake_case())].to_ident()), get_them); } #[test] #[allow(non_snake_case)] fn test_ident_camel() { - my_assert_ident_eq!([!ident_camel! a B C _D E], ABcDe); - my_assert_ident_eq!([!ident_camel! a 12 "3"], A123); - my_assert_ident_eq!([!ident_camel! "MyString"], MyString); - my_assert_ident_eq!([!ident_camel! get_ [!snake! them]], GetThem); - my_assert_ident_eq!([!ident_camel! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], MyMixedCaseStringWhichisAwesomeWhatdoYouthink); + assert_ident!((%ident_camel[a B C _D E]), ABcDe); + assert_ident!((%[a B C _D E].to_ident_camel()), ABcDe); + assert_ident!((%[a 12 "3"].to_ident_camel()), A123); + assert_ident!((%["MyString"].to_ident_camel()), MyString); + assert_ident!((%[get_ them].to_ident_camel()), GetThem); + assert_ident!((%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_ident_camel()), MyMixedCaseStringWhichisAwesomeWhatdoYouthink); } #[test] #[allow(non_snake_case)] fn test_ident_snake() { - my_assert_ident_eq!([!ident_snake! a B C _D E], a_bc_de); - my_assert_ident_eq!([!ident_snake! a 12 "3"], a123); - my_assert_ident_eq!([!ident_snake! "MyString"], my_string); - my_assert_ident_eq!([!ident_snake! get_ [!snake! them]], get_them); - my_assert_ident_eq!([!ident_snake! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], my_mixed_case_string_whichis_awesome_whatdo_youthink); + assert_ident!((%ident_snake[a B C _D E]), a_bc_de); + assert_ident!((%[a B C _D E].to_ident_snake()), a_bc_de); + assert_ident!((%[a 12 "3"].to_ident_snake()), a123); + assert_ident!((%["MyString"].to_ident_snake()), my_string); + assert_ident!((%[get_ them].to_ident_snake()), get_them); + assert_ident!((%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_ident_snake()), my_mixed_case_string_whichis_awesome_whatdo_youthink); } #[test] #[allow(non_snake_case)] fn test_ident_upper_snake() { - my_assert_ident_eq!([!ident_upper_snake! a B C _D E], A_BC_DE); - my_assert_ident_eq!([!ident_upper_snake! a 12 "3"], A123); - my_assert_ident_eq!([!ident_upper_snake! "MyString"], MY_STRING); - my_assert_ident_eq!([!ident_upper_snake! get_ [!snake! them]], GET_THEM); - my_assert_ident_eq!([!ident_upper_snake! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], MY_MIXED_CASE_STRING_WHICHIS_AWESOME_WHATDO_YOUTHINK); + assert_ident!((%ident_upper_snake[a B C _D E]), A_BC_DE); + assert_ident!((%[a B C _D E].to_ident_upper_snake()), A_BC_DE); + assert_ident!((%[a 12 "3"].to_ident_upper_snake()), A123); + assert_ident!((%["MyString"].to_ident_upper_snake()), MY_STRING); + assert_ident!((%[get_ them].to_ident_upper_snake()), GET_THEM); + assert_ident!((%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_ident_upper_snake()), MY_MIXED_CASE_STRING_WHICHIS_AWESOME_WHATDO_YOUTHINK); } diff --git a/tests/iteration.rs b/tests/iteration.rs new file mode 100644 index 00000000..743c0f52 --- /dev/null +++ b/tests/iteration.rs @@ -0,0 +1,466 @@ +#![allow(clippy::assertions_on_constants)] + +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; + +#[test] +#[cfg_attr(miri, ignore = "incompatible with miri")] +fn test_tokens_compilation_failures() { + if !should_run_ui_tests() { + // Some of the outputs are different on nightly, so don't test these + return; + } + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compilation_failures/iteration/*.rs"); +} + +#[test] +fn test_len() { + // Various iterators + assert_eq!(run!([1, 2, 3].into_iter().len()), 3); + assert_eq!(run!(%[%group[a b] c d].into_iter().len()), 3); + assert_eq!(run!((3..=5).into_iter().len()), 3); + assert_eq!(run!(%{ a: 1 }.into_iter().len()), 1); + assert_eq!(run!("Hello World".into_iter().len()), 11); + + // Others + assert_eq!(run!([1, 2, 3].len()), 3); + assert_eq!(run!(%[%group[a b] c d].len()), 3); + assert_eq!(run!((3..=5).len()), 3); + assert_eq!(run!(%{ a: 1 }.len()), 1); + assert_eq!(run!("Hello World".len()), 11); +} + +#[test] +fn test_is_empty() { + // Various iterators + assert!(run!([].into_iter().is_empty())); + assert!(!run!([1, 2, 3].into_iter().is_empty())); + assert!(run!(%[].into_iter().is_empty())); + assert!(!run!(%[%group[a b] c d].into_iter().is_empty())); + + // Others + assert!(run!([].is_empty())); + assert!(!run!([1, 2, 3].is_empty())); + assert!(run!(%[].is_empty())); + assert!(!run!(%[%group[a b] c d].is_empty())); + assert!(run!((3..3).is_empty())); + assert!(!run!((3..=5).is_empty())); + assert!(run!(%{}.is_empty())); + assert!(!run!(%{ a: 1 }.is_empty())); + assert!(run!("".is_empty())); + assert!(!run!("Hello World".is_empty())); +} + +#[test] +fn iterator_to_debug_string() { + assert_eq!( + run!([1, 2, 3].into_iter().to_debug_string()), + "[ 1, 2, 3]" + ); + assert_eq!( + run!(%[%group[a b] c d].into_iter().to_debug_string()), + "[ %[%group[a b]], %[c], %[d]]" + ); + assert_eq!( + run!((3..=5).into_iter().to_debug_string()), + "[ 3, 4, 5]" + ); + assert_eq!( + run!(%{ a: 1 }.into_iter().to_debug_string()), + r#"[ ["a", 1]]"# + ); + assert_eq!( + run!("Hello World".into_iter().to_debug_string()), + "[ 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd']" + ); + assert_eq!(run!("The world is such a great place and this is a very long string".into_iter().to_debug_string()), "[ 'T', 'h', 'e', ' ', 'w', 'o', 'r', 'l', 'd', ' ', 'i', 's', ' ', 's', 'u', 'c', 'h', ' ', 'a', ' ', ..<42 further items>]"); + assert_eq!(run!((0..10000).into_iter().to_debug_string()), "[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ..<9980 further items>]"); +} + +#[test] +fn iterator_to_string() { + assert_eq!(run!([1, 2, 3].into_iter().to_string()), "123"); + assert_eq!(run!(%[%group[a b] c d].into_iter().to_string()), "abcd"); + assert_eq!(run!((3..=5).into_iter().to_string()), "345"); + assert_eq!(run!(%{ a: 1 }.into_iter().to_string()), r#"a1"#); + assert_eq!(run!("Hello World".into_iter().to_string()), "Hello World"); + assert_eq!(run!((0..10).into_iter().to_string()), "0123456789"); +} + +#[test] +fn iterator_next() { + run! { + let iterator = ('A'..).into_iter(); + %[].assert(iterator.next() == 'A'); + %[].assert(iterator.next() == 'B'); + %[].assert(iterator.next() == 'C'); + } + run! { + let iterator = [1, 2].into_iter(); + %[].assert(iterator.next() == 1); + %[].assert(iterator.next() == 2); + %[].assert(iterator.next().is_none()); + } +} + +#[test] +fn iterator_skip_and_take() { + run! { + let iterator = ('A'..).into_iter(); + %[].assert_eq(iterator.skip(1).take(4).to_string(), "BCDE"); + } +} + +#[test] +fn test_empty_stream_is_empty() { + assert_eq!( + stream! { + %[] "hello" %[] %[] + }, + "hello" + ); + assert!(run!(%[].is_empty())); + assert!(run!(%[%[]].is_empty())); + assert!(run!(%[%[] %[]].is_empty())); + assert!(!run!(%[Not Empty].is_empty())); + assert!(!run!(%[%group[]].is_empty())); + assert!(!run!(%group[].is_empty())); + assert!(run! { + let x = %[]; + x.is_empty() + }); + assert!(!run! { + let x = %[]; + let x = %[#x is no longer empty]; + x.is_empty() + }); +} + +#[test] +fn test_length_and_group() { + assert_eq!( + run! { + %["hello" World].len() + }, + 2 + ); + assert_eq!(run! { %[("hello" World)].len() }, 1); + assert_eq!(run! { %[%group["hello" World]].len() }, 1); + assert_eq!( + run! { + let x = %[Hello "World" (1 2 3 4 5)]; + x.len() + }, + 3 + ); + assert_eq!( + run! { + let x = %[Hello "World" (1 2 3 4 5)]; + x.to_group().len() + }, + 1 + ); +} + +#[test] +fn test_output_array_to_stream() { + let x = run! { + let arr_contents = [1, %[,], 2]; + %[ + [#arr_contents] + ] + }; + assert_eq!(x[0], 1); + assert_eq!(x[1], 2); +} + +#[test] +fn test_intersperse() { + assert_eq!( + run! { + %[Hello World].intersperse(" ").to_debug_string() + }, + r#"[%[Hello], " ", %[World]]"# + ); + assert_eq!( + run! { + %[Hello World].intersperse(%{}).to_debug_string() + }, + "[%[Hello], %{}, %[World]]" + ); + assert_eq!( + run!(%[Hello World].intersperse(", ").to_string()), + "Hello, World" + ); + assert_eq!( + run!(%[Hello World].intersperse(%[_ "and" _]).to_ident().to_debug_string()), + "%[Hello_and_World]" + ); + assert_eq!( + run!( + %[Hello World] + .intersperse( + %[_ "and" _], + %{ add_trailing: true }, + ).to_string() + ), + "Hello_and_World_and_" + ); + assert_eq!( + run!( + %[The Quick Brown Fox].intersperse(%[]).to_string() + ), + "TheQuickBrownFox" + ); + assert_eq!( + run!(%[The Quick Brown Fox].intersperse(%[,], %{ add_trailing: true }).to_string()), + "The,Quick,Brown,Fox," + ); + assert_eq!( + run!(%[Red Green Blue].intersperse(", ", %{ final_separator: " and " }).to_string()), + "Red, Green and Blue" + ); + assert_eq!( + run!( + let settings = %{ add_trailing: true, final_separator: " and " }; + %[Red Green Blue].intersperse(", ", settings).to_string() + ), + "Red, Green, Blue and " + ); + assert_eq!( + run!( + let settings = %{ add_trailing: true, final_separator: " and " }; + %[].intersperse(", ", settings).to_string() + ), + "" + ); + assert_eq!( + run!( + let settings = %{ final_separator: "!" }; + %[SingleItem].intersperse(%[","], settings).to_string() + ), + "SingleItem" + ); + assert_eq!( + run!( + let settings = %{ final_separator: "!", add_trailing: true }; + %[SingleItem].intersperse(%[","], settings).to_string() + ), + "SingleItem!" + ); + assert_eq!( + run!( + let settings = %{ add_trailing: true }; + %[SingleItem].intersperse(",", settings).to_string() + ), + "SingleItem," + ); +} + +#[test] +fn complex_cases_for_intersperse_and_input_types() { + assert_eq!( + run! { + %[0 1 2 3].intersperse(%[_]).to_string() + }, + "0_1_2_3" + ); + assert_eq!( + run! { + (0..4).intersperse(%[_]).to_string() + }, + "0_1_2_3" + ); + assert_eq!( + run! { + %[0 1 2 3].intersperse("_").to_string() + }, + "0_1_2_3" + ); + assert_eq!( + run! { + [0, 1, 2, 3].intersperse("_").to_string() + }, + "0_1_2_3" + ); + // Stream containing two groups + assert_eq!( + run! { + let items = %group[0 1]; + let stream = %[#items #items]; // %[%group[0 1] %group[0 1]] + stream.intersperse(%[_]).to_string() + }, + "01_01", + ); + assert_eq!( + run! { + if false { %[0 1] } else { %[2 3] } + .intersperse(%[_]) as string + }, + "2_3" + ); + // All inputs can be variables + // Inputs can be in any order + assert_eq!( + run!( + let people = %[Anna Barbara Charlie]; + let separator = [", "]; + let final_separator = [" and "]; + let add_trailing = false; + people.intersperse( + separator, + %{ final_separator: final_separator, add_trailing }, + ).to_string() + ), + "Anna, Barbara and Charlie" + ); + // Add trailing is executed even if it's irrelevant because there are no items + // This is no longer particularly unexpected due to the expression model + assert_eq!( + run!( + let x = "NOT_EXECUTED"; + let _ = [].intersperse([], %{ add_trailing: { x = "EXECUTED"; false } }); + x + ), + "EXECUTED", + ); +} + +#[test] +fn test_zip() { + assert_eq!( + run!([%[Hello "Goodbye"], ["World", "Friend"]].zip().to_debug_string()), + r#"[[%[Hello], "World"], ["Goodbye", "Friend"]]"#, + ); + assert_eq!( + run!( + let countries = %["France" "Germany" "Italy"]; + let flags = %["🇫🇷" "🇩🇪" "🇮🇹"]; + let capitals = %["Paris" "Berlin" "Rome"]; + [countries, flags, capitals].zip().to_debug_string() + ), + r#"[["France", "🇫🇷", "Paris"], ["Germany", "🇩🇪", "Berlin"], ["Italy", "🇮🇹", "Rome"]]"#, + ); + assert_eq!( + run!( + let longer = %[A B C D]; + let shorter = [1, 2, 3]; + [longer, shorter].zip_truncated().to_debug_string() + ), + r#"[[%[A], 1], [%[B], 2], [%[C], 3]]"#, + ); + assert_eq!( + run!( + let letters = %[A B C]; + let numbers = [1, 2, 3]; + [letters, numbers].zip().to_debug_string() + ), + r#"[[%[A], 1], [%[B], 2], [%[C], 3]]"#, + ); + assert_eq!( + run!( + let letters = %[A B C]; + let numbers = [1, 2, 3]; + %{ number: numbers, letter: letters }.zip().to_debug_string() + ), + r#"[%{ letter: %[A], number: 1 }, %{ letter: %[B], number: 2 }, %{ letter: %[C], number: 3 }]"#, + ); + assert_eq!(run!([].zip().to_debug_string()), r#"[]"#); + assert_eq!(run!(%{}.zip().to_debug_string()), r#"[]"#); + // When a stream iterates, we look at each token tree, form a singleton stream from it to be the coerced value. + // In reality this means that a stream is an iterator of singleton streams, so zipping it just returns itself + // (wrapped in a couple of arrays) + assert_eq!( + run!(%[%group[A B] C Hello D].zip().to_debug_string()), + r#"[[%[%group[A B]], %[C], %[Hello], %[D]]]"# + ); +} + +#[test] +fn test_zip_with_for() { + assert_eq!( + run! { + let countries = %[France Germany Italy]; + let flags = ["🇫🇷", "🇩🇪", "🇮🇹"]; + let capitals = %["Paris" "Berlin" "Rome"]; + let facts = []; + for [country, flag, capital] in [countries, flags, capitals].zip() { + facts.push(%["=> The capital of " #country " is " #capital " and its flag is " #flag].to_string()); + } + + "The facts are:\n" + facts.intersperse("\n").to_string() + "\n" + }, + r#"The facts are: +=> The capital of France is Paris and its flag is 🇫🇷 +=> The capital of Germany is Berlin and its flag is 🇩🇪 +=> The capital of Italy is Rome and its flag is 🇮🇹 +"#, + ); +} + +#[test] +fn test_split() { + // Empty separators are allowed, and split on every token + // In this case, drop_empty_start / drop_empty_end are ignored + assert_eq!( + run!(%[A::B].split(%[]).to_debug_string()), + "[%[A], %[:], %[:], %[B]]" + ); + + // Double separators are allowed + assert_eq!( + run!(%[A::B::C].split(%[::]).to_debug_string()), + "[%[A], %[B], %[C]]" + ); + // Trailing separator is ignored by default + assert_eq!( + run!(%[Pizza, Mac and Cheese, Hamburger,].split(%[,]).to_debug_string()), + "[%[Pizza], %[Mac and Cheese], %[Hamburger]]" + ); + // Split typically returns an array. + // When using to_stream_grouped(), empty groups are included except at the end. + assert_eq!( + run!(%[::A::B::::C::].split(%[::]).to_stream_grouped().to_debug_string()), + "%[%group[] %group[A] %group[B] %group[] %group[C]]" + ); + // Stream and separator are both interpreted + assert_eq!( + run!( + let x = %[;]; + let options = %{ + drop_empty_start: true, + drop_empty_middle: true, + drop_empty_end: true, + }; + %[;A;;B;C;D #x E;].split(x, options).to_stream_grouped().to_debug_string() + ), + "%[%group[A] %group[B] %group[C] %group[D] %group[E]]" + ); + // Drop empty false works + assert_eq!( + run!( + let x = %[;]; + let options = %{ + drop_empty_start: false, + drop_empty_middle: false, + drop_empty_end: false, + }; + %[;A;;B;C;D #x E;].split(x, options).to_stream_grouped().to_debug_string() + ), + "%[%group[] %group[A] %group[] %group[B] %group[C] %group[D] %group[E] %group[]]" + ); + // Drop empty middle works + assert_eq!( + run!( + let options = %{ + drop_empty_start: false, + drop_empty_middle: true, + drop_empty_end: false, + }; + %[;A;;B;;;;E;].split(%[;], options).to_debug_string() + ), + "[%[], %[A], %[B], %[E], %[]]" + ); +} diff --git a/tests/literal.rs b/tests/literal.rs index 0f9ad07d..935da30f 100644 --- a/tests/literal.rs +++ b/tests/literal.rs @@ -1,46 +1,108 @@ -use preinterpret::preinterpret; +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; -macro_rules! my_assert_eq { - ($input:tt, $($output:tt)*) => { - assert_eq!(preinterpret!($input), $($output)*); - }; +#[test] +fn test_concatenated_stream_string_literal() { + assert_eq!(run!(%string[hello World! "\""]), "helloWorld!\""); } #[test] fn test_string_literal() { - my_assert_eq!([!literal! '"' hello World! "\""], "helloWorld!"); + assert_eq!(run!(%literal['"' hello World! "\""]), "helloWorld!"); + assert_eq!(run!(%['"' hello World! "\""].to_literal()), "helloWorld!"); +} + +#[test] +fn test_raw_string_literal() { + // This demonstrates that rust raw strings become string values when interpreted + // Because if they became an UnsupportedLiteral, then + wouldn't work + assert_eq!(run!(r#"hello " World""# + "!!!"), "hello \" World\"!!!"); } #[test] fn test_byte_string_literal() { - my_assert_eq!([!literal! b '"' hello World! "\""], b"helloWorld!"); + assert_eq!( + run!(%[b '"' hello World! "\""].to_literal()), + b"helloWorld!" + ); } #[test] fn test_c_string_literal() { - my_assert_eq!([!literal! c '"' hello World! "\""], c"helloWorld!"); + assert_eq!( + run!(%[c '"' hello World! "\""].to_literal()), + c"helloWorld!" + ); + run!(%[].assert_eq(%[c '"' hello World! "\""].to_literal().to_debug_string(), "c\"helloWorld!\"")); } #[test] fn test_integer_literal() { - my_assert_eq!([!literal! "123" 456], 123456); - my_assert_eq!([!literal! 456u "32"], 456); - my_assert_eq!([!literal! 000 u64], 0); + assert_eq!(run!(%["123" 456].to_literal()), 123456); + assert_eq!(run!(%[456u "32"].to_literal()), 456); + assert_eq!(run!(%[000 u64].to_literal()), 0); + + run!(%[].assert_eq(%[456u "32"].to_literal().to_debug_string(), "456u32")); +} + +#[test] +fn test_max_u128_literal() { + // Verifies that u128::MAX (which doesn't fit in i128) can still be passed through as-is + // because it falls back to being an UnsupportedLiteral + let x: u128 = run!(340_282_366_920_938_463_463_374_607_431_768_211_455u128); + assert_eq!(x, u128::MAX); + let x2: u128 = run!(u128::MAX); + assert_eq!(x2, u128::MAX); +} + +#[test] +fn test_min_literals() { + assert_eq!(run!(i8::MIN), i8::MIN); + assert_eq!(run!(i16::MIN), i16::MIN); + assert_eq!(run!(i32::MIN), i32::MIN); + assert_eq!(run!(i64::MIN), i64::MIN); + assert_eq!(run!(i128::MIN), i128::MIN); + assert_eq!(run!(isize::MIN), isize::MIN); + assert_eq!(run!(u8::MIN), u8::MIN); + assert_eq!(run!(u16::MIN), u16::MIN); + assert_eq!(run!(u32::MIN), u32::MIN); + assert_eq!(run!(u64::MIN), u64::MIN); + assert_eq!(run!(u128::MIN), u128::MIN); + assert_eq!(run!(usize::MIN), usize::MIN); +} + +#[test] +fn test_max_literals() { + assert_eq!(run!(i8::MAX), i8::MAX); + assert_eq!(run!(i16::MAX), i16::MAX); + assert_eq!(run!(i32::MAX), i32::MAX); + assert_eq!(run!(i64::MAX), i64::MAX); + assert_eq!(run!(i128::MAX), i128::MAX); + assert_eq!(run!(isize::MAX), isize::MAX); + assert_eq!(run!(u8::MAX), u8::MAX); + assert_eq!(run!(u16::MAX), u16::MAX); + assert_eq!(run!(u32::MAX), u32::MAX); + assert_eq!(run!(u64::MAX), u64::MAX); + assert_eq!(run!(u128::MAX), u128::MAX); + assert_eq!(run!(usize::MAX), usize::MAX); } #[test] fn test_float_literal() { - my_assert_eq!([!literal! 0 . 123], 0.123); - my_assert_eq!([!literal! 677f32], 677f32); - my_assert_eq!([!literal! "12" 9f64], 129f64); + assert_eq!(run!(%[0 . 123].to_literal()), 0.123); + assert_eq!(run!(%[677f32].to_literal()), 677f32); + assert_eq!(run!(%["12" 9f64].to_literal()), 129f64); + + run!(%[].assert_eq(%["12" 9f64].to_literal().to_debug_string(), "129f64")); } #[test] fn test_character() { - my_assert_eq!([!literal! "'" 7 "'"], '7'); + assert_eq!(run!(%["'" 7 "'"].to_literal()), '7'); } #[test] fn test_byte_character() { - my_assert_eq!([!literal! "b'a'"], b'a'); + assert_eq!(run!(%["b'a'"].to_literal()), b'a'); } diff --git a/tests/operations.rs b/tests/operations.rs new file mode 100644 index 00000000..a1c5a95a --- /dev/null +++ b/tests/operations.rs @@ -0,0 +1,1661 @@ +//! Tests for all binary and unary operations on all value types. +//! +//! This test file covers: +//! - All binary operations: +, -, *, /, %, &&, ||, ^, &, |, <<, >>, ==, !=, <, <=, >, >= +//! - All compound assignment operations: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= +//! - All type combinations for integers and floats +//! - Operations on booleans, strings, chars, and streams + +#![allow(clippy::assertions_on_constants)] +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; + +#[test] +#[cfg_attr(miri, ignore = "incompatible with miri")] +fn test_operation_compilation_failures() { + if !should_run_ui_tests() { + return; + } + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compilation_failures/operations/*.rs"); +} + +// ============================================================================= +// INTEGER ARITHMETIC OPERATIONS +// ============================================================================= + +mod integer_arithmetic { + use super::*; + + // ------------------------------------------------------------------------- + // Addition (+) + // ------------------------------------------------------------------------- + #[test] + fn addition_untyped_untyped() { + assert_eq!(run!(1 + 2), 3); + assert_eq!(run!(0 + 0), 0); + assert_eq!(run!(100 + 200), 300); + } + + #[test] + fn addition_typed_typed() { + // Signed integers + assert_eq!(run!(1i8 + 2i8), 3i8); + assert_eq!(run!(1i16 + 2i16), 3i16); + assert_eq!(run!(1i32 + 2i32), 3i32); + assert_eq!(run!(1i64 + 2i64), 3i64); + assert_eq!(run!(1i128 + 2i128), 3i128); + assert_eq!(run!(1isize + 2isize), 3isize); + + // Unsigned integers + assert_eq!(run!(1u8 + 2u8), 3u8); + assert_eq!(run!(1u16 + 2u16), 3u16); + assert_eq!(run!(1u32 + 2u32), 3u32); + assert_eq!(run!(1u64 + 2u64), 3u64); + assert_eq!(run!(1u128 + 2u128), 3u128); + assert_eq!(run!(1usize + 2usize), 3usize); + } + + #[test] + fn addition_typed_untyped() { + // Signed integers + assert_eq!(run!(1i8 + 2), 3i8); + assert_eq!(run!(1i16 + 2), 3i16); + assert_eq!(run!(1i32 + 2), 3i32); + assert_eq!(run!(1i64 + 2), 3i64); + assert_eq!(run!(1i128 + 2), 3i128); + assert_eq!(run!(1isize + 2), 3isize); + + // Unsigned integers + assert_eq!(run!(1u8 + 2), 3u8); + assert_eq!(run!(1u16 + 2), 3u16); + assert_eq!(run!(1u32 + 2), 3u32); + assert_eq!(run!(1u64 + 2), 3u64); + assert_eq!(run!(1u128 + 2), 3u128); + assert_eq!(run!(1usize + 2), 3usize); + } + + #[test] + fn addition_untyped_typed() { + // Signed integers + assert_eq!(run!(1 + 2i8), 3i8); + assert_eq!(run!(1 + 2i16), 3i16); + assert_eq!(run!(1 + 2i32), 3i32); + assert_eq!(run!(1 + 2i64), 3i64); + assert_eq!(run!(1 + 2i128), 3i128); + assert_eq!(run!(1 + 2isize), 3isize); + + // Unsigned integers + assert_eq!(run!(1 + 2u8), 3u8); + assert_eq!(run!(1 + 2u16), 3u16); + assert_eq!(run!(1 + 2u32), 3u32); + assert_eq!(run!(1 + 2u64), 3u64); + assert_eq!(run!(1 + 2u128), 3u128); + assert_eq!(run!(1 + 2usize), 3usize); + } + + // ------------------------------------------------------------------------- + // Subtraction (-) + // ------------------------------------------------------------------------- + #[test] + fn subtraction_untyped_untyped() { + assert_eq!(run!(5 - 3), 2); + assert_eq!(run!(0 - 0), 0); + assert_eq!(run!(100 - 50), 50); + } + + #[test] + fn subtraction_typed_typed() { + // Signed integers + assert_eq!(run!(5i8 - 3i8), 2i8); + assert_eq!(run!(5i16 - 3i16), 2i16); + assert_eq!(run!(5i32 - 3i32), 2i32); + assert_eq!(run!(5i64 - 3i64), 2i64); + assert_eq!(run!(5i128 - 3i128), 2i128); + assert_eq!(run!(5isize - 3isize), 2isize); + + // Unsigned integers + assert_eq!(run!(5u8 - 3u8), 2u8); + assert_eq!(run!(5u16 - 3u16), 2u16); + assert_eq!(run!(5u32 - 3u32), 2u32); + assert_eq!(run!(5u64 - 3u64), 2u64); + assert_eq!(run!(5u128 - 3u128), 2u128); + assert_eq!(run!(5usize - 3usize), 2usize); + } + + #[test] + fn subtraction_typed_untyped() { + // Signed integers + assert_eq!(run!(5i8 - 3), 2i8); + assert_eq!(run!(5i16 - 3), 2i16); + assert_eq!(run!(5i32 - 3), 2i32); + assert_eq!(run!(5i64 - 3), 2i64); + assert_eq!(run!(5i128 - 3), 2i128); + assert_eq!(run!(5isize - 3), 2isize); + + // Unsigned integers + assert_eq!(run!(5u8 - 3), 2u8); + assert_eq!(run!(5u16 - 3), 2u16); + assert_eq!(run!(5u32 - 3), 2u32); + assert_eq!(run!(5u64 - 3), 2u64); + assert_eq!(run!(5u128 - 3), 2u128); + assert_eq!(run!(5usize - 3), 2usize); + } + + #[test] + fn subtraction_untyped_typed() { + // Signed integers + assert_eq!(run!(5 - 3i8), 2i8); + assert_eq!(run!(5 - 3i16), 2i16); + assert_eq!(run!(5 - 3i32), 2i32); + assert_eq!(run!(5 - 3i64), 2i64); + assert_eq!(run!(5 - 3i128), 2i128); + assert_eq!(run!(5 - 3isize), 2isize); + + // Unsigned integers + assert_eq!(run!(5 - 3u8), 2u8); + assert_eq!(run!(5 - 3u16), 2u16); + assert_eq!(run!(5 - 3u32), 2u32); + assert_eq!(run!(5 - 3u64), 2u64); + assert_eq!(run!(5 - 3u128), 2u128); + assert_eq!(run!(5 - 3usize), 2usize); + } + + // ------------------------------------------------------------------------- + // Multiplication (*) + // ------------------------------------------------------------------------- + #[test] + fn multiplication_untyped_untyped() { + assert_eq!(run!(3 * 4), 12); + assert_eq!(run!(0 * 100), 0); + assert_eq!(run!(7 * 8), 56); + } + + #[test] + fn multiplication_typed_typed() { + // Signed integers + assert_eq!(run!(3i8 * 4i8), 12i8); + assert_eq!(run!(3i16 * 4i16), 12i16); + assert_eq!(run!(3i32 * 4i32), 12i32); + assert_eq!(run!(3i64 * 4i64), 12i64); + assert_eq!(run!(3i128 * 4i128), 12i128); + assert_eq!(run!(3isize * 4isize), 12isize); + + // Unsigned integers + assert_eq!(run!(3u8 * 4u8), 12u8); + assert_eq!(run!(3u16 * 4u16), 12u16); + assert_eq!(run!(3u32 * 4u32), 12u32); + assert_eq!(run!(3u64 * 4u64), 12u64); + assert_eq!(run!(3u128 * 4u128), 12u128); + assert_eq!(run!(3usize * 4usize), 12usize); + } + + #[test] + fn multiplication_typed_untyped() { + // Signed integers + assert_eq!(run!(3i8 * 4), 12i8); + assert_eq!(run!(3i16 * 4), 12i16); + assert_eq!(run!(3i32 * 4), 12i32); + assert_eq!(run!(3i64 * 4), 12i64); + assert_eq!(run!(3i128 * 4), 12i128); + assert_eq!(run!(3isize * 4), 12isize); + + // Unsigned integers + assert_eq!(run!(3u8 * 4), 12u8); + assert_eq!(run!(3u16 * 4), 12u16); + assert_eq!(run!(3u32 * 4), 12u32); + assert_eq!(run!(3u64 * 4), 12u64); + assert_eq!(run!(3u128 * 4), 12u128); + assert_eq!(run!(3usize * 4), 12usize); + } + + #[test] + fn multiplication_untyped_typed() { + // Signed integers + assert_eq!(run!(3 * 4i8), 12i8); + assert_eq!(run!(3 * 4i16), 12i16); + assert_eq!(run!(3 * 4i32), 12i32); + assert_eq!(run!(3 * 4i64), 12i64); + assert_eq!(run!(3 * 4i128), 12i128); + assert_eq!(run!(3 * 4isize), 12isize); + + // Unsigned integers + assert_eq!(run!(3 * 4u8), 12u8); + assert_eq!(run!(3 * 4u16), 12u16); + assert_eq!(run!(3 * 4u32), 12u32); + assert_eq!(run!(3 * 4u64), 12u64); + assert_eq!(run!(3 * 4u128), 12u128); + assert_eq!(run!(3 * 4usize), 12usize); + } + + // ------------------------------------------------------------------------- + // Division (/) + // ------------------------------------------------------------------------- + #[test] + fn division_untyped_untyped() { + assert_eq!(run!(10 / 2), 5); + assert_eq!(run!(7 / 3), 2); + assert_eq!(run!(100 / 10), 10); + } + + #[test] + fn division_typed_typed() { + // Signed integers + assert_eq!(run!(10i8 / 2i8), 5i8); + assert_eq!(run!(10i16 / 2i16), 5i16); + assert_eq!(run!(10i32 / 2i32), 5i32); + assert_eq!(run!(10i64 / 2i64), 5i64); + assert_eq!(run!(10i128 / 2i128), 5i128); + assert_eq!(run!(10isize / 2isize), 5isize); + + // Unsigned integers + assert_eq!(run!(10u8 / 2u8), 5u8); + assert_eq!(run!(10u16 / 2u16), 5u16); + assert_eq!(run!(10u32 / 2u32), 5u32); + assert_eq!(run!(10u64 / 2u64), 5u64); + assert_eq!(run!(10u128 / 2u128), 5u128); + assert_eq!(run!(10usize / 2usize), 5usize); + } + + #[test] + fn division_typed_untyped() { + // Signed integers + assert_eq!(run!(10i8 / 2), 5i8); + assert_eq!(run!(10i16 / 2), 5i16); + assert_eq!(run!(10i32 / 2), 5i32); + assert_eq!(run!(10i64 / 2), 5i64); + assert_eq!(run!(10i128 / 2), 5i128); + assert_eq!(run!(10isize / 2), 5isize); + + // Unsigned integers + assert_eq!(run!(10u8 / 2), 5u8); + assert_eq!(run!(10u16 / 2), 5u16); + assert_eq!(run!(10u32 / 2), 5u32); + assert_eq!(run!(10u64 / 2), 5u64); + assert_eq!(run!(10u128 / 2), 5u128); + assert_eq!(run!(10usize / 2), 5usize); + } + + #[test] + fn division_untyped_typed() { + // Signed integers + assert_eq!(run!(10 / 2i8), 5i8); + assert_eq!(run!(10 / 2i16), 5i16); + assert_eq!(run!(10 / 2i32), 5i32); + assert_eq!(run!(10 / 2i64), 5i64); + assert_eq!(run!(10 / 2i128), 5i128); + assert_eq!(run!(10 / 2isize), 5isize); + + // Unsigned integers + assert_eq!(run!(10 / 2u8), 5u8); + assert_eq!(run!(10 / 2u16), 5u16); + assert_eq!(run!(10 / 2u32), 5u32); + assert_eq!(run!(10 / 2u64), 5u64); + assert_eq!(run!(10 / 2u128), 5u128); + assert_eq!(run!(10 / 2usize), 5usize); + } + + // ------------------------------------------------------------------------- + // Remainder (%) + // ------------------------------------------------------------------------- + #[test] + fn remainder_untyped_untyped() { + assert_eq!(run!(10 % 3), 1); + assert_eq!(run!(7 % 2), 1); + assert_eq!(run!(100 % 10), 0); + } + + #[test] + fn remainder_typed_typed() { + // Signed integers + assert_eq!(run!(10i8 % 3i8), 1i8); + assert_eq!(run!(10i16 % 3i16), 1i16); + assert_eq!(run!(10i32 % 3i32), 1i32); + assert_eq!(run!(10i64 % 3i64), 1i64); + assert_eq!(run!(10i128 % 3i128), 1i128); + assert_eq!(run!(10isize % 3isize), 1isize); + + // Unsigned integers + assert_eq!(run!(10u8 % 3u8), 1u8); + assert_eq!(run!(10u16 % 3u16), 1u16); + assert_eq!(run!(10u32 % 3u32), 1u32); + assert_eq!(run!(10u64 % 3u64), 1u64); + assert_eq!(run!(10u128 % 3u128), 1u128); + assert_eq!(run!(10usize % 3usize), 1usize); + } + + #[test] + fn remainder_typed_untyped() { + // Signed integers + assert_eq!(run!(10i8 % 3), 1i8); + assert_eq!(run!(10i16 % 3), 1i16); + assert_eq!(run!(10i32 % 3), 1i32); + assert_eq!(run!(10i64 % 3), 1i64); + assert_eq!(run!(10i128 % 3), 1i128); + assert_eq!(run!(10isize % 3), 1isize); + + // Unsigned integers + assert_eq!(run!(10u8 % 3), 1u8); + assert_eq!(run!(10u16 % 3), 1u16); + assert_eq!(run!(10u32 % 3), 1u32); + assert_eq!(run!(10u64 % 3), 1u64); + assert_eq!(run!(10u128 % 3), 1u128); + assert_eq!(run!(10usize % 3), 1usize); + } + + #[test] + fn remainder_untyped_typed() { + // Signed integers + assert_eq!(run!(10 % 3i8), 1i8); + assert_eq!(run!(10 % 3i16), 1i16); + assert_eq!(run!(10 % 3i32), 1i32); + assert_eq!(run!(10 % 3i64), 1i64); + assert_eq!(run!(10 % 3i128), 1i128); + assert_eq!(run!(10 % 3isize), 1isize); + + // Unsigned integers + assert_eq!(run!(10 % 3u8), 1u8); + assert_eq!(run!(10 % 3u16), 1u16); + assert_eq!(run!(10 % 3u32), 1u32); + assert_eq!(run!(10 % 3u64), 1u64); + assert_eq!(run!(10 % 3u128), 1u128); + assert_eq!(run!(10 % 3usize), 1usize); + } + + // ------------------------------------------------------------------------- + // Negation (unary -) + // ------------------------------------------------------------------------- + #[test] + fn negation_signed_integers() { + assert_eq!(run!(-5i8), -5i8); + assert_eq!(run!(-5i16), -5i16); + assert_eq!(run!(-5i32), -5i32); + assert_eq!(run!(-5i64), -5i64); + assert_eq!(run!(-5i128), -5i128); + assert_eq!(run!(-5isize), -5isize); + assert_eq!(run!(-(-10i32)), 10i32); + } + + #[test] + fn negation_untyped() { + assert_eq!(run!(-5), -5); + assert_eq!(run!(-(-10)), 10); + } +} + +// ============================================================================= +// INTEGER BITWISE OPERATIONS +// ============================================================================= + +mod integer_bitwise { + use super::*; + + // ------------------------------------------------------------------------- + // Bitwise XOR (^) + // ------------------------------------------------------------------------- + #[test] + fn bitxor_untyped_untyped() { + assert_eq!(run!(0b1010 ^ 0b1100), 0b0110); + assert_eq!(run!(0xFF ^ 0x0F), 0xF0); + } + + #[test] + fn bitxor_typed_typed() { + assert_eq!(run!(0b1010u8 ^ 0b1100u8), 0b0110u8); + assert_eq!(run!(0b1010u16 ^ 0b1100u16), 0b0110u16); + assert_eq!(run!(0b1010u32 ^ 0b1100u32), 0b0110u32); + assert_eq!(run!(0b1010u64 ^ 0b1100u64), 0b0110u64); + assert_eq!(run!(0b1010u128 ^ 0b1100u128), 0b0110u128); + assert_eq!(run!(0b1010usize ^ 0b1100usize), 0b0110usize); + assert_eq!(run!(0b1010i8 ^ 0b1100i8), 0b0110i8); + assert_eq!(run!(0b1010i16 ^ 0b1100i16), 0b0110i16); + assert_eq!(run!(0b1010i32 ^ 0b1100i32), 0b0110i32); + assert_eq!(run!(0b1010i64 ^ 0b1100i64), 0b0110i64); + assert_eq!(run!(0b1010i128 ^ 0b1100i128), 0b0110i128); + assert_eq!(run!(0b1010isize ^ 0b1100isize), 0b0110isize); + } + + #[test] + fn bitxor_typed_untyped() { + assert_eq!(run!(0b1010u8 ^ 0b1100), 0b0110u8); + assert_eq!(run!(0b1010u32 ^ 0b1100), 0b0110u32); + assert_eq!(run!(0b1010i32 ^ 0b1100), 0b0110i32); + } + + #[test] + fn bitxor_untyped_typed() { + assert_eq!(run!(0b1010 ^ 0b1100u8), 0b0110u8); + assert_eq!(run!(0b1010 ^ 0b1100u32), 0b0110u32); + assert_eq!(run!(0b1010 ^ 0b1100i32), 0b0110i32); + } + + // ------------------------------------------------------------------------- + // Bitwise AND (&) + // ------------------------------------------------------------------------- + #[test] + fn bitand_untyped_untyped() { + assert_eq!(run!(0b1010 & 0b1100), 0b1000); + assert_eq!(run!(0xFF & 0x0F), 0x0F); + } + + #[test] + fn bitand_typed_typed() { + assert_eq!(run!(0b1010u8 & 0b1100u8), 0b1000u8); + assert_eq!(run!(0b1010u16 & 0b1100u16), 0b1000u16); + assert_eq!(run!(0b1010u32 & 0b1100u32), 0b1000u32); + assert_eq!(run!(0b1010u64 & 0b1100u64), 0b1000u64); + assert_eq!(run!(0b1010u128 & 0b1100u128), 0b1000u128); + assert_eq!(run!(0b1010usize & 0b1100usize), 0b1000usize); + assert_eq!(run!(0b1010i8 & 0b1100i8), 0b1000i8); + assert_eq!(run!(0b1010i16 & 0b1100i16), 0b1000i16); + assert_eq!(run!(0b1010i32 & 0b1100i32), 0b1000i32); + assert_eq!(run!(0b1010i64 & 0b1100i64), 0b1000i64); + assert_eq!(run!(0b1010i128 & 0b1100i128), 0b1000i128); + assert_eq!(run!(0b1010isize & 0b1100isize), 0b1000isize); + } + + #[test] + fn bitand_typed_untyped() { + assert_eq!(run!(0b1010u8 & 0b1100), 0b1000u8); + assert_eq!(run!(0b1010u32 & 0b1100), 0b1000u32); + assert_eq!(run!(0b1010i32 & 0b1100), 0b1000i32); + } + + #[test] + fn bitand_untyped_typed() { + assert_eq!(run!(0b1010 & 0b1100u8), 0b1000u8); + assert_eq!(run!(0b1010 & 0b1100u32), 0b1000u32); + assert_eq!(run!(0b1010 & 0b1100i32), 0b1000i32); + } + + // ------------------------------------------------------------------------- + // Bitwise OR (|) + // ------------------------------------------------------------------------- + #[test] + fn bitor_untyped_untyped() { + assert_eq!(run!(0b1010 | 0b1100), 0b1110); + assert_eq!(run!(0xF0 | 0x0F), 0xFF); + } + + #[test] + fn bitor_typed_typed() { + assert_eq!(run!(0b1010u8 | 0b1100u8), 0b1110u8); + assert_eq!(run!(0b1010u16 | 0b1100u16), 0b1110u16); + assert_eq!(run!(0b1010u32 | 0b1100u32), 0b1110u32); + assert_eq!(run!(0b1010u64 | 0b1100u64), 0b1110u64); + assert_eq!(run!(0b1010u128 | 0b1100u128), 0b1110u128); + assert_eq!(run!(0b1010usize | 0b1100usize), 0b1110usize); + assert_eq!(run!(0b1010i8 | 0b1100i8), 0b1110i8); + assert_eq!(run!(0b1010i16 | 0b1100i16), 0b1110i16); + assert_eq!(run!(0b1010i32 | 0b1100i32), 0b1110i32); + assert_eq!(run!(0b1010i64 | 0b1100i64), 0b1110i64); + assert_eq!(run!(0b1010i128 | 0b1100i128), 0b1110i128); + assert_eq!(run!(0b1010isize | 0b1100isize), 0b1110isize); + } + + #[test] + fn bitor_typed_untyped() { + assert_eq!(run!(0b1010u8 | 0b0100), 0b1110u8); + assert_eq!(run!(0b1010u32 | 0b0100), 0b1110u32); + assert_eq!(run!(0b1010i32 | 0b0100), 0b1110i32); + } + + #[test] + fn bitor_untyped_typed() { + assert_eq!(run!(0b1010 | 0b0100u8), 0b1110u8); + assert_eq!(run!(0b1010 | 0b0100u32), 0b1110u32); + assert_eq!(run!(0b1010 | 0b0100i32), 0b1110i32); + } + + // ------------------------------------------------------------------------- + // Shift Left (<<) + // ------------------------------------------------------------------------- + #[test] + fn shift_left_untyped() { + assert_eq!(run!(1 << 4), 16); + assert_eq!(run!(5 << 2), 20); + } + + #[test] + fn shift_left_typed() { + assert_eq!(run!(1u8 << 4), 16u8); + assert_eq!(run!(1u16 << 4), 16u16); + assert_eq!(run!(1u32 << 4), 16u32); + assert_eq!(run!(1u64 << 4), 16u64); + assert_eq!(run!(1u128 << 4), 16u128); + assert_eq!(run!(1usize << 4), 16usize); + assert_eq!(run!(1i8 << 4), 16i8); + assert_eq!(run!(1i16 << 4), 16i16); + assert_eq!(run!(1i32 << 4), 16i32); + assert_eq!(run!(1i64 << 4), 16i64); + assert_eq!(run!(1i128 << 4), 16i128); + assert_eq!(run!(1isize << 4), 16isize); + } + + // ------------------------------------------------------------------------- + // Shift Right (>>) + // ------------------------------------------------------------------------- + #[test] + fn shift_right_untyped() { + assert_eq!(run!(16 >> 2), 4); + assert_eq!(run!(100 >> 1), 50); + } + + #[test] + fn shift_right_typed() { + assert_eq!(run!(16u8 >> 2), 4u8); + assert_eq!(run!(16u16 >> 2), 4u16); + assert_eq!(run!(16u32 >> 2), 4u32); + assert_eq!(run!(16u64 >> 2), 4u64); + assert_eq!(run!(16u128 >> 2), 4u128); + assert_eq!(run!(16usize >> 2), 4usize); + assert_eq!(run!(16i8 >> 2), 4i8); + assert_eq!(run!(16i16 >> 2), 4i16); + assert_eq!(run!(16i32 >> 2), 4i32); + assert_eq!(run!(16i64 >> 2), 4i64); + assert_eq!(run!(16i128 >> 2), 4i128); + assert_eq!(run!(16isize >> 2), 4isize); + } +} + +// ============================================================================= +// INTEGER COMPARISON OPERATIONS +// ============================================================================= + +mod integer_comparison { + use super::*; + + #[test] + fn equal_untyped() { + assert!(run!(5 == 5)); + assert!(!run!(5 == 6)); + } + + #[test] + fn equal_typed() { + assert!(run!(5u8 == 5u8)); + assert!(run!(5u16 == 5u16)); + assert!(run!(5u32 == 5u32)); + assert!(run!(5u64 == 5u64)); + assert!(run!(5u128 == 5u128)); + assert!(run!(5usize == 5usize)); + assert!(run!(5i8 == 5i8)); + assert!(run!(5i16 == 5i16)); + assert!(run!(5i32 == 5i32)); + assert!(run!(5i64 == 5i64)); + assert!(run!(5i128 == 5i128)); + assert!(run!(5isize == 5isize)); + } + + #[test] + fn equal_typed_untyped() { + assert!(run!(5u32 == 5)); + assert!(run!(5i32 == 5)); + assert!(run!(5 == 5u32)); + assert!(run!(5 == 5i32)); + } + + #[test] + fn not_equal_untyped() { + assert!(run!(5 != 6)); + assert!(!run!(5 != 5)); + } + + #[test] + fn not_equal_typed() { + assert!(run!(5u32 != 6u32)); + assert!(run!(5i32 != 6i32)); + assert!(!run!(5u32 != 5u32)); + } + + #[test] + fn less_than_untyped() { + assert!(run!(3 < 5)); + assert!(!run!(5 < 5)); + assert!(!run!(7 < 5)); + } + + #[test] + fn less_than_typed() { + assert!(run!(3u32 < 5u32)); + assert!(run!(3i32 < 5i32)); + assert!(run!(-5i32 < 5i32)); + } + + #[test] + fn less_than_typed_untyped() { + assert!(run!(3u32 < 5)); + assert!(run!(3 < 5u32)); + } + + #[test] + fn less_than_or_equal_untyped() { + assert!(run!(3 <= 5)); + assert!(run!(5 <= 5)); + assert!(!run!(7 <= 5)); + } + + #[test] + fn less_than_or_equal_typed() { + assert!(run!(3u32 <= 5u32)); + assert!(run!(5u32 <= 5u32)); + assert!(!run!(7u32 <= 5u32)); + } + + #[test] + fn greater_than_untyped() { + assert!(run!(7 > 5)); + assert!(!run!(5 > 5)); + assert!(!run!(3 > 5)); + } + + #[test] + fn greater_than_typed() { + assert!(run!(7u32 > 5u32)); + assert!(run!(7i32 > 5i32)); + } + + #[test] + fn greater_than_or_equal_untyped() { + assert!(run!(7 >= 5)); + assert!(run!(5 >= 5)); + assert!(!run!(3 >= 5)); + } + + #[test] + fn greater_than_or_equal_typed() { + assert!(run!(7u32 >= 5u32)); + assert!(run!(5u32 >= 5u32)); + assert!(!run!(3u32 >= 5u32)); + } +} + +// ============================================================================= +// INTEGER COMPOUND ASSIGNMENT OPERATIONS +// ============================================================================= + +mod integer_compound_assignment { + use super::*; + + #[test] + fn add_assign_all_types() { + // Untyped + assert_eq!(run!(let x = 5; x += 3; x), 8); + + // Typed + assert_eq!(run!(let x = 5u8; x += 3; x), 8u8); + assert_eq!(run!(let x = 5u16; x += 3; x), 8u16); + assert_eq!(run!(let x = 5u32; x += 3; x), 8u32); + assert_eq!(run!(let x = 5u64; x += 3; x), 8u64); + assert_eq!(run!(let x = 5u128; x += 3; x), 8u128); + assert_eq!(run!(let x = 5usize; x += 3; x), 8usize); + assert_eq!(run!(let x = 5i8; x += 3; x), 8i8); + assert_eq!(run!(let x = 5i16; x += 3; x), 8i16); + assert_eq!(run!(let x = 5i32; x += 3; x), 8i32); + assert_eq!(run!(let x = 5i64; x += 3; x), 8i64); + assert_eq!(run!(let x = 5i128; x += 3; x), 8i128); + assert_eq!(run!(let x = 5isize; x += 3; x), 8isize); + } + + #[test] + fn sub_assign_all_types() { + assert_eq!(run!(let x = 10; x -= 3; x), 7); + assert_eq!(run!(let x = 10u32; x -= 3; x), 7u32); + assert_eq!(run!(let x = 10i32; x -= 3; x), 7i32); + } + + #[test] + fn mul_assign_all_types() { + assert_eq!(run!(let x = 5; x *= 3; x), 15); + assert_eq!(run!(let x = 5u32; x *= 3; x), 15u32); + assert_eq!(run!(let x = 5i32; x *= 3; x), 15i32); + } + + #[test] + fn div_assign_all_types() { + assert_eq!(run!(let x = 15; x /= 3; x), 5); + assert_eq!(run!(let x = 15u32; x /= 3; x), 5u32); + assert_eq!(run!(let x = 15i32; x /= 3; x), 5i32); + } + + #[test] + fn rem_assign_all_types() { + assert_eq!(run!(let x = 10; x %= 3; x), 1); + assert_eq!(run!(let x = 10u32; x %= 3; x), 1u32); + assert_eq!(run!(let x = 10i32; x %= 3; x), 1i32); + } + + #[test] + fn bitand_assign_all_types() { + assert_eq!(run!(let x = 0b1111; x &= 0b1010; x), 0b1010); + assert_eq!(run!(let x = 0b1111u32; x &= 0b1010; x), 0b1010u32); + } + + #[test] + fn bitor_assign_all_types() { + assert_eq!(run!(let x = 0b1010; x |= 0b0101; x), 0b1111); + assert_eq!(run!(let x = 0b1010u32; x |= 0b0101; x), 0b1111u32); + } + + #[test] + fn bitxor_assign_all_types() { + assert_eq!(run!(let x = 0b1111; x ^= 0b1010; x), 0b0101); + assert_eq!(run!(let x = 0b1111u32; x ^= 0b1010; x), 0b0101u32); + } + + #[test] + fn shl_assign_all_types() { + assert_eq!(run!(let x = 1; x <<= 4; x), 16); + assert_eq!(run!(let x = 1u32; x <<= 4; x), 16u32); + } + + #[test] + fn shr_assign_all_types() { + assert_eq!(run!(let x = 16; x >>= 2; x), 4); + assert_eq!(run!(let x = 16u32; x >>= 2; x), 4u32); + } +} + +// ============================================================================= +// FLOAT ARITHMETIC OPERATIONS +// ============================================================================= + +mod float_arithmetic { + use super::*; + + // ------------------------------------------------------------------------- + // Addition (+) + // ------------------------------------------------------------------------- + #[test] + fn addition_untyped_untyped() { + assert_eq!(run!(1.5 + 2.5), 4.0); + assert_eq!(run!(0.0 + 0.0), 0.0); + } + + #[test] + fn addition_typed_typed() { + assert_eq!(run!(1.5f32 + 2.5f32), 4.0f32); + assert_eq!(run!(1.5f64 + 2.5f64), 4.0f64); + } + + #[test] + fn addition_typed_untyped() { + assert_eq!(run!(1.5f32 + 2.5), 4.0f32); + assert_eq!(run!(1.5f64 + 2.5), 4.0f64); + } + + #[test] + fn addition_untyped_typed() { + assert_eq!(run!(1.5 + 2.5f32), 4.0f32); + assert_eq!(run!(1.5 + 2.5f64), 4.0f64); + } + + // ------------------------------------------------------------------------- + // Subtraction (-) + // ------------------------------------------------------------------------- + #[test] + fn subtraction_untyped_untyped() { + assert_eq!(run!(5.5 - 2.5), 3.0); + } + + #[test] + fn subtraction_typed_typed() { + assert_eq!(run!(5.5f32 - 2.5f32), 3.0f32); + assert_eq!(run!(5.5f64 - 2.5f64), 3.0f64); + } + + #[test] + fn subtraction_typed_untyped() { + assert_eq!(run!(5.5f32 - 2.5), 3.0f32); + assert_eq!(run!(5.5f64 - 2.5), 3.0f64); + } + + #[test] + fn subtraction_untyped_typed() { + assert_eq!(run!(5.5 - 2.5f32), 3.0f32); + assert_eq!(run!(5.5 - 2.5f64), 3.0f64); + } + + // ------------------------------------------------------------------------- + // Multiplication (*) + // ------------------------------------------------------------------------- + #[test] + fn multiplication_untyped_untyped() { + assert_eq!(run!(2.0 * 3.5), 7.0); + } + + #[test] + fn multiplication_typed_typed() { + assert_eq!(run!(2.0f32 * 3.5f32), 7.0f32); + assert_eq!(run!(2.0f64 * 3.5f64), 7.0f64); + } + + #[test] + fn multiplication_typed_untyped() { + assert_eq!(run!(2.0f32 * 3.5), 7.0f32); + assert_eq!(run!(2.0f64 * 3.5), 7.0f64); + } + + #[test] + fn multiplication_untyped_typed() { + assert_eq!(run!(2.0 * 3.5f32), 7.0f32); + assert_eq!(run!(2.0 * 3.5f64), 7.0f64); + } + + // ------------------------------------------------------------------------- + // Division (/) + // ------------------------------------------------------------------------- + #[test] + fn division_untyped_untyped() { + assert_eq!(run!(10.0 / 2.0), 5.0); + } + + #[test] + fn division_typed_typed() { + assert_eq!(run!(10.0f32 / 2.0f32), 5.0f32); + assert_eq!(run!(10.0f64 / 2.0f64), 5.0f64); + } + + #[test] + fn division_typed_untyped() { + assert_eq!(run!(10.0f32 / 2.0), 5.0f32); + assert_eq!(run!(10.0f64 / 2.0), 5.0f64); + } + + #[test] + fn division_untyped_typed() { + assert_eq!(run!(10.0 / 2.0f32), 5.0f32); + assert_eq!(run!(10.0 / 2.0f64), 5.0f64); + } + + // ------------------------------------------------------------------------- + // Remainder (%) + // ------------------------------------------------------------------------- + #[test] + fn remainder_untyped_untyped() { + assert_eq!(run!(10.5 % 3.0), 1.5); + } + + #[test] + fn remainder_typed_typed() { + assert_eq!(run!(10.5f32 % 3.0f32), 1.5f32); + assert_eq!(run!(10.5f64 % 3.0f64), 1.5f64); + } + + #[test] + fn remainder_typed_untyped() { + assert_eq!(run!(10.5f32 % 3.0), 1.5f32); + assert_eq!(run!(10.5f64 % 3.0), 1.5f64); + } + + #[test] + fn remainder_untyped_typed() { + assert_eq!(run!(10.5 % 3.0f32), 1.5f32); + assert_eq!(run!(10.5 % 3.0f64), 1.5f64); + } + + // ------------------------------------------------------------------------- + // Negation (unary -) + // ------------------------------------------------------------------------- + #[test] + fn negation_floats() { + assert_eq!(run!(-3.5), -3.5); + assert_eq!(run!(-3.5f32), -3.5f32); + assert_eq!(run!(-3.5f64), -3.5f64); + assert_eq!(run!(-(-3.5f32)), 3.5f32); + } +} + +// ============================================================================= +// FLOAT COMPARISON OPERATIONS +// ============================================================================= + +mod float_comparison { + use super::*; + + #[test] + fn equal_untyped() { + assert!(run!(3.14 == 3.14)); + assert!(!run!(3.14 == 2.71)); + } + + #[test] + fn equal_typed() { + assert!(run!(3.14f32 == 3.14f32)); + assert!(run!(3.14f64 == 3.14f64)); + } + + #[test] + fn equal_typed_untyped() { + assert!(run!(3.14f32 == 3.14)); + assert!(run!(3.14 == 3.14f64)); + } + + #[test] + fn not_equal_all_types() { + assert!(run!(3.14 != 2.71)); + assert!(run!(3.14f32 != 2.71f32)); + assert!(run!(3.14f64 != 2.71f64)); + } + + #[test] + fn less_than_all_types() { + assert!(run!(2.0 < 3.0)); + assert!(run!(2.0f32 < 3.0f32)); + assert!(run!(2.0f64 < 3.0f64)); + assert!(run!(-1.0 < 1.0)); + } + + #[test] + fn less_than_or_equal_all_types() { + assert!(run!(2.0 <= 3.0)); + assert!(run!(3.0 <= 3.0)); + assert!(run!(3.0f32 <= 3.0f32)); + assert!(run!(3.0f64 <= 3.0f64)); + } + + #[test] + fn greater_than_all_types() { + assert!(run!(3.0 > 2.0)); + assert!(run!(3.0f32 > 2.0f32)); + assert!(run!(3.0f64 > 2.0f64)); + } + + #[test] + fn greater_than_or_equal_all_types() { + assert!(run!(3.0 >= 2.0)); + assert!(run!(3.0 >= 3.0)); + assert!(run!(3.0f32 >= 3.0f32)); + assert!(run!(3.0f64 >= 3.0f64)); + } +} + +// ============================================================================= +// FLOAT COMPOUND ASSIGNMENT OPERATIONS +// ============================================================================= + +mod float_compound_assignment { + use super::*; + + #[test] + fn add_assign() { + assert_eq!(run!(let x = 1.5; x += 2.5; x), 4.0); + assert_eq!(run!(let x = 1.5; x += 2.5f32; x), 4.0); + assert_eq!(run!(let x = 1.5; x += 2.5f64; x), 4.0); + assert_eq!(run!(let x = 1.5f32; x += 2.5; x), 4.0f32); + assert_eq!(run!(let x = 1.5f64; x += 2.5; x), 4.0f64); + } + + #[test] + fn sub_assign() { + assert_eq!(run!(let x = 5.5; x -= 2.5; x), 3.0); + assert_eq!(run!(let x = 5.5f32; x -= 2.5; x), 3.0f32); + assert_eq!(run!(let x = 5.5f64; x -= 2.5; x), 3.0f64); + } + + #[test] + fn mul_assign() { + assert_eq!(run!(let x = 2.0; x *= 3.5; x), 7.0); + assert_eq!(run!(let x = 2.0f32; x *= 3.5; x), 7.0f32); + assert_eq!(run!(let x = 2.0f64; x *= 3.5; x), 7.0f64); + } + + #[test] + fn div_assign() { + assert_eq!(run!(let x = 10.0; x /= 2.0; x), 5.0); + assert_eq!(run!(let x = 10.0f32; x /= 2.0; x), 5.0f32); + assert_eq!(run!(let x = 10.0f64; x /= 2.0; x), 5.0f64); + } + + #[test] + fn rem_assign() { + assert_eq!(run!(let x = 10.5; x %= 3.0; x), 1.5); + assert_eq!(run!(let x = 10.5f32; x %= 3.0; x), 1.5f32); + assert_eq!(run!(let x = 10.5f64; x %= 3.0; x), 1.5f64); + } +} + +// ============================================================================= +// FLOAT CONSTANTS AND METHODS +// ============================================================================= + +mod float_constants { + use super::*; + + // ------------------------------------------------------------------------- + // f32 constants + // ------------------------------------------------------------------------- + #[test] + fn f32_max_min() { + assert_eq!(run!(f32::MAX), f32::MAX); + assert_eq!(run!(f32::MIN), f32::MIN); + assert_eq!(run!(f32::MIN_POSITIVE), f32::MIN_POSITIVE); + assert_eq!(run!(f32::EPSILON), f32::EPSILON); + } + + #[test] + fn f32_infinity() { + assert_eq!(run!(f32::INFINITY), f32::INFINITY); + assert_eq!(run!(f32::NEG_INFINITY), f32::NEG_INFINITY); + } + + #[test] + fn f32_nan() { + // NaN is not equal to itself, so use is_nan() to test + assert!(run!(f32::NAN).is_nan()); + } + + // ------------------------------------------------------------------------- + // f64 constants + // ------------------------------------------------------------------------- + #[test] + fn f64_max_min() { + assert_eq!(run!(f64::MAX), f64::MAX); + assert_eq!(run!(f64::MIN), f64::MIN); + assert_eq!(run!(f64::MIN_POSITIVE), f64::MIN_POSITIVE); + assert_eq!(run!(f64::EPSILON), f64::EPSILON); + } + + #[test] + fn f64_infinity() { + assert_eq!(run!(f64::INFINITY), f64::INFINITY); + assert_eq!(run!(f64::NEG_INFINITY), f64::NEG_INFINITY); + } + + #[test] + fn f64_nan() { + // NaN is not equal to itself, so use is_nan() to test + assert!(run!(f64::NAN).is_nan()); + } + + // ------------------------------------------------------------------------- + // Operations producing infinity + // ------------------------------------------------------------------------- + #[test] + fn division_by_zero_produces_infinity() { + assert_eq!(run!(1.0f32 / 0.0f32), f32::INFINITY); + assert_eq!(run!(-1.0f32 / 0.0f32), f32::NEG_INFINITY); + assert_eq!(run!(1.0f64 / 0.0f64), f64::INFINITY); + assert_eq!(run!(-1.0f64 / 0.0f64), f64::NEG_INFINITY); + } + + #[test] + fn overflow_produces_infinity() { + assert_eq!(run!(f32::MAX + f32::MAX), f32::INFINITY); + assert_eq!(run!(f64::MAX + f64::MAX), f64::INFINITY); + assert_eq!(run!(f32::MIN + f32::MIN), f32::NEG_INFINITY); + assert_eq!(run!(f64::MIN + f64::MIN), f64::NEG_INFINITY); + } + + // ------------------------------------------------------------------------- + // Operations producing NaN + // ------------------------------------------------------------------------- + #[test] + fn zero_divided_by_zero_produces_nan() { + assert!(run!(0.0f32 / 0.0f32).is_nan()); + assert!(run!(0.0f64 / 0.0f64).is_nan()); + } + + #[test] + fn infinity_minus_infinity_produces_nan() { + assert!(run!(f32::INFINITY - f32::INFINITY).is_nan()); + assert!(run!(f64::INFINITY - f64::INFINITY).is_nan()); + } +} + +mod float_methods { + use super::*; + + // ------------------------------------------------------------------------- + // is_nan() + // ------------------------------------------------------------------------- + #[test] + fn is_nan_f32() { + assert!(run!(f32::NAN.is_nan())); + assert!(!run!(1.0f32.is_nan())); + assert!(!run!(f32::INFINITY.is_nan())); + assert!(!run!(0.0f32.is_nan())); + } + + #[test] + fn is_nan_f64() { + assert!(run!(f64::NAN.is_nan())); + assert!(!run!(1.0f64.is_nan())); + assert!(!run!(f64::INFINITY.is_nan())); + assert!(!run!(0.0f64.is_nan())); + } + + #[test] + fn is_nan_untyped() { + // Untyped floats (which use f64 internally) + assert!(!run!(1.0.is_nan())); + assert!(!run!(0.0.is_nan())); + } + + // ------------------------------------------------------------------------- + // is_infinite() + // ------------------------------------------------------------------------- + #[test] + fn is_infinite_f32() { + assert!(run!(f32::INFINITY.is_infinite())); + assert!(run!(f32::NEG_INFINITY.is_infinite())); + assert!(!run!(f32::NAN.is_infinite())); + assert!(!run!(1.0f32.is_infinite())); + assert!(!run!(0.0f32.is_infinite())); + assert!(!run!(f32::MAX.is_infinite())); + } + + #[test] + fn is_infinite_f64() { + assert!(run!(f64::INFINITY.is_infinite())); + assert!(run!(f64::NEG_INFINITY.is_infinite())); + assert!(!run!(f64::NAN.is_infinite())); + assert!(!run!(1.0f64.is_infinite())); + assert!(!run!(0.0f64.is_infinite())); + assert!(!run!(f64::MAX.is_infinite())); + } + + #[test] + fn is_infinite_untyped() { + // Untyped floats (which use f64 internally) + assert!(!run!(1.0.is_infinite())); + assert!(!run!(0.0.is_infinite())); + } + + // ------------------------------------------------------------------------- + // is_finite() + // ------------------------------------------------------------------------- + #[test] + fn is_finite_f32() { + assert!(run!(1.0f32.is_finite())); + assert!(run!(0.0f32.is_finite())); + assert!(run!(f32::MAX.is_finite())); + assert!(run!(f32::MIN.is_finite())); + assert!(!run!(f32::INFINITY.is_finite())); + assert!(!run!(f32::NEG_INFINITY.is_finite())); + assert!(!run!(f32::NAN.is_finite())); + } + + #[test] + fn is_finite_f64() { + assert!(run!(1.0f64.is_finite())); + assert!(run!(0.0f64.is_finite())); + assert!(run!(f64::MAX.is_finite())); + assert!(run!(f64::MIN.is_finite())); + assert!(!run!(f64::INFINITY.is_finite())); + assert!(!run!(f64::NEG_INFINITY.is_finite())); + assert!(!run!(f64::NAN.is_finite())); + } + + #[test] + fn is_finite_untyped() { + // Untyped floats (which use f64 internally) + assert!(run!(1.0.is_finite())); + assert!(run!(0.0.is_finite())); + } + + // ------------------------------------------------------------------------- + // is_sign_positive() + // ------------------------------------------------------------------------- + #[test] + fn is_sign_positive_f32() { + assert!(run!(1.0f32.is_sign_positive())); + assert!(run!(0.0f32.is_sign_positive())); + assert!(run!(f32::INFINITY.is_sign_positive())); + assert!(!run!((-1.0f32).is_sign_positive())); + assert!(!run!(f32::NEG_INFINITY.is_sign_positive())); + } + + #[test] + fn is_sign_positive_f64() { + assert!(run!(1.0f64.is_sign_positive())); + assert!(run!(0.0f64.is_sign_positive())); + assert!(run!(f64::INFINITY.is_sign_positive())); + assert!(!run!((-1.0f64).is_sign_positive())); + assert!(!run!(f64::NEG_INFINITY.is_sign_positive())); + } + + #[test] + fn is_sign_positive_untyped() { + assert!(run!(1.0.is_sign_positive())); + assert!(run!(0.0.is_sign_positive())); + assert!(!run!((-1.0).is_sign_positive())); + } + + // ------------------------------------------------------------------------- + // is_sign_negative() + // ------------------------------------------------------------------------- + #[test] + fn is_sign_negative_f32() { + assert!(!run!(1.0f32.is_sign_negative())); + assert!(!run!(0.0f32.is_sign_negative())); + assert!(!run!(f32::INFINITY.is_sign_negative())); + assert!(run!((-1.0f32).is_sign_negative())); + assert!(run!(f32::NEG_INFINITY.is_sign_negative())); + } + + #[test] + fn is_sign_negative_f64() { + assert!(!run!(1.0f64.is_sign_negative())); + assert!(!run!(0.0f64.is_sign_negative())); + assert!(!run!(f64::INFINITY.is_sign_negative())); + assert!(run!((-1.0f64).is_sign_negative())); + assert!(run!(f64::NEG_INFINITY.is_sign_negative())); + } + + #[test] + fn is_sign_negative_untyped() { + assert!(!run!(1.0.is_sign_negative())); + assert!(!run!(0.0.is_sign_negative())); + assert!(run!((-1.0).is_sign_negative())); + } +} + +// ============================================================================= +// BOOLEAN OPERATIONS +// ============================================================================= + +mod boolean_operations { + use super::*; + + // ------------------------------------------------------------------------- + // Logical AND (&&) + // ------------------------------------------------------------------------- + #[test] + fn logical_and() { + assert!(run!(true && true)); + assert!(!run!(true && false)); + assert!(!run!(false && true)); + assert!(!run!(false && false)); + } + + #[test] + fn logical_and_short_circuits() { + // The second operand should not be evaluated if the first is false + assert!(!run!( + let evaluated = false; + let _ = false && { evaluated = true; true }; + evaluated + )); + } + + // ------------------------------------------------------------------------- + // Logical OR (||) + // ------------------------------------------------------------------------- + #[test] + fn logical_or() { + assert!(run!(true || true)); + assert!(run!(true || false)); + assert!(run!(false || true)); + assert!(!run!(false || false)); + } + + #[test] + fn logical_or_short_circuits() { + // The second operand should not be evaluated if the first is true + assert!(!run!( + let evaluated = false; + let _ = true || { evaluated = true; false }; + evaluated + )); + } + + // ------------------------------------------------------------------------- + // Bitwise XOR (^) + // ------------------------------------------------------------------------- + #[test] + fn bitwise_xor_on_bool() { + assert!(!run!(true ^ true)); + assert!(run!(true ^ false)); + assert!(run!(false ^ true)); + assert!(!run!(false ^ false)); + } + + // ------------------------------------------------------------------------- + // Bitwise AND (&) - non-short-circuiting + // ------------------------------------------------------------------------- + #[test] + fn bitwise_and_on_bool() { + assert!(run!(true & true)); + assert!(!run!(true & false)); + assert!(!run!(false & true)); + assert!(!run!(false & false)); + } + + #[test] + fn bitwise_and_does_not_short_circuit() { + // Unlike &&, & evaluates both operands + assert!(run!( + let evaluated = false; + let _ = false & { evaluated = true; true }; + evaluated + )); + } + + // ------------------------------------------------------------------------- + // Bitwise OR (|) - non-short-circuiting + // ------------------------------------------------------------------------- + #[test] + fn bitwise_or_on_bool() { + assert!(run!(true | true)); + assert!(run!(true | false)); + assert!(run!(false | true)); + assert!(!run!(false | false)); + } + + // ------------------------------------------------------------------------- + // Boolean NOT (!) + // ------------------------------------------------------------------------- + #[test] + fn logical_not() { + assert!(!run!(!true)); + assert!(run!(!false)); + assert!(run!(!!true)); + assert!(run!(!!!false)); + } + + // ------------------------------------------------------------------------- + // Boolean Comparison + // ------------------------------------------------------------------------- + #[test] + fn boolean_equal() { + assert!(run!(true == true)); + assert!(run!(false == false)); + assert!(!run!(true == false)); + } + + #[test] + fn boolean_not_equal() { + assert!(run!(true != false)); + assert!(!run!(true != true)); + } + + #[test] + fn boolean_ordering() { + // In Rust, false < true + assert!(run!(false < true)); + assert!(!run!(true < false)); + assert!(run!(false <= false)); + assert!(run!(true >= true)); + assert!(run!(true > false)); + } +} + +// ============================================================================= +// STRING OPERATIONS +// ============================================================================= + +mod string_operations { + use super::*; + + // ------------------------------------------------------------------------- + // Addition (+) - concatenation + // ------------------------------------------------------------------------- + #[test] + fn string_concatenation() { + assert_eq!(run!("Hello" + " " + "World"), "Hello World"); + assert_eq!(run!("" + "test"), "test"); + assert_eq!(run!("test" + ""), "test"); + } + + // ------------------------------------------------------------------------- + // Add Assign (+=) - concatenation + // ------------------------------------------------------------------------- + #[test] + fn string_add_assign() { + assert_eq!(run!(let s = "Hello"; s += " World"; s), "Hello World"); + } + + // ------------------------------------------------------------------------- + // Comparison Operations + // ------------------------------------------------------------------------- + #[test] + fn string_equal() { + assert!(run!("hello" == "hello")); + assert!(!run!("hello" == "world")); + } + + #[test] + fn string_not_equal() { + assert!(run!("hello" != "world")); + assert!(!run!("hello" != "hello")); + } + + #[test] + fn string_less_than() { + assert!(run!("aaa" < "bbb")); + assert!(run!("abc" < "abd")); + assert!(!run!("abc" < "abc")); + } + + #[test] + fn string_less_than_or_equal() { + assert!(run!("aaa" <= "bbb")); + assert!(run!("abc" <= "abc")); + assert!(!run!("bbb" <= "aaa")); + } + + #[test] + fn string_greater_than() { + assert!(run!("bbb" > "aaa")); + assert!(run!("Zoo" > "Aardvark")); + } + + #[test] + fn string_greater_than_or_equal() { + assert!(run!("bbb" >= "aaa")); + assert!(run!("abc" >= "abc")); + } +} + +// ============================================================================= +// CHARACTER OPERATIONS +// ============================================================================= + +mod char_operations { + use super::*; + + // ------------------------------------------------------------------------- + // Comparison Operations + // ------------------------------------------------------------------------- + #[test] + fn char_equal() { + assert!(run!('a' == 'a')); + assert!(!run!('a' == 'b')); + } + + #[test] + fn char_not_equal() { + assert!(run!('a' != 'b')); + assert!(!run!('a' != 'a')); + } + + #[test] + fn char_less_than() { + assert!(run!('A' < 'B')); + assert!(run!('a' < 'b')); + assert!(run!('A' < 'a')); // uppercase < lowercase in ASCII + } + + #[test] + fn char_less_than_or_equal() { + assert!(run!('A' <= 'B')); + assert!(run!('A' <= 'A')); + } + + #[test] + fn char_greater_than() { + assert!(run!('B' > 'A')); + assert!(run!('z' > 'a')); + } + + #[test] + fn char_greater_than_or_equal() { + assert!(run!('B' >= 'A')); + assert!(run!('A' >= 'A')); + } +} + +// ============================================================================= +// STREAM OPERATIONS +// ============================================================================= + +mod stream_operations { + use super::*; + + // ------------------------------------------------------------------------- + // Addition (+) - concatenation + // ------------------------------------------------------------------------- + #[test] + fn stream_concatenation() { + assert_eq!( + run!((%[Hello] + %[World]).to_debug_string()), + "%[Hello World]" + ); + assert_eq!(run!((%[] + %[test]).to_debug_string()), "%[test]"); + assert_eq!(run!((%[test] + %[]).to_debug_string()), "%[test]"); + } + + // ------------------------------------------------------------------------- + // Add Assign (+=) - concatenation + // ------------------------------------------------------------------------- + #[test] + fn stream_add_assign() { + assert_eq!( + run!(let s = %[Hello]; s += %[World]; s.to_debug_string()), + "%[Hello World]" + ); + } +} + +// ============================================================================= +// CAST OPERATIONS +// ============================================================================= + +mod cast_operations { + use super::*; + + #[test] + fn cast_integer_to_integer() { + assert_eq!(run!(5u8 as u32), 5u32); + assert_eq!(run!(5u32 as u8), 5u8); + assert_eq!(run!(256u32 as u8), 0u8); // overflow wraps + assert_eq!(run!(-1i32 as u32), u32::MAX); + assert_eq!(run!(5 as i32), 5i32); + assert_eq!(run!(5 as untyped_int), 5); + } + + #[test] + fn cast_integer_to_float() { + assert_eq!(run!(5u32 as f32), 5.0f32); + assert_eq!(run!(5i32 as f64), 5.0f64); + assert_eq!(run!(5 as untyped_float), 5.0); + } + + #[test] + fn cast_float_to_integer() { + assert_eq!(run!(5.7f32 as i32), 5i32); + assert_eq!(run!(5.7f64 as u32), 5u32); + assert_eq!(run!(5.7 as untyped_int), 5); + } + + #[test] + fn cast_float_to_float() { + assert_eq!(run!(5.5f32 as f64), 5.5f64); + // Note: f64 to f32 may lose precision + assert_eq!(run!(5.5f64 as f32), 5.5f32); + assert_eq!(run!(5.5 as f32), 5.5f32); + } + + #[test] + fn cast_bool_to_integer() { + assert_eq!(run!(true as u32), 1u32); + assert_eq!(run!(false as u32), 0u32); + assert_eq!(run!(true as i8), 1i8); + } + + #[test] + fn cast_char_to_integer() { + assert_eq!(run!('A' as u8), 65u8); + assert_eq!(run!('A' as u32), 65u32); + assert_eq!(run!('A' as untyped_int), 65); + } + + #[test] + fn cast_u8_to_char() { + assert_eq!(run!(65u8 as char), 'A'); + assert_eq!(run!(97u8 as char), 'a'); + } + + #[test] + fn cast_to_string() { + assert_eq!(run!(42 as string), "42"); + assert_eq!(run!(42u32 as string), "42"); + assert_eq!(run!(3.14 as string), "3.14"); + assert_eq!(run!(true as string), "true"); + assert_eq!(run!('X' as string), "X"); + } + + #[test] + fn cast_to_bool() { + assert!(run!(true as bool)); + assert!(!run!(false as bool)); + } +} + +// ============================================================================= +// EDGE CASES AND SPECIAL BEHAVIOR +// ============================================================================= + +mod edge_cases { + use super::*; + + #[test] + fn signed_integer_with_negative_values() { + assert_eq!(run!(-5i8 + 3i8), -2i8); + assert_eq!(run!(-5i32 * -3i32), 15i32); + assert_eq!(run!(-10i64 / 3i64), -3i64); + assert_eq!(run!(-10i32 % 3i32), -1i32); + } + + #[test] + fn signed_shift_right_preserves_sign() { + // Arithmetic shift right on signed integers preserves sign + assert_eq!(run!(-8i8 >> 1), -4i8); + assert_eq!(run!(-16i32 >> 2), -4i32); + } + + #[test] + fn boundary_values() { + assert_eq!(run!(127i8 + 0i8), 127i8); + // Note: -128i8 as a literal doesn't work because it's parsed as -(128i8) + // and 128 overflows i8. Use an expression instead. + assert_eq!(run!(-127i8 - 1i8 + 0i8), -128i8); + assert_eq!(run!(255u8 - 0u8), 255u8); + assert_eq!(run!(0u8 + 0u8), 0u8); + } + + #[test] + fn chained_operations() { + assert_eq!(run!(1 + 2 + 3 + 4 + 5), 15); + assert_eq!(run!(10 - 3 - 2 - 1), 4); + assert_eq!(run!(2 * 3 * 4), 24); + } + + #[test] + fn mixed_operations_precedence() { + // Verify operator precedence + assert_eq!(run!(2 + 3 * 4), 14); // * before + + assert_eq!(run!(10 - 6 / 2), 7); // / before - + assert_eq!(run!(1 + 2 << 3), 24); // + before << + assert_eq!(run!(8 >> 2 + 1), 1); // + before >> + } + + #[test] + fn self_assignment() { + // Assign can reference itself + assert_eq!(run!(let x = 5; x += x; x), 10); + assert_eq!(run!(let x = 2; x *= x; x), 4); + } +} diff --git a/tests/parsing.rs b/tests/parsing.rs new file mode 100644 index 00000000..9bd571f9 --- /dev/null +++ b/tests/parsing.rs @@ -0,0 +1,1764 @@ +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; + +#[test] +#[cfg_attr(miri, ignore = "incompatible with miri")] +fn test_parsing_compilation_failures() { + if !should_run_ui_tests() { + // Some of the outputs are different on nightly, so don't test these + return; + } + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compilation_failures/parsing/*.rs"); +} + +#[test] +fn test_variable_parsing() { + run! { + let stream = %[]; + let output = parse stream => |input| { + %[#{ + let _ = input.punct(); + emit input.ident(); + let _ = input.ident(); + emit input.ident(); + let _ = input.punct(); + }] + }; + %[].assert_eq(output.to_debug_string(), "%[Hello World]") + } +} + +#[test] +fn test_parse_template_literal() { + assert_eq!( + run! { + let @parser[] = %[]; + inner.to_debug_string() + }, + "%[Beautiful]" + ); +} + +#[test] +fn test_parser_ident_method() { + assert_eq!( + run! { + let @parser[The "quick" #{ let x = parser.ident(); } fox "jumps"] = %[The "quick" brown fox "jumps"]; + x.to_string() + }, + "brown" + ); + assert_eq!( + run! { + let x = %[]; + let @parser[The quick #{ x += parser.ident(); } fox jumps #{ x += parser.ident(); } the lazy dog] = %[The quick brown fox jumps over the lazy dog]; + x.to_debug_string() + }, + "%[brown over]" + ); +} + +#[test] +fn test_parser_literal_method() { + assert_eq!( + run! { + let @parser[The "quick" #{ let x = parser.inferred_literal(); } fox "jumps"] = %[The "quick" "brown" fox "jumps"]; + x.to_debug_string() + }, + r#""brown""# + ); + assert_eq!( + run! { + let @parser[The "quick" #{ let x = parser.literal(); } fox "jumps"] = %[The "quick" "brown" fox "jumps"]; + x.to_debug_string() + }, + r#"%["brown"]"# + ); + assert_eq!( + run! { + let x = %[]; + let @parser[#{ let _ = parser.literal(); } #{ let _ = parser.literal(); } #{ let _ = parser.literal(); } #{ x += parser.literal(); } #{ let _ = parser.literal(); } #{ x += parser.literal(); } #{ x += parser.literal(); }] = %["Hello" 9 3.4 'c' 41u16 0b1010 r#"123"#]; + x.to_debug_string() + }, + "%['c' 0b1010 r#\"123\"#]" + ); +} + +#[test] +fn test_parser_punct_method() { + assert_eq!( + run! { + let @parser[The "quick" brown fox "jumps" #{ let x = parser.punct(); }] = %[The "quick" brown fox "jumps"!]; + x.to_debug_string() + }, + "%[!]" + ); +} + +#[test] +fn test_parser_token_tree_method() { + assert_eq!( + run! { + let x = %[]; + let @parser[ + #{ + // Matches one tt: Why + x += parser.token_tree(); + // Matches one tt: %group[it is fun to be here] + x += parser.token_tree(); + // Matches stream until (, then group it: %group[Hello Everyone] + x += parser.until(%[()]).to_group(); + } + ( + #{ + // Matches one tt and flatten it: This is an exciting adventure + x += parser.token_tree().flatten(); + // Matches rest and flatten it: do you agree ? + x += parser.rest().flatten(); + } + ) + ] = %[Why %group[it is fun to be here] Hello Everyone (%group[This is an exciting adventure] do you agree?)]; + x.to_debug_string() + }, + "%[Why %group[it is fun to be here] %group[Hello Everyone] This is an exciting adventure do you agree ?]" + ); +} + +#[test] +fn test_parser_rest_method() { + assert_eq!( + run! { + let @parser[#{ let inner = parser.rest(); }] = %[]; + inner.to_debug_string() + }, + "%[< Hello Beautiful World >]" + ); + assert_eq!( + run! { + let @parser[#{ let x = parser.rest(); }] = %[Hello => World]; + x.to_debug_string() + }, + "%[Hello => World]" + ); +} + +#[test] +fn test_parser_until_method() { + // parser.until() - equivalent to old @[UNTIL ...] transformer + assert_eq!( + run! { + let @parser[Hello #{ let x = parser.until(%[!]); } !!] = %[Hello => World!!]; + x.to_debug_string() + }, + "%[=> World]" + ); + assert_eq!( + run! { + let @parser[Hello #{ let x = parser.until(%[World]); } World] = %[Hello => World]; + x.to_debug_string() + }, + "%[=>]" + ); + assert_eq!( + run! { + let @parser[Hello #{ let x = parser.until(%[World]); } World] = %[Hello And Welcome To The Wonderful World]; + x.to_debug_string() + }, + "%[And Welcome To The Wonderful]" + ); + assert_eq!( + run! { + let @parser[Hello #{ let x = parser.until(%["World"]); } "World"] = %[Hello World And Welcome To The Wonderful "World"]; + x.to_debug_string() + }, + "%[World And Welcome To The Wonderful]" + ); + assert_eq!( + run! { + let @parser[#{ let x = parser.until(%[()]); } (#{ let y = parser.rest(); })] = %[Why Hello (World)]; + %["#x = " #x "; #y = " #y].to_string() + }, + "#x = WhyHello; #y = World" + ); +} + +#[test] +fn test_parser_read_method() { + // parser.read() - equivalent to old @[EXACT(...)] transformer + assert_eq!( + run!( + let x = %[true]; + let @parser[The #{ let _ = parser.read(%[#x]); }] = %[The true]; + x + ), + true + ); + // EXACT/read is evaluated at execution time + run! { + let @parser[The #{ let a = parser.token_tree(); } fox is #{ let b = parser.token_tree(); }. It 's super #{ let _ = parser.read(%[#a #b]); }.] = %[The brown fox is brown. It 's super brown brown.]; + }; +} + +#[test] +fn test_parser_commands_mid_parse() { + // Test executing commands mid-parse + assert_eq!( + run! { + let @parser[The "quick" #{ let x = parser.literal(); } fox #{ let y = x.clone().infer(); } #{ x = parser.ident(); }] = %[The "quick" "brown" fox jumps]; + ["#x = ", x.to_debug_string(), "; #y = ", y.to_debug_string()].to_string() + }, + "#x = %[jumps]; #y = \"brown\"" + ); +} + +#[test] +fn test_parser_open_close_methods() { + // Basic open/close with parentheses + run! { + parse %[(Hello World)] => |parser| { + parser.open('('); + let a = parser.ident(); + let b = parser.ident(); + parser.close(')'); + %[].assert_eq(%{ a, b }, %{ a: %[Hello], b: %[World] }); + }; + } + + // Basic open/close with braces + run! { + parse %[{ Test }] => |parser| { + parser.open('{'); + let x = parser.ident(); + parser.close('}'); + %[].assert_eq(x, %[Test]); + }; + } + + // Basic open/close with brackets + run! { + parse %[[Inner]] => |parser| { + parser.open('['); + let x = parser.ident(); + parser.close(']'); + %[].assert_eq(x, %[Inner]); + }; + } + + // Nested open/close + run! { + parse %[(outer { inner } after)] => |parser| { + parser.open('('); + let a = parser.ident(); + parser.open('{'); + let b = parser.ident(); + parser.close('}'); + let c = parser.ident(); + parser.close(')'); + %[].assert_eq(%{ a, b, c }, %{ a: %[outer], b: %[inner], c: %[after] }); + }; + } +} + +#[test] +fn test_attempt_block_with_parsing_rollback() { + // Simple attempt with parsing that rolls back + run! { + parse %[Hello World] => |parser| { + let result = attempt { + { + let _ = parser.ident(); + let _ = parser.ident(); + revert; + } => { %{ reverted: true } } + { + let a = parser.ident(); + let b = parser.ident(); + } => { %{ reverted: false, a, b } } + }; + %[].assert_eq(result, %{ reverted: false, a: %[Hello], b: %[World] }); + }; + } + + // Parsing rolls back on revert - verify position reset + run! { + parse %[Hello World] => |parser| { + let first = attempt { + { + // Parse two idents, then revert + let _ = parser.ident(); + let _ = parser.ident(); + revert; + } => { None } + { + // After rollback, should be back at start + let x = parser.ident(); + } => { x } + }; + // Should have consumed only "Hello" + let second = parser.ident(); + %[].assert_eq(%{ first, second }, %{ first: %[Hello], second: %[World] }); + }; + } +} + +#[test] +fn test_nested_attempt_blocks_with_parsing() { + // Nested attempt blocks - inner success, outer success + run! { + parse %[Hello World] => |parser| { + let result = attempt { + { + let x = parser.ident(); + let y = attempt { + { let inner = parser.ident(); } => { inner } + }; + } => { %{ x, y } } + }; + %[].assert_eq(result, %{ x: %[Hello], y: %[World] }); + }; + } + + // Nested attempt blocks - inner rollback, outer success + run! { + parse %[Hello World] => |parser| { + let result = attempt { + { + let x = parser.ident(); + let y = attempt { + { + let _ = parser.ident(); + revert; + } => { %[wrong] } + { let inner = parser.ident(); } => { inner } + }; + } => { %{ x, y } } + }; + %[].assert_eq(result, %{ x: %[Hello], y: %[World] }); + }; + } + + // Nested attempt blocks - inner success, outer rollback + run! { + parse %[Hello World] => |parser| { + let result = attempt { + { + let _ = parser.ident(); + let _ = attempt { + { let _ = parser.ident(); } => { None } + }; + revert; + } => { %{ arm: "first" } } + { + // After rollback, should be back at start + let a = parser.ident(); + let b = parser.ident(); + } => { %{ arm: "second", a, b } } + }; + %[].assert_eq(result, %{ arm: "second", a: %[Hello], b: %[World] }); + }; + } +} + +#[test] +fn test_deeply_nested_attempt_blocks_with_parsing() { + // Three levels of nesting with various rollback patterns + run! { + parse %[A B C] => |parser| { + let result = 'outer: attempt { + { + let a = parser.ident(); + let inner = 'middle: attempt { + { + let b = parser.ident(); + let c = 'inner: attempt { + { let x = parser.ident(); } => { x } + }; + } => { %{ b, c } } + }; + } => { %{ a, b: inner.b.clone(), c: inner.c.clone() } } + }; + %[].assert_eq(result, %{ a: %[A], b: %[B], c: %[C] }); + }; + } + + // Three levels - rollback inner, continue middle and outer + run! { + parse %[A B C] => |parser| { + let result = 'outer: attempt { + { + let a = parser.ident(); + let inner = 'middle: attempt { + { + let b = parser.ident(); + let c = 'inner: attempt { + { + let _ = parser.ident(); + revert; + } => { %[wrong] } + { let x = parser.ident(); } => { x } + }; + } => { %{ b, c } } + }; + } => { %{ a, b: inner.b.clone(), c: inner.c.clone() } } + }; + %[].assert_eq(result, %{ a: %[A], b: %[B], c: %[C] }); + }; + } + + // Three levels - rollback middle (includes inner work), continue outer + run! { + parse %[A B C] => |parser| { + let result = 'outer: attempt { + { + let a = parser.ident(); + let inner = 'middle: attempt { + { + let _ = parser.ident(); + let _ = 'inner: attempt { + { let _ = parser.ident(); } => { None } + }; + revert; + } => { %{ from: "first" } } + { + let b = parser.ident(); + let c = parser.ident(); + } => { %{ from: "second", b, c } } + }; + } => { %{ a, from: inner.from.clone(), b: inner.b.clone(), c: inner.c.clone() } } + }; + %[].assert_eq(result, %{ a: %[A], from: "second", b: %[B], c: %[C] }); + }; + } + + // Three levels - rollback directly to outer from inner + run! { + parse %[A B C] => |parser| { + let result = 'outer: attempt { + { + let _ = parser.ident(); + let _ = 'middle: attempt { + { + let _ = parser.ident(); + 'inner: attempt { + { + let _ = parser.ident(); + revert 'outer; + } => { None } + } + } => { None } + }; + } => { %{ arm: "first" } } + { + let a = parser.ident(); + let b = parser.ident(); + let c = parser.ident(); + } => { %{ arm: "second", a, b, c } } + }; + %[].assert_eq(result, %{ arm: "second", a: %[A], b: %[B], c: %[C] }); + }; + } +} + +#[test] +fn test_attempt_with_open_close_rollback() { + // Open/close inside attempt block that rolls back + run! { + parse %[(Hello)] => |parser| { + let result = attempt { + { + parser.open('('); + let _ = parser.ident(); + parser.close(')'); + revert; + } => { %{ arm: "first" } } + { + parser.open('('); + let x = parser.ident(); + parser.close(')'); + } => { %{ arm: "second", x } } + }; + %[].assert_eq(result, %{ arm: "second", x: %[Hello] }); + }; + } + + // Nested groups with rollback + run! { + parse %[({ a } b)] => |parser| { + let result = attempt { + { + parser.open('('); + parser.open('{'); + let _ = parser.ident(); + parser.close('}'); + let _ = parser.ident(); + parser.close(')'); + revert; + } => { %{ arm: "first" } } + { + parser.open('('); + parser.open('{'); + let inner = parser.ident(); + parser.close('}'); + let outer = parser.ident(); + parser.close(')'); + } => { %{ arm: "second", inner, outer } } + }; + %[].assert_eq(result, %{ arm: "second", inner: %[a], outer: %[b] }); + }; + } +} + +#[test] +fn test_attempt_with_partial_group_parsing_rollback() { + // Enter group, parse partially, then rollback before closing + run! { + parse %[(Hello World)] => |parser| { + let result = attempt { + { + parser.open('('); + let _ = parser.ident(); + // Don't close - rollback mid-group + revert; + } => { %{ arm: "first" } } + { + // After rollback, back at start + parser.open('('); + let x = parser.ident(); + let y = parser.ident(); + parser.close(')'); + } => { %{ arm: "second", x, y } } + }; + %[].assert_eq(result, %{ arm: "second", x: %[Hello], y: %[World] }); + }; + } +} + +#[test] +fn test_open_in_attempt_arm_close_in_result() { + // Open group in the left part of attempt arm, close in the right part + // This is a common pattern: parse `#(` then `)` and return the ident + run! { + parse %[(hello)] => |parser| { + let result = attempt { + { + parser.open('('); + let x = parser.ident(); + } => { + parser.close(')'); + x + } + }; + %[].assert_eq(result, %[hello]); + }; + } + + // More complex: open in arm, do more parsing, close in result + run! { + parse %[(a b c)] => |parser| { + let result = attempt { + { + parser.open('('); + let first = parser.ident(); + } => { + let second = parser.ident(); + let third = parser.ident(); + parser.close(')'); + %{ first, second, third } + } + }; + %[].assert_eq(result, %{ first: %[a], second: %[b], third: %[c] }); + }; + } + + // With revert - open in first arm, revert, then open again in second arm and close in result + run! { + parse %[(value)] => |parser| { + let result = attempt { + { + parser.open('('); + // Pretend we don't like what we see + revert; + } => { %{ from: "first" } } + { + parser.open('('); + let x = parser.ident(); + } => { + parser.close(')'); + %{ from: "second", x } + } + }; + %[].assert_eq(result, %{ from: "second", x: %[value] }); + }; + } + + // Nested groups: open outer in arm, open/close inner normally, close outer in result + run! { + parse %[({ inner } after)] => |parser| { + let result = attempt { + { + parser.open('('); + parser.open('{'); + let inner = parser.ident(); + parser.close('}'); + let after = parser.ident(); + } => { + parser.close(')'); + %{ inner, after } + } + }; + %[].assert_eq(result, %{ inner: %[inner], after: %[after] }); + }; + } +} + +#[test] +fn test_nested_attempts_with_groups_at_different_levels() { + // Open group in outer, parse in inner attempt + run! { + parse %[(Test)] => |parser| { + let result = attempt { + { + parser.open('('); + let inner_result = attempt { + { + let _ = parser.ident(); + revert; + } => { %[wrong] } + { let x = parser.ident(); } => { x } + }; + parser.close(')'); + } => { inner_result } + }; + %[].assert_eq(result, %[Test]); + }; + } + + // Multiple nested groups across multiple attempt levels + run! { + parse %[({ value })] => |parser| { + let result = 'outer: attempt { + { + parser.open('('); + let level1 = 'middle: attempt { + { + parser.open('{'); + let level2 = 'inner: attempt { + { let x = parser.ident(); } => { x } + }; + parser.close('}'); + } => { %{ level: "inner", value: level2 } } + }; + parser.close(')'); + } => { %{ level: "outer", inner: level1 } } + }; + %[].assert_eq(result.level, "outer"); + %[].assert_eq(result.inner.level, "inner"); + %[].assert_eq(result.inner.value, %[value]); + }; + } +} + +#[test] +fn test_fork_close_open_commit() { + // Test: open '(', fork, close ')', open '[', close ']', commit + // This verifies that closing one group and opening another inside a fork works correctly + run! { + parse %[()[]] => |parser| { + parser.open('('); + let result = attempt { + { + // Inside fork: close ')', open '[', close ']' + parser.close(')'); + parser.open('['); + parser.close(']'); + } => { "committed" } + }; + parser.end(); + %[].assert_eq(result, "committed"); + }; + } +} + +#[test] +fn test_fork_close_open_revert() { + // Test: open '(', fork, close ')', open '[', close ']', revert, close ')', token_tree, end + // This verifies that reverting restores the original group state + run! { + parse %[()[]] => |parser| { + parser.open('('); + let result = attempt { + { + // Inside fork: close ')', open '[', close ']', then revert + parser.close(')'); + parser.open('['); + parser.close(']'); + revert; + } => { %{ arm: "first" } } + { + // After revert: back inside '(' group, close it normally + parser.close(')'); + parser.open('['); + } => { + parser.close(']'); + %{ arm: "second" } + } + }; + parser.end(); + %[].assert_eq(result.arm, "second"); + }; + } +} + +// ============================================================================ +// Tests for is_end() and end() methods +// ============================================================================ + +#[test] +fn test_parser_is_end_method() { + // is_end() returns true when parser is at end + run! { + parse %[] => |parser| { + %[].assert_eq(parser.is_end(), true); + }; + } + + // is_end() returns false when parser has tokens remaining + run! { + parse %[hello] => |parser| { + %[].assert_eq(parser.is_end(), false); + let _ = parser.ident(); + %[].assert_eq(parser.is_end(), true); + }; + } + + // is_end() works correctly with multiple tokens + run! { + parse %[a b c] => |parser| { + %[].assert_eq(parser.is_end(), false); + let _ = parser.ident(); + %[].assert_eq(parser.is_end(), false); + let _ = parser.ident(); + %[].assert_eq(parser.is_end(), false); + let _ = parser.ident(); + %[].assert_eq(parser.is_end(), true); + }; + } + + // is_end() works inside groups + run! { + parse %[(inner)] => |parser| { + %[].assert_eq(parser.is_end(), false); + parser.open('('); + %[].assert_eq(parser.is_end(), false); + let _ = parser.ident(); + %[].assert_eq(parser.is_end(), true); // end of inner group + parser.close(')'); + %[].assert_eq(parser.is_end(), true); // end of outer + }; + } +} + +#[test] +fn test_parser_end_method() { + // end() succeeds when parser is at end + run! { + parse %[] => |parser| { + parser.end(); + }; + } + + // end() succeeds after consuming all tokens + run! { + parse %[hello world] => |parser| { + let _ = parser.ident(); + let _ = parser.ident(); + parser.end(); + }; + } + + // end() works at end of groups + run! { + parse %[()] => |parser| { + parser.open('('); + parser.end(); + parser.close(')'); + parser.end(); + }; + } + + // end() works with nested groups + run! { + parse %[({})] => |parser| { + parser.open('('); + parser.open('{'); + parser.end(); + parser.close('}'); + parser.end(); + parser.close(')'); + parser.end(); + }; + } +} + +// ============================================================================ +// Tests for any_ident() method +// ============================================================================ + +#[test] +fn test_parser_any_ident_method() { + // any_ident() parses regular identifiers + assert_eq!( + run! { + let @parser[#{ let x = parser.any_ident(); }] = %[hello]; + x.to_string() + }, + "hello" + ); + + // any_ident() parses keywords (unlike ident()) + assert_eq!( + run! { + let @parser[#{ let x = parser.any_ident(); }] = %[fn]; + x.to_string() + }, + "fn" + ); + + assert_eq!( + run! { + let @parser[#{ let x = parser.any_ident(); }] = %[struct]; + x.to_string() + }, + "struct" + ); + + assert_eq!( + run! { + let @parser[#{ let x = parser.any_ident(); }] = %[let]; + x.to_string() + }, + "let" + ); + + // any_ident() parses reserved keywords + assert_eq!( + run! { + let @parser[#{ let x = parser.any_ident(); }] = %[if]; + x.to_string() + }, + "if" + ); + + assert_eq!( + run! { + let @parser[#{ let x = parser.any_ident(); }] = %[else]; + x.to_string() + }, + "else" + ); + + assert_eq!( + run! { + let @parser[#{ let x = parser.any_ident(); }] = %[for]; + x.to_string() + }, + "for" + ); + + assert_eq!( + run! { + let @parser[#{ let x = parser.any_ident(); }] = %[while]; + x.to_string() + }, + "while" + ); + + // any_ident() works in sequence + run! { + parse %[fn my_function struct MyStruct] => |parser| { + let a = parser.any_ident(); + let b = parser.any_ident(); + let c = parser.any_ident(); + let d = parser.any_ident(); + %[].assert_eq(a, %[fn]); + %[].assert_eq(b, %[my_function]); + %[].assert_eq(c, %[struct]); + %[].assert_eq(d, %[MyStruct]); + }; + } +} + +// ============================================================================ +// Tests for char(), char_literal(), is_char() methods +// ============================================================================ + +#[test] +fn test_parser_is_char_method() { + // Test is_char() in context - extract char literals from mixed stream + run! { + parse %[hello 'a' world 'b' end 'c'] => |parser| { + let chars = %[]; + while !parser.is_end() { + if parser.is_char() { + chars += parser.char_literal(); + } else { + let _ = parser.ident(); + } + } + %[].assert_eq(chars.to_debug_string(), "%['a' 'b' 'c']"); + }; + } +} + +#[test] +fn test_parser_char_literal_method() { + // char_literal() returns the char literal as a stream + assert_eq!( + run! { + let @parser[#{ let x = parser.char_literal(); }] = %['a']; + x.to_debug_string() + }, + "%['a']" + ); + + // char_literal() preserves escaped characters + assert_eq!( + run! { + let @parser[#{ let x = parser.char_literal(); }] = %['\n']; + x.to_debug_string() + }, + "%['\\n']" + ); + + // char_literal() preserves unicode characters + assert_eq!( + run! { + let @parser[#{ let x = parser.char_literal(); }] = %['日']; + x.to_debug_string() + }, + "%['日']" + ); +} + +#[test] +fn test_parser_char_method() { + // char() returns the char value + run! { + parse %['a'] => |parser| { + let c = parser.char(); + %[].assert_eq(c, 'a'); + }; + } + + // char() handles various characters + run! { + parse %['z'] => |parser| { + %[].assert_eq(parser.char(), 'z'); + }; + } + + run! { + parse %['0'] => |parser| { + %[].assert_eq(parser.char(), '0'); + }; + } + + // char() handles special characters + run! { + parse %['\n'] => |parser| { + %[].assert_eq(parser.char(), '\n'); + }; + } + + run! { + parse %['\t'] => |parser| { + %[].assert_eq(parser.char(), '\t'); + }; + } + + run! { + parse %['\\'] => |parser| { + %[].assert_eq(parser.char(), '\\'); + }; + } + + // char() handles unicode + run! { + parse %['日'] => |parser| { + %[].assert_eq(parser.char(), '日'); + }; + } + + run! { + parse %['🦀'] => |parser| { + %[].assert_eq(parser.char(), '🦀'); + }; + } + + // char() in sequence + run! { + parse %['a' 'b' 'c'] => |parser| { + let a = parser.char(); + let b = parser.char(); + let c = parser.char(); + %[].assert_eq([a, b, c].to_string(), "abc"); + }; + } +} + +// ============================================================================ +// Tests for string(), string_literal(), is_string() methods +// ============================================================================ + +#[test] +fn test_parser_is_string_method() { + // Test is_string() in context - extract string literals from mixed stream + run! { + parse %[hello "a" world "b" end "c"] => |parser| { + let strings = %[]; + while !parser.is_end() { + if parser.is_string() { + strings += parser.string_literal(); + } else { + let _ = parser.ident(); + } + } + %[].assert_eq(strings.to_debug_string(), "%[\"a\" \"b\" \"c\"]"); + }; + } +} + +#[test] +fn test_parser_string_literal_method() { + // string_literal() returns the string literal as a stream + assert_eq!( + run! { + let @parser[#{ let x = parser.string_literal(); }] = %["hello"]; + x.to_debug_string() + }, + "%[\"hello\"]" + ); + + // string_literal() preserves raw strings + assert_eq!( + run! { + let @parser[#{ let x = parser.string_literal(); }] = %[r#"raw string"#]; + x.to_debug_string() + }, + "%[r#\"raw string\"#]" + ); +} + +#[test] +fn test_parser_string_method() { + // string() returns the string value + run! { + parse %["hello"] => |parser| { + let s = parser.string(); + %[].assert_eq(s, "hello"); + }; + } + + // string() handles empty strings + run! { + parse %[""] => |parser| { + %[].assert_eq(parser.string(), ""); + }; + } + + // string() handles strings with spaces + run! { + parse %["hello world"] => |parser| { + %[].assert_eq(parser.string(), "hello world"); + }; + } + + // string() handles escape sequences + run! { + parse %["line1\nline2"] => |parser| { + %[].assert_eq(parser.string(), "line1\nline2"); + }; + } + + run! { + parse %["tab\there"] => |parser| { + %[].assert_eq(parser.string(), "tab\there"); + }; + } + + // string() handles raw strings + run! { + parse %[r#"raw\nstring"#] => |parser| { + %[].assert_eq(parser.string(), "raw\\nstring"); + }; + } + + // string() handles unicode + run! { + parse %["日本語"] => |parser| { + %[].assert_eq(parser.string(), "日本語"); + }; + } + + run! { + parse %["🦀 Rust 🦀"] => |parser| { + %[].assert_eq(parser.string(), "🦀 Rust 🦀"); + }; + } + + // string() in sequence + run! { + parse %["hello" "world" "!"] => |parser| { + let a = parser.string(); + let b = parser.string(); + let c = parser.string(); + %[].assert_eq([a, b, c].to_string(), "helloworld!"); + }; + } +} + +// ============================================================================ +// Tests for integer(), integer_literal(), is_integer() methods +// ============================================================================ + +#[test] +fn test_parser_is_integer_method() { + // Test is_integer() in context - extract integers from mixed stream + run! { + parse %[hello 42 world 100 end 0xFF] => |parser| { + let ints = %[]; + while !parser.is_end() { + if parser.is_integer() { + ints += parser.integer_literal(); + } else { + let _ = parser.ident(); + } + } + %[].assert_eq(ints.to_debug_string(), "%[42 100 0xFF]"); + }; + } +} + +#[test] +fn test_parser_integer_literal_method() { + // integer_literal() returns the integer literal as a stream + assert_eq!( + run! { + let @parser[#{ let x = parser.integer_literal(); }] = %[42]; + x.to_debug_string() + }, + "%[42]" + ); + + // integer_literal() preserves type suffix + assert_eq!( + run! { + let @parser[#{ let x = parser.integer_literal(); }] = %[42u32]; + x.to_debug_string() + }, + "%[42u32]" + ); + + // integer_literal() preserves hex format + assert_eq!( + run! { + let @parser[#{ let x = parser.integer_literal(); }] = %[0xFF]; + x.to_debug_string() + }, + "%[0xFF]" + ); + + // integer_literal() preserves binary format + assert_eq!( + run! { + let @parser[#{ let x = parser.integer_literal(); }] = %[0b1010]; + x.to_debug_string() + }, + "%[0b1010]" + ); + + // integer_literal() preserves octal format + assert_eq!( + run! { + let @parser[#{ let x = parser.integer_literal(); }] = %[0o777]; + x.to_debug_string() + }, + "%[0o777]" + ); +} + +#[test] +fn test_parser_integer_method() { + // integer() returns the integer value (untyped by default) + run! { + parse %[42] => |parser| { + let n = parser.integer(); + %[].assert_eq(n, 42); + }; + } + + // integer() handles zero + run! { + parse %[0] => |parser| { + %[].assert_eq(parser.integer(), 0); + }; + } + + // integer() handles negative (requires explicit test with unary minus) + // Note: parser.integer() parses integer literals, -42 is parsed as minus + 42 + run! { + parse %[42] => |parser| { + let n = parser.integer(); + %[].assert_eq(-n, -42); + }; + } + + // integer() handles typed integers + run! { + parse %[42u8] => |parser| { + %[].assert_eq(parser.integer(), 42u8); + }; + } + + run! { + parse %[42u16] => |parser| { + %[].assert_eq(parser.integer(), 42u16); + }; + } + + run! { + parse %[42u32] => |parser| { + %[].assert_eq(parser.integer(), 42u32); + }; + } + + run! { + parse %[42u64] => |parser| { + %[].assert_eq(parser.integer(), 42u64); + }; + } + + run! { + parse %[42usize] => |parser| { + %[].assert_eq(parser.integer(), 42usize); + }; + } + + run! { + parse %[42i8] => |parser| { + %[].assert_eq(parser.integer(), 42i8); + }; + } + + run! { + parse %[42i16] => |parser| { + %[].assert_eq(parser.integer(), 42i16); + }; + } + + run! { + parse %[42i32] => |parser| { + %[].assert_eq(parser.integer(), 42i32); + }; + } + + run! { + parse %[42i64] => |parser| { + %[].assert_eq(parser.integer(), 42i64); + }; + } + + run! { + parse %[42isize] => |parser| { + %[].assert_eq(parser.integer(), 42isize); + }; + } + + // integer() handles hex + run! { + parse %[0xFF] => |parser| { + %[].assert_eq(parser.integer(), 255); + }; + } + + run! { + parse %[0xCAFE] => |parser| { + %[].assert_eq(parser.integer(), 0xCAFE); + }; + } + + // integer() handles binary + run! { + parse %[0b1010] => |parser| { + %[].assert_eq(parser.integer(), 0b1010); + }; + } + + run! { + parse %[0b11111111] => |parser| { + %[].assert_eq(parser.integer(), 255); + }; + } + + // integer() handles octal + run! { + parse %[0o777] => |parser| { + %[].assert_eq(parser.integer(), 0o777); + }; + } + + // integer() in sequence + run! { + parse %[1 2 3 4 5] => |parser| { + let sum = parser.integer() + parser.integer() + parser.integer() + parser.integer() + parser.integer(); + %[].assert_eq(sum, 15); + }; + } +} + +// ============================================================================ +// Tests for float(), float_literal(), is_float() methods +// ============================================================================ + +#[test] +fn test_parser_is_float_method() { + // Test is_float() in context - extract floats from mixed stream + run! { + parse %[hello 3.14 world 2.5 end 1.0] => |parser| { + let floats = %[]; + while !parser.is_end() { + if parser.is_float() { + floats += parser.float_literal(); + } else { + let _ = parser.ident(); + } + } + %[].assert_eq(floats.to_debug_string(), "%[3.14 2.5 1.0]"); + }; + } +} + +#[test] +fn test_parser_float_literal_method() { + // float_literal() returns the float literal as a stream + assert_eq!( + run! { + let @parser[#{ let x = parser.float_literal(); }] = %[3.14]; + x.to_debug_string() + }, + "%[3.14]" + ); + + // float_literal() preserves type suffix + assert_eq!( + run! { + let @parser[#{ let x = parser.float_literal(); }] = %[3.14f32]; + x.to_debug_string() + }, + "%[3.14f32]" + ); + + assert_eq!( + run! { + let @parser[#{ let x = parser.float_literal(); }] = %[3.14f64]; + x.to_debug_string() + }, + "%[3.14f64]" + ); + + // float_literal() preserves scientific notation + assert_eq!( + run! { + let @parser[#{ let x = parser.float_literal(); }] = %[1e10]; + x.to_debug_string() + }, + "%[1e10]" + ); + + assert_eq!( + run! { + let @parser[#{ let x = parser.float_literal(); }] = %[1.5e-3]; + x.to_debug_string() + }, + "%[1.5e-3]" + ); +} + +#[test] +fn test_parser_float_method() { + // float() returns the float value (untyped by default) + run! { + parse %[3.14] => |parser| { + let f = parser.float(); + // Use approximate comparison for floats + %[].assert_eq(f > 3.13 && f < 3.15, true); + }; + } + + // float() handles zero + run! { + parse %[0.0] => |parser| { + %[].assert_eq(parser.float(), 0.0); + }; + } + + // float() handles typed floats + run! { + parse %[3.14f32] => |parser| { + %[].assert_eq(parser.float(), 3.14f32); + }; + } + + run! { + parse %[3.14f64] => |parser| { + %[].assert_eq(parser.float(), 3.14f64); + }; + } + + // float() handles scientific notation + run! { + parse %[1e10] => |parser| { + %[].assert_eq(parser.float(), 1e10); + }; + } + + run! { + parse %[1.5e-3] => |parser| { + let f = parser.float(); + %[].assert_eq(f > 0.0014 && f < 0.0016, true); + }; + } + + // float() in sequence + run! { + parse %[1.0 2.0 3.0] => |parser| { + let sum = parser.float() + parser.float() + parser.float(); + %[].assert_eq(sum, 6.0); + }; + } +} + +// ============================================================================ +// Tests for is_literal() method +// ============================================================================ + +#[test] +fn test_parser_is_literal_method() { + // Test is_literal() in context - extract all literals from mixed stream + run! { + parse %[hello 42 world "test" end 3.14] => |parser| { + let literals = %[]; + let idents = %[]; + while !parser.is_end() { + if parser.is_literal() { + literals += parser.literal(); + } else { + idents += parser.ident(); + } + } + %[].assert_eq(literals.to_debug_string(), "%[42 \"test\" 3.14]"); + %[].assert_eq(idents.to_debug_string(), "%[hello world end]"); + }; + } +} + +// ============================================================================ +// Tests for nested parse statements +// ============================================================================ + +#[test] +fn test_nested_parse_statements() { + // Simple nested parse + run! { + parse %[(inner content)] => |outer| { + outer.open('('); + parse outer.rest() => |inner| { + let a = inner.ident(); + let b = inner.ident(); + %[].assert_eq(%{ a, b }, %{ a: %[inner], b: %[content] }); + }; + outer.close(')'); + }; + } + + // Deeply nested parse statements + run! { + parse %[level1 (level2 {level3})] => |p1| { + let l1 = p1.ident(); + p1.open('('); + parse p1.rest() => |p2| { + let l2 = p2.ident(); + p2.open('{'); + parse p2.rest() => |p3| { + let l3 = p3.ident(); + %[].assert_eq(l3, %[level3]); + }; + p2.close('}'); + %[].assert_eq(l2, %[level2]); + }; + p1.close(')'); + %[].assert_eq(l1, %[level1]); + }; + } + + // Nested parse with return values + assert_eq!( + run! { + let result = parse %[(a b c)] => |outer| { + outer.open('('); + let inner_result = parse outer.rest() => |inner| { + let x = inner.ident(); + let y = inner.ident(); + let z = inner.ident(); + %{ x, y, z } + }; + outer.close(')'); + inner_result + }; + result.x.to_string() + result.y.to_string() + result.z.to_string() + }, + "abc" + ); +} + +#[test] +fn test_nested_parse_with_until() { + // Nested parse using until() to extract portions + run! { + parse %[before | middle | after] => |parser| { + let before = parse parser.until(%[|]) => |p| { + p.rest() + }; + let _ = parser.punct(); + let middle = parse parser.until(%[|]) => |p| { + p.rest() + }; + let _ = parser.punct(); + let after = parser.rest(); + %[].assert_eq(before.to_debug_string(), "%[before]"); + %[].assert_eq(middle.to_debug_string(), "%[middle]"); + %[].assert_eq(after.to_debug_string(), "%[after]"); + }; + } +} + +#[test] +fn test_nested_parse_with_groups() { + // Parse a function-like structure with nested parsing for each group + run! { + parse %[fn_keyword (a: i32, b: String) -> Result] => |parser| { + let fn_kw = parser.ident(); + %[].assert_eq(fn_kw, %[fn_keyword]); + + parser.open('('); + let params = parse parser.rest() => |p| { + let params = %[]; + let count = 0; + while !p.is_end() { + let name = p.ident(); + let _ = p.punct(); // : + let ty = p.ident(); + count = count + 1; + if !p.is_end() { + let _ = p.punct(); // , + } + } + count + }; + parser.close(')'); + + let _ = parser.punct(); // - + let _ = parser.punct(); // > + let ret_type = parser.ident(); + + %[].assert_eq(params, 2); + %[].assert_eq(ret_type, %[Result]); + }; + } +} + +#[test] +fn test_nested_parse_with_attempt() { + // Nested parse inside attempt block + run! { + parse %[(valid)] => |parser| { + let result = attempt { + { + parser.open('('); + let inner = parse parser.rest() => |p| { + p.ident() + }; + parser.close(')'); + } => { inner } + }; + %[].assert_eq(result, %[valid]); + }; + } + + // Attempt inside nested parse - test with identifiers in parens + run! { + parse %[wrapper (first)] => |parser| { + let _ = parser.ident(); // consume wrapper + parser.open('('); + // Use attempt to check parsing behavior + let result = attempt { + { + // Try to parse as integer - will fail + let _ = parser.integer(); + } => { %[was_integer] } + { + // Parse as ident - will succeed + let _ = parser.ident(); + } => { %[was_ident] } + }; + parser.close(')'); + %[].assert_eq(result, %[was_ident]); + }; + } +} + +#[test] +fn test_parse_recursive_structure() { + // Simulate parsing a simple expression tree like (+ (+ 1 2) 3) + // Uses nested parsing to handle each group + run! { + parse %[(add (add 1 2) 3)] => |parser| { + parser.open('('); + let op = parser.ident(); + %[].assert_eq(op, %[add]); + + // First argument is a group + parser.open('('); + let inner_op = parser.ident(); + let inner_a = parser.integer(); + let inner_b = parser.integer(); + parser.close(')'); + + // Second argument is an integer + let outer_b = parser.integer(); + + parser.close(')'); + + %[].assert_eq(inner_op, %[add]); + %[].assert_eq(inner_a + inner_b + outer_b, 6); + }; + } +} + +// ============================================================================ +// Tests for mixed parsing patterns +// ============================================================================ + +#[test] +fn test_mixed_literal_parsing() { + // Parse a mix of different literal types + run! { + let @parser[#{ + let s1 = parser.string(); + let n = parser.integer(); + let c = parser.char(); + let f = parser.float(); + let s2 = parser.string(); + + %[].assert_eq(s1, "hello"); + %[].assert_eq(n, 42); + %[].assert_eq(c, 'x'); + %[].assert_eq(f > 3.13 && f < 3.15, true); + %[].assert_eq(s2, "world"); + }] = %["hello" 42 'x' 3.14 "world"]; + } +} + +#[test] +fn test_conditional_parsing_with_is_methods() { + // Use is_* methods to conditionally parse different token types + run! { + let result = ""; + let @parser[#{ + while !parser.is_end() { + if parser.is_integer() { + let _ = parser.integer(); + result = result + "int,"; + } else if parser.is_string() { + let _ = parser.string(); + result = result + "str,"; + } else if parser.is_float() { + let _ = parser.float(); + result = result + "float,"; + } else if parser.is_char() { + let _ = parser.char(); + result = result + "char,"; + } else { + let _ = parser.ident(); + result = result + "ident,"; + } + } + }] = %[42 "hello" 3.14 'c' ident]; + + %[].assert_eq(result, "int,str,float,char,ident,"); + } +} + +#[test] +fn test_parse_template_with_nested_groups() { + // Test @parser[] syntax with nested group handling + run! { + parse %[MyStruct { field1 field2 }] => |parser| { + let name = parser.ident(); + parser.open('{'); + let fields = parser.rest(); + parser.close('}'); + + %[].assert_eq(name, %[MyStruct]); + %[].assert_eq(fields.to_debug_string(), "%[field1 field2]"); + }; + } +} + +#[test] +fn test_parse_collect_all_tokens() { + // Collect all tokens of a specific type using loops + run! { + let idents = %[]; + let ints = %[]; + let @parser[#{ + while !parser.is_end() { + if parser.is_integer() { + ints += parser.integer_literal(); + } else { + idents += parser.ident(); + } + } + }] = %[a 1 b 2 c 3 d 4 e 5]; + + %[].assert_eq(idents.to_debug_string(), "%[a b c d e]"); + %[].assert_eq(ints.to_debug_string(), "%[1 2 3 4 5]"); + } +} + +#[test] +fn test_parse_with_inferred_literal() { + // Test inferred_literal() for different literal types + run! { + let @parser[#{ + let int_val = parser.inferred_literal(); + let str_val = parser.inferred_literal(); + let char_val = parser.inferred_literal(); + let float_val = parser.inferred_literal(); + + // Integer infers to untyped integer + %[].assert_eq(int_val, 42); + + // String infers to string value + %[].assert_eq(str_val, "hello"); + + // Char infers to char value + %[].assert_eq(char_val, 'c'); + + // Float infers to untyped float + %[].assert_eq(float_val > 3.13 && float_val < 3.15, true); + }] = %[42 "hello" 'c' 3.14]; + } +} diff --git a/tests/scope_debugging.rs b/tests/scope_debugging.rs new file mode 100644 index 00000000..bc1eb8f0 --- /dev/null +++ b/tests/scope_debugging.rs @@ -0,0 +1,132 @@ +#![cfg(feature = "debug")] + +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; + +// Run with `cargo test --features debug --test scope_debugging -- --no-capture` +#[test] +fn test() { + let output = scope_debug! { + let input = %raw[42 101 666 1024]; + let input_length = input.len(); + if input_length != 3 { + input.error(%["Expected 3 inputs, got " #input_length].to_string()); + } + }; + eprintln!("{}", output); +} + +#[test] +fn last_use_assertions() { + let _ = run! { + let x = 1; + x:FINAL + }; + run! { + let x = %[1]; + let x = %[2] + x:FINAL; // Because the x below is a new binding! + let _ = x:FINAL; + }; + run! { + let x = 0; + let y = [None]; + y[x:FINAL] = x:NONFINAL + 1; // Demonstrates that the RHS is calculated before the LHS + let _ = y; + }; + run! { + let x = %[1]; + { let x = %[2] + x:NONFINAL.clone(); }; + let _ = x:FINAL; // This refers to the outer x + } + run! { + let x; + x:NONFINAL = { + let x = 123; // Inner x + x:NONFINAL = 456; + let _ = x; + }; + let _ = x; + } + run! { + let x = 1; + if (x:NONFINAL == 1) { + } else if (x:NONFINAL == 2) { + } else if (false) { + } else { + let _ = x:FINAL; + } + } + run! { + let x = 1; + if (x:NONFINAL == 1) { + } else if (x:NONFINAL == 2) { + } else if (x:FINAL == 2) {} + } + run! { + let x = 1; + let _ = x:NONFINAL; + if (x:NONFINAL == 1) { + } else if (x:NONFINAL == 2) { + let _ = x:FINAL; + } else if (true) { + let _ = x:FINAL; + } + } + run! { + let x = 1; + let _ = x:NONFINAL; + if (x:NONFINAL == 1) { + } else if (x:NONFINAL == 2) { + let _ = x:NONFINAL; + } else if (true) { + let _ = x:NONFINAL; + } + let _ = x:FINAL; + } + run! { + let i = 0; + loop { + i:NONFINAL += 1; + let x = 1; + let _ = x:FINAL; + if i:NONFINAL > 5 { + break; + } + } + } + run! { + let i = 0; + while i:NONFINAL < 5 { + i:NONFINAL += 1; + let x = 1; + let _ = x:FINAL; + } + } + run! { + let i = 0; + // NONFINAL even though it's not used again, because it's repeated + while i:NONFINAL < 5 { + let x = 1; + let _ = x:FINAL; + break; + } + } + run! { + let x = 0; + // FINAL because the iterator creation is only run once + for x in x:FINAL..5 { + // This is the new x each iteration + let _ = x:FINAL; + } + } + run! { + let y = 1; + for x in 0..5 { + if true { + let y = y:NONFINAL + x; // Line is repeated + let _ = y:FINAL; // Covers old y + } + } + } +} diff --git a/tests/simple_test.rs b/tests/simple_test.rs deleted file mode 100644 index 2775098b..00000000 --- a/tests/simple_test.rs +++ /dev/null @@ -1,21 +0,0 @@ -use preinterpret::preinterpret; - -preinterpret! { - [!set! #bytes = 32] - [!set! #postfix = Hello World #bytes] - [!set! #MyRawVar = [!raw! Test no #str [!ident! replacement]]] - [!ignore! non - sensical !code :D - ignored (!)] - struct MyStruct; - type [!ident! X "Boo" [!string! Hello 1] #postfix] = MyStruct; - const NUM: u32 = [!literal! 1337u #bytes]; - const STRING: &str = [!string! #MyRawVar]; - const SNAKE_CASE: &str = [!snake! MyVar]; -} - -#[test] -fn complex_example_evaluates_correctly() { - let _x: XBooHello1HelloWorld32 = MyStruct; - assert_eq!(NUM, 1337u32); - assert_eq!(STRING, "Testno#str[!ident!replacement]"); - assert_eq!(SNAKE_CASE, "my_var"); -} diff --git a/tests/string.rs b/tests/string.rs index c9102b59..d6420b1b 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -1,295 +1,876 @@ -use preinterpret::preinterpret; - -macro_rules! my_assert_eq { - ($input:tt, $($output:tt)*) => { - assert_eq!(preinterpret!($input), $($output)*); - }; -} +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; #[test] fn test_string() { - my_assert_eq!([!string! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "my_MixedCaseSTRINGWhichis #awesome -whatdo youthink?"); - my_assert_eq!([!string! UPPER], "UPPER"); - my_assert_eq!([!string! lower], "lower"); - my_assert_eq!([!string! lower_snake_case], "lower_snake_case"); - my_assert_eq!([!string! UPPER_SNAKE_CASE], "UPPER_SNAKE_CASE"); - my_assert_eq!([!string! lowerCamelCase], "lowerCamelCase"); - my_assert_eq!([!string! UpperCamelCase], "UpperCamelCase"); - my_assert_eq!([!string! Capitalized], "Capitalized"); - my_assert_eq!([!string! "THEY SAID: A quick brown fox jumps over the lazy dog."], "THEY SAID: A quick brown fox jumps over the lazy dog."); - my_assert_eq!([!string! "hello_w🌎rld"], "hello_w🌎rld"); - my_assert_eq!([!string! "kebab-case"], "kebab-case"); - my_assert_eq!([!string! "~~h4xx0rZ <3 1337c0de"], "~~h4xx0rZ <3 1337c0de"); - my_assert_eq!([!string! PostgreSQLConnection], "PostgreSQLConnection"); - my_assert_eq!([!string! PostgreSqlConnection], "PostgreSqlConnection"); - my_assert_eq!([!string! "U+000A LINE FEED (LF)"], "U+000A LINE FEED (LF)"); - my_assert_eq!([!string! "\nThis\r\n is a\tmulti-line\nstring"], "\nThis\r\n is a\tmulti-line\nstring"); - my_assert_eq!([!string! " lots of _ space and _whacky |c$ara_cte>>rs|"], " lots of _ space and _whacky |c$ara_cte>>rs|"); - my_assert_eq!([!string! "über CöÖl"], "über CöÖl"); - my_assert_eq!([!string! "◌̈ubër Cöol"], "◌̈ubër Cöol"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!string! "真是难以置信!"], "真是难以置信!"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_string()), + "my_MixedCaseSTRINGWhichis #awesome -whatdo youthink?" + ); + assert_eq!(run!(%[UPPER].to_string()), "UPPER"); + assert_eq!(run!(%[lower].to_string()), "lower"); + assert_eq!(run!(%[lower_snake_case].to_string()), "lower_snake_case"); + assert_eq!(run!(%[UPPER_SNAKE_CASE].to_string()), "UPPER_SNAKE_CASE"); + assert_eq!(run!(%[lowerCamelCase].to_string()), "lowerCamelCase"); + assert_eq!(run!(%[UpperCamelCase].to_string()), "UpperCamelCase"); + assert_eq!(run!(%[Capitalized].to_string()), "Capitalized"); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string()), + "THEY SAID: A quick brown fox jumps over the lazy dog." + ); + assert_eq!(run!(%["hello_w🌎rld"].to_string()), "hello_w🌎rld"); + assert_eq!(run!(%["kebab-case"].to_string()), "kebab-case"); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string()), + "~~h4xx0rZ <3 1337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string()), + "PostgreSQLConnection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string()), + "PostgreSqlConnection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string()), + "U+000A LINE FEED (LF)" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string()), + "\nThis\r\n is a\tmulti-line\nstring" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string()), + " lots of _ space and _whacky |c$ara_cte>>rs|" + ); + assert_eq!(run!(%["über CöÖl"].to_string()), "über CöÖl"); + assert_eq!(run!(%["◌̈ubër Cöol"].to_string()), "◌̈ubër Cöol"); // The ë (and only the e) uses a post-fix combining character + assert_eq!(run!(%["真是难以置信!"].to_string()), "真是难以置信!"); } #[test] fn test_upper() { - my_assert_eq!([!upper! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "MY_MIXEDCASESTRINGWHICHIS #AWESOME -WHATDO YOUTHINK?"); - my_assert_eq!([!upper! UPPER], "UPPER"); - my_assert_eq!([!upper! lower], "LOWER"); - my_assert_eq!([!upper! lower_snake_case], "LOWER_SNAKE_CASE"); - my_assert_eq!([!upper! UPPER_SNAKE_CASE], "UPPER_SNAKE_CASE"); - my_assert_eq!([!upper! lowerCamelCase], "LOWERCAMELCASE"); - my_assert_eq!([!upper! UpperCamelCase], "UPPERCAMELCASE"); - my_assert_eq!([!upper! Capitalized], "CAPITALIZED"); - my_assert_eq!([!upper! "THEY SAID: A quick brown fox jumps over the lazy dog."], "THEY SAID: A QUICK BROWN FOX JUMPS OVER THE LAZY DOG."); - my_assert_eq!([!upper! "hello_w🌎rld"], "HELLO_W🌎RLD"); - my_assert_eq!([!upper! "kebab-case"], "KEBAB-CASE"); - my_assert_eq!([!upper! "~~h4xx0rZ <3 1337c0de"], "~~H4XX0RZ <3 1337C0DE"); - my_assert_eq!([!upper! PostgreSQLConnection], "POSTGRESQLCONNECTION"); - my_assert_eq!([!upper! PostgreSqlConnection], "POSTGRESQLCONNECTION"); - my_assert_eq!([!upper! "U+000A LINE FEED (LF)"], "U+000A LINE FEED (LF)"); - my_assert_eq!([!upper! "\nThis\r\n is a\tmulti-line\nstring"], "\nTHIS\r\n IS A\tMULTI-LINE\nSTRING"); - my_assert_eq!([!upper! " lots of _ space and _whacky |c$ara_cte>>rs|"], " LOTS OF _ SPACE AND _WHACKY |C$ARA_CTE>>RS|"); - my_assert_eq!([!upper! "über CöÖl"], "ÜBER CÖÖL"); - my_assert_eq!([!upper! "◌̈ubër Cöol"], "◌̈UBËR CÖOL"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!upper! "真是难以置信!"], "真是难以置信!"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_string().to_uppercase()), + "MY_MIXEDCASESTRINGWHICHIS #AWESOME -WHATDO YOUTHINK?" + ); + assert_eq!(run!(%[UPPER].to_string().to_uppercase()), "UPPER"); + assert_eq!(run!(%[lower].to_string().to_uppercase()), "LOWER"); + assert_eq!( + run!(%[lower_snake_case].to_string().to_uppercase()), + "LOWER_SNAKE_CASE" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().to_uppercase()), + "UPPER_SNAKE_CASE" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().to_uppercase()), + "LOWERCAMELCASE" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().to_uppercase()), + "UPPERCAMELCASE" + ); + assert_eq!( + run!(%[Capitalized].to_string().to_uppercase()), + "CAPITALIZED" + ); + assert_eq!( + run!("THEY SAID: A quick brown fox jumps over the lazy dog.".to_uppercase()), + "THEY SAID: A QUICK BROWN FOX JUMPS OVER THE LAZY DOG." + ); + assert_eq!(run!("hello_w🌎rld".to_uppercase()), "HELLO_W🌎RLD"); + assert_eq!(run!("kebab-case".to_uppercase()), "KEBAB-CASE"); + assert_eq!( + run!("~~h4xx0rZ <3 1337c0de".to_uppercase()), + "~~H4XX0RZ <3 1337C0DE" + ); + assert_eq!( + run!("PostgreSQLConnection".to_uppercase()), + "POSTGRESQLCONNECTION" + ); + assert_eq!( + run!("PostgreSqlConnection".to_uppercase()), + "POSTGRESQLCONNECTION" + ); + assert_eq!( + run!("U+000A LINE FEED (LF)".to_uppercase()), + "U+000A LINE FEED (LF)" + ); + assert_eq!( + run!("\nThis\r\n is a\tmulti-line\nstring".to_uppercase()), + "\nTHIS\r\n IS A\tMULTI-LINE\nSTRING" + ); + assert_eq!( + run!(" lots of _ space and _whacky |c$ara_cte>>rs|".to_uppercase()), + " LOTS OF _ SPACE AND _WHACKY |C$ARA_CTE>>RS|" + ); + assert_eq!(run!("über CöÖl".to_uppercase()), "ÜBER CÖÖL"); + assert_eq!(run!("◌̈ubër Cöol".to_uppercase()), "◌̈UBËR CÖOL"); // The ë (and only the e) uses a post-fix combining character + assert_eq!(run!("真是难以置信!".to_uppercase()), "真是难以置信!"); } #[test] fn test_lower() { - my_assert_eq!([!lower! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "my_mixedcasestringwhichis #awesome -whatdo youthink?"); - my_assert_eq!([!lower! UPPER], "upper"); - my_assert_eq!([!lower! lower], "lower"); - my_assert_eq!([!lower! lower_snake_case], "lower_snake_case"); - my_assert_eq!([!lower! UPPER_SNAKE_CASE], "upper_snake_case"); - my_assert_eq!([!lower! lowerCamelCase], "lowercamelcase"); - my_assert_eq!([!lower! UpperCamelCase], "uppercamelcase"); - my_assert_eq!([!lower! Capitalized], "capitalized"); - my_assert_eq!([!lower! "THEY SAID: A quick brown fox jumps over the lazy dog."], "they said: a quick brown fox jumps over the lazy dog."); - my_assert_eq!([!lower! "hello_w🌎rld"], "hello_w🌎rld"); - my_assert_eq!([!lower! "kebab-case"], "kebab-case"); - my_assert_eq!([!lower! "~~h4xx0rZ <3 1337c0de"], "~~h4xx0rz <3 1337c0de"); - my_assert_eq!([!lower! PostgreSQLConnection], "postgresqlconnection"); - my_assert_eq!([!lower! PostgreSqlConnection], "postgresqlconnection"); - my_assert_eq!([!lower! "U+000A LINE FEED (LF)"], "u+000a line feed (lf)"); - my_assert_eq!([!lower! "\nThis\r\n is a\tmulti-line\nstring"], "\nthis\r\n is a\tmulti-line\nstring"); - my_assert_eq!([!lower! " lots of _ space and _whacky |c$ara_cte>>rs|"], " lots of _ space and _whacky |c$ara_cte>>rs|"); - my_assert_eq!([!lower! "über CöÖl"], "über cööl"); - my_assert_eq!([!lower! "◌̈ubër Cööl"], "◌̈ubër cööl"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!lower! "真是难以置信!"], "真是难以置信!"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_string().to_lowercase()), + "my_mixedcasestringwhichis #awesome -whatdo youthink?" + ); + assert_eq!(run!(%[UPPER].to_string().to_lowercase()), "upper"); + assert_eq!(run!(%[lower].to_string().to_lowercase()), "lower"); + assert_eq!( + run!(%[lower_snake_case].to_string().to_lowercase()), + "lower_snake_case" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().to_lowercase()), + "upper_snake_case" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().to_lowercase()), + "lowercamelcase" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().to_lowercase()), + "uppercamelcase" + ); + assert_eq!( + run!(%[Capitalized].to_string().to_lowercase()), + "capitalized" + ); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().to_lowercase()), + "they said: a quick brown fox jumps over the lazy dog." + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().to_lowercase()), + "hello_w🌎rld" + ); + assert_eq!( + run!(%["kebab-case"].to_string().to_lowercase()), + "kebab-case" + ); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().to_lowercase()), + "~~h4xx0rz <3 1337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().to_lowercase()), + "postgresqlconnection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().to_lowercase()), + "postgresqlconnection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().to_lowercase()), + "u+000a line feed (lf)" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().to_lowercase()), + "\nthis\r\n is a\tmulti-line\nstring" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().to_lowercase()), + " lots of _ space and _whacky |c$ara_cte>>rs|" + ); + assert_eq!(run!(%["über CöÖl"].to_string().to_lowercase()), "über cööl"); + assert_eq!( + run!(%["◌̈ubër Cööl"].to_string().to_lowercase()), + "◌̈ubër cööl" + ); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().to_lowercase()), + "真是难以置信!" + ); } #[test] fn test_snake() { - my_assert_eq!([!snake! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "my_mixed_case_string_whichis_awesome_whatdo_youthink"); - my_assert_eq!([!snake! UPPER], "upper"); - my_assert_eq!([!snake! lower], "lower"); - my_assert_eq!([!snake! lower_snake_case], "lower_snake_case"); - my_assert_eq!([!snake! UPPER_SNAKE_CASE], "upper_snake_case"); - my_assert_eq!([!snake! lowerCamelCase], "lower_camel_case"); - my_assert_eq!([!snake! UpperCamelCase], "upper_camel_case"); - my_assert_eq!([!snake! Capitalized], "capitalized"); - my_assert_eq!([!snake! "THEY SAID: A quick brown fox jumps over the lazy dog."], "they_said_a_quick_brown_fox_jumps_over_the_lazy_dog"); - my_assert_eq!([!snake! "hello_w🌎rld"], "hello_w_rld"); - my_assert_eq!([!snake! "kebab-case"], "kebab_case"); - my_assert_eq!([!snake! "~~h4xx0rZ <3 1337c0de"], "h4xx0r_z_3_1337c0de"); - my_assert_eq!([!snake! PostgreSQLConnection], "postgre_sql_connection"); - my_assert_eq!([!snake! PostgreSqlConnection], "postgre_sql_connection"); - my_assert_eq!([!snake! "U+000A LINE FEED (LF)"], "u_000a_line_feed_lf"); - my_assert_eq!([!snake! "\nThis\r\n is a\tmulti-line\nstring"], "this_is_a_multi_line_string"); - my_assert_eq!([!snake! " lots of _ space and _whacky |c$ara_cte>>rs|"], "lots_of_space_and_whacky_c_ara_cte_rs"); - my_assert_eq!([!snake! "über CöÖl"], "über_cö_öl"); - my_assert_eq!([!snake! "◌̈ubër Cööl"], "ube_r_cööl"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!snake! "真是难以置信!"], "真是难以置信"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_string().to_lower_snake_case()), + "my_mixed_case_string_whichis_awesome_whatdo_youthink" + ); + assert_eq!(run!(%[UPPER].to_string().to_lower_snake_case()), "upper"); + assert_eq!(run!(%[lower].to_string().to_lower_snake_case()), "lower"); + assert_eq!( + run!(%[lower_snake_case].to_string().to_lower_snake_case()), + "lower_snake_case" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().to_lower_snake_case()), + "upper_snake_case" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().to_lower_snake_case()), + "lower_camel_case" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().to_lower_snake_case()), + "upper_camel_case" + ); + assert_eq!( + run!(%[Capitalized].to_string().to_lower_snake_case()), + "capitalized" + ); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().to_lower_snake_case()), + "they_said_a_quick_brown_fox_jumps_over_the_lazy_dog" + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().to_lower_snake_case()), + "hello_w_rld" + ); + assert_eq!( + run!(%["kebab-case"].to_string().to_lower_snake_case()), + "kebab_case" + ); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().to_lower_snake_case()), + "h4xx0r_z_3_1337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().to_lower_snake_case()), + "postgre_sql_connection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().to_lower_snake_case()), + "postgre_sql_connection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().to_lower_snake_case()), + "u_000a_line_feed_lf" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().to_lower_snake_case()), + "this_is_a_multi_line_string" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().to_lower_snake_case()), + "lots_of_space_and_whacky_c_ara_cte_rs" + ); + assert_eq!( + run!(%["über CöÖl"].to_string().to_lower_snake_case()), + "über_cö_öl" + ); + assert_eq!( + run!(%["◌̈ubër Cööl"].to_string().to_lower_snake_case()), + "ube_r_cööl" + ); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().to_lower_snake_case()), + "真是难以置信" + ); } #[test] fn test_upper_snake() { - my_assert_eq!([!upper_snake! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "MY_MIXED_CASE_STRING_WHICHIS_AWESOME_WHATDO_YOUTHINK"); - my_assert_eq!([!upper_snake! UPPER], "UPPER"); - my_assert_eq!([!upper_snake! lower], "LOWER"); - my_assert_eq!([!upper_snake! lower_snake_case], "LOWER_SNAKE_CASE"); - my_assert_eq!([!upper_snake! UPPER_SNAKE_CASE], "UPPER_SNAKE_CASE"); - my_assert_eq!([!upper_snake! lowerCamelCase], "LOWER_CAMEL_CASE"); - my_assert_eq!([!upper_snake! UpperCamelCase], "UPPER_CAMEL_CASE"); - my_assert_eq!([!upper_snake! Capitalized], "CAPITALIZED"); - my_assert_eq!([!upper_snake! "THEY SAID: A quick brown fox jumps over the lazy dog."], "THEY_SAID_A_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG"); - my_assert_eq!([!upper_snake! "hello_w🌎rld"], "HELLO_W_RLD"); - my_assert_eq!([!upper_snake! "kebab-case"], "KEBAB_CASE"); - my_assert_eq!([!upper_snake! "~~h4xx0rZ <3 1337c0de"], "H4XX0R_Z_3_1337C0DE"); - my_assert_eq!([!upper_snake! PostgreSQLConnection], "POSTGRE_SQL_CONNECTION"); - my_assert_eq!([!upper_snake! PostgreSqlConnection], "POSTGRE_SQL_CONNECTION"); - my_assert_eq!([!upper_snake! "U+000A LINE FEED (LF)"], "U_000A_LINE_FEED_LF"); - my_assert_eq!([!upper_snake! "\nThis\r\n is a\tmulti-line\nstring"], "THIS_IS_A_MULTI_LINE_STRING"); - my_assert_eq!([!upper_snake! " lots of _ space and _whacky |c$ara_cte>>rs|"], "LOTS_OF_SPACE_AND_WHACKY_C_ARA_CTE_RS"); - my_assert_eq!([!upper_snake! "über CöÖl"], "ÜBER_CÖ_ÖL"); - my_assert_eq!([!upper_snake! "◌̈ubër Cöol"], "UBE_R_CÖOL"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!upper_snake! "真是难以置信!"], "真是难以置信"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_string().to_upper_snake_case()), + "MY_MIXED_CASE_STRING_WHICHIS_AWESOME_WHATDO_YOUTHINK" + ); + assert_eq!(run!(%[UPPER].to_string().to_upper_snake_case()), "UPPER"); + assert_eq!(run!(%[lower].to_string().to_upper_snake_case()), "LOWER"); + assert_eq!( + run!(%[lower_snake_case].to_string().to_upper_snake_case()), + "LOWER_SNAKE_CASE" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().to_upper_snake_case()), + "UPPER_SNAKE_CASE" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().to_upper_snake_case()), + "LOWER_CAMEL_CASE" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().to_upper_snake_case()), + "UPPER_CAMEL_CASE" + ); + assert_eq!( + run!(%[Capitalized].to_string().to_upper_snake_case()), + "CAPITALIZED" + ); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().to_upper_snake_case()), + "THEY_SAID_A_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG" + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().to_upper_snake_case()), + "HELLO_W_RLD" + ); + assert_eq!( + run!(%["kebab-case"].to_string().to_upper_snake_case()), + "KEBAB_CASE" + ); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().to_upper_snake_case()), + "H4XX0R_Z_3_1337C0DE" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().to_upper_snake_case()), + "POSTGRE_SQL_CONNECTION" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().to_upper_snake_case()), + "POSTGRE_SQL_CONNECTION" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().to_upper_snake_case()), + "U_000A_LINE_FEED_LF" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().to_upper_snake_case()), + "THIS_IS_A_MULTI_LINE_STRING" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().to_upper_snake_case()), + "LOTS_OF_SPACE_AND_WHACKY_C_ARA_CTE_RS" + ); + assert_eq!( + run!(%["über CöÖl"].to_string().to_upper_snake_case()), + "ÜBER_CÖ_ÖL" + ); + assert_eq!( + run!(%["◌̈ubër Cöol"].to_string().to_upper_snake_case()), + "UBE_R_CÖOL" + ); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().to_upper_snake_case()), + "真是难以置信" + ); } #[test] fn test_to_lower_kebab_case() { - my_assert_eq!([!kebab! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "my-mixed-case-string-whichis-awesome-whatdo-youthink"); - my_assert_eq!([!kebab! UPPER], "upper"); - my_assert_eq!([!kebab! lower], "lower"); - my_assert_eq!([!kebab! lower_snake_case], "lower-snake-case"); - my_assert_eq!([!kebab! UPPER_SNAKE_CASE], "upper-snake-case"); - my_assert_eq!([!kebab! lowerCamelCase], "lower-camel-case"); - my_assert_eq!([!kebab! UpperCamelCase], "upper-camel-case"); - my_assert_eq!([!kebab! Capitalized], "capitalized"); - my_assert_eq!([!kebab! "THEY SAID: A quick brown fox jumps over the lazy dog."], "they-said-a-quick-brown-fox-jumps-over-the-lazy-dog"); - my_assert_eq!([!kebab! "hello_w🌎rld"], "hello-w-rld"); - my_assert_eq!([!kebab! "kebab-case"], "kebab-case"); - my_assert_eq!([!kebab! "~~h4xx0rZ <3 1337c0de"], "h4xx0r-z-3-1337c0de"); - my_assert_eq!([!kebab! PostgreSQLConnection], "postgre-sql-connection"); - my_assert_eq!([!kebab! PostgreSqlConnection], "postgre-sql-connection"); - my_assert_eq!([!kebab! "U+000A LINE FEED (LF)"], "u-000a-line-feed-lf"); - my_assert_eq!([!kebab! "\nThis\r\n is a\tmulti-line\nstring"], "this-is-a-multi-line-string"); - my_assert_eq!([!kebab! " lots of _ space and _whacky |c$ara_cte>>rs|"], "lots-of-space-and-whacky-c-ara-cte-rs"); - my_assert_eq!([!kebab! "über CöÖl"], "über-cö-öl"); - my_assert_eq!([!kebab! "◌̈ubër Cööl"], "ube-r-cööl"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!kebab! "真是难以置信!"], "真是难以置信"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_string().to_kebab_case()), + "my-mixed-case-string-whichis-awesome-whatdo-youthink" + ); + assert_eq!(run!(%[UPPER].to_string().to_kebab_case()), "upper"); + assert_eq!(run!(%[lower].to_string().to_kebab_case()), "lower"); + assert_eq!( + run!(%[lower_snake_case].to_string().to_kebab_case()), + "lower-snake-case" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().to_kebab_case()), + "upper-snake-case" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().to_kebab_case()), + "lower-camel-case" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().to_kebab_case()), + "upper-camel-case" + ); + assert_eq!( + run!(%[Capitalized].to_string().to_kebab_case()), + "capitalized" + ); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().to_kebab_case()), + "they-said-a-quick-brown-fox-jumps-over-the-lazy-dog" + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().to_kebab_case()), + "hello-w-rld" + ); + assert_eq!( + run!(%["kebab-case"].to_string().to_kebab_case()), + "kebab-case" + ); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().to_kebab_case()), + "h4xx0r-z-3-1337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().to_kebab_case()), + "postgre-sql-connection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().to_kebab_case()), + "postgre-sql-connection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().to_kebab_case()), + "u-000a-line-feed-lf" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().to_kebab_case()), + "this-is-a-multi-line-string" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().to_kebab_case()), + "lots-of-space-and-whacky-c-ara-cte-rs" + ); + assert_eq!( + run!(%["über CöÖl"].to_string().to_kebab_case()), + "über-cö-öl" + ); + assert_eq!( + run!(%["◌̈ubër Cööl"].to_string().to_kebab_case()), + "ube-r-cööl" + ); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().to_kebab_case()), + "真是难以置信" + ); } #[test] fn test_lower_camel() { - my_assert_eq!([!lower_camel! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "myMixedCaseStringWhichisAwesomeWhatdoYouthink"); - my_assert_eq!([!lower_camel! UPPER], "upper"); - my_assert_eq!([!lower_camel! lower], "lower"); - my_assert_eq!([!lower_camel! lower_snake_case], "lowerSnakeCase"); - my_assert_eq!([!lower_camel! UPPER_SNAKE_CASE], "upperSnakeCase"); - my_assert_eq!([!lower_camel! lowerCamelCase], "lowerCamelCase"); - my_assert_eq!([!lower_camel! UpperCamelCase], "upperCamelCase"); - my_assert_eq!([!lower_camel! Capitalized], "capitalized"); - my_assert_eq!([!lower_camel! "THEY SAID: A quick brown fox jumps over the lazy dog."], "theySaidAQuickBrownFoxJumpsOverTheLazyDog"); - my_assert_eq!([!lower_camel! "hello_w🌎rld"], "helloWRld"); - my_assert_eq!([!lower_camel! "kebab-case"], "kebabCase"); - my_assert_eq!([!lower_camel! "~~h4xx0rZ <3 1337c0de"], "h4xx0rZ31337c0de"); - my_assert_eq!([!lower_camel! PostgreSQLConnection], "postgreSqlConnection"); - my_assert_eq!([!lower_camel! PostgreSqlConnection], "postgreSqlConnection"); - my_assert_eq!([!lower_camel! "U+000A LINE FEED (LF)"], "u000aLineFeedLf"); - my_assert_eq!([!lower_camel! "\nThis\r\n is a\tmulti-line\nstring"], "thisIsAMultiLineString"); - my_assert_eq!([!lower_camel! " lots of _ space and _whacky |c$ara_cte>>rs|"], "lotsOfSpaceAndWhackyCAraCteRs"); - my_assert_eq!([!lower_camel! "über CöÖl"], "überCöÖl"); - my_assert_eq!([!lower_camel! "◌̈ubër Cööl"], "ubeRCööl"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!lower_camel! "真是难以置信!"], "真是难以置信"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_string().to_lower_camel_case()), + "myMixedCaseStringWhichisAwesomeWhatdoYouthink" + ); + assert_eq!(run!(%[UPPER].to_string().to_lower_camel_case()), "upper"); + assert_eq!(run!(%[lower].to_string().to_lower_camel_case()), "lower"); + assert_eq!( + run!(%[lower_snake_case].to_string().to_lower_camel_case()), + "lowerSnakeCase" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().to_lower_camel_case()), + "upperSnakeCase" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().to_lower_camel_case()), + "lowerCamelCase" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().to_lower_camel_case()), + "upperCamelCase" + ); + assert_eq!( + run!(%[Capitalized].to_string().to_lower_camel_case()), + "capitalized" + ); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().to_lower_camel_case()), + "theySaidAQuickBrownFoxJumpsOverTheLazyDog" + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().to_lower_camel_case()), + "helloWRld" + ); + assert_eq!( + run!(%["kebab-case"].to_string().to_lower_camel_case()), + "kebabCase" + ); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().to_lower_camel_case()), + "h4xx0rZ31337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().to_lower_camel_case()), + "postgreSqlConnection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().to_lower_camel_case()), + "postgreSqlConnection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().to_lower_camel_case()), + "u000aLineFeedLf" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().to_lower_camel_case()), + "thisIsAMultiLineString" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().to_lower_camel_case()), + "lotsOfSpaceAndWhackyCAraCteRs" + ); + assert_eq!( + run!(%["über CöÖl"].to_string().to_lower_camel_case()), + "überCöÖl" + ); + assert_eq!( + run!(%["◌̈ubër Cööl"].to_string().to_lower_camel_case()), + "ubeRCööl" + ); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().to_lower_camel_case()), + "真是难以置信" + ); } #[test] -fn test_camel() { - my_assert_eq!([!camel! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "MyMixedCaseStringWhichisAwesomeWhatdoYouthink"); - my_assert_eq!([!camel! UPPER], "Upper"); - my_assert_eq!([!camel! lower], "Lower"); - my_assert_eq!([!camel! lower_snake_case], "LowerSnakeCase"); - my_assert_eq!([!camel! UPPER_SNAKE_CASE], "UpperSnakeCase"); - my_assert_eq!([!camel! lowerCamelCase], "LowerCamelCase"); - my_assert_eq!([!camel! UpperCamelCase], "UpperCamelCase"); - my_assert_eq!([!camel! Capitalized], "Capitalized"); - my_assert_eq!([!camel! "THEY SAID: A quick brown fox jumps over the lazy dog."], "TheySaidAQuickBrownFoxJumpsOverTheLazyDog"); - my_assert_eq!([!camel! "hello_w🌎rld"], "HelloWRld"); - my_assert_eq!([!camel! "kebab-case"], "KebabCase"); - my_assert_eq!([!camel! "~~h4xx0rZ <3 1337c0de"], "H4xx0rZ31337c0de"); - my_assert_eq!([!camel! PostgreSQLConnection], "PostgreSqlConnection"); - my_assert_eq!([!camel! PostgreSqlConnection], "PostgreSqlConnection"); - my_assert_eq!([!camel! "U+000A LINE FEED (LF)"], "U000aLineFeedLf"); - my_assert_eq!([!camel! "\nThis\r\n is a\tmulti-line\nstring"], "ThisIsAMultiLineString"); - my_assert_eq!([!camel! " lots of _ space and _whacky |c$ara_cte>>rs|"], "LotsOfSpaceAndWhackyCAraCteRs"); - my_assert_eq!([!camel! "über CöÖl"], "ÜberCöÖl"); - my_assert_eq!([!camel! "◌̈ubër Cööl"], "UbeRCööl"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!camel! "真是难以置信!"], "真是难以置信"); +fn test_upper_camel() { + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what "do you" think?].to_string().to_upper_camel_case()), + "MyMixedCaseStringWhichisAwesomeWhatdoYouthink" + ); + assert_eq!(run!(%[UPPER].to_string().to_upper_camel_case()), "Upper"); + assert_eq!(run!(%[lower].to_string().to_upper_camel_case()), "Lower"); + assert_eq!( + run!(%[lower_snake_case].to_string().to_upper_camel_case()), + "LowerSnakeCase" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().to_upper_camel_case()), + "UpperSnakeCase" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().to_upper_camel_case()), + "LowerCamelCase" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().to_upper_camel_case()), + "UpperCamelCase" + ); + assert_eq!( + run!(%[Capitalized].to_string().to_upper_camel_case()), + "Capitalized" + ); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().to_upper_camel_case()), + "TheySaidAQuickBrownFoxJumpsOverTheLazyDog" + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().to_upper_camel_case()), + "HelloWRld" + ); + assert_eq!( + run!(%["kebab-case"].to_string().to_upper_camel_case()), + "KebabCase" + ); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().to_upper_camel_case()), + "H4xx0rZ31337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().to_upper_camel_case()), + "PostgreSqlConnection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().to_upper_camel_case()), + "PostgreSqlConnection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().to_upper_camel_case()), + "U000aLineFeedLf" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().to_upper_camel_case()), + "ThisIsAMultiLineString" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().to_upper_camel_case()), + "LotsOfSpaceAndWhackyCAraCteRs" + ); + assert_eq!( + run!(%["über CöÖl"].to_string().to_upper_camel_case()), + "ÜberCöÖl" + ); + assert_eq!( + run!(%["◌̈ubër Cööl"].to_string().to_upper_camel_case()), + "UbeRCööl" + ); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().to_upper_camel_case()), + "真是难以置信" + ); } #[test] fn test_capitalize() { - my_assert_eq!([!capitalize! my_ MixedCase STRING Which is " #awesome " - what do you think?], "My_MixedCaseSTRINGWhichis #awesome -whatdoyouthink?"); - my_assert_eq!([!capitalize! UPPER], "UPPER"); - my_assert_eq!([!capitalize! lower], "Lower"); - my_assert_eq!([!capitalize! lower_snake_case], "Lower_snake_case"); - my_assert_eq!([!capitalize! UPPER_SNAKE_CASE], "UPPER_SNAKE_CASE"); - my_assert_eq!([!capitalize! lowerCamelCase], "LowerCamelCase"); - my_assert_eq!([!capitalize! UpperCamelCase], "UpperCamelCase"); - my_assert_eq!([!capitalize! Capitalized], "Capitalized"); - my_assert_eq!([!capitalize! "THEY SAID: A quick brown fox jumps over the lazy dog."], "THEY SAID: A quick brown fox jumps over the lazy dog."); - my_assert_eq!([!capitalize! "hello_w🌎rld"], "Hello_w🌎rld"); - my_assert_eq!([!capitalize! "kebab-case"], "Kebab-case"); - my_assert_eq!([!capitalize! "~~h4xx0rZ <3 1337c0de"], "~~H4xx0rZ <3 1337c0de"); - my_assert_eq!([!capitalize! PostgreSQLConnection], "PostgreSQLConnection"); - my_assert_eq!([!capitalize! PostgreSqlConnection], "PostgreSqlConnection"); - my_assert_eq!([!capitalize! "U+000A LINE FEED (LF)"], "U+000A LINE FEED (LF)"); - my_assert_eq!([!capitalize! "\nThis\r\n is a\tmulti-line\nstring"], "\nThis\r\n is a\tmulti-line\nstring"); - my_assert_eq!([!capitalize! " lots of _ space and _whacky |c$ara_cte>>rs|"], " Lots of _ space and _whacky |c$ara_cte>>rs|"); - my_assert_eq!([!capitalize! "über CöÖl"], "Über CöÖl"); - my_assert_eq!([!capitalize! "◌̈ubër Cööl"], "◌̈Ubër Cööl"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!capitalize! "真是难以置信!"], "真是难以置信!"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what do you think?].to_string().capitalize()), + "My_MixedCaseSTRINGWhichis #awesome -whatdoyouthink?" + ); + assert_eq!(run!(%[UPPER].to_string().capitalize()), "UPPER"); + assert_eq!(run!(%[lower].to_string().capitalize()), "Lower"); + assert_eq!( + run!(%[lower_snake_case].to_string().capitalize()), + "Lower_snake_case" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().capitalize()), + "UPPER_SNAKE_CASE" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().capitalize()), + "LowerCamelCase" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().capitalize()), + "UpperCamelCase" + ); + assert_eq!(run!(%[Capitalized].to_string().capitalize()), "Capitalized"); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().capitalize()), + "THEY SAID: A quick brown fox jumps over the lazy dog." + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().capitalize()), + "Hello_w🌎rld" + ); + assert_eq!(run!(%["kebab-case"].to_string().capitalize()), "Kebab-case"); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().capitalize()), + "~~H4xx0rZ <3 1337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().capitalize()), + "PostgreSQLConnection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().capitalize()), + "PostgreSqlConnection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().capitalize()), + "U+000A LINE FEED (LF)" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().capitalize()), + "\nThis\r\n is a\tmulti-line\nstring" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().capitalize()), + " Lots of _ space and _whacky |c$ara_cte>>rs|" + ); + assert_eq!(run!(%["über CöÖl"].to_string().capitalize()), "Über CöÖl"); + assert_eq!(run!(%["◌̈ubër Cööl"].to_string().capitalize()), "◌̈Ubër Cööl"); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().capitalize()), + "真是难以置信!" + ); } #[test] fn test_decapitalize() { - my_assert_eq!([!decapitalize! my_ MixedCase STRING Which is " #awesome " - what do you think?], "my_MixedCaseSTRINGWhichis #awesome -whatdoyouthink?"); - my_assert_eq!([!decapitalize! UPPER], "uPPER"); - my_assert_eq!([!decapitalize! lower], "lower"); - my_assert_eq!([!decapitalize! lower_snake_case], "lower_snake_case"); - my_assert_eq!([!decapitalize! UPPER_SNAKE_CASE], "uPPER_SNAKE_CASE"); - my_assert_eq!([!decapitalize! lowerCamelCase], "lowerCamelCase"); - my_assert_eq!([!decapitalize! UpperCamelCase], "upperCamelCase"); - my_assert_eq!([!decapitalize! Capitalized], "capitalized"); - my_assert_eq!([!decapitalize! "THEY SAID: A quick brown fox jumps over the lazy dog."], "tHEY SAID: A quick brown fox jumps over the lazy dog."); - my_assert_eq!([!decapitalize! "hello_w🌎rld"], "hello_w🌎rld"); - my_assert_eq!([!decapitalize! "kebab-case"], "kebab-case"); - my_assert_eq!([!decapitalize! "~~h4xx0rZ <3 1337c0de"], "~~h4xx0rZ <3 1337c0de"); - my_assert_eq!([!decapitalize! PostgreSQLConnection], "postgreSQLConnection"); - my_assert_eq!([!decapitalize! PostgreSqlConnection], "postgreSqlConnection"); - my_assert_eq!([!decapitalize! "U+000A LINE FEED (LF)"], "u+000A LINE FEED (LF)"); - my_assert_eq!([!decapitalize! "\nThis\r\n is a\tmulti-line\nstring"], "\nthis\r\n is a\tmulti-line\nstring"); - my_assert_eq!([!decapitalize! " lots of _ space and _whacky |c$ara_cte>>rs|"], " lots of _ space and _whacky |c$ara_cte>>rs|"); - my_assert_eq!([!decapitalize! "über CöÖl"], "über CöÖl"); - my_assert_eq!([!decapitalize! "◌̈ubër Cööl"], "◌̈ubër Cööl"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!decapitalize! "真是难以置信!"], "真是难以置信!"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what do you think?].to_string().decapitalize()), + "my_MixedCaseSTRINGWhichis #awesome -whatdoyouthink?" + ); + assert_eq!(run!(%[UPPER].to_string().decapitalize()), "uPPER"); + assert_eq!(run!(%[lower].to_string().decapitalize()), "lower"); + assert_eq!( + run!(%[lower_snake_case].to_string().decapitalize()), + "lower_snake_case" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().decapitalize()), + "uPPER_SNAKE_CASE" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().decapitalize()), + "lowerCamelCase" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().decapitalize()), + "upperCamelCase" + ); + assert_eq!( + run!(%[Capitalized].to_string().decapitalize()), + "capitalized" + ); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().decapitalize()), + "tHEY SAID: A quick brown fox jumps over the lazy dog." + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().decapitalize()), + "hello_w🌎rld" + ); + assert_eq!( + run!(%["kebab-case"].to_string().decapitalize()), + "kebab-case" + ); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().decapitalize()), + "~~h4xx0rZ <3 1337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().decapitalize()), + "postgreSQLConnection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().decapitalize()), + "postgreSqlConnection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().decapitalize()), + "u+000A LINE FEED (LF)" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().decapitalize()), + "\nthis\r\n is a\tmulti-line\nstring" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().decapitalize()), + " lots of _ space and _whacky |c$ara_cte>>rs|" + ); + assert_eq!(run!(%["über CöÖl"].to_string().decapitalize()), "über CöÖl"); + assert_eq!( + run!(%["◌̈ubër Cööl"].to_string().decapitalize()), + "◌̈ubër Cööl" + ); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().decapitalize()), + "真是难以置信!" + ); } #[test] fn test_title() { - my_assert_eq!([!title! my_ MixedCase STRING Which is " #awesome " - what do you think?], "My Mixed Case String Whichis Awesome Whatdoyouthink"); - my_assert_eq!([!title! UPPER], "Upper"); - my_assert_eq!([!title! lower], "Lower"); - my_assert_eq!([!title! lower_snake_case], "Lower Snake Case"); - my_assert_eq!([!title! UPPER_SNAKE_CASE], "Upper Snake Case"); - my_assert_eq!([!title! lowerCamelCase], "Lower Camel Case"); - my_assert_eq!([!title! UpperCamelCase], "Upper Camel Case"); - my_assert_eq!([!title! Capitalized], "Capitalized"); - my_assert_eq!([!title! "THEY SAID: A quick brown fox jumps over the lazy dog."], "They Said A Quick Brown Fox Jumps Over The Lazy Dog"); - my_assert_eq!([!title! "hello_w🌎rld"], "Hello W Rld"); - my_assert_eq!([!title! "kebab-case"], "Kebab Case"); - my_assert_eq!([!title! "~~h4xx0rZ <3 1337c0de"], "H4xx0r Z 3 1337c0de"); - my_assert_eq!([!title! PostgreSQLConnection], "Postgre Sql Connection"); - my_assert_eq!([!title! PostgreSqlConnection], "Postgre Sql Connection"); - my_assert_eq!([!title! "U+000A LINE FEED (LF)"], "U 000a Line Feed Lf"); - my_assert_eq!([!title! "\nThis\r\n is a\tmulti-line\nstring"], "This Is A Multi Line String"); - my_assert_eq!([!title! " lots of _ space and _whacky |c$ara_cte>>rs|"], "Lots Of Space And Whacky C Ara Cte Rs"); - my_assert_eq!([!title! "über CöÖl"], "Über Cö Öl"); - my_assert_eq!([!title! "◌̈ubër Cööl"], "Ube R Cööl"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!title! "真是难以置信!"], "真是难以置信"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what do you think?].to_string().to_title_case()), + "My Mixed Case String Whichis Awesome Whatdoyouthink" + ); + assert_eq!(run!(%[UPPER].to_string().to_title_case()), "Upper"); + assert_eq!(run!(%[lower].to_string().to_title_case()), "Lower"); + assert_eq!( + run!(%[lower_snake_case].to_string().to_title_case()), + "Lower Snake Case" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().to_title_case()), + "Upper Snake Case" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().to_title_case()), + "Lower Camel Case" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().to_title_case()), + "Upper Camel Case" + ); + assert_eq!( + run!(%[Capitalized].to_string().to_title_case()), + "Capitalized" + ); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().to_title_case()), + "They Said A Quick Brown Fox Jumps Over The Lazy Dog" + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().to_title_case()), + "Hello W Rld" + ); + assert_eq!( + run!(%["kebab-case"].to_string().to_title_case()), + "Kebab Case" + ); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().to_title_case()), + "H4xx0r Z 3 1337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().to_title_case()), + "Postgre Sql Connection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().to_title_case()), + "Postgre Sql Connection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().to_title_case()), + "U 000a Line Feed Lf" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().to_title_case()), + "This Is A Multi Line String" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().to_title_case()), + "Lots Of Space And Whacky C Ara Cte Rs" + ); + assert_eq!( + run!(%["über CöÖl"].to_string().to_title_case()), + "Über Cö Öl" + ); + assert_eq!( + run!(%["◌̈ubër Cööl"].to_string().to_title_case()), + "Ube R Cööl" + ); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().to_title_case()), + "真是难以置信" + ); } #[test] fn test_insert_spaces() { - my_assert_eq!([!insert_spaces! my_ MixedCase STRING Which is " #awesome " - what do you think?], "my Mixed Case STRING Whichis awesome whatdoyouthink"); - my_assert_eq!([!insert_spaces! UPPER], "UPPER"); - my_assert_eq!([!insert_spaces! lower], "lower"); - my_assert_eq!([!insert_spaces! lower_snake_case], "lower snake case"); - my_assert_eq!([!insert_spaces! UPPER_SNAKE_CASE], "UPPER SNAKE CASE"); - my_assert_eq!([!insert_spaces! lowerCamelCase], "lower Camel Case"); - my_assert_eq!([!insert_spaces! UpperCamelCase], "Upper Camel Case"); - my_assert_eq!([!insert_spaces! Capitalized], "Capitalized"); - my_assert_eq!([!insert_spaces! "THEY SAID: A quick brown fox jumps over the lazy dog."], "THEY SAID A quick brown fox jumps over the lazy dog"); - my_assert_eq!([!insert_spaces! "hello_w🌎rld"], "hello w rld"); - my_assert_eq!([!insert_spaces! "kebab-case"], "kebab case"); - my_assert_eq!([!insert_spaces! "~~h4xx0rZ <3 1337c0de"], "h4xx0r Z 3 1337c0de"); - my_assert_eq!([!insert_spaces! PostgreSQLConnection], "Postgre SQL Connection"); - my_assert_eq!([!insert_spaces! PostgreSqlConnection], "Postgre Sql Connection"); - my_assert_eq!([!insert_spaces! "U+000A LINE FEED (LF)"], "U 000A LINE FEED LF"); - my_assert_eq!([!insert_spaces! "\nThis\r\n is a\tmulti-line\nstring"], "This is a multi line string"); - my_assert_eq!([!insert_spaces! " lots of _ space and _whacky |c$ara_cte>>rs|"], "lots of space and whacky c ara cte rs"); - my_assert_eq!([!insert_spaces! "über CöÖl"], "über Cö Öl"); - my_assert_eq!([!insert_spaces! "◌̈ubër Cööl"], "ube r Cööl"); // The ë (and only the e) uses a post-fix combining character - my_assert_eq!([!insert_spaces! "真是难以置信!"], "真是难以置信"); + assert_eq!( + run!(%[my_ MixedCase STRING Which is " #awesome " - what do you think?].to_string().insert_spaces()), + "my Mixed Case STRING Whichis awesome whatdoyouthink" + ); + assert_eq!(run!(%[UPPER].to_string().insert_spaces()), "UPPER"); + assert_eq!(run!(%[lower].to_string().insert_spaces()), "lower"); + assert_eq!( + run!(%[lower_snake_case].to_string().insert_spaces()), + "lower snake case" + ); + assert_eq!( + run!(%[UPPER_SNAKE_CASE].to_string().insert_spaces()), + "UPPER SNAKE CASE" + ); + assert_eq!( + run!(%[lowerCamelCase].to_string().insert_spaces()), + "lower Camel Case" + ); + assert_eq!( + run!(%[UpperCamelCase].to_string().insert_spaces()), + "Upper Camel Case" + ); + assert_eq!( + run!(%[Capitalized].to_string().insert_spaces()), + "Capitalized" + ); + assert_eq!( + run!(%["THEY SAID: A quick brown fox jumps over the lazy dog."].to_string().insert_spaces()), + "THEY SAID A quick brown fox jumps over the lazy dog" + ); + assert_eq!( + run!(%["hello_w🌎rld"].to_string().insert_spaces()), + "hello w rld" + ); + assert_eq!( + run!(%["kebab-case"].to_string().insert_spaces()), + "kebab case" + ); + assert_eq!( + run!(%["~~h4xx0rZ <3 1337c0de"].to_string().insert_spaces()), + "h4xx0r Z 3 1337c0de" + ); + assert_eq!( + run!(%[PostgreSQLConnection].to_string().insert_spaces()), + "Postgre SQL Connection" + ); + assert_eq!( + run!(%[PostgreSqlConnection].to_string().insert_spaces()), + "Postgre Sql Connection" + ); + assert_eq!( + run!(%["U+000A LINE FEED (LF)"].to_string().insert_spaces()), + "U 000A LINE FEED LF" + ); + assert_eq!( + run!(%["\nThis\r\n is a\tmulti-line\nstring"].to_string().insert_spaces()), + "This is a multi line string" + ); + assert_eq!( + run!(%[" lots of _ space and _whacky |c$ara_cte>>rs|"].to_string().insert_spaces()), + "lots of space and whacky c ara cte rs" + ); + assert_eq!( + run!(%["über CöÖl"].to_string().insert_spaces()), + "über Cö Öl" + ); + assert_eq!( + run!(%["◌̈ubër Cööl"].to_string().insert_spaces()), + "ube r Cööl" + ); // The ë (and only the e) uses a post-fix combining character + assert_eq!( + run!(%["真是难以置信!"].to_string().insert_spaces()), + "真是难以置信" + ); }