diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dffae887..bb670873 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,14 +41,14 @@ jobs: path: Cargo.lock msrv: - name: MSRV (1.61) Compiles + name: MSRV (1.63) Compiles runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.61 + toolchain: 1.63 - run: cargo check style-check: @@ -61,6 +63,17 @@ jobs: toolchain: stable - run: ./style-check.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/CHANGELOG.md b/CHANGELOG.md index f5a420e2..8e199194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,455 @@ +# Major Version 0.3 + +## 0.3.0 + +### Variable Expansions + +* `#x` now outputs the contents of `x` in a transparent group. +* `#..x` outputs the contents of `x` "flattened" directly to the output stream. + +### New Commands + +* Core commands: + * `[!error! ...]` to output a compile error. + * `[!set! #x += ...]` to performantly add extra characters to a variable's stream. + * `[!set! _ = ...]` interprets its arguments but then ignores any outputs. + * `[!stream! ...]` can be used to just output its interpreted contents. It's useful to create a stream value inside an expression. + * `[!reinterpret! ...]` is like an `eval` command in scripting languages. It takes a stream, and parses/interprets it. + * `[!settings! { ... }]` can be used to adjust the iteration limit. +* Expression commands: + * The expression block `#(let x = 123; 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! #stream]` + * `[!length! #stream]` which 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 separator tokens between each token tree in a stream. + * `[!split! ...]` which can be used to split a stream with a given separating stream. + * `[!comma_split! ...]` which can be used to split a stream on `,` tokens. + * `[!zip! [#countries #flags #capitals]]` which can be used to combine multiple streams together. +* Destructuring commands: + * `[!let! = ...]` does destructuring/parsing (see next section). Note `[!let! #..x = ...]` is equivalent to `[!set! #x = ...]` + +### 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 `+`. + +The following are recognized values: +* 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`, to a grouped stream with `as group` 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. Equivalent to `[!error! #(x.debug_string())]` + * `.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. +* Variable bindings: + * `#x` - Reads a token tree, writes its content (opposite of `#x`). Equivalent to `@(x = @TOKEN_OR_GROUP_CONTENT)` + * `#..x` - Reads a stream, writes a stream (opposite of `#..x`). + * If it's at the end of the transformer stream, it's equivalent to `@(x = @REST)`. + * If it's followed by a token `T` in the transformer stream, it's equivalent to `@(x = @[UNTIL T])` + * `#>>x` - Reads a token tree, appends a token tree (can be read back with `!for! #y in #x { ... }`) + * `#>>..x` - Reads a token tree, appends a stream (i.e. flatten it if it's a group) + * `#..>>x` - Reads a stream, appends a group (can be read back with `!for! #y in #x { ... }`) + * `#..>>..x` - Reads a stream, appends a stream +* Named destructurings: + * `@IDENT` - Consumes and output any ident. + * `@PUNCT` - Consumes and outputs any punctation + * `@LITERAL` - Consumes and outputs any literal + * `@[GROUP ...]` - Consumes a none-delimited group. Its arguments are used to transform the group's contents. + * `@[EXACT ...]` - Interprets its arguments (i.e. variables are substituted, not bound; and command output is gathered) into an "exact match stream". And then expects to consume exactly the same stream from the input. It outputs the parsed stream. +* Commands: Their output is appended to the transform's output. Useful patterns include: + * `@(inner = ...) [!stream! #inner]` - wraps the output in a transparent group + +### To come +* Method calls continued + * Create `CopyOnWrite<..>` in `bindings.rs`, add `CopyOnWriteValue` support to `wrap_method!` and change it so that `debug_string` takes CowValue + * ... maybe we just use `CopyOnWriteValue` everywhere instead of `OwnedValue`?? + * Check if we actually need `RequestedPlaceOwnership::SharedReference` or `RequestedPlaceOwnership::LateBound` and potentially remove them if not + * Consider how to align: + * `ResolvedValue::into_mutable` (used by LateBound Owned => Mutable Arg; such as a method object) + * `context::return_owned` (used by Owned returned when Mutable requested, + such as a method argument) + * CURRENTLY WE HAVE: + * Mapping "context.return_shared(X)" => requested kind + * Late bound method arguments => needed argument + * PERHAPS WE HAVE: + * Late bound => Resolve similar to context.return_X + * In method call, we do expect/assert it's the right type + * Add tests for TODO[access-refactor] (i.e. changing places to resolve correctly) + * Add more impl_resolvable_argument_for + * Scrap `#>>x` etc in favour of `#(x.push(@TOKEN))` + * TODO[range-refactor] & some kind of more thought through typed reference support - e.g. slices, mutable slices? + * TODO[operation-refactor] + * Including no clone required for testing equality of streams, objects and arrays + * Add better way of defining methods once / lazily, and binding them to + an object type. +* Consider: + * Changing evaluation to allow `ResolvedValue` (i.e. allow references) + * Removing span range from value: + * Moving it to an `OwnedValue` or `Owned` + * Using `EvaluationError` (without a span!) inside the calculation, and adding the span in the evaluator (nb. it may still need to be able to propogate an `ExecutionInterrupt` internally) + * Using ResolvedValue in place of ExpressionValue e.g. inside arrays +* Introduce `~(...)` and `r~(...)` streams instead of `[!stream! ...]` and `[!raw! ...]` +* Introduce interpreter stack frames + * Read the `REVERSION` comment in the `Parsers Revisited` section below to consider approaches, which will work with possibly needing to revert state if a parser fails. + * Design the data model => is it some kind of linked list of frames? + * If we introduce functions in future, then a reference is a closure, in _lexical_ scope, which is different from stack frames. + * We probably don't want to allow closures to start with. + * Maybe we simply pass in a lexical parent frame when resolving variable references? + * Possibly with some local cache of parent references, so we don't need to re-discover them if they're used multiple times in a loop + e.g. `x` in `#(let x = 0; {{{ loop { x++; if x < 10 { break }} }}})` + * Add a new frame inside loops, or wherever we see `{ ... }` + * Merge `GroupedVariable` and `ExpressionBlock` into an `ExplicitExpression`: + * Either `#ident` or `#(...)` or `#{ ... }`... + the latter defines a new variable stack frame, just like Rust + * To avoid confusion (such as below) and teach the user to only include #var where necessary, only expression _blocks_ are allowed in an expression. + * Confusion example: `let x; x = #(let x = 123; 5)`. This isn't allowed in normal rust because the inside is a `{ .. }` which defines a new scope. + * The `#()` syntax can be used in certain places (such as parse streams) +* Support for/while/loop/break/continue inside expressions + * And allow them to start with an expression block with `{}` or `#{}` or a stream block with `~{}` + QUESTION: Do we require either `#{}` or `~{}`? Maybe! That way we can give a helpful error message. + * ... and remove the control flow commands `[!if! ...]` / `[!while! ...]` / `[!break! ...]` / `[!for! ...]`, `[!while! ...]` and `[!loop! ...]` +* TRANSFORMERS => PARSERS cont + * Manually search for transform and rename to parse in folder names and file. + * Parsers no longer output to a stream. + * See all of `Parsers Revisited` below! + * We let `#(let x)` INSIDE a `@(...)` bind to the same scope as its surroundings... + Now some repetition like `@{..}*` needs to introduce its own scope. + ==> Annoyingly, a `@(..)*` has to really push/output to an array internally to have good developer experience; which means we need to solve the "relatively performant staged/reverted interpreter state" problem regardless; and putting an artificial limitation on conditional parse streams to not do that doesn't really work. TBC - needs more consideration. + * Don't support `@(x = ...)` - instead we can have `#(x = @[STREAM ...])` + * 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) + * Scrap `[!set!]` in favour of `#(x = ..)` and `#(x += ..)` + * Scrap `[!let!]` in favour of `#(let = x)` +* Implement the following named parsers + * `@TOKEN_TREE` + * `@TOKEN_OR_GROUP_CONTENT` - Literal, Ident, Punct or None-group content. + * `@INFER_TOKEN_TREE` - Infers parsing as a value, falls back to Stream + * `@[ANY_GROUP ...]` + * `@REST` + * `@[FORK @{ ...parser... }]` the parser block creates a `commit=false` variable, if this is set to `commit=true` then it commits the fork. + * `@[PEEK ...]` which does a `@[FORK ...]` internally + * `@[FIELDS { ... }]` and `@[SUBFIELDS { ... }]` + * Add ability to add scope to interpreter state (see `Parsers Revisited`) and can then add: + * `@[OPTIONAL ...]` and `@(...)?` + * `@[REPEATED { ... }]` (see below) + * `@[UNTIL @{...}]` - takes an explicit parse block or parser. Reverts any state change if it doesn't match. + * `[!match! ...]` command + * `@[MATCH { ... }]` (with `#..x` as a catch-all) with optional arms... + * `#(..)?`, `#(..)+`, `#(..),+`, `#(..)*`, `#(..),*` +```rust +@[REPEATED { + item: @(...), // Captured into #item variable + separator?: @(), // Captured into #separator variable + min?: 0, + max?: 1000000, + handle_item?: { #item }, // Default is to output the grouped item. #()+ instead uses `{ (#..item) }` + handle_separator?: { }, // Default is to not output the separator +}] +``` +* 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 +* 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! + => Why don't they use a cheap hash of the code as a cache key? + - 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 +* Add `LiteralPattern` (wrapping a `Literal`) +* Add `Eq` support on composite types and streams +* Consider: + * Moving control flow (`for` and `while`) to the expression side? Possibly with an `output` auto-variable with `output += [!stream! ...]` + * Dropping lots of the `group` wrappers? + * If any types should have reference semantics instead of clone/value semantics? + * Supporting anonymous functions, and support them in e.g. `intersperse` + * Adding all of these: https://veykril.github.io/tlborm/decl-macros/minutiae/fragment-specifiers.html#ty + * Adding `preinterpret::macro` + * Adding `!define_command!` + * Adding `!define_parser!` + * Whether `preinterpret` should start in expression mode? + => Or whether to have `preinterpet::stream` / `preinterpret::run` / `preinterpret::define_macro` options? + => Maybe `preinterpret::preinterpret` is marked as deprecated; starts in `stream` mode, and enables `[!set!]`? +* `[!is_set! #x]` +* Have UntypedInteger have an inner representation of either i128 or literal (and same with float) +* Maybe add a `@[REINTERPRET ..]` parser. +* CastTarget expansion: + * Add `as iterator` and uncomment the test at the end of `test_range()` + * Support a CastTarget of `array` using `into_iterator()`. + * Add `as ident` and `as literal` casting and support it for string, array and stream using concat recursive. + * Add casts of other integers to char, via `char::from_u32(u32::try_from(x))` +* Put `[!set! ...]` inside an opt-in feature because it's quite confusing. +* TODO check +* Check all `#[allow(unused)]` and remove any which aren't needed +* Add benches inspired by this: https://github.com/dtolnay/quote/tree/master/benches +* Work on book + * Input paradigms: + * Streams + * StreamInput / ValueInput / CodeInput + * Including documenting expressions + * There are three main kinds of commands: + * Those taking a stream as-is + * Those taking some { fields } + * Those taking some custom syntax, e.g. `!set!`, `!if!`, `!while!` etc + +### Parsers Revisited +```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) +// +// ==> D might be easiest, most flexible AND most performant... But it might not cover enough use cases. +// ... but I think advising people to only mutate lexical-closure'd state at the end, when a parse is confirmed, is reasonably... +// +// +// 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 {} +} +``` + +### Pushed to 0.4: +* Performance: + * Use a small-vec optimization in some places + * Get rid of needless cloning of commands/variables etc + * Avoid needless token stream clones: Have `x += y` take `y` as OwnedOrRef, and either handles it as owned or shared reference (by first cloning) +* Iterators: + * Iterator value type (with an inbuilt iteration count / limit check) + * Allow `for` lazily reading from iterators + * Support unbounded iterators `[!range! xx..]` +* Fork of syn to: + * Fix issues in Rust Analyzer + * 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 whichcan 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` ?? + * Allow variables to use CoW semantics. Variables can be Owned(ParseBuffer) or `Slice(ParseBufferSlice), where a ParseBufferSlice is some form of reference counting to a ParseBufferCore, and a FromLocation and ToLocation which are assumed to be at the same level. + * Permit `[!parse_while! (!stream! ...) from #x { ... }]` + * Fix `any_punct()` to ignore none groups + * Groups can either be: + * Raw Groups + * Or created groups, where we store `DelimSpan` for re-parsing and accessing the open/close delimiters (this will let us improve `invalid_content_wrong_group`) + * 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... +* Further syn parsings (e.g. item, fields, etc) + # Major Version 0.2 ## 0.2.0 diff --git a/Cargo.toml b/Cargo.toml index 25735ffe..5a99a9b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,17 @@ keywords = ["macros", "declarative-macros", "toolkit", "interpreter", "preproces categories = ["development-tools", "compilers"] # MSRV 1.56.0 is the start of Edition 2021 # MSRV 1.61.0 is the current MSRV of syn +# MRSV 1.63.0 is needed to support RefMut::filter_map # If changing this, also update the local-check-msrv.sh script and ci.yml -rust-version = "1.61" +rust-version = "1.63" [lib] proc-macro = true [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.98", default-features = false, features = ["parsing", "derive", "printing", "clone-impls", "full"] } +quote = { version = "1.0.38", default-features = false } + +[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..16151473 --- /dev/null +++ b/KNOWN_ISSUES.md @@ -0,0 +1,8 @@ +# 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. +* `&&` and `||` are not lazy in expressions + * This needs a custom expression parser, rather than just leveraging syn diff --git a/README.md b/README.md index d68d9a39..9a77cf9f 100644 --- a/README.md +++ b/README.md @@ -334,10 +334,6 @@ macro_rules! impl_new_type { ## Future Extension Possibilities -### Add github docs page / rust book - -Add a github docs page / rust book at this repository, to allow us to build out a suite of examples, like `serde` or the little book of macros. - ### Destructuring / Parsing Syntax, and Declarative Macros 2.0 I have a vision for having preinterpret effectively replace the use of declarative macros in the Rust ecosystem, by: @@ -359,7 +355,7 @@ The idea is that we create two new tools: In more detail: -* `[!parse! (DESTRUCTURING) = (INPUT)]` is a more general `[!set!]` which acts like a `let = else { panic!() }`. It takes a `()`-wrapped parse destructuring on the left and a token stream as input on the right. Any `#x` in the parse definition acts as a binding rather than as a substitution. Parsing will handled commas intelligently, and accept intelligent parse operations to do heavy-lifting for the user. Parse operations look like `[!OPERATION! DESTRUCTURING]` with the operation name in `UPPER_SNAKE_CASE`. Some examples might be: +* `[!let! (DESTRUCTURING) = (INPUT)]` (or maybe `!let!` or `!set!`) is a more general `[!set!]` which acts like a `let = else { panic!() }`. It takes a `()`-wrapped parse destructuring on the left and a `()`-wrapped token stream as input on the right. Any `#x` in the parse definition acts as a binding rather than as a substitution. Parsing will handled commas intelligently, and accept intelligent parse operations to do heavy-lifting for the user. Parse operations look like `[!OPERATION! DESTRUCTURING]` with the operation name in `UPPER_SNAKE_CASE`. Some examples might be: * `[!FIELDS! { hello: #a, world?: #b }]` - which can be parsed in any order, cope with trailing commas, and forbid fields in the source stream which aren't in the destructuring. * `[!SUBFIELDS! { hello: #a, world?: #b }]` - which can parse fields in any order, cope with trailing commas, and allow fields in the source stream which aren't in the destructuring. * `[!ITEM! { #ident, #impl_generics, ... }]` - which calls syn's parse item on the token @@ -367,9 +363,11 @@ In more detail: * More tailored examples, such as `[!GENERICS! { impl: #x, type: #y, where: #z }]` which uses syn to parse the generics, and then uses subfields on the result. * Possibly `[!GROUPED! #x]` to parse a group with no brackets, to avoid parser ambiguity in some cases * `[!OPTIONAL! ...]` might be supported, but other complex logic (loops, matching) is delayed lazily until interpretation time - which feels more intuitive. -* `[!for! (DESTRUCTURING) in (INPUT) { ... }]` which operates like the rust `for` loop, and uses a parse destructuring on the left, and has support for optional commas between values -* `[!match! (INPUT) => { (DESTRUCTURING_1) => { ... }, (DESTRUCTURING_2) => { ... }, (#fallback) => { ... } }]` which operates like a rust `match` expression, and can replace the function of the branches of declarative macro inputs. -* `[!macro_rules! name!(DESTRUCTURING) = { ... }]` which can define a declarative macro, but just parses its inputs as a token stream, and uses preinterpret for its heavy lifting. +* `[!for! (DESTRUCTURING) in [!split_at! INPUT ,] { ... }]` which operates like the rust `for` loop over token trees, allowing them to be parsed. Instead of the destructuring, you can also just use a variable e.g. `#x` Operates well with the `[!split_at! ..]` command. +* `[!while! [!parse_start_from! #variable DESTRUCTURING] { ... }]` can be used to consume a destructuring from the start of #variable. `[!parse_start_from! ..]` returns true if the parse succeeded, false if the variable is an empty token stream, and errors otherwise. +* `[!parse_loop! #variable as (DESTRUCTURING), { ... }]` where the `,` is a separator and cleverly handles trailing `,` or `;` - similar to `syn::Punctuated` +* `[!match! (INPUT) => { (DESTRUCTURING_1) => { ... }, (DESTRUCTURING_2) => { ... }, #fallback => { ... } }]` which operates like a rust `match` expression, and can replace the function of the branches of declarative macro inputs. +* `[!macro_rules! name!(DESTRUCTURING) = { ... }]` which can define a declarative macro, but just parses its inputs as a token stream, and uses preinterpret for its heavy lifting. This could alternatively exist as `preinterpret::define_macro!{ my_macro!(DESTRUCTURING) = { ... }}`. And then we can end up with syntax like the following: @@ -381,7 +379,7 @@ And then we can end up with syntax like the following: // A simple macro can just take a token stream as input preinterpret::preinterpret! { [!macro_rules! my_macro!(#input) { - [!for! (#trait for #type) in (#input) { + [!parse_loop! #input as (#trait for #type), { impl #trait for #type }] }] @@ -403,9 +401,9 @@ preinterpret::preinterpret! { punctuation?: #punct = ("!") // Default }] ) = { - [!for! ( + [!parse_loop! #type_list as ( #type [!GENERICS! { impl: #impl_generics, type: #type_generics }] - ) in (#type_list) { + ) { impl<#impl_generics> SuperDuper for #type #type_generics { const Hello: &'static str = [!string! #hello " " #world #punct]; } @@ -414,95 +412,43 @@ preinterpret::preinterpret! { } ``` -### Possible extension: Integer commands - -Each of these commands functions in three steps: -* Apply the interpreter to the token stream, which recursively executes preinterpret commands. -* Iterate over each token (recursing into groups), expecting each to be an integer literal. -* Apply some command-specific mapping to this stream of integer literals, and output a single integer literal without its type suffix. The suffix can be added back manually if required with a wrapper such as `[!literal! [!add! 1 2] u64]`. +### Possible extension: Token stream commands -Integer commands under consideration are: +* `[!ungroup! #stream]` expects `#stream` to be a single group and unwraps it once +* `[!flatten! #stream]` removes all singleton groups from `#stream`, leaving a token stream of idents, literals and punctuation -* `[!add! 5u64 9 32]` outputs `46`. It takes any number of integers and outputs their sum. The calculation operates in `u128` space. -* `[!sub! 64u32 1u32]` outputs `63`. It takes two integers and outputs their difference. The calculation operates in `i128` space. -* `[!mod! $length 2]` outputs `0` if `$length` is even, else `1`. It takes two integers `a` and `b`, and outputs `a mod b`. +We could support a postfix calling convention, such as the `[!pipe! ...]` special command: `[!pipe! #stream > #x [!index! #x[4]] > #x [!ungroup! #x]]` or something like a scala for comprehension. But honestly, just saving things to variables might be cleaner. -We also support the following assignment commands: +### Possible extension: Better performance via incremental parsing -* `[!increment! #i]` is shorthand for `[!set! #i = [!add! #i 1]]` and outputs no tokens. +Incremental parsing using a fork of syn ([see issue](https://github.com/dtolnay/syn/issues/1842)) would allow: -Even better - we could even support calculator-style expression interpretation: +* Cheaper conversions between variables and parsing. +* `[!consume_from! #stream #x]` where `#x` is read as the first token tree from `#stream` +* `[!consume_from! #stream ()]` where the parser is read greedily from `#stream` -* `[!usize! (5 + 10) / mod(4, 2)]` outputs `7usize` +Forking syn may also allow some parts to be made more performant. ### Possible extension: User-defined commands -* `[!define! [!my_command! ] { }]` +* `[!define_command! [!my_command! ] { }]` +* Some ability to define and re-use commands across multiple invocations + without being too expensive. Still unsure how to make this work. -### Possible extension: Boolean commands +### Possible extension: Further utility commands -Each of these commands functions in three steps: -* Apply the interpreter to the token stream, which recursively executes preinterpret commands. -* Expects to read exactly two token trees (unless otherwise specified) -* Apply some command-specific comparison, and outputs the boolean literal `true` or `false`. - -Comparison commands under consideration are: -* `[!eq! #foo #bar]` outputs `true` if `#foo` and `#bar` are exactly the same token tree, via structural equality. For example: - * `[!eq! (3 4) (3 4)]` outputs `true` because the token stream ignores spacing. - * `[!eq! 1u64 1]` outputs `false` because these are different literals. -* `[!lt! #foo #bar]` outputs `true` if `#foo` is an integer literal and less than `#bar` -* `[!gt! #foo #bar]` outputs `true` if `#foo` is an integer literal and greater than `#bar` -* `[!lte! #foo #bar]` outputs `true` if `#foo` is an integer literal and less than or equal to `#bar` -* `[!gte! #foo #bar]` outputs `true` if `#foo` is an integer literal and greater than or equal to `#bar` -* `[!not! #foo]` expects a single boolean literal, and outputs the negation of `#foo` +Other boolean commands could be possible, similar to numeric commands: +* `[!tokens_eq! #foo #bar]` outputs `true` if `#foo` and `#bar` are exactly the same token tree, via structural equality. For example: + * `[!tokens_eq! (3 4) (3 4)]` outputs `true` because the token stream ignores spacing. + * `[!tokens_eq! 1u64 1]` outputs `false` because these are different literals. + * This can be effectively done already with `#(x.debug_string() == y.debug_string())` +* `[!str_split! { input: Value, separator: Value, }]` * `[!str_contains! "needle" [!string! haystack]]` expects two string literals, and outputs `true` if the first string is a substring of the second string. -### Possible extension: Token stream commands - -* `[!skip! 4 from [#stream]]` reads and drops the first 4 token trees from the stream, and outputs the rest -* `[!ungroup! (#stream)]` outputs `#stream`. It expects to receive a single group (i.e. wrapped in brackets), and unwraps it. - -### Possible extension: Control flow commands - -#### If statement - -`[!if! #cond then { #a } else { #b }]` outputs `#a` if `#cond` is `true`, else `#b` if `#cond` is false. - -The `if` command works as follows: -* It starts by only interpreting its first token tree, and expects to see a single `true` or `false` literal. -* It then expects to reads an unintepreted `then` ident, following by a single `{ .. }` group, whose contents get interpreted and output only if the condition was `true`. -* It optionally also reads an `else` ident and a by a single `{ .. }` group, whose contents get interpreted and output only if the condition was `false`. - -#### For loop - -* `[!for! #token_tree in [#stream] { ... }]` - -#### Goto and label - -* `[!label! loop_start]` - defines a label which can be returned to. Effectively, it takes a clones of the remaining token stream after the label in the interpreter. -* `[!goto! loop_start]` - jumps to the last execution of `[!label! loop_start]`. It unrolls the preinterpret stack (dropping all unwritten token streams) until it finds a stackframe in which the interpreter has the defined label, and continues the token stream from there. - -```rust,ignore -// Hypothetical future syntax - not yet implemented! -preinterpret::preinterpret!{ - [!set! #i = 0] - [!label! loop] - const [!ident! AB #i]: u8 = 0; - [!increment! #i] - [!if! [!lte! #i 100] then { [!goto! loop] }] -} -``` - ### Possible extension: Eager expansion of macros When [eager expansion of macros returning literals](https://github.com/rust-lang/rust/issues/90765) is stabilized, it would be nice to include a command to do that, which could be used to include code, for example: `[!expand_literal_macros! include!("my-poem.txt")]`. -### Possible extension: Explicit parsing feature to enable syn - -The heavy `syn` library is (in basic preinterpret) only needed for literal parsing, and error conversion into compile errors. - -We could add a parsing feature to speed up compile times a lot for stacks which don't need the parsing functionality. - ## License Licensed under either of the [Apache License, Version 2.0](LICENSE-APACHE) 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..5d8bc280 --- /dev/null +++ b/book/src/SUMMARY.md @@ -0,0 +1,16 @@ +# Summary + +- [Introduction](./introduction.md) + - [Quick Reference](./introduction/quick-reference.md) + - [Motivation](./introduction/motivation.md) + - [Examples](./introduction/examples.md) +- [Concepts](./concepts.md) + - [Commands](./concepts/commands.md) + - [Variables](./concepts/variables.md) + - [Expressions](./concepts/expressions.md) + - [Parsing](./concepts/parsing.md) +- [Command Reference](./command-reference.md) + - [Strings and Idents](./command-reference/strings.md) + - [Expressions](./command-reference/expressions.md) + - [Control Flow](./command-reference/control-flow.md) + diff --git a/book/src/command-reference.md b/book/src/command-reference.md new file mode 100644 index 00000000..f91c8879 --- /dev/null +++ b/book/src/command-reference.md @@ -0,0 +1,3 @@ +# Command Reference + +Coming soon... \ No newline at end of file diff --git a/book/src/command-reference/control-flow.md b/book/src/command-reference/control-flow.md new file mode 100644 index 00000000..0ca4f252 --- /dev/null +++ b/book/src/command-reference/control-flow.md @@ -0,0 +1 @@ +# Control Flow diff --git a/book/src/command-reference/expressions.md b/book/src/command-reference/expressions.md new file mode 100644 index 00000000..d52b5748 --- /dev/null +++ b/book/src/command-reference/expressions.md @@ -0,0 +1 @@ +# Expressions diff --git a/book/src/command-reference/strings.md b/book/src/command-reference/strings.md new file mode 100644 index 00000000..69131c15 --- /dev/null +++ b/book/src/command-reference/strings.md @@ -0,0 +1 @@ +# Strings and Idents 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..a307d955 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.63 +rm Cargo.lock && rustup run 1.63 cargo check 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/array.rs b/src/expressions/array.rs new file mode 100644 index 00000000..abc2ee98 --- /dev/null +++ b/src/expressions/array.rs @@ -0,0 +1,265 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionArray { + pub(crate) items: Vec, + /// The span range that generated this value. + /// For a complex expression, the start span is the most left part + /// of the expression, and the end span is the most right part. + pub(crate) span_range: SpanRange, +} + +impl ExpressionArray { + pub(super) fn handle_unary_operation( + mut self, + operation: OutputSpanned, + ) -> ExecutionResult { + Ok(match operation.operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => { + return operation.unsupported(self) + } + UnaryOperation::Cast { + target, + target_ident, + .. + } => match target { + CastTarget::Stream => operation.output(self.stream_with_grouped_items()?), + CastTarget::Group => operation.output( + operation + .output(self.stream_with_grouped_items()?) + .into_new_output_stream( + Grouping::Grouped, + StreamOutputBehaviour::Standard, + )?, + ), + CastTarget::String => operation.output({ + let mut output = String::new(); + self.concat_recursive_into(&mut output, &ConcatBehaviour::standard())?; + output + }), + CastTarget::Boolean + | CastTarget::Char + | CastTarget::Integer(_) + | CastTarget::Float(_) => { + if self.items.len() == 1 { + self.items + .pop() + .unwrap() + .handle_unary_operation(operation)? + } else { + return operation.execution_err(format!( + "Only a singleton array can be cast to {} but the array has {} elements", + target_ident, + self.items.len(), + )); + } + } + }, + }) + } + + fn stream_with_grouped_items(&self) -> ExecutionResult { + let mut output = OutputStream::new(); + self.output_grouped_items_to(&mut output)?; + Ok(output) + } + + pub(crate) fn output_grouped_items_to(&self, output: &mut OutputStream) -> ExecutionResult<()> { + for item in &self.items { + item.output_to( + Grouping::Grouped, + output, + StreamOutputBehaviour::PermitArrays, + )?; + } + Ok(()) + } + + pub(super) fn handle_integer_binary_operation( + self, + _right: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + operation.unsupported(self) + } + + pub(super) fn handle_paired_binary_operation( + self, + rhs: Self, + operation: OutputSpanned, + ) -> ExecutionResult { + let lhs = self.items; + let rhs = rhs.items; + Ok(match operation.operation { + PairedBinaryOperation::Addition { .. } => operation.output({ + let mut stream = lhs; + stream.extend(rhs); + stream + }), + PairedBinaryOperation::Subtraction { .. } + | PairedBinaryOperation::Multiplication { .. } + | PairedBinaryOperation::Division { .. } + | PairedBinaryOperation::LogicalAnd { .. } + | PairedBinaryOperation::LogicalOr { .. } + | PairedBinaryOperation::Remainder { .. } + | PairedBinaryOperation::BitXor { .. } + | PairedBinaryOperation::BitAnd { .. } + | PairedBinaryOperation::BitOr { .. } + | PairedBinaryOperation::Equal { .. } + | PairedBinaryOperation::LessThan { .. } + | PairedBinaryOperation::LessThanOrEqual { .. } + | PairedBinaryOperation::NotEqual { .. } + | PairedBinaryOperation::GreaterThanOrEqual { .. } + | PairedBinaryOperation::GreaterThan { .. } => return operation.unsupported(lhs), + }) + } + + pub(super) fn into_indexed( + mut self, + access: IndexAccess, + index: &ExpressionValue, + ) -> ExecutionResult { + let span_range = SpanRange::new_between(self.span_range, access); + Ok(match index { + ExpressionValue::Integer(integer) => { + let index = self.resolve_valid_index_from_integer(integer, false)?; + self.items[index].clone().with_span_range(span_range) + } + ExpressionValue::Range(range) => { + let range = range.resolve_to_index_range(&self)?; + let new_items: Vec<_> = self.items.drain(range).collect(); + new_items.to_value(span_range) + } + _ => return index.execution_err("The index must be an integer or a range"), + }) + } + + pub(super) fn index_mut( + &mut self, + access: IndexAccess, + index: &ExpressionValue, + ) -> ExecutionResult<&mut ExpressionValue> { + Ok(match index { + ExpressionValue::Integer(integer) => { + let index = self.resolve_valid_index_from_integer(integer, false)?; + &mut self.items[index] + } + ExpressionValue::Range(..) => { + // Temporary until we add slice types - we error here + return access.execution_err("Currently, a range-indexed array must be owned. Use `.take()` or `.clone()` before indexing [..]"); + } + _ => return index.execution_err("The index must be an integer or a range"), + }) + } + + pub(super) fn index_ref( + &self, + access: IndexAccess, + index: &ExpressionValue, + ) -> ExecutionResult<&ExpressionValue> { + Ok(match index { + ExpressionValue::Integer(integer) => { + let index = self.resolve_valid_index_from_integer(integer, false)?; + &self.items[index] + } + ExpressionValue::Range(..) => { + // Temporary until we add slice types - we error here + return access.execution_err("Currently, a range-indexed array must be owned. Use `.take()` or `.clone()` before indexing [..]"); + } + _ => return index.execution_err("The index must be an integer or a range"), + }) + } + + pub(super) fn resolve_valid_index( + &self, + index: &ExpressionValue, + is_exclusive: bool, + ) -> ExecutionResult { + match index { + ExpressionValue::Integer(int) => { + self.resolve_valid_index_from_integer(int, is_exclusive) + } + _ => index.execution_err("The index must be an integer"), + } + } + + fn resolve_valid_index_from_integer( + &self, + integer: &ExpressionInteger, + is_exclusive: bool, + ) -> ExecutionResult { + let span_range = integer.span_range; + let index = integer.expect_usize()?; + if is_exclusive { + if index <= self.items.len() { + Ok(index) + } else { + span_range.execution_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_range.execution_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<()> { + if behaviour.output_array_structure { + output.push('['); + } + let mut is_first = true; + for item in self.items { + if !is_first && behaviour.output_array_structure { + output.push(','); + } + if !is_first && behaviour.add_space_between_token_trees { + output.push(' '); + } + item.concat_recursive_into(output, behaviour)?; + is_first = false; + } + if behaviour.output_array_structure { + output.push(']'); + } + Ok(()) + } +} + +impl HasSpanRange for ExpressionArray { + fn span_range(&self) -> SpanRange { + self.span_range + } +} + +impl HasValueType for ExpressionArray { + fn value_type(&self) -> &'static str { + self.items.value_type() + } +} + +impl HasValueType for Vec { + fn value_type(&self) -> &'static str { + "array" + } +} + +impl ToExpressionValue for Vec { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Array(ExpressionArray { + items: self, + span_range, + }) + } +} diff --git a/src/expressions/boolean.rs b/src/expressions/boolean.rs new file mode 100644 index 00000000..362fe584 --- /dev/null +++ b/src/expressions/boolean.rs @@ -0,0 +1,121 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionBoolean { + pub(crate) value: bool, + /// The span range that generated this value. + /// For a complex expression, the start span is the most left part + /// of the expression, and the end span is the most right part. + pub(super) span_range: SpanRange, +} + +impl ExpressionBoolean { + pub(super) fn for_litbool(lit: syn::LitBool) -> Self { + Self { + span_range: lit.span().span_range(), + value: lit.value, + } + } + + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + let input = self.value; + Ok(match operation.operation { + UnaryOperation::Neg { .. } => return operation.unsupported(self), + UnaryOperation::Not { .. } => operation.output(!input), + UnaryOperation::Cast { target, .. } => match target { + CastTarget::Integer(IntegerKind::Untyped) => { + operation.output(UntypedInteger::from_fallback(input as FallbackInteger)) + } + CastTarget::Integer(IntegerKind::I8) => operation.output(input as i8), + CastTarget::Integer(IntegerKind::I16) => operation.output(input as i16), + CastTarget::Integer(IntegerKind::I32) => operation.output(input as i32), + CastTarget::Integer(IntegerKind::I64) => operation.output(input as i64), + CastTarget::Integer(IntegerKind::I128) => operation.output(input as i128), + CastTarget::Integer(IntegerKind::Isize) => operation.output(input as isize), + CastTarget::Integer(IntegerKind::U8) => operation.output(input as u8), + CastTarget::Integer(IntegerKind::U16) => operation.output(input as u16), + CastTarget::Integer(IntegerKind::U32) => operation.output(input as u32), + CastTarget::Integer(IntegerKind::U64) => operation.output(input as u64), + CastTarget::Integer(IntegerKind::U128) => operation.output(input as u128), + CastTarget::Integer(IntegerKind::Usize) => operation.output(input as usize), + CastTarget::Float(_) | CastTarget::Char => { + return operation.execution_err("This cast is not supported") + } + CastTarget::Boolean => operation.output(input), + CastTarget::String => operation.output(input.to_string()), + CastTarget::Stream => { + operation.output(operation.output(input).into_new_output_stream( + Grouping::Flattened, + StreamOutputBehaviour::Standard, + )?) + } + CastTarget::Group => { + operation.output(operation.output(input).into_new_output_stream( + Grouping::Grouped, + StreamOutputBehaviour::Standard, + )?) + } + }, + }) + } + + pub(super) fn handle_integer_binary_operation( + self, + _right: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + match operation.operation { + IntegerBinaryOperation::ShiftLeft { .. } + | IntegerBinaryOperation::ShiftRight { .. } => operation.unsupported(self), + } + } + + pub(super) fn handle_paired_binary_operation( + self, + rhs: Self, + operation: OutputSpanned, + ) -> ExecutionResult { + let lhs = self.value; + let rhs = rhs.value; + Ok(match operation.operation { + PairedBinaryOperation::Addition { .. } + | PairedBinaryOperation::Subtraction { .. } + | PairedBinaryOperation::Multiplication { .. } + | PairedBinaryOperation::Division { .. } => return operation.unsupported(self), + PairedBinaryOperation::LogicalAnd { .. } => operation.output(lhs && rhs), + PairedBinaryOperation::LogicalOr { .. } => operation.output(lhs || rhs), + PairedBinaryOperation::Remainder { .. } => return operation.unsupported(self), + PairedBinaryOperation::BitXor { .. } => operation.output(lhs ^ rhs), + PairedBinaryOperation::BitAnd { .. } => operation.output(lhs & rhs), + PairedBinaryOperation::BitOr { .. } => operation.output(lhs | rhs), + PairedBinaryOperation::Equal { .. } => operation.output(lhs == rhs), + PairedBinaryOperation::LessThan { .. } => operation.output(!lhs & rhs), + PairedBinaryOperation::LessThanOrEqual { .. } => operation.output(lhs <= rhs), + PairedBinaryOperation::NotEqual { .. } => operation.output(lhs != rhs), + PairedBinaryOperation::GreaterThanOrEqual { .. } => operation.output(lhs >= rhs), + PairedBinaryOperation::GreaterThan { .. } => operation.output(lhs & !rhs), + }) + } + + pub(super) fn to_ident(&self) -> Ident { + Ident::new_bool(self.value, self.span_range.join_into_span_else_start()) + } +} + +impl HasValueType for ExpressionBoolean { + fn value_type(&self) -> &'static str { + "bool" + } +} + +impl ToExpressionValue for bool { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Boolean(ExpressionBoolean { + value: self, + span_range, + }) + } +} diff --git a/src/expressions/character.rs b/src/expressions/character.rs new file mode 100644 index 00000000..a3720d28 --- /dev/null +++ b/src/expressions/character.rs @@ -0,0 +1,117 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionChar { + pub(super) value: char, + /// The span range that generated this value. + /// For a complex expression, the start span is the most left part + /// of the expression, and the end span is the most right part. + pub(super) span_range: SpanRange, +} + +impl ExpressionChar { + pub(super) fn for_litchar(lit: syn::LitChar) -> Self { + Self { + value: lit.value(), + span_range: lit.span().span_range(), + } + } + + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + let char = self.value; + Ok(match operation.operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => { + return operation.unsupported(self) + } + UnaryOperation::Cast { target, .. } => match target { + CastTarget::Integer(IntegerKind::Untyped) => { + operation.output(UntypedInteger::from_fallback(char as FallbackInteger)) + } + CastTarget::Integer(IntegerKind::I8) => operation.output(char as i8), + CastTarget::Integer(IntegerKind::I16) => operation.output(char as i16), + CastTarget::Integer(IntegerKind::I32) => operation.output(char as i32), + CastTarget::Integer(IntegerKind::I64) => operation.output(char as i64), + CastTarget::Integer(IntegerKind::I128) => operation.output(char as i128), + CastTarget::Integer(IntegerKind::Isize) => operation.output(char as isize), + CastTarget::Integer(IntegerKind::U8) => operation.output(char as u8), + CastTarget::Integer(IntegerKind::U16) => operation.output(char as u16), + CastTarget::Integer(IntegerKind::U32) => operation.output(char as u32), + CastTarget::Integer(IntegerKind::U64) => operation.output(char as u64), + CastTarget::Integer(IntegerKind::U128) => operation.output(char as u128), + CastTarget::Integer(IntegerKind::Usize) => operation.output(char as usize), + CastTarget::Char => operation.output(char), + CastTarget::Boolean | CastTarget::Float(_) => return operation.unsupported(self), + CastTarget::String => operation.output(char.to_string()), + CastTarget::Stream => { + operation.output(operation.output(char).into_new_output_stream( + Grouping::Flattened, + StreamOutputBehaviour::Standard, + )?) + } + CastTarget::Group => { + operation.output(operation.output(char).into_new_output_stream( + Grouping::Grouped, + StreamOutputBehaviour::Standard, + )?) + } + }, + }) + } + + pub(super) fn handle_integer_binary_operation( + self, + _right: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + operation.unsupported(self) + } + + pub(super) fn handle_paired_binary_operation( + self, + rhs: Self, + operation: OutputSpanned, + ) -> ExecutionResult { + let lhs = self.value; + let rhs = rhs.value; + Ok(match operation.operation { + PairedBinaryOperation::Addition { .. } + | PairedBinaryOperation::Subtraction { .. } + | PairedBinaryOperation::Multiplication { .. } + | PairedBinaryOperation::Division { .. } + | PairedBinaryOperation::LogicalAnd { .. } + | PairedBinaryOperation::LogicalOr { .. } + | PairedBinaryOperation::Remainder { .. } + | PairedBinaryOperation::BitXor { .. } + | PairedBinaryOperation::BitAnd { .. } + | PairedBinaryOperation::BitOr { .. } => return operation.unsupported(self), + PairedBinaryOperation::Equal { .. } => operation.output(lhs == rhs), + PairedBinaryOperation::LessThan { .. } => operation.output(lhs < rhs), + PairedBinaryOperation::LessThanOrEqual { .. } => operation.output(lhs <= rhs), + PairedBinaryOperation::NotEqual { .. } => operation.output(lhs != rhs), + PairedBinaryOperation::GreaterThanOrEqual { .. } => operation.output(lhs >= rhs), + PairedBinaryOperation::GreaterThan { .. } => operation.output(lhs > rhs), + }) + } + + pub(super) fn to_literal(&self) -> Literal { + Literal::character(self.value).with_span(self.span_range.join_into_span_else_start()) + } +} + +impl HasValueType for ExpressionChar { + fn value_type(&self) -> &'static str { + "char" + } +} + +impl ToExpressionValue for char { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Char(ExpressionChar { + value: self, + span_range, + }) + } +} diff --git a/src/expressions/evaluation/assignment_frames.rs b/src/expressions/evaluation/assignment_frames.rs new file mode 100644 index 00000000..c126904d --- /dev/null +++ b/src/expressions/evaluation/assignment_frames.rs @@ -0,0 +1,335 @@ +use super::*; + +pub(super) struct AssignmentCompletion { + pub(super) span_range: SpanRange, +} + +/// Handlers which return an AssignmentCompletion +pub(super) enum AnyAssignmentFrame { + Place(PlaceAssigner), + Grouped(GroupedAssigner), + Array(ArrayBasedAssigner), + Object(Box), +} + +impl AnyAssignmentFrame { + pub(super) fn handle_item( + self, + context: Context, + item: EvaluationItem, + ) -> ExecutionResult { + match self { + Self::Place(frame) => frame.handle_item(context, item), + Self::Grouped(frame) => frame.handle_item(context, item), + Self::Object(frame) => frame.handle_item(context, item), + Self::Array(frame) => frame.handle_item(context, item), + } + } +} + +struct PrivateUnit; + +pub(super) struct PlaceAssigner { + value: ExpressionValue, +} + +impl PlaceAssigner { + pub(super) fn start( + context: AssignmentContext, + place: ExpressionNodeId, + value: ExpressionValue, + ) -> NextAction { + let frame = Self { value }; + context.handle_node_as_place(frame, place, RequestedPlaceOwnership::Mutable) + } +} + +impl EvaluationFrame for PlaceAssigner { + type ReturnType = AssignmentType; + + fn into_any(self) -> AnyAssignmentFrame { + AnyAssignmentFrame::Place(self) + } + + fn handle_item( + self, + context: AssignmentContext, + item: EvaluationItem, + ) -> ExecutionResult { + let mut mutable_place = item.expect_mutable(); + let value = self.value; + let span_range = SpanRange::new_between(mutable_place.span_range(), value.span_range()); + mutable_place.set(value); + Ok(context.return_assignment_completion(span_range)) + } +} + +pub(super) struct GroupedAssigner(PrivateUnit); + +impl GroupedAssigner { + pub(super) fn start( + context: AssignmentContext, + inner: ExpressionNodeId, + value: ExpressionValue, + ) -> NextAction { + let frame = Self(PrivateUnit); + context.handle_node_as_assignment(frame, inner, value) + } +} + +impl EvaluationFrame for GroupedAssigner { + type ReturnType = AssignmentType; + + fn into_any(self) -> AnyAssignmentFrame { + AnyAssignmentFrame::Grouped(self) + } + + fn handle_item( + self, + context: AssignmentContext, + item: EvaluationItem, + ) -> ExecutionResult { + let AssignmentCompletion { span_range } = item.expect_assignment_complete(); + Ok(context.return_assignment_completion(span_range)) + } +} + +pub(super) struct ArrayBasedAssigner { + span_range: SpanRange, + assignee_stack: Vec<(ExpressionNodeId, ExpressionValue)>, +} + +impl ArrayBasedAssigner { + pub(super) fn start( + context: AssignmentContext, + nodes: &[ExpressionNode], + brackets: &Brackets, + assignee_item_node_ids: &[ExpressionNodeId], + value: ExpressionValue, + ) -> ExecutionResult { + let frame = Self::new(nodes, brackets.join(), assignee_item_node_ids, value)?; + Ok(frame.handle_next(context)) + } + + /// See also `ArrayPattern` in `patterns.rs` + fn new( + nodes: &[ExpressionNode], + assignee_span: Span, + assignee_item_node_ids: &[ExpressionNodeId], + value: ExpressionValue, + ) -> ExecutionResult { + let array = value.expect_array("The value destructured as an array")?; + let span_range = SpanRange::new_between(assignee_span, array.span_range.end()); + 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[node.0] { + ExpressionNode::Range { + left: None, + range_limits: syn::RangeLimits::HalfOpen(_), + right: None, + } => { + if has_seen_dot_dot { + return assignee_span + .execution_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.execution_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.execution_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(mut self, context: AssignmentContext) -> NextAction { + match self.assignee_stack.pop() { + Some((node, value)) => context.handle_node_as_assignment(self, node, value), + None => context.return_assignment_completion(self.span_range), + } + } +} + +impl EvaluationFrame for ArrayBasedAssigner { + type ReturnType = AssignmentType; + + fn into_any(self) -> AnyAssignmentFrame { + AnyAssignmentFrame::Array(self) + } + + fn handle_item( + self, + context: AssignmentContext, + item: EvaluationItem, + ) -> ExecutionResult { + let AssignmentCompletion { .. } = item.expect_assignment_complete(); + Ok(self.handle_next(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: ExpressionValue, + ) -> ExecutionResult { + let frame = Box::new(Self::new(braces.join(), assignee_pairs, value)?); + frame.handle_next(context) + } + + fn new( + assignee_span: Span, + assignee_pairs: &[(ObjectKey, ExpressionNodeId)], + value: ExpressionValue, + ) -> ExecutionResult { + let object = value.expect_object("The value destructured as an object")?; + let span_range = SpanRange::new_between(assignee_span, object.span_range.end()); + + 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: &ExpressionValue, + assignee_node: ExpressionNodeId, + ) -> ExecutionResult { + let key = index.ref_expect_string("An object key")?.clone().value; + let value = self.resolve_value(key, access.span())?; + Ok(context.handle_node_as_assignment(self, assignee_node, value)) + } + + fn handle_next(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.handle_node_as_assignment(self, assignee_node, value) + } + Some((ObjectKey::Indexed { index, access }, assignee_node)) => { + self.state = ObjectAssignmentState::ResolvingIndex { + assignee_node, + access, + }; + context.handle_node_as_value( + self, + index, + // This only needs to be read-only, as we are just using it to work out which field/s to assign + RequestedValueOwnership::Shared, + ) + } + 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.execution_err(format!("The key `{}` has already used", key)); + } + let value = self + .entries + .remove(&key) + .map(|entry| entry.value) + .unwrap_or_else(|| ExpressionValue::None(key_span.span_range())); + self.already_used_keys.insert(key); + Ok(value) + } +} + +impl EvaluationFrame for Box { + type ReturnType = AssignmentType; + + fn into_any(self) -> AnyAssignmentFrame { + AnyAssignmentFrame::Object(self) + } + + fn handle_item( + self, + context: AssignmentContext, + item: EvaluationItem, + ) -> ExecutionResult { + match self.state { + ObjectAssignmentState::ResolvingIndex { + assignee_node, + access, + } => { + let index_place = item.expect_shared(); + self.handle_index_value(context, access, index_place.as_ref(), assignee_node) + } + ObjectAssignmentState::WaitingForSubassignment => { + let AssignmentCompletion { .. } = item.expect_assignment_complete(); + self.handle_next(context) + } + } + } +} diff --git a/src/expressions/evaluation/evaluator.rs b/src/expressions/evaluation/evaluator.rs new file mode 100644 index 00000000..9b49c106 --- /dev/null +++ b/src/expressions/evaluation/evaluator.rs @@ -0,0 +1,483 @@ +#![allow(unused)] // TODO[unused-clearup] +use super::*; + +pub(in super::super) struct ExpressionEvaluator<'a, K: Expressionable> { + nodes: &'a [ExpressionNode], + stack: EvaluationStack, +} + +impl<'a> ExpressionEvaluator<'a, Source> { + pub(in super::super) fn new(nodes: &'a [ExpressionNode]) -> Self { + Self { + nodes, + stack: EvaluationStack::new(), + } + } + + pub(in super::super) fn evaluate( + mut self, + root: ExpressionNodeId, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + let mut next_action = + NextActionInner::ReadNodeAsValue(root, RequestedValueOwnership::Owned); + + 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[node.0] + .handle_as_value( + interpreter, + Context { + request: ownership, + stack: &mut self.stack, + }, + )?, + NextActionInner::ReadNodeAsAssignee(node, value) => self.nodes[node.0] + .handle_as_assignee( + interpreter, + Context { + stack: &mut self.stack, + request: (), + }, + self.nodes, + node, + value, + )?, + NextActionInner::ReadNodeAsPlace(node, ownership) => self.nodes[node.0] + .handle_as_place( + interpreter, + Context { + stack: &mut self.stack, + request: ownership, + }, + )?, + NextActionInner::HandleReturnedItem(item) => { + let top_of_stack = match self.stack.handlers.pop() { + Some(top) => top, + None => { + // This aligns with the request for an owned value in evaluate + return Ok(StepResult::Return(item.expect_owned_value().into_inner())); + } + }; + top_of_stack.handle_item(&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(ExpressionValue), +} + +pub(super) struct NextAction(NextActionInner); + +impl NextAction { + pub(super) fn return_owned(value: OwnedValue) -> Self { + NextActionInner::HandleReturnedItem(EvaluationItem::Owned(value)).into() + } + + pub(super) fn return_mutable(mutable: MutableValue) -> Self { + NextActionInner::HandleReturnedItem(EvaluationItem::Mutable { mutable }) + .into() + } + + pub(super) fn return_shared( + shared: SharedValue, + reason_not_mutable: Option, + ) -> Self { + NextActionInner::HandleReturnedItem(EvaluationItem::Shared { + shared, + reason_not_mutable, + }) + .into() + } +} + +enum NextActionInner { + /// Enters an expression node to output a value + ReadNodeAsValue(ExpressionNodeId, RequestedValueOwnership), + // Enters an expression node for assignment purposes + ReadNodeAsAssignee(ExpressionNodeId, ExpressionValue), + // Enters an expression node to output a place (a location in memory) + // A place can be thought of as a mutable reference for e.g. a += operation + ReadNodeAsPlace(ExpressionNodeId, RequestedPlaceOwnership), + HandleReturnedItem(EvaluationItem), +} + +impl From for NextAction { + fn from(value: NextActionInner) -> Self { + Self(value) + } +} + +pub(super) enum EvaluationItem { + Owned(OwnedValue), + Shared { + shared: SharedValue, + /// This is only populated if we request a "late bound" reference, and fail to resolve + /// a mutable reference. + reason_not_mutable: Option, + }, + Mutable { + mutable: MutableValue, + }, + AssignmentCompletion(AssignmentCompletion), +} + +impl EvaluationItem { + pub(super) fn expect_owned_value(self) -> OwnedValue { + match self { + EvaluationItem::Owned(value) => value, + _ => panic!("expect_owned_value() called on a non-owned-value EvaluationItem"), + } + } + + pub(super) fn expect_cow_value(self) -> CowValue { + match self { + EvaluationItem::Owned(value) => CowValue::Owned(value), + EvaluationItem::Shared { shared, .. } => CowValue::Shared(shared), + _ => panic!("expect_cow_value() called on a non-cow-value EvaluationItem"), + } + } + + pub(super) fn expect_any_value(self) -> ResolvedValue { + match self { + EvaluationItem::Owned(value) => ResolvedValue::Owned(value), + EvaluationItem::Mutable { mutable } => ResolvedValue::Mutable(mutable), + EvaluationItem::Shared { shared, .. } => ResolvedValue::Shared { + shared, + reason_not_mutable: None, + }, + _ => panic!("expect_any_value() called on a non-value EvaluationItem"), + } + } + + pub(super) fn expect_any_place(self) -> Place { + match self { + EvaluationItem::Mutable { mutable } => Place::Mutable { mutable }, + EvaluationItem::Shared { + shared, + reason_not_mutable, + } => Place::Shared { + shared, + reason_not_mutable, + }, + _ => panic!("expect_any_place() called on a non-place EvaluationItem"), + } + } + + pub(super) fn expect_shared(self) -> SharedValue { + match self { + EvaluationItem::Shared { shared, .. } => shared, + _ => { + panic!("expect_shared() called on a non-shared-reference EvaluationItem") + } + } + } + + pub(super) fn expect_mutable(self) -> MutableValue { + match self { + EvaluationItem::Mutable { mutable } => mutable, + _ => panic!("expect_mutable() called on a non-mutable-place EvaluationItem"), + } + } + + pub(super) fn expect_assignment_complete(self) -> AssignmentCompletion { + match self { + EvaluationItem::AssignmentCompletion(completion) => completion, + _ => panic!( + "expect_assignment_complete() called on a non-assignment-completion EvaluationItem" + ), + } + } +} + +/// 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, RequestedValueOwnership), + Place(AnyPlaceFrame, RequestedPlaceOwnership), + Assignment(AnyAssignmentFrame), +} + +impl AnyEvaluationHandler { + fn handle_item( + self, + stack: &mut EvaluationStack, + item: EvaluationItem, + ) -> ExecutionResult { + match self { + AnyEvaluationHandler::Value(handler, ownership) => handler.handle_item( + Context { + stack, + request: ownership, + }, + item, + ), + AnyEvaluationHandler::Place(handler, ownership) => handler.handle_item( + Context { + stack, + request: ownership, + }, + item, + ), + AnyEvaluationHandler::Assignment(handler) => { + handler.handle_item(Context { stack, request: () }, item) + } + } + } +} + +pub(super) struct Context<'a, T: EvaluationItemType> { + stack: &'a mut EvaluationStack, + request: T::RequestConstraints, +} + +impl<'a, T: EvaluationItemType> Context<'a, T> { + pub(super) fn handle_node_as_value>( + self, + handler: H, + node: ExpressionNodeId, + requested_ownership: RequestedValueOwnership, + ) -> NextAction { + self.stack + .handlers + .push(T::into_unkinded_handler(handler.into_any(), self.request)); + NextActionInner::ReadNodeAsValue(node, requested_ownership).into() + } + + pub(super) fn handle_node_as_place>( + self, + handler: H, + node: ExpressionNodeId, + requested_ownership: RequestedPlaceOwnership, + ) -> NextAction { + self.stack + .handlers + .push(T::into_unkinded_handler(handler.into_any(), self.request)); + NextActionInner::ReadNodeAsPlace(node, requested_ownership).into() + } + + pub(super) fn handle_node_as_assignment>( + self, + handler: H, + node: ExpressionNodeId, + value: ExpressionValue, + ) -> NextAction { + self.stack + .handlers + .push(T::into_unkinded_handler(handler.into_any(), self.request)); + NextActionInner::ReadNodeAsAssignee(node, value).into() + } +} + +pub(super) trait EvaluationFrame: Sized { + type ReturnType: EvaluationItemType; + + fn into_any(self) -> ::AnyHandler; + + fn handle_item( + self, + context: Context, + item: EvaluationItem, + ) -> ExecutionResult; +} + +pub(super) trait EvaluationItemType { + type RequestConstraints; + type AnyHandler; + fn into_unkinded_handler( + handler: Self::AnyHandler, + request: Self::RequestConstraints, + ) -> AnyEvaluationHandler; +} + +pub(super) struct ValueType; +pub(super) type ValueContext<'a> = Context<'a, ValueType>; + +impl EvaluationItemType for ValueType { + type RequestConstraints = RequestedValueOwnership; + type AnyHandler = AnyValueFrame; + + fn into_unkinded_handler( + handler: Self::AnyHandler, + request: Self::RequestConstraints, + ) -> AnyEvaluationHandler { + AnyEvaluationHandler::Value(handler, request) + } +} + +impl<'a> Context<'a, ValueType> { + pub(super) fn requested_ownership(&self) -> RequestedValueOwnership { + self.request + } + + pub(super) fn return_any_value(self, value: ResolvedValue) -> ExecutionResult { + Ok(match value { + ResolvedValue::Owned(owned) => self.return_owned(owned)?, + ResolvedValue::Mutable(mutable) => self.return_mutable(mutable), + ResolvedValue::Shared { + shared, + reason_not_mutable, + } => self.return_shared(shared, reason_not_mutable)?, + }) + } + + pub(super) fn return_any_place(self, value: Place) -> ExecutionResult { + Ok(match value { + Place::Mutable { mutable } => self.return_mutable(mutable), + Place::Shared { + shared, + reason_not_mutable, + } => self.return_shared(shared, reason_not_mutable)?, + }) + } + + pub(super) fn return_owned(self, value: impl Into) -> ExecutionResult { + let value = value.into(); + Ok(match self.request { + RequestedValueOwnership::LateBound + | RequestedValueOwnership::CopyOnWrite + | RequestedValueOwnership::Owned => NextAction::return_owned(value), + RequestedValueOwnership::Shared => { + NextAction::return_shared(Shared::new_from_owned(value), None) + } + RequestedValueOwnership::Mutable => { + // This aligns with ResolveValue::into_mutable + return value.execution_err("A mutable reference is required, but an owned value was received, this indicates a possible bug as the updated value won't be accessible. To proceed regardless, use `.as_mut()` to get a mutable reference.") + } + }) + } + + pub(super) fn return_mutable(self, mutable: MutableValue) -> NextAction { + match self.request { + RequestedValueOwnership::LateBound + | RequestedValueOwnership::Mutable => NextAction::return_mutable(mutable), + RequestedValueOwnership::Owned + | RequestedValueOwnership::CopyOnWrite + | RequestedValueOwnership::Shared => panic!("Returning a mutable reference should only be used when the requested ownership is mutable or late-bound"), + } + } + + pub(super) fn return_shared( + self, + shared: SharedValue, + reason_not_mutable: Option, + ) -> ExecutionResult { + Ok(match self.request { + RequestedValueOwnership::LateBound + | RequestedValueOwnership::CopyOnWrite + | RequestedValueOwnership::Shared => NextAction::return_shared(shared, reason_not_mutable), + RequestedValueOwnership::Owned => { + // This can happen if a requested of "Owned" was mapped to "CopyOnWrite" + NextAction::return_owned(shared.transparent_clone()?) + } + RequestedValueOwnership::Mutable => panic!("Returning a reference should only be used when the requested ownership is shared or late-bound"), + }) + } +} + +pub(super) struct PlaceType; + +pub(super) type PlaceContext<'a> = Context<'a, PlaceType>; + +impl EvaluationItemType for PlaceType { + type RequestConstraints = RequestedPlaceOwnership; + type AnyHandler = AnyPlaceFrame; + + fn into_unkinded_handler( + handler: Self::AnyHandler, + request: Self::RequestConstraints, + ) -> AnyEvaluationHandler { + AnyEvaluationHandler::Place(handler, request) + } +} + +impl<'a> Context<'a, PlaceType> { + pub(super) fn requested_ownership(&self) -> RequestedPlaceOwnership { + self.request + } + + pub(super) fn return_any(self, place: Place) -> NextAction { + match place { + Place::Mutable { mutable } => self.return_mutable(mutable), + Place::Shared { + shared, + reason_not_mutable, + } => self.return_shared(shared, reason_not_mutable), + } + } + + pub(super) fn return_mutable(self, mutable: MutableValue) -> NextAction { + match self.request { + RequestedPlaceOwnership::LateBound + | RequestedPlaceOwnership::Mutable => NextAction::return_mutable(mutable), + RequestedPlaceOwnership::Shared => panic!("Returning a mutable reference should only be used when the requested ownership is mutable or late-bound"), + } + } + + pub(super) fn return_shared( + self, + shared: SharedValue, + reason_not_mutable: Option, + ) -> NextAction { + match self.request { + RequestedPlaceOwnership::LateBound + | RequestedPlaceOwnership::Shared => NextAction::return_shared(shared, reason_not_mutable), + RequestedPlaceOwnership::Mutable => panic!("Returning a shared reference should only be used when the requested ownership is shared or late-bound"), + } + } +} + +pub(super) struct AssignmentType; + +pub(super) type AssignmentContext<'a> = Context<'a, AssignmentType>; + +impl EvaluationItemType for AssignmentType { + type RequestConstraints = (); + type AnyHandler = AnyAssignmentFrame; + + fn into_unkinded_handler(handler: Self::AnyHandler, _request: ()) -> AnyEvaluationHandler { + AnyEvaluationHandler::Assignment(handler) + } +} + +impl<'a> Context<'a, AssignmentType> { + pub(super) fn return_assignment_completion(self, span_range: SpanRange) -> NextAction { + NextActionInner::HandleReturnedItem(EvaluationItem::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..66b6d29f --- /dev/null +++ b/src/expressions/evaluation/mod.rs @@ -0,0 +1,14 @@ +mod assignment_frames; +mod evaluator; +mod node_conversion; +mod place_frames; +mod type_resolution; +mod value_frames; + +use super::*; +use assignment_frames::*; +pub(super) use evaluator::ExpressionEvaluator; +use evaluator::*; +use place_frames::*; +pub(super) use type_resolution::*; +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..aa280fdb --- /dev/null +++ b/src/expressions/evaluation/node_conversion.rs @@ -0,0 +1,163 @@ +use super::*; + +impl ExpressionNode { + pub(super) fn handle_as_value( + &self, + interpreter: &mut Interpreter, + context: Context, + ) -> ExecutionResult { + Ok(match self { + ExpressionNode::Leaf(leaf) => { + match leaf { + SourceExpressionLeaf::Command(command) => { + // TODO[interpret_to_value]: Allow command to return a reference + context.return_owned(command.clone().interpret_to_value(interpreter)?)? + } + SourceExpressionLeaf::Discarded(token) => { + return token.execution_err("This cannot be used in a value expression"); + } + SourceExpressionLeaf::Variable(variable_path) => { + let variable_ref = variable_path.binding(interpreter)?; + match context.requested_ownership() { + RequestedValueOwnership::LateBound => { + context.return_any_place(variable_ref.into_late_bound()?)? + } + RequestedValueOwnership::Shared + | RequestedValueOwnership::CopyOnWrite => { + context.return_shared(variable_ref.into_shared()?, None)? + } + RequestedValueOwnership::Mutable => { + context.return_mutable(variable_ref.into_mut()?) + } + RequestedValueOwnership::Owned => { + context.return_owned(variable_ref.into_transparently_cloned()?)? + } + } + } + SourceExpressionLeaf::ExpressionBlock(block) => { + // TODO[interpret_to_value]: Allow block to return reference + context.return_owned(block.interpret_to_value(interpreter)?)? + } + SourceExpressionLeaf::Value(value) => context.return_owned(value.clone())?, + } + } + 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.clone(), *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::CompoundAssignment { + place, + operation, + value, + } => CompoundAssignmentBuilder::start(context, *place, *operation, *value), + ExpressionNode::MethodCall { + node, + method, + parameters, + } => MethodCallBuilder::start(context, *node, method.clone(), parameters), + }) + } + + pub(super) fn handle_as_assignee( + &self, + _: &mut Interpreter, + context: AssignmentContext, + nodes: &[ExpressionNode], + self_node_id: ExpressionNodeId, + value: ExpressionValue, + ) -> ExecutionResult { + Ok(match self { + ExpressionNode::Leaf(SourceExpressionLeaf::Variable(_)) + | ExpressionNode::Index { .. } + | ExpressionNode::Property { .. } => PlaceAssigner::start(context, self_node_id, value), + ExpressionNode::Leaf(SourceExpressionLeaf::Discarded(underscore)) => { + context.return_assignment_completion(SpanRange::new_between(*underscore, value)) + } + 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), + other => { + return other + .operator_span_range() + .execution_err("This type of expression is not supported as an assignee. You may wish to use `_` to ignore the value."); + } + }) + } + + pub(super) fn handle_as_place( + &self, + interpreter: &mut Interpreter, + context: PlaceContext, + ) -> ExecutionResult { + Ok(match self { + ExpressionNode::Leaf(SourceExpressionLeaf::Variable(variable)) => { + let variable_ref = variable.binding(interpreter)?; + match context.requested_ownership() { + RequestedPlaceOwnership::LateBound => { + context.return_any(variable_ref.into_late_bound()?) + } + RequestedPlaceOwnership::Shared => { + context.return_shared(variable_ref.into_shared()?, None) + } + RequestedPlaceOwnership::Mutable => { + context.return_mutable(variable_ref.into_mut()?) + } + } + } + ExpressionNode::Index { + node, + access, + index, + } => PlaceIndexer::start(context, *node, *access, *index), + ExpressionNode::Property { node, access, .. } => { + PlacePropertyAccessor::start(context, *node, access.clone()) + } + ExpressionNode::Grouped { inner, .. } => PlaceGrouper::start(context, *inner), + other => { + return other + .operator_span_range() + .execution_err("This expression cannot be resolved into a memory location."); + } + }) + } +} diff --git a/src/expressions/evaluation/place_frames.rs b/src/expressions/evaluation/place_frames.rs new file mode 100644 index 00000000..fe8cfb57 --- /dev/null +++ b/src/expressions/evaluation/place_frames.rs @@ -0,0 +1,169 @@ +//! A preinterpret place frame is just used for the target of an assignment. +//! The name is inspired by Rust places, but it is a subtly different concept. +//! +//! They're similar to mutable references, but behave slightly differently: +//! * They can create entries in objects, e.g. `x["new_key"] = value`` +#![allow(unused)] // TODO[unused-clearup] +use super::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum RequestedPlaceOwnership { + LateBound, + Shared, + Mutable, +} + +/// Handlers which return a Place +pub(super) enum AnyPlaceFrame { + Grouped(PlaceGrouper), + Indexed(PlaceIndexer), + PropertyAccessed(PlacePropertyAccessor), +} + +impl AnyPlaceFrame { + pub(super) fn handle_item( + self, + context: Context, + item: EvaluationItem, + ) -> ExecutionResult { + match self { + Self::Grouped(frame) => frame.handle_item(context, item), + Self::Indexed(frame) => frame.handle_item(context, item), + Self::PropertyAccessed(frame) => frame.handle_item(context, item), + } + } +} + +struct PrivateUnit; + +pub(super) struct PlaceGrouper(PrivateUnit); + +impl PlaceGrouper { + pub(super) fn start(context: PlaceContext, inner: ExpressionNodeId) -> NextAction { + let frame = Self(PrivateUnit); + let ownership = context.requested_ownership(); + context.handle_node_as_place(frame, inner, ownership) + } +} + +impl EvaluationFrame for PlaceGrouper { + type ReturnType = PlaceType; + + fn into_any(self) -> AnyPlaceFrame { + AnyPlaceFrame::Grouped(self) + } + + fn handle_item( + self, + context: PlaceContext, + item: EvaluationItem, + ) -> ExecutionResult { + Ok(context.return_any(item.expect_any_place())) + } +} + +pub(super) struct PlaceIndexer { + access: IndexAccess, + state: PlaceIndexerPath, +} + +enum PlaceIndexerPath { + PlacePath { index: ExpressionNodeId }, + IndexPath { place: Place }, +} + +impl PlaceIndexer { + pub(super) fn start( + context: PlaceContext, + source: ExpressionNodeId, + access: IndexAccess, + index: ExpressionNodeId, + ) -> NextAction { + let frame = Self { + access, + state: PlaceIndexerPath::PlacePath { index }, + }; + let ownership = context.requested_ownership(); + context.handle_node_as_place(frame, source, ownership) + } +} + +impl EvaluationFrame for PlaceIndexer { + type ReturnType = PlaceType; + + fn into_any(self) -> AnyPlaceFrame { + AnyPlaceFrame::Indexed(self) + } + + fn handle_item( + mut self, + context: PlaceContext, + item: EvaluationItem, + ) -> ExecutionResult { + Ok(match self.state { + PlaceIndexerPath::PlacePath { index } => { + let place = item.expect_any_place(); + self.state = PlaceIndexerPath::IndexPath { place }; + // If we do my_obj["my_key"] = 1 then the "my_key" place is created, + // so mutable reference indexing takes an owned index... + // But we auto-clone the key in that case, so we can still pass a shared ref + context.handle_node_as_value(self, index, RequestedValueOwnership::Shared) + } + PlaceIndexerPath::IndexPath { place } => { + let index = item.expect_shared(); + match place { + Place::Mutable { mutable } => context + .return_mutable(mutable.resolve_indexed(self.access, &index, true)?), + Place::Shared { + shared, + reason_not_mutable, + } => context.return_shared( + shared.resolve_indexed(self.access, &index)?, + reason_not_mutable, + ), + } + } + }) + } +} + +pub(super) struct PlacePropertyAccessor { + access: PropertyAccess, +} + +impl PlacePropertyAccessor { + pub(super) fn start( + context: PlaceContext, + source: ExpressionNodeId, + access: PropertyAccess, + ) -> NextAction { + let frame = Self { access }; + let requested_ownership = context.requested_ownership(); + context.handle_node_as_place(frame, source, requested_ownership) + } +} + +impl EvaluationFrame for PlacePropertyAccessor { + type ReturnType = PlaceType; + + fn into_any(self) -> AnyPlaceFrame { + AnyPlaceFrame::PropertyAccessed(self) + } + + fn handle_item( + self, + context: PlaceContext, + item: EvaluationItem, + ) -> ExecutionResult { + let place = item.expect_any_place(); + Ok(match place { + Place::Mutable { mutable } => { + context.return_mutable(mutable.resolve_property(&self.access, true)?) + } + Place::Shared { + shared, + reason_not_mutable, + } => context.return_shared(shared.resolve_property(&self.access)?, reason_not_mutable), + }) + } +} diff --git a/src/expressions/evaluation/type_resolution.rs b/src/expressions/evaluation/type_resolution.rs new file mode 100644 index 00000000..831ebb3e --- /dev/null +++ b/src/expressions/evaluation/type_resolution.rs @@ -0,0 +1,502 @@ +#![allow(unused)] +use std::mem; + +// TODO[unused-clearup] +use super::*; + +#[macro_use] +mod macros { + macro_rules! handle_arg_mapping { + // No more args + ([$(,)?] [$($bindings:tt)*]) => { + $($bindings)* + }; + // By captured shared reference (i.e. can return a sub-reference from it) + ([$($arg_part:ident)+ : Shared<$ty:ty>, $($rest:tt)*] [$($bindings:tt)*]) => { + handle_arg_mapping!([$($rest)*] [ + $($bindings)* + let tmp = handle_arg_name!($($arg_part)+).into_shared(); + let handle_arg_name!($($arg_part)+): Shared<$ty> = tmp.try_map(|value, _| <$ty as ResolvableArgument>::resolve_from_ref(value))?; + ]) + }; + // SharedValue is an alias for Shared + ([$($arg_part:ident)+ : SharedValue, $($rest:tt)*] [$($bindings:tt)*]) => { + handle_arg_mapping!([$($rest)*] [ + $($bindings)* + let handle_arg_name!($($arg_part)+) = handle_arg_name!($($arg_part)+).into_shared(); + ]) + }; + // By captured mutable reference (i.e. can return a sub-reference from it) + ([$($arg_part:ident)+ : Mutable<$ty:ty>, $($rest:tt)*] [$($bindings:tt)*]) => { + handle_arg_mapping!([$($rest)*] [ + $($bindings)* + let mut tmp = handle_arg_name!($($arg_part)+).into_mutable()?; + let handle_arg_name!($($arg_part)+): Mutable<$ty> = tmp.try_map(|value, _| <$ty as ResolvableArgument>::resolve_from_mut(value))?; + ]) + }; + // MutableValue is an alias for Mutable + ([$($arg_part:ident)+ : MutableValue, $($rest:tt)*] [$($bindings:tt)*]) => { + handle_arg_mapping!([$($rest)*] [ + $($bindings)* + let handle_arg_name!($($arg_part)+) = handle_arg_name!($($arg_part)+).into_mutable()?; + ]) + }; + // By value + ([$($arg_part:ident)+ : Owned<$ty:ty>, $($rest:tt)*] [$($bindings:tt)*]) => { + handle_arg_mapping!([$($rest)*] [ + $($bindings)* + let tmp = handle_arg_name!($($arg_part)+).into_owned()?; + let handle_arg_name!($($arg_part)+): $ty = tmp.try_map(|value, _| <$ty as ResolvableArgument>::resolve_from_owned(value))?; + ]) + }; + // By value + ([$($arg_part:ident)+ : OwnedValue, $($rest:tt)*] [$($bindings:tt)*]) => { + handle_arg_mapping!([$($rest)*] [ + $($bindings)* + let handle_arg_name!($($arg_part)+) = handle_arg_name!($($arg_part)+).into_owned()?; + ]) + }; + } + + macro_rules! handle_arg_ownerships { + // No more args + ([$(,)?] [$($outputs:tt)*]) => { + vec![$($outputs)*] + }; + // By captured shared reference (i.e. can return a sub-reference from it) + ([$($arg_part:ident)+ : Shared<$ty:ty>, $($rest:tt)*] [$($outputs:tt)*]) => { + handle_arg_ownerships!([$($rest)*] [$($outputs)* RequestedValueOwnership::Shared,]) + }; + // SharedValue is an alias for Shared + ([$($arg_part:ident)+ : SharedValue, $($rest:tt)*] [$($outputs:tt)*]) => { + handle_arg_ownerships!([$($rest)*] [$($outputs)* RequestedValueOwnership::Shared,]) + }; + // By captured mutable reference (i.e. can return a sub-reference from it) + ([$($arg_part:ident)+ : Mutable<$ty:ty>, $($rest:tt)*] [$($outputs:tt)*]) => { + handle_arg_ownerships!([$($rest)*] [$($outputs)* RequestedValueOwnership::Mutable,]) + }; + // MutableValue is an alias for Mutable + ([$($arg_part:ident)+ : MutableValue, $($rest:tt)*] [$($outputs:tt)*]) => { + handle_arg_ownerships!([$($rest)*] [$($outputs)* RequestedValueOwnership::Mutable,]) + }; + // By value + ([$($arg_part:ident)+ : Owned<$ty:ty>, $($rest:tt)*] [$($outputs:tt)*]) => { + handle_arg_ownerships!([$($rest)*] [$($outputs)* RequestedValueOwnership::Owned,]) + }; + // By value + ([$($arg_part:ident)+ : OwnedValue, $($rest:tt)*] [$($outputs:tt)*]) => { + handle_arg_ownerships!([$($rest)*] [$($outputs)* RequestedValueOwnership::Owned,]) + }; + } + + macro_rules! count { + () => { 0 }; + ($head:tt $($tail:tt)*) => { 1 + count!($($tail)*) }; + } + + // Creating an inner method vastly improves IDE support when writing the method body + macro_rules! handle_define_inner_method { + // The $arg_part+ allows for mut x in the argument list + ($method_name:ident [$($($arg_part:ident)+ : $ty:ty),* $(,)?] $body:block $output_ty:ty) => { + fn $method_name($($($arg_part)+: $ty),*) -> ExecutionResult<$output_ty> { + $body + } + }; + } + + macro_rules! handle_arg_name { + (mut $name:ident) => { + $name + }; + ($name:ident) => { + $name + }; + } + + macro_rules! handle_arg_separation { + ([$($($arg_part:ident)+ : $ty:ty),* $(,)?], $all_arguments:ident, $output_span_range:ident) => { + const LEN: usize = count!($($ty)*); + let Ok([ + $(handle_arg_name!($($arg_part)+),)* + ]) = <[ResolvedValue; LEN]>::try_from($all_arguments) else { + return $output_span_range.execution_err(format!("Expected {LEN} argument/s")); + }; + }; + } + + macro_rules! handle_call_inner_method { + ($method_name:ident [$($($arg_part:ident)+ : $ty:ty),* $(,)?]) => { + $method_name($(handle_arg_name!($($arg_part)+)),*) + }; + } + + macro_rules! wrap_method { + (($($args:tt)*) -> ExecutionResult<$output_ty:ty> $body:block) => { + MethodInterface { + method: Box::new(move | + all_arguments: Vec, + output_span_range: SpanRange, + | -> ExecutionResult { + handle_define_inner_method!(inner_method [$($args)*] $body $output_ty); + handle_arg_separation!([$($args)*], all_arguments, output_span_range); + handle_arg_mapping!([$($args)*,] []); + let output: ExecutionResult<$output_ty> = handle_call_inner_method!(inner_method [$($args)*]); + <$output_ty as ResolvableOutput>::to_resolved_value(output?, output_span_range) + }), + argument_ownerships: handle_arg_ownerships!([$($args)*,] []), + } + }; + } +} + +pub(crate) trait ResolvedTypeDetails { + /// 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. + fn supports_transparent_cloning(&self) -> bool; + + /// Resolves a method for this resolved type with the given arguments. + fn resolve_method( + &self, + method: &MethodAccess, + num_arguments: usize, + ) -> ExecutionResult; + + // TODO[operation-refactor]: Eventually we can migrate operations under this umbrella too + // fn resolve_unary_operation(&self, operation: UnaryOperation) -> ExecutionResult; + // fn resolve_binary_operation(&self, operation: BinaryOperation) -> ExecutionResult; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ValueKind { + None, + Integer, + Float, + Boolean, + String, + Char, + UnsupportedLiteral, + Array, + Object, + Stream, + Range, + Iterator, +} + +impl ResolvedTypeDetails for ValueKind { + fn supports_transparent_cloning(&self) -> bool { + match self { + ValueKind::None => true, + ValueKind::Integer => true, + ValueKind::Float => true, + ValueKind::Boolean => true, + // Strings are value-like, so it makes sense to transparently clone them + ValueKind::String => true, + ValueKind::Char => true, + ValueKind::UnsupportedLiteral => false, + ValueKind::Array => false, + ValueKind::Object => false, + // It's super common to want to embed a stream in another stream + // Having to embed it as #(type_name.clone()) instead of + // #type_name would be awkward + ValueKind::Stream => true, + ValueKind::Range => true, + ValueKind::Iterator => false, + } + } + + fn resolve_method( + &self, + method: &MethodAccess, + num_arguments: usize, + ) -> ExecutionResult { + let method_name = method.method.to_string(); + let method = match (self, method_name.as_str(), num_arguments) { + (_, "clone", 0) => { + wrap_method! {(this: SharedValue) -> ExecutionResult { + Ok(this.clone()) + }} + } + (_, "take", 0) => { + wrap_method! {(mut this: MutableValue) -> ExecutionResult { + let span_range = this.span_range(); + Ok(mem::replace(this.deref_mut(), ExpressionValue::None(span_range))) + }} + } + (_, "as_mut", 0) => { + wrap_method! {(this: OwnedValue) -> ExecutionResult { + Ok(MutableValue::new_from_owned(this)) + }} + } + (_, "debug_string", 0) => { + wrap_method! {(this: SharedValue) -> ExecutionResult { + this.clone().into_debug_string() + }} + } + (_, "debug", 0) => wrap_method! {(this: SharedValue) -> ExecutionResult { + let message = this.clone().into_debug_string()?; + this.execution_err(message) + }}, + // Mostly just a test of mutable values + (_, "swap", 1) => wrap_method! {(mut a: MutableValue, mut b: MutableValue) -> ExecutionResult<()> { + mem::swap(a.deref_mut(), b.deref_mut()); + Ok(()) + }}, + (ValueKind::Array, "len", 0) => { + wrap_method! {(this: Shared) -> ExecutionResult { + Ok(this.items.len()) + }} + } + (ValueKind::Array, "push", 1) => { + wrap_method! {(mut this: Mutable, item: OwnedValue) -> ExecutionResult<()> { + this.items.push(item.into()); + Ok(()) + }} + } + (ValueKind::Stream, "len", 0) => { + wrap_method! {(this: Shared) -> ExecutionResult { + Ok(this.value.len()) + }} + } + _ => { + return method.execution_err(format!( + "{self:?} has no method `{method_name}` with {num_arguments} arguments" + )) + } + }; + Ok(method) + } +} + +pub(crate) struct MethodInterface { + method: Box<(dyn Fn(Vec, SpanRange) -> ExecutionResult)>, + argument_ownerships: Vec, +} + +impl MethodInterface { + pub fn execute( + &self, + arguments: Vec, + span_range: SpanRange, + ) -> ExecutionResult { + (self.method)(arguments, span_range) + } + + pub(super) fn ownerships(&self) -> &[RequestedValueOwnership] { + &self.argument_ownerships + } +} + +use outputs::*; + +mod outputs { + use super::*; + + #[diagnostic::on_unimplemented( + message = "`ResolvableOutput` is not implemented for `{Self}`", + note = "`ResolvableOutput` is not implemented for `Shared` or `Mutable` unless `X` is `ExpressionValue`. 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 ResolvableOutput { + fn to_resolved_value(self, output_span_range: SpanRange) -> ExecutionResult; + } + + impl ResolvableOutput for Shared { + fn to_resolved_value(self, output_span_range: SpanRange) -> ExecutionResult { + Ok(ResolvedValue::Shared { + shared: self.update_span_range(|_| output_span_range), + reason_not_mutable: Some(syn::Error::new( + output_span_range.join_into_span_else_start(), + "It was output from a method a captured shared reference", + )), + }) + } + } + + impl ResolvableOutput for Mutable { + fn to_resolved_value(self, output_span_range: SpanRange) -> ExecutionResult { + Ok(ResolvedValue::Mutable( + self.update_span_range(|_| output_span_range), + )) + } + } + + impl ResolvableOutput for Owned { + fn to_resolved_value(self, output_span_range: SpanRange) -> ExecutionResult { + Ok(ResolvedValue::Owned( + self.update_span_range(|_| output_span_range), + )) + } + } + + impl ResolvableOutput for T { + fn to_resolved_value(self, output_span_range: SpanRange) -> ExecutionResult { + Ok(ResolvedValue::Owned( + self.to_value(output_span_range).into(), + )) + } + } +} + +pub(crate) use arguments::*; + +mod arguments { + use super::*; + + pub(crate) trait ResolveAs { + fn resolve_as(self) -> ExecutionResult; + } + + impl ResolveAs for ExpressionValue { + fn resolve_as(self) -> ExecutionResult { + T::resolve_from_owned(self) + } + } + + impl<'a, T: ResolvableArgument> ResolveAs<&'a T> for &'a ExpressionValue { + fn resolve_as(self) -> ExecutionResult<&'a T> { + T::resolve_from_ref(self) + } + } + + impl<'a, T: ResolvableArgument> ResolveAs<&'a mut T> for &'a mut ExpressionValue { + fn resolve_as(self) -> ExecutionResult<&'a mut T> { + T::resolve_from_mut(self) + } + } + + pub(crate) trait ResolvableArgument: Sized { + fn resolve_from_owned(value: ExpressionValue) -> ExecutionResult; + fn resolve_from_ref(value: &ExpressionValue) -> ExecutionResult<&Self>; + fn resolve_from_mut(value: &mut ExpressionValue) -> ExecutionResult<&mut Self>; + } + + impl ResolvableArgument for ExpressionValue { + fn resolve_from_owned(value: ExpressionValue) -> ExecutionResult { + Ok(value) + } + + fn resolve_from_ref(value: &ExpressionValue) -> ExecutionResult<&Self> { + Ok(value) + } + + fn resolve_from_mut(value: &mut ExpressionValue) -> ExecutionResult<&mut Self> { + Ok(value) + } + } + + macro_rules! impl_resolvable_argument_for { + (($value:ident) -> $type:ty $body:block) => { + impl ResolvableArgument for $type { + fn resolve_from_owned($value: ExpressionValue) -> ExecutionResult { + $body + } + + fn resolve_from_ref($value: &ExpressionValue) -> ExecutionResult<&Self> { + $body + } + + fn resolve_from_mut($value: &mut ExpressionValue) -> ExecutionResult<&mut Self> { + $body + } + } + }; + } + + pub(crate) use impl_resolvable_argument_for; + + impl_resolvable_argument_for! {(value) -> ExpressionInteger { + match value { + ExpressionValue::Integer(value) => Ok(value), + _ => value.execution_err("Expected integer"), + } + }} + + impl_resolvable_argument_for! {(value) -> u8 { + match value { + ExpressionValue::Integer(ExpressionInteger { value: ExpressionIntegerValue::U8(x), ..}) => Ok(x), + _ => value.execution_err("Expected u8"), + } + }} + + impl_resolvable_argument_for! {(value) -> usize { + match value { + ExpressionValue::Integer(ExpressionInteger { value: ExpressionIntegerValue::Usize(x), ..}) => Ok(x), + _ => value.execution_err("Expected usize"), + } + }} + + impl_resolvable_argument_for! {(value) -> ExpressionFloat { + match value { + ExpressionValue::Float(value) => Ok(value), + _ => value.execution_err("Expected float"), + } + }} + + impl_resolvable_argument_for! {(value) -> ExpressionBoolean { + match value { + ExpressionValue::Boolean(value) => Ok(value), + _ => value.execution_err("Expected boolean"), + } + }} + + impl_resolvable_argument_for! {(value) -> ExpressionString { + match value { + ExpressionValue::String(value) => Ok(value), + _ => value.execution_err("Expected string"), + } + }} + + impl<'a> ResolveAs<&'a str> for &'a ExpressionValue { + fn resolve_as(self) -> ExecutionResult<&'a str> { + match self { + ExpressionValue::String(s) => Ok(&s.value), + _ => self.execution_err("Expected string"), + } + } + } + + impl_resolvable_argument_for! {(value) -> ExpressionChar { + match value { + ExpressionValue::Char(value) => Ok(value), + _ => value.execution_err("Expected char"), + } + }} + + impl_resolvable_argument_for! {(value) -> ExpressionArray { + match value { + ExpressionValue::Array(value) => Ok(value), + _ => value.execution_err("Expected array"), + } + }} + + impl_resolvable_argument_for! {(value) -> ExpressionObject { + match value { + ExpressionValue::Object(value) => Ok(value), + _ => value.execution_err("Expected object"), + } + }} + + impl_resolvable_argument_for! {(value) -> ExpressionStream { + match value { + ExpressionValue::Stream(value) => Ok(value), + _ => value.execution_err("Expected stream"), + } + }} + + impl_resolvable_argument_for! {(value) -> ExpressionRange { + match value { + ExpressionValue::Range(value) => Ok(value), + _ => value.execution_err("Expected range"), + } + }} + + impl_resolvable_argument_for! {(value) -> ExpressionIterator { + match value { + ExpressionValue::Iterator(value) => Ok(value), + _ => value.execution_err("Expected iterator"), + } + }} +} diff --git a/src/expressions/evaluation/value_frames.rs b/src/expressions/evaluation/value_frames.rs new file mode 100644 index 00000000..d7115a47 --- /dev/null +++ b/src/expressions/evaluation/value_frames.rs @@ -0,0 +1,950 @@ +#![allow(unused)] // TODO[unused-clearup] +use super::*; + +/// # Late Binding +/// +/// Sometimes, a value which can be accessed, but we don't yet know *how* we need to access it. +/// In this case, we attempt to load it as a Mutable place, and failing that, as a Shared place. +/// +/// ## Example of requirement +/// For example, if we have `x.y(z)`, we first need to resolve the type of `x` to know +/// whether `y` takes a shared reference, mutable reference or an owned value. +/// +/// So instead, we take the most powerful access we can have, and convert it later. +pub(crate) enum ResolvedValue { + /// This has been requested as an owned value. + Owned(OwnedValue), + /// This has been requested as a mutable reference. + Mutable(MutableValue), + /// This has been requested as a shared reference. + Shared { + shared: SharedValue, + reason_not_mutable: Option, + }, +} + +impl ResolvedValue { + pub(crate) fn into_owned(self) -> ExecutionResult { + match self { + ResolvedValue::Owned(value) => Ok(value), + ResolvedValue::Shared { shared, .. } => shared.transparent_clone(), + ResolvedValue::Mutable(mutable) => mutable.transparent_clone(), + } + } + + pub(crate) fn into_mutable(self) -> ExecutionResult { + Ok(match self { + ResolvedValue::Owned(value) => { + return value.execution_err("A mutable reference is required, but an owned value was received, this indicates a possible bug as the updated value won't be accessible. To proceed regardless, use `.as_mut()` to get a mutable reference.") + }, + ResolvedValue::Mutable(reference) => reference, + ResolvedValue::Shared { shared, reason_not_mutable: Some(reason_not_mutable), } => return shared.execution_err(format!( + "A mutable reference is required, but only a shared reference was received because {reason_not_mutable}." + )), + ResolvedValue::Shared { shared, reason_not_mutable: None, } => return shared.execution_err("A mutable reference is required, but only a shared reference was received.".to_string()), + }) + } + + pub(crate) fn into_shared(self) -> SharedValue { + match self { + ResolvedValue::Owned(value) => SharedValue::new_from_owned(value), + ResolvedValue::Mutable(reference) => reference.into_shared(), + ResolvedValue::Shared { shared, .. } => shared, + } + } + + pub(crate) fn kind(&self) -> ValueKind { + self.as_value_ref().kind() + } + + pub(crate) fn as_value_ref(&self) -> &ExpressionValue { + match self { + ResolvedValue::Owned(owned) => owned.as_ref(), + ResolvedValue::Mutable(mutable) => mutable.as_ref(), + ResolvedValue::Shared { shared, .. } => shared.as_ref(), + } + } + + pub(crate) fn as_value_mut(&mut self) -> ExecutionResult<&mut ExpressionValue> { + match self { + ResolvedValue::Owned(value) => Ok(value.as_mut()), + ResolvedValue::Mutable(reference) => Ok(reference.as_mut()), + ResolvedValue::Shared { + shared, + reason_not_mutable: Some(reason_not_mutable), + } => shared.execution_err(format!( + "Cannot get a mutable reference: {}", + reason_not_mutable + )), + ResolvedValue::Shared { + shared, + reason_not_mutable: None, + } => shared.execution_err("Cannot get a mutable reference: Unknown reason"), + } + } +} + +impl HasSpanRange for ResolvedValue { + fn span_range(&self) -> SpanRange { + self.as_value_ref().span_range() + } +} + +impl WithSpanExt for ResolvedValue { + fn with_span(self, span: Span) -> Self { + match self { + ResolvedValue::Owned(value) => ResolvedValue::Owned(value.with_span(span)), + ResolvedValue::Mutable(reference) => ResolvedValue::Mutable(reference.with_span(span)), + ResolvedValue::Shared { + shared, + reason_not_mutable, + } => ResolvedValue::Shared { + shared: shared.with_span(span), + reason_not_mutable, + }, + } + } +} + +impl AsRef for ResolvedValue { + fn as_ref(&self) -> &ExpressionValue { + self.as_value_ref() + } +} + +pub(crate) enum CowValue { + /// This has been requested as an owned value. + Owned(OwnedValue), + /// This has been requested as a mutable reference. + Shared(SharedValue), +} + +impl CowValue { + pub(crate) fn into_owned(self) -> ExecutionResult { + match self { + CowValue::Owned(owned) => Ok(owned), + // A CoW value is used in place of a mutable reference. + CowValue::Shared(shared) => shared.transparent_clone(), + } + } + + pub(crate) fn kind(&self) -> ValueKind { + self.as_value_ref().kind() + } + + pub(crate) fn as_value_ref(&self) -> &ExpressionValue { + match self { + CowValue::Owned(owned) => owned.as_ref(), + CowValue::Shared(shared) => shared.as_ref(), + } + } +} + +impl HasSpanRange for CowValue { + fn span_range(&self) -> SpanRange { + self.as_value_ref().span_range() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum RequestedValueOwnership { + /// Receives an Owned value (or an error!) + Owned, + /// Receives a Shared Reference + Shared, + /// Receives a Mutable Reference (or an error!) + Mutable, + /// 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, + /// Receives either a SharedReference or Owned. + CopyOnWrite, +} + +/// 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), + CompoundAssignment(CompoundAssignmentBuilder), + MethodCall(MethodCallBuilder), +} + +impl AnyValueFrame { + pub(super) fn handle_item( + self, + context: Context, + item: EvaluationItem, + ) -> ExecutionResult { + match self { + AnyValueFrame::Group(frame) => frame.handle_item(context, item), + AnyValueFrame::Array(frame) => frame.handle_item(context, item), + AnyValueFrame::Object(frame) => frame.handle_item(context, item), + AnyValueFrame::UnaryOperation(frame) => frame.handle_item(context, item), + AnyValueFrame::BinaryOperation(frame) => frame.handle_item(context, item), + AnyValueFrame::PropertyAccess(frame) => frame.handle_item(context, item), + AnyValueFrame::IndexAccess(frame) => frame.handle_item(context, item), + AnyValueFrame::Range(frame) => frame.handle_item(context, item), + AnyValueFrame::Assignment(frame) => frame.handle_item(context, item), + AnyValueFrame::CompoundAssignment(frame) => frame.handle_item(context, item), + AnyValueFrame::MethodCall(frame) => frame.handle_item(context, item), + } + } +} + +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.handle_node_as_value(frame, inner, RequestedValueOwnership::Owned) + } +} + +impl EvaluationFrame for GroupBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Group(self) + } + + fn handle_item( + self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + let inner = item.expect_owned_value(); + context.return_owned(inner.update_span_range(|_| self.span.into())) + } +} + +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.handle_node_as_value(self, next, RequestedValueOwnership::Owned), + None => context.return_owned(ExpressionValue::Array(ExpressionArray { + items: self.evaluated_items, + span_range: self.span.span_range(), + }))?, + }) + } +} + +impl EvaluationFrame for ArrayBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Array(self) + } + + fn handle_item( + mut self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + let value = item.expect_owned_value(); + self.evaluated_items.push(value.into_inner()); + 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 + .execution_err(format!("The key {} has already been set", key)); + } + self.pending = Some(PendingEntryPath::OnValueBranch { + key, + key_span: ident.span(), + }); + context.handle_node_as_value(self, value_node, RequestedValueOwnership::Owned) + } + Some((ObjectKey::Indexed { access, index }, value_node)) => { + self.pending = Some(PendingEntryPath::OnIndexKeyBranch { access, value_node }); + context.handle_node_as_value(self, index, RequestedValueOwnership::Owned) + } + None => { + context.return_owned(self.evaluated_entries.to_value(self.span.span_range()))? + } + }, + ) + } +} + +impl EvaluationFrame for Box { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Object(self) + } + + fn handle_item( + mut self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + let pending = self.pending.take(); + Ok(match pending { + Some(PendingEntryPath::OnIndexKeyBranch { access, value_node }) => { + let value = item.expect_owned_value(); + let key = value.into_inner().expect_string("An object key")?.value; + if self.evaluated_entries.contains_key(&key) { + return access.execution_err(format!("The key {} has already been set", key)); + } + self.pending = Some(PendingEntryPath::OnValueBranch { + key, + key_span: access.span(), + }); + context.handle_node_as_value(self, value_node, RequestedValueOwnership::Owned) + } + Some(PendingEntryPath::OnValueBranch { key, key_span }) => { + let value = item.expect_owned_value().into_inner(); + 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 }; + // TODO[operation-refactor]: Change to be LateBound to resolve what it actually should be + context.handle_node_as_value(frame, input, RequestedValueOwnership::Owned) + } +} + +impl EvaluationFrame for UnaryOperationBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::UnaryOperation(self) + } + + fn handle_item( + self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + let value = item.expect_owned_value().into_inner(); + context.return_owned(self.operation.evaluate(value)?) + } +} + +pub(super) struct BinaryOperationBuilder { + operation: BinaryOperation, + state: BinaryPath, +} + +enum BinaryPath { + OnLeftBranch { right: ExpressionNodeId }, + OnRightBranch { left: ExpressionValue }, +} + +impl BinaryOperationBuilder { + pub(super) fn start( + context: ValueContext, + operation: BinaryOperation, + left: ExpressionNodeId, + right: ExpressionNodeId, + ) -> NextAction { + let frame = Self { + operation, + state: BinaryPath::OnLeftBranch { right }, + }; + // TODO[operation-refactor]: Change to be LateBound to resolve what it actually should be + context.handle_node_as_value(frame, left, RequestedValueOwnership::Owned) + } +} + +impl EvaluationFrame for BinaryOperationBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::BinaryOperation(self) + } + + fn handle_item( + mut self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + Ok(match self.state { + BinaryPath::OnLeftBranch { right } => { + let value = item.expect_owned_value().into_inner(); + if let Some(result) = self.operation.lazy_evaluate(&value)? { + context.return_owned(result)? + } else { + self.state = BinaryPath::OnRightBranch { left: value }; + context.handle_node_as_value( + self, + right, + // TODO[operation-refactor]: Change to late bound as the operation may dictate what ownership it needs for its right operand + RequestedValueOwnership::Owned, + ) + } + } + BinaryPath::OnRightBranch { left } => { + let value = item.expect_owned_value().into_inner(); + context.return_owned(self.operation.evaluate(left, value)?)? + } + }) + } +} + +pub(super) struct ValuePropertyAccessBuilder { + access: PropertyAccess, +} + +impl ValuePropertyAccessBuilder { + pub(super) fn start( + context: ValueContext, + access: PropertyAccess, + node: ExpressionNodeId, + ) -> NextAction { + let frame = Self { access }; + let ownership = match context.requested_ownership() { + // 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. + RequestedValueOwnership::Owned => RequestedValueOwnership::CopyOnWrite, + other => other, + }; + context.handle_node_as_value(frame, node, ownership) + } +} + +impl EvaluationFrame for ValuePropertyAccessBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::PropertyAccess(self) + } + + fn handle_item( + self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + let value = item.expect_any_value(); + Ok(match value { + ResolvedValue::Owned(value) => { + let output = value.resolve_property(&self.access)?; + context.return_owned(output)? + } + ResolvedValue::Mutable(mutable) => { + let output = mutable.resolve_property(&self.access, false)?; + context.return_mutable(output) + } + ResolvedValue::Shared { + shared, + reason_not_mutable, + } => { + let output = shared.resolve_property(&self.access)?; + context.return_shared(output, reason_not_mutable)? + } + }) + } +} + +pub(super) struct ValueIndexAccessBuilder { + access: IndexAccess, + state: IndexPath, +} + +enum IndexPath { + OnSourceBranch { index: ExpressionNodeId }, + OnIndexBranch { source: ResolvedValue }, +} + +impl ValueIndexAccessBuilder { + pub(super) fn start( + context: ValueContext, + source: ExpressionNodeId, + access: IndexAccess, + index: ExpressionNodeId, + ) -> NextAction { + let frame = Self { + access, + state: IndexPath::OnSourceBranch { index }, + }; + let ownership = match context.requested_ownership() { + // 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. + RequestedValueOwnership::Owned => RequestedValueOwnership::CopyOnWrite, + other => other, + }; + context.handle_node_as_value(frame, source, ownership) + } +} + +impl EvaluationFrame for ValueIndexAccessBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::IndexAccess(self) + } + + fn handle_item( + mut self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + Ok(match self.state { + IndexPath::OnSourceBranch { index } => { + let value = item.expect_any_value(); + self.state = IndexPath::OnIndexBranch { source: value }; + context.handle_node_as_value( + self, + index, + // This is a value, so we are _accessing it_ and can't create values + // (that's only possible in a place!) - therefore we don't need an owned key, + // and can use &index for reading values from our array + RequestedValueOwnership::Shared, + ) + } + IndexPath::OnIndexBranch { source } => { + let index = item.expect_shared(); + let is_range = matches!(index.kind(), ValueKind::Range); + match source { + ResolvedValue::Owned(value) => { + let output = value.resolve_indexed(self.access, index.as_ref())?; + context.return_owned(output)? + } + ResolvedValue::Mutable(mutable) => { + let output = mutable.resolve_indexed(self.access, index.as_ref(), false)?; + context.return_mutable(output) + } + ResolvedValue::Shared { + shared, + reason_not_mutable, + } => { + let output = shared.resolve_indexed(self.access, index.as_ref())?; + context.return_shared(output, reason_not_mutable)? + } + } + } + }) + } +} + +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 = ExpressionRangeInner::RangeFull { token: *token }; + context.return_owned(inner.to_value(token.span_range()))? + } + syn::RangeLimits::Closed(_) => { + unreachable!( + "A closed range should have been given a right in continue_range(..)" + ) + } + }, + (None, Some(right)) => context.handle_node_as_value( + Self { + range_limits: *range_limits, + state: RangePath::OnRightBranch { left: None }, + }, + *right, + RequestedValueOwnership::Owned, + ), + (Some(left), right) => context.handle_node_as_value( + Self { + range_limits: *range_limits, + state: RangePath::OnLeftBranch { right: *right }, + }, + *left, + RequestedValueOwnership::Owned, + ), + }) + } +} + +impl EvaluationFrame for RangeBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Range(self) + } + + fn handle_item( + mut self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + // TODO[range-refactor]: Change to not always clone the value + let value = item.expect_owned_value().into_inner(); + Ok(match (self.state, self.range_limits) { + (RangePath::OnLeftBranch { right: Some(right) }, _) => { + self.state = RangePath::OnRightBranch { left: Some(value) }; + context.handle_node_as_value(self, right, RequestedValueOwnership::Owned) + } + (RangePath::OnLeftBranch { right: None }, syn::RangeLimits::HalfOpen(token)) => { + let inner = ExpressionRangeInner::RangeFrom { + start_inclusive: value, + token, + }; + context.return_owned(inner.to_value(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 = ExpressionRangeInner::Range { + start_inclusive: left, + token, + end_exclusive: value, + }; + context.return_owned(inner.to_value(token.span_range()))? + } + (RangePath::OnRightBranch { left: Some(left) }, syn::RangeLimits::Closed(token)) => { + let inner = ExpressionRangeInner::RangeInclusive { + start_inclusive: left, + token, + end_inclusive: value, + }; + context.return_owned(inner.to_value(token.span_range()))? + } + (RangePath::OnRightBranch { left: None }, syn::RangeLimits::HalfOpen(token)) => { + let inner = ExpressionRangeInner::RangeTo { + token, + end_exclusive: value, + }; + context.return_owned(inner.to_value(token.span_range()))? + } + (RangePath::OnRightBranch { left: None }, syn::RangeLimits::Closed(token)) => { + let inner = ExpressionRangeInner::RangeToInclusive { + token, + end_inclusive: value, + }; + context.return_owned(inner.to_value(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.handle_node_as_value(frame, value, RequestedValueOwnership::Owned) + } +} + +impl EvaluationFrame for AssignmentBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::Assignment(self) + } + + fn handle_item( + mut self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + Ok(match self.state { + AssignmentPath::OnValueBranch { assignee } => { + let value = item.expect_owned_value().into_inner(); + self.state = AssignmentPath::OnAwaitingAssignment; + context.handle_node_as_assignment(self, assignee, value) + } + AssignmentPath::OnAwaitingAssignment => { + let AssignmentCompletion { span_range } = item.expect_assignment_complete(); + context.return_owned(ExpressionValue::None(span_range))? + } + }) + } +} + +pub(super) struct CompoundAssignmentBuilder { + operation: CompoundAssignmentOperation, + state: CompoundAssignmentPath, +} + +enum CompoundAssignmentPath { + OnValueBranch { target: ExpressionNodeId }, + OnTargetBranch { value: ExpressionValue }, +} + +impl CompoundAssignmentBuilder { + pub(super) fn start( + context: ValueContext, + target: ExpressionNodeId, + operation: CompoundAssignmentOperation, + value: ExpressionNodeId, + ) -> NextAction { + let frame = Self { + operation, + state: CompoundAssignmentPath::OnValueBranch { target }, + }; + context.handle_node_as_value(frame, value, RequestedValueOwnership::Owned) + } +} + +impl EvaluationFrame for CompoundAssignmentBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::CompoundAssignment(self) + } + + fn handle_item( + mut self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + Ok(match self.state { + CompoundAssignmentPath::OnValueBranch { target } => { + let value = item.expect_owned_value().into_inner(); + self.state = CompoundAssignmentPath::OnTargetBranch { value }; + // TODO[compound-assignment-refactor]: Resolve as LateBound, and then convert to what is needed based on the operation + context.handle_node_as_value(self, target, RequestedValueOwnership::Mutable) + } + CompoundAssignmentPath::OnTargetBranch { value } => { + let mut mutable = item.expect_mutable(); + let span_range = SpanRange::new_between(mutable.span_range(), value.span_range()); + mutable + .as_mut() + .handle_compound_assignment(&self.operation, value, span_range)?; + context.return_owned(ExpressionValue::None(span_range))? + } + }) + } +} + +pub(super) struct MethodCallBuilder { + method: MethodAccess, + unevaluated_parameters_stack: Vec<(ExpressionNodeId, RequestedValueOwnership)>, + state: MethodCallPath, +} + +enum MethodCallPath { + CallerPath, + ArgumentsPath { + method: MethodInterface, + 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, RequestedValueOwnership::LateBound) + }) + .collect(), + state: MethodCallPath::CallerPath, + }; + context.handle_node_as_value(frame, caller, RequestedValueOwnership::LateBound) + } +} + +impl EvaluationFrame for MethodCallBuilder { + type ReturnType = ValueType; + + fn into_any(self) -> AnyValueFrame { + AnyValueFrame::MethodCall(self) + } + + fn handle_item( + mut self, + context: ValueContext, + item: EvaluationItem, + ) -> ExecutionResult { + // Handle expected item based on current state + match self.state { + MethodCallPath::CallerPath => { + let caller = item.expect_any_value(); + let method = caller + .kind() + .resolve_method(&self.method, self.unevaluated_parameters_stack.len())?; + assert!( + method.ownerships().len() == 1 + self.unevaluated_parameters_stack.len(), + "The method resolution should ensure the argument count is correct" + ); + + // We skip 1 to ignore the caller + let argument_ownerships_stack = method.ownerships().iter().skip(1).rev(); + for ((_, requested_ownership), ownership) in self + .unevaluated_parameters_stack + .iter_mut() + .zip(argument_ownerships_stack) + { + *requested_ownership = *ownership; + } + + self.state = MethodCallPath::ArgumentsPath { + evaluated_arguments_including_caller: { + let mut params = + Vec::with_capacity(1 + self.unevaluated_parameters_stack.len()); + params.push(caller); + params + }, + method, + }; + } + MethodCallPath::ArgumentsPath { + evaluated_arguments_including_caller: ref mut evaluated_parameters_including_caller, + .. + } => { + let argument = item.expect_any_value(); + evaluated_parameters_including_caller.push(argument); + } + }; + // Now plan the next action + Ok(match self.unevaluated_parameters_stack.pop() { + Some((parameter, ownership)) => { + context.handle_node_as_value(self, parameter, ownership) + } + None => { + let (arguments, method) = match self.state { + MethodCallPath::CallerPath => unreachable!("Already updated above"), + MethodCallPath::ArgumentsPath { + evaluated_arguments_including_caller, + method, + } => (evaluated_arguments_including_caller, method), + }; + let output = method.execute(arguments, self.method.span_range())?; + context.return_any_value(output)? + } + }) + } +} diff --git a/src/expressions/expression.rs b/src/expressions/expression.rs new file mode 100644 index 00000000..a8abdde6 --- /dev/null +++ b/src/expressions/expression.rs @@ -0,0 +1,334 @@ +use syn::token; + +use super::*; + +// Source +// ======================= + +#[derive(Clone)] +pub(crate) struct SourceExpression { + inner: Expression, +} + +impl Parse for SourceExpression { + fn parse(input: ParseStream) -> ParseResult { + Ok(Self { + inner: input.parse()?, + }) + } +} + +impl InterpretToValue for &SourceExpression { + type OutputValue = ExpressionValue; + + fn interpret_to_value( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + ExpressionEvaluator::new(&self.inner.nodes).evaluate(self.inner.root, interpreter) + } +} + +pub(super) enum SourceExpressionLeaf { + Command(Command), + Variable(VariableIdentifier), + Discarded(Token![_]), + ExpressionBlock(ExpressionBlock), + Value(ExpressionValue), +} + +impl HasSpanRange for SourceExpressionLeaf { + fn span_range(&self) -> SpanRange { + match self { + SourceExpressionLeaf::Command(command) => command.span_range(), + SourceExpressionLeaf::Variable(variable) => variable.span_range(), + SourceExpressionLeaf::Discarded(token) => token.span_range(), + SourceExpressionLeaf::ExpressionBlock(block) => block.span_range(), + SourceExpressionLeaf::Value(value) => value.span_range(), + } + } +} + +impl Expressionable for Source { + type Leaf = SourceExpressionLeaf; + type EvaluationContext = Interpreter; + + fn parse_unary_atom(input: &mut ParseStreamStack) -> ParseResult> { + Ok(match input.peek_grammar() { + SourcePeekMatch::Command(_) => UnaryAtom::Leaf(Self::Leaf::Command(input.parse()?)), + SourcePeekMatch::Variable(Grouping::Grouped) => { + 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.", + ) + } + SourcePeekMatch::Variable(Grouping::Flattened) => { + 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 sream.", + ) + } + SourcePeekMatch::ExpressionBlock(_) => { + UnaryAtom::Leaf(Self::Leaf::ExpressionBlock(input.parse()?)) + } + SourcePeekMatch::AppendVariableParser => { + return input + .parse_err("Append variable operations are not supported in an expression") + } + SourcePeekMatch::ExplicitTransformStream | SourcePeekMatch::Transformer(_) => { + return input.parse_err("Destructurings are not supported in an expression") + } + SourcePeekMatch::Group(Delimiter::None | Delimiter::Parenthesis) => { + let (_, delim_span) = input.parse_and_enter_group()?; + UnaryAtom::Group(delim_span) + } + SourcePeekMatch::Group(Delimiter::Brace) => { + let (_, delim_span) = input.parse_and_enter_group()?; + UnaryAtom::Object(Braces { delim_span }) + } + 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()?; + UnaryAtom::Array(Brackets { delim_span }) + } + SourcePeekMatch::Punct(punct) => { + if punct.as_char() == '.' { + UnaryAtom::Range(input.parse()?) + } else { + UnaryAtom::PrefixUnaryOperation(input.parse()?) + } + } + SourcePeekMatch::Ident(_) => { + if input.peek(Token![_]) { + return Ok(UnaryAtom::Leaf(Self::Leaf::Discarded(input.parse()?))); + } + match input.try_parse_or_revert() { + Ok(bool) => UnaryAtom::Leaf(Self::Leaf::Value(ExpressionValue::Boolean( + ExpressionBoolean::for_litbool(bool), + ))), + Err(_) => UnaryAtom::Leaf(Self::Leaf::Variable(input.parse()?)), + } + }, + SourcePeekMatch::Literal(_) => { + let value = ExpressionValue::for_syn_lit(input.parse()?); + UnaryAtom::Leaf(Self::Leaf::Value(value)) + } + SourcePeekMatch::End => return input.parse_err("Expected an expression"), + }) + } + + fn parse_extension( + input: &mut ParseStreamStack, + 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()?; + 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) => { + 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()?; + return Ok(NodeExtension::MethodCall(MethodAccess { + dot, + method: ident, + parentheses: Parentheses { delim_span }, + })); + } + return Ok(NodeExtension::Property(PropertyAccess { + dot, + property: ident, + })); + } + if let Ok(operation) = input.try_parse_or_revert() { + return Ok(NodeExtension::CompoundAssignmentOperation(operation)); + } + 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)); + } + if let Ok(eq) = input.try_parse_or_revert() { + return Ok(NodeExtension::AssignmentOperation(eq)); + } + } + 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 { .. } + | ExpressionStackFrame::IncompleteCompoundAssignment { .. } => { + Ok(NodeExtension::NoValidExtensionForCurrentParent) + } + } + } +} + +impl Parse for Expression { + fn parse(input: ParseStream) -> ParseResult { + ExpressionParser::parse(input) + } +} + +// Generic +// ======= + +pub(super) struct Expression { + pub(super) root: ExpressionNodeId, + pub(super) nodes: std::rc::Rc<[ExpressionNode]>, +} + +impl Clone for Expression { + fn clone(&self) -> Self { + Self { + root: self.root, + nodes: self.nodes.clone(), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) struct ExpressionNodeId(pub(super) usize); + +pub(super) enum ExpressionNode { + Leaf(K::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, + }, + CompoundAssignment { + place: ExpressionNodeId, + operation: CompoundAssignmentOperation, + value: ExpressionNodeId, + }, +} + +impl ExpressionNode { + pub(super) fn operator_span_range(&self) -> SpanRange { + match self { + ExpressionNode::Leaf(leaf) => leaf.span_range(), + ExpressionNode::Grouped { delim_span, .. } => delim_span.span_range(), + ExpressionNode::Array { brackets, .. } => brackets.span_range(), + ExpressionNode::Object { braces, .. } => braces.span_range(), + ExpressionNode::MethodCall { method, .. } => method.span_range(), + ExpressionNode::Property { access, .. } => access.span_range(), + ExpressionNode::Index { access, .. } => access.span_range(), + ExpressionNode::UnaryOperation { operation, .. } => operation.span_range(), + ExpressionNode::BinaryOperation { operation, .. } => operation.span_range(), + ExpressionNode::Range { range_limits, .. } => range_limits.span_range(), + ExpressionNode::Assignment { equals_token, .. } => equals_token.span_range(), + ExpressionNode::CompoundAssignment { operation, .. } => operation.span_range(), + } + } +} + +pub(super) trait Expressionable: Sized { + type Leaf; + type EvaluationContext; + + fn parse_unary_atom(input: &mut ParseStreamStack) -> ParseResult>; + fn parse_extension( + input: &mut ParseStreamStack, + parent_stack_frame: &ExpressionStackFrame, + ) -> ParseResult; +} diff --git a/src/expressions/expression_block.rs b/src/expressions/expression_block.rs new file mode 100644 index 00000000..12bd9005 --- /dev/null +++ b/src/expressions/expression_block.rs @@ -0,0 +1,197 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionBlock { + marker: Token![#], + flattening: Option, + parentheses: Parentheses, + standard_statements: Vec<(Statement, Token![;])>, + return_statement: Option, +} + +impl Parse for ExpressionBlock { + fn parse(input: ParseStream) -> ParseResult { + let marker = input.parse()?; + let flattening = if input.peek(Token![..]) { + Some(input.parse()?) + } else { + None + }; + let (parentheses, inner) = input.parse_parentheses()?; + let mut standard_statements = Vec::new(); + let return_statement = loop { + if inner.is_empty() { + break None; + } + let statement = inner.parse()?; + if inner.is_empty() { + break Some(statement); + } else if inner.peek(Token![;]) { + standard_statements.push((statement, inner.parse()?)); + } else { + return inner.parse_err("Expected an operator to continue the expression, or ; to mark the end of the expression statement"); + } + }; + Ok(Self { + marker, + flattening, + parentheses, + standard_statements, + return_statement, + }) + } +} + +impl HasSpanRange for ExpressionBlock { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.marker.span, self.parentheses.close()) + } +} + +impl ExpressionBlock { + pub(crate) fn evaluate( + &self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + let output_span_range = self.span_range(); + for (statement, ..) in &self.standard_statements { + let value = statement.interpret_to_value(interpreter)?; + match value { + ExpressionValue::None { .. } => {}, + other_value => return other_value.execution_err("A statement ending with ; must not return a value. If you wish to explicitly discard the expression's result, use `let _ = ...;`"), + } + } + if let Some(return_statement) = &self.return_statement { + return_statement.interpret_to_value(interpreter) + } else { + Ok(ExpressionValue::None(output_span_range)) + } + } +} + +impl Interpret for &ExpressionBlock { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let grouping = match self.flattening { + Some(_) => Grouping::Flattened, + None => Grouping::Grouped, + }; + self.evaluate(interpreter)? + .output_to(grouping, output, StreamOutputBehaviour::Standard)?; + Ok(()) + } +} + +impl InterpretToValue for &ExpressionBlock { + type OutputValue = ExpressionValue; + + fn interpret_to_value( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + if let Some(flattening) = &self.flattening { + return flattening + .execution_err("Flattening is not supported when outputting as a value"); + } + self.evaluate(interpreter) + } +} + +#[derive(Clone)] +pub(crate) enum Statement { + LetStatement(LetStatement), + Expression(SourceExpression), +} + +impl Parse for Statement { + fn parse(input: ParseStream) -> ParseResult { + Ok(if input.peek(Token![let]) { + Statement::LetStatement(input.parse()?) + } else { + Statement::Expression(input.parse()?) + }) + } +} + +impl InterpretToValue for &Statement { + type OutputValue = ExpressionValue; + + fn interpret_to_value( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + match self { + Statement::LetStatement(assignment) => assignment.interpret_to_value(interpreter), + Statement::Expression(expression) => expression.interpret_to_value(interpreter), + } + } +} + +/// 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. +#[derive(Clone)] +pub(crate) struct LetStatement { + let_token: Token![let], + pattern: Pattern, + assignment: Option, +} + +#[derive(Clone)] +struct LetStatementAssignment { + #[allow(unused)] + equals: Token![=], + expression: SourceExpression, +} + +impl Parse for LetStatement { + fn parse(input: ParseStream) -> ParseResult { + let let_token = input.parse()?; + let pattern = input.parse()?; + if input.peek(Token![=]) { + Ok(Self { + let_token, + pattern, + assignment: Some(LetStatementAssignment { + equals: input.parse()?, + expression: input.parse()?, + }), + }) + } else if input.is_empty() || input.peek(Token![;]) { + Ok(Self { + let_token, + pattern, + assignment: None, + }) + } else { + input.parse_err("Expected = or ;") + } + } +} + +impl InterpretToValue for &LetStatement { + type OutputValue = ExpressionValue; + + fn interpret_to_value( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + let LetStatement { + let_token, + pattern, + assignment, + } = self; + let value = match assignment { + Some(assignment) => assignment.expression.interpret_to_value(interpreter)?, + None => ExpressionValue::None(let_token.span.span_range()), + }; + let mut span_range = value.span_range(); + pattern.handle_destructure(interpreter, value)?; + span_range.set_start(let_token.span); + Ok(ExpressionValue::None(span_range)) + } +} diff --git a/src/expressions/expression_parsing.rs b/src/expressions/expression_parsing.rs new file mode 100644 index 00000000..aa817349 --- /dev/null +++ b/src/expressions/expression_parsing.rs @@ -0,0 +1,912 @@ +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, K: Expressionable> { + streams: ParseStreamStack<'a, K>, + nodes: ExpressionNodes, + expression_stack: Vec, + kind: PhantomData, +} + +impl<'a> ExpressionParser<'a, Source> { + pub(super) fn parse(input: ParseStream<'a, Source>) -> ParseResult> { + Self { + streams: ParseStreamStack::new(input), + nodes: ExpressionNodes::new(), + expression_stack: Vec::with_capacity(10), + kind: PhantomData, + } + .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 = Source::parse_unary_atom(&mut self.streams)?; + self.extend_with_unary_atom(unary_atom)? + } + WorkItem::TryParseAndApplyExtension { node } => { + let extension = Source::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 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(); + 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(); + 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::CompoundAssignmentOperation(operation) => { + self.push_stack_frame(ExpressionStackFrame::IncompleteCompoundAssignment { + place: node, + operation, + }) + } + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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) + } + ExpressionStackFrame::IncompleteCompoundAssignment { place, operation } => { + let node = self.nodes.add_node(ExpressionNode::CompoundAssignment { + place, + operation, + 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. + // Some examples of such ranges include: `[3.., 4]`, `[3..]`, `let x = 3..;`, + // `(3..).first()` or even `3...first()` + let can_parse_unary_atom = { + let forked = self.streams.fork_current(); + let mut forked_stack = ParseStreamStack::new(&forked); + Source::parse_unary_atom(&mut forked_stack).is_ok() + }; + if can_parse_unary_atom { + // 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(); + 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(SourceExpressionLeaf::Variable( + VariableIdentifier { ident: key.clone() }, + ))); + complete_entries.push((ObjectKey::Identifier(key), node)); + continue; + } else if self.streams.peek(token::Bracket) { + let (_, delim_span) = self.streams.parse_and_enter_group()?; + 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: SourceExpressionLeaf) -> 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() + } +} + +pub(super) struct ExpressionNodes { + nodes: Vec>, +} + +impl ExpressionNodes { + pub(super) fn new() -> Self { + Self { nodes: Vec::new() } + } + + pub(super) fn add_node(&mut self, node: ExpressionNode) -> ExpressionNodeId { + let node_id = ExpressionNodeId(self.nodes.len()); + self.nodes.push(node); + node_id + } + + pub(super) fn complete(self, root: ExpressionNodeId) -> Expression { + Expression { + root, + nodes: self.nodes.into(), + } + } +} + +//=============================================================================================== +// 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 { + BinaryOperation::Integer(op) => Self::of_integer_binary_operator(op), + BinaryOperation::Paired(op) => Self::of_paired_binary_operator(op), + } + } + + fn of_integer_binary_operator(op: &IntegerBinaryOperation) -> Self { + match op { + IntegerBinaryOperation::ShiftLeft { .. } + | IntegerBinaryOperation::ShiftRight { .. } => OperatorPrecendence::Shift, + } + } + + fn of_paired_binary_operator(op: &PairedBinaryOperation) -> Self { + match op { + PairedBinaryOperation::Addition { .. } | PairedBinaryOperation::Subtraction { .. } => { + Self::Sum + } + PairedBinaryOperation::Multiplication { .. } + | PairedBinaryOperation::Division { .. } + | PairedBinaryOperation::Remainder { .. } => Self::Product, + PairedBinaryOperation::LogicalAnd { .. } => Self::And, + PairedBinaryOperation::LogicalOr { .. } => Self::Or, + PairedBinaryOperation::BitXor { .. } => Self::BitXor, + PairedBinaryOperation::BitAnd { .. } => Self::BitAnd, + PairedBinaryOperation::BitOr { .. } => Self::BitOr, + PairedBinaryOperation::Equal { .. } + | PairedBinaryOperation::LessThan { .. } + | PairedBinaryOperation::LessThanOrEqual { .. } + | PairedBinaryOperation::NotEqual { .. } + | PairedBinaryOperation::GreaterThanOrEqual { .. } + | PairedBinaryOperation::GreaterThan { .. } => Self::Compare, + } + } +} + +/// 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![=], + }, + /// An incomplete assignment operation + /// It's left side is a place expression, according to the [rust reference]. + /// + /// [rust reference]: https://doc.rust-lang.org/reference/expressions.html#place-expressions-and-value-expressions + IncompleteCompoundAssignment { + place: ExpressionNodeId, + operation: CompoundAssignmentOperation, + }, + /// 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::IncompleteCompoundAssignment { .. } => { + 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(K::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![=]), + CompoundAssignmentOperation(CompoundAssignmentOperation), + 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::CompoundAssignmentOperation(_) => 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::CompoundAssignmentOperation { .. } + | 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/float.rs b/src/expressions/float.rs new file mode 100644 index 00000000..3c185aa4 --- /dev/null +++ b/src/expressions/float.rs @@ -0,0 +1,389 @@ +use super::*; +use crate::internal_prelude::*; + +#[derive(Clone)] +pub(crate) struct ExpressionFloat { + pub(super) value: ExpressionFloatValue, + /// The span range that generated this value. + /// For a complex expression, the start span is the most left part + /// of the expression, and the end span is the most right part. + pub(super) span_range: SpanRange, +} + +impl ExpressionFloat { + pub(super) fn for_litfloat(lit: &syn::LitFloat) -> ParseResult { + let span_range = lit.span().span_range(); + Ok(Self { + value: ExpressionFloatValue::for_litfloat(lit)?, + span_range, + }) + } + + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + match self.value { + ExpressionFloatValue::Untyped(input) => input.handle_unary_operation(operation), + ExpressionFloatValue::F32(input) => input.handle_unary_operation(operation), + ExpressionFloatValue::F64(input) => input.handle_unary_operation(operation), + } + } + + pub(super) fn handle_integer_binary_operation( + self, + right: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + match self.value { + ExpressionFloatValue::Untyped(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionFloatValue::F32(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionFloatValue::F64(input) => { + input.handle_integer_binary_operation(right, operation) + } + } + } + + pub(super) fn to_literal(&self) -> Literal { + self.value + .to_unspanned_literal() + .with_span(self.span_range.join_into_span_else_start()) + } +} + +impl HasValueType for ExpressionFloat { + fn value_type(&self) -> &'static str { + self.value.value_type() + } +} + +pub(super) enum ExpressionFloatValuePair { + Untyped(UntypedFloat, UntypedFloat), + F32(f32, f32), + F64(f64, f64), +} + +impl ExpressionFloatValuePair { + pub(super) fn handle_paired_binary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + match self { + Self::Untyped(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::F32(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::F64(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + } + } +} + +#[derive(Clone)] +pub(super) enum ExpressionFloatValue { + Untyped(UntypedFloat), + F32(f32), + F64(f64), +} + +impl ExpressionFloatValue { + pub(super) fn for_litfloat(lit: &syn::LitFloat) -> ParseResult { + Ok(match lit.suffix() { + "" => Self::Untyped(UntypedFloat::new_from_lit_float(lit.clone())), + "f32" => Self::F32(lit.base10_parse()?), + "f64" => Self::F64(lit.base10_parse()?), + suffix => { + return lit.span().parse_err(format!( + "The literal suffix {suffix} is not supported in preinterpret expressions" + )); + } + }) + } + + fn to_unspanned_literal(&self) -> Literal { + match self { + ExpressionFloatValue::Untyped(float) => float.to_unspanned_literal(), + ExpressionFloatValue::F32(float) => Literal::f32_suffixed(*float), + ExpressionFloatValue::F64(float) => Literal::f64_suffixed(*float), + } + } +} + +impl HasValueType for ExpressionFloatValue { + fn value_type(&self) -> &'static str { + match self { + ExpressionFloatValue::Untyped(_) => "untyped float", + ExpressionFloatValue::F32(_) => "f32", + ExpressionFloatValue::F64(_) => "f64", + } + } +} + +#[derive(Copy, Clone)] +pub(super) enum FloatKind { + Untyped, + F32, + F64, +} + +#[derive(Clone)] +pub(super) struct UntypedFloat( + /// The span of the literal is ignored, and will be set when converted to an output. + LitFloat, + SpanRange, +); +pub(super) type FallbackFloat = f64; + +impl UntypedFloat { + pub(super) fn new_from_lit_float(lit_float: LitFloat) -> Self { + let span_range = lit_float.span().span_range(); + Self(lit_float, span_range) + } + + pub(super) fn new_from_literal(literal: Literal) -> Self { + Self::new_from_lit_float(literal.into()) + } + + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + let input = self.parse_fallback()?; + Ok(match operation.operation { + UnaryOperation::Neg { .. } => operation.output(Self::from_fallback(-input)), + UnaryOperation::Not { .. } => return operation.unsupported(self), + UnaryOperation::Cast { target, .. } => match target { + CastTarget::Integer(IntegerKind::Untyped) => { + operation.output(UntypedInteger::from_fallback(input as FallbackInteger)) + } + CastTarget::Integer(IntegerKind::I8) => operation.output(input as i8), + CastTarget::Integer(IntegerKind::I16) => operation.output(input as i16), + CastTarget::Integer(IntegerKind::I32) => operation.output(input as i32), + CastTarget::Integer(IntegerKind::I64) => operation.output(input as i64), + CastTarget::Integer(IntegerKind::I128) => operation.output(input as i128), + CastTarget::Integer(IntegerKind::Isize) => operation.output(input as isize), + CastTarget::Integer(IntegerKind::U8) => operation.output(input as u8), + CastTarget::Integer(IntegerKind::U16) => operation.output(input as u16), + CastTarget::Integer(IntegerKind::U32) => operation.output(input as u32), + CastTarget::Integer(IntegerKind::U64) => operation.output(input as u64), + CastTarget::Integer(IntegerKind::U128) => operation.output(input as u128), + CastTarget::Integer(IntegerKind::Usize) => operation.output(input as usize), + CastTarget::Float(FloatKind::Untyped) => { + operation.output(UntypedFloat::from_fallback(input as FallbackFloat)) + } + CastTarget::Float(FloatKind::F32) => operation.output(input as f32), + CastTarget::Float(FloatKind::F64) => operation.output(input), + CastTarget::String => operation.output(input.to_string()), + CastTarget::Boolean | CastTarget::Char => { + return operation.execution_err("This cast is not supported") + } + CastTarget::Stream => { + operation.output(operation.output(self).into_new_output_stream( + Grouping::Flattened, + StreamOutputBehaviour::Standard, + )?) + } + CastTarget::Group => { + operation.output(operation.output(self).into_new_output_stream( + Grouping::Grouped, + StreamOutputBehaviour::Standard, + )?) + } + }, + }) + } + + pub(super) fn handle_integer_binary_operation( + self, + _rhs: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + match operation.operation { + IntegerBinaryOperation::ShiftLeft { .. } + | IntegerBinaryOperation::ShiftRight { .. } => operation.unsupported(self), + } + } + + pub(super) fn handle_paired_binary_operation( + self, + rhs: Self, + operation: OutputSpanned, + ) -> ExecutionResult { + let lhs = self.parse_fallback()?; + let rhs = rhs.parse_fallback()?; + Ok(match operation.operation { + PairedBinaryOperation::Addition { .. } => { + operation.output(Self::from_fallback(lhs + rhs)) + } + PairedBinaryOperation::Subtraction { .. } => { + operation.output(Self::from_fallback(lhs - rhs)) + } + PairedBinaryOperation::Multiplication { .. } => { + operation.output(Self::from_fallback(lhs * rhs)) + } + PairedBinaryOperation::Division { .. } => { + operation.output(Self::from_fallback(lhs / rhs)) + } + PairedBinaryOperation::LogicalAnd { .. } | PairedBinaryOperation::LogicalOr { .. } => { + return operation.unsupported(self) + } + PairedBinaryOperation::Remainder { .. } => { + operation.output(Self::from_fallback(lhs % rhs)) + } + PairedBinaryOperation::BitXor { .. } + | PairedBinaryOperation::BitAnd { .. } + | PairedBinaryOperation::BitOr { .. } => return operation.unsupported(self), + PairedBinaryOperation::Equal { .. } => operation.output(lhs == rhs), + PairedBinaryOperation::LessThan { .. } => operation.output(lhs < rhs), + PairedBinaryOperation::LessThanOrEqual { .. } => operation.output(lhs <= rhs), + PairedBinaryOperation::NotEqual { .. } => operation.output(lhs != rhs), + PairedBinaryOperation::GreaterThanOrEqual { .. } => operation.output(lhs >= rhs), + PairedBinaryOperation::GreaterThan { .. } => operation.output(lhs > rhs), + }) + } + + pub(super) fn from_fallback(value: FallbackFloat) -> Self { + Self::new_from_literal(Literal::f64_unsuffixed(value)) + } + + fn parse_fallback(&self) -> ExecutionResult { + self.0.base10_digits().parse().map_err(|err| { + self.1.execution_error(format!( + "Could not parse as the default inferred type {}: {}", + core::any::type_name::(), + err + )) + }) + } + + pub(super) fn parse_as(&self) -> ExecutionResult + where + N: FromStr, + N::Err: core::fmt::Display, + { + self.0.base10_digits().parse().map_err(|err| { + self.1.execution_error(format!( + "Could not parse as {}: {}", + core::any::type_name::(), + err + )) + }) + } + + fn to_unspanned_literal(&self) -> Literal { + self.0.token() + } +} + +impl HasValueType for UntypedFloat { + fn value_type(&self) -> &'static str { + "untyped float" + } +} + +impl ToExpressionValue for UntypedFloat { + fn to_value(mut self, span_range: SpanRange) -> ExpressionValue { + self.1 = span_range; + ExpressionValue::Float(ExpressionFloat { + value: ExpressionFloatValue::Untyped(self), + span_range, + }) + } +} + +macro_rules! impl_float_operations { + ( + $($float_enum_variant:ident($float_type:ident)),* $(,)? + ) => {$( + impl HasValueType for $float_type { + fn value_type(&self) -> &'static str { + stringify!($float_type) + } + } + + impl ToExpressionValue for $float_type { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Float(ExpressionFloat { + value: ExpressionFloatValue::$float_enum_variant(self), + span_range, + }) + } + } + + impl HandleUnaryOperation for $float_type { + fn handle_unary_operation(self, operation: OutputSpanned) -> ExecutionResult { + Ok(match operation.operation { + UnaryOperation::Neg { .. } => operation.output(-self), + UnaryOperation::Not { .. } => return operation.unsupported(self), + UnaryOperation::Cast { target, .. } => match target { + CastTarget::Integer(IntegerKind::Untyped) => operation.output(UntypedInteger::from_fallback(self as FallbackInteger)), + CastTarget::Integer(IntegerKind::I8) => operation.output(self as i8), + CastTarget::Integer(IntegerKind::I16) => operation.output(self as i16), + CastTarget::Integer(IntegerKind::I32) => operation.output(self as i32), + CastTarget::Integer(IntegerKind::I64) => operation.output(self as i64), + CastTarget::Integer(IntegerKind::I128) => operation.output(self as i128), + CastTarget::Integer(IntegerKind::Isize) => operation.output(self as isize), + CastTarget::Integer(IntegerKind::U8) => operation.output(self as u8), + CastTarget::Integer(IntegerKind::U16) => operation.output(self as u16), + CastTarget::Integer(IntegerKind::U32) => operation.output(self as u32), + CastTarget::Integer(IntegerKind::U64) => operation.output(self as u64), + CastTarget::Integer(IntegerKind::U128) => operation.output(self as u128), + CastTarget::Integer(IntegerKind::Usize) => operation.output(self as usize), + CastTarget::Float(FloatKind::Untyped) => operation.output(UntypedFloat::from_fallback(self as FallbackFloat)), + CastTarget::Float(FloatKind::F32) => operation.output(self as f32), + CastTarget::Float(FloatKind::F64) => operation.output(self as f64), + CastTarget::String => operation.output(self.to_string()), + CastTarget::Boolean | CastTarget::Char => return operation.execution_err("This cast is not supported"), + CastTarget::Stream => operation.output(operation.output(self).into_new_output_stream(Grouping::Flattened, StreamOutputBehaviour::Standard)?), + CastTarget::Group => operation.output(operation.output(self).into_new_output_stream(Grouping::Grouped, StreamOutputBehaviour::Standard)?), + } + }) + } + } + + impl HandleBinaryOperation for $float_type { + fn handle_paired_binary_operation(self, rhs: Self, operation: OutputSpanned) -> ExecutionResult { + // Unlike integer arithmetic, float arithmetic does not overflow + // and instead falls back to NaN or infinity. In future we could + // allow trapping on these codes, but for now this is good enough + let lhs = self; + Ok(match operation.operation { + PairedBinaryOperation::Addition { .. } => operation.output(lhs + rhs), + PairedBinaryOperation::Subtraction { .. } => operation.output(lhs - rhs), + PairedBinaryOperation::Multiplication { .. } => operation.output(lhs * rhs), + PairedBinaryOperation::Division { .. } => operation.output(lhs / rhs), + PairedBinaryOperation::LogicalAnd { .. } + | PairedBinaryOperation::LogicalOr { .. } => { + return operation.unsupported(self) + } + PairedBinaryOperation::Remainder { .. } => operation.output(lhs % rhs), + PairedBinaryOperation::BitXor { .. } + | PairedBinaryOperation::BitAnd { .. } + | PairedBinaryOperation::BitOr { .. } => { + return operation.unsupported(self) + } + PairedBinaryOperation::Equal { .. } => operation.output(lhs == rhs), + PairedBinaryOperation::LessThan { .. } => operation.output(lhs < rhs), + PairedBinaryOperation::LessThanOrEqual { .. } => operation.output(lhs <= rhs), + PairedBinaryOperation::NotEqual { .. } => operation.output(lhs != rhs), + PairedBinaryOperation::GreaterThanOrEqual { .. } => operation.output(lhs >= rhs), + PairedBinaryOperation::GreaterThan { .. } => operation.output(lhs > rhs), + }) + } + + fn handle_integer_binary_operation( + self, + _rhs: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + match operation.operation { + IntegerBinaryOperation::ShiftLeft { .. } | IntegerBinaryOperation::ShiftRight { .. } => { + operation.unsupported(self) + }, + } + } + } + )*}; +} +impl_float_operations!(F32(f32), F64(f64)); diff --git a/src/expressions/integer.rs b/src/expressions/integer.rs new file mode 100644 index 00000000..03df69a7 --- /dev/null +++ b/src/expressions/integer.rs @@ -0,0 +1,709 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionInteger { + pub(super) value: ExpressionIntegerValue, + /// The span range that generated this value. + /// For a complex expression, the start span is the most left part + /// of the expression, and the end span is the most right part. + pub(super) span_range: SpanRange, +} + +impl ExpressionInteger { + pub(super) fn for_litint(lit: &syn::LitInt) -> ParseResult { + Ok(Self { + span_range: lit.span().span_range(), + value: ExpressionIntegerValue::for_litint(lit)?, + }) + } + + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + match self.value { + ExpressionIntegerValue::Untyped(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::U8(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::U16(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::U32(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::U64(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::U128(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::Usize(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::I8(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::I16(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::I32(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::I64(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::I128(input) => input.handle_unary_operation(operation), + ExpressionIntegerValue::Isize(input) => input.handle_unary_operation(operation), + } + } + + pub(super) fn handle_integer_binary_operation( + self, + right: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + match self.value { + ExpressionIntegerValue::Untyped(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::U8(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::U16(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::U32(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::U64(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::U128(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::Usize(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::I8(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::I16(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::I32(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::I64(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::I128(input) => { + input.handle_integer_binary_operation(right, operation) + } + ExpressionIntegerValue::Isize(input) => { + input.handle_integer_binary_operation(right, operation) + } + } + } + + pub(crate) fn expect_usize(&self) -> ExecutionResult { + Ok(match &self.value { + ExpressionIntegerValue::Untyped(input) => input.parse_as()?, + ExpressionIntegerValue::Usize(input) => *input, + _ => { + return self + .span_range + .execution_err("Expected a usize or untyped integer") + } + }) + } + + pub(super) fn to_literal(&self) -> Literal { + self.value + .to_unspanned_literal() + .with_span(self.span_range.join_into_span_else_start()) + } +} + +impl HasValueType for ExpressionInteger { + fn value_type(&self) -> &'static str { + self.value.value_type() + } +} + +pub(super) enum ExpressionIntegerValuePair { + Untyped(UntypedInteger, UntypedInteger), + U8(u8, u8), + U16(u16, u16), + U32(u32, u32), + U64(u64, u64), + U128(u128, u128), + Usize(usize, usize), + I8(i8, i8), + I16(i16, i16), + I32(i32, i32), + I64(i64, i64), + I128(i128, i128), + Isize(isize, isize), +} + +impl ExpressionIntegerValuePair { + pub(super) fn handle_paired_binary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + match self { + Self::Untyped(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::U8(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::U16(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::U32(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::U64(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::U128(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::Usize(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::I8(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::I16(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::I32(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::I64(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::I128(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::Isize(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + } + } +} + +#[derive(Copy, Clone)] +pub(super) enum IntegerKind { + Untyped, + I8, + I16, + I32, + I64, + I128, + Isize, + U8, + U16, + U32, + U64, + U128, + Usize, +} + +#[derive(Clone)] +pub(super) enum ExpressionIntegerValue { + Untyped(UntypedInteger), + U8(u8), + U16(u16), + U32(u32), + U64(u64), + U128(u128), + Usize(usize), + I8(i8), + I16(i16), + I32(i32), + I64(i64), + I128(i128), + Isize(isize), +} + +impl ExpressionIntegerValue { + pub(super) fn for_litint(lit: &syn::LitInt) -> ParseResult { + Ok(match lit.suffix() { + "" => Self::Untyped(UntypedInteger::new_from_lit_int(lit.clone())), + "u8" => Self::U8(lit.base10_parse()?), + "u16" => Self::U16(lit.base10_parse()?), + "u32" => Self::U32(lit.base10_parse()?), + "u64" => Self::U64(lit.base10_parse()?), + "u128" => Self::U128(lit.base10_parse()?), + "usize" => Self::Usize(lit.base10_parse()?), + "i8" => Self::I8(lit.base10_parse()?), + "i16" => Self::I16(lit.base10_parse()?), + "i32" => Self::I32(lit.base10_parse()?), + "i64" => Self::I64(lit.base10_parse()?), + "i128" => Self::I128(lit.base10_parse()?), + "isize" => Self::Isize(lit.base10_parse()?), + suffix => { + return lit.span().parse_err(format!( + "The literal suffix {suffix} is not supported in preinterpret expressions" + )); + } + }) + } + + fn to_unspanned_literal(&self) -> Literal { + match self { + ExpressionIntegerValue::Untyped(int) => int.to_unspanned_literal(), + ExpressionIntegerValue::U8(int) => Literal::u8_suffixed(*int), + ExpressionIntegerValue::U16(int) => Literal::u16_suffixed(*int), + ExpressionIntegerValue::U32(int) => Literal::u32_suffixed(*int), + ExpressionIntegerValue::U64(int) => Literal::u64_suffixed(*int), + ExpressionIntegerValue::U128(int) => Literal::u128_suffixed(*int), + ExpressionIntegerValue::Usize(int) => Literal::usize_suffixed(*int), + ExpressionIntegerValue::I8(int) => Literal::i8_suffixed(*int), + ExpressionIntegerValue::I16(int) => Literal::i16_suffixed(*int), + ExpressionIntegerValue::I32(int) => Literal::i32_suffixed(*int), + ExpressionIntegerValue::I64(int) => Literal::i64_suffixed(*int), + ExpressionIntegerValue::I128(int) => Literal::i128_suffixed(*int), + ExpressionIntegerValue::Isize(int) => Literal::isize_suffixed(*int), + } + } +} + +impl HasValueType for ExpressionIntegerValue { + fn value_type(&self) -> &'static str { + match self { + ExpressionIntegerValue::Untyped(value) => value.value_type(), + ExpressionIntegerValue::U8(value) => value.value_type(), + ExpressionIntegerValue::U16(value) => value.value_type(), + ExpressionIntegerValue::U32(value) => value.value_type(), + ExpressionIntegerValue::U64(value) => value.value_type(), + ExpressionIntegerValue::U128(value) => value.value_type(), + ExpressionIntegerValue::Usize(value) => value.value_type(), + ExpressionIntegerValue::I8(value) => value.value_type(), + ExpressionIntegerValue::I16(value) => value.value_type(), + ExpressionIntegerValue::I32(value) => value.value_type(), + ExpressionIntegerValue::I64(value) => value.value_type(), + ExpressionIntegerValue::I128(value) => value.value_type(), + ExpressionIntegerValue::Isize(value) => value.value_type(), + } + } +} + +impl HasValueType for UntypedInteger { + fn value_type(&self) -> &'static str { + "untyped integer" + } +} + +#[derive(Clone)] +pub(crate) struct UntypedInteger( + /// The span of the literal is ignored, and will be set when converted to an output. + syn::LitInt, + SpanRange, +); +pub(crate) type FallbackInteger = i128; + +impl UntypedInteger { + pub(super) fn new_from_lit_int(lit_int: LitInt) -> Self { + let span_range = lit_int.span().span_range(); + Self(lit_int, span_range) + } + + pub(super) fn new_from_literal(literal: Literal) -> Self { + Self::new_from_lit_int(literal.into()) + } + + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + let input = self.parse_fallback()?; + Ok(match operation.operation { + UnaryOperation::Neg { .. } => operation.output(Self::from_fallback(-input)), + UnaryOperation::Not { .. } => return operation.unsupported(self), + UnaryOperation::Cast { target, .. } => match target { + CastTarget::Integer(IntegerKind::Untyped) => { + operation.output(UntypedInteger::from_fallback(input as FallbackInteger)) + } + CastTarget::Integer(IntegerKind::I8) => operation.output(input as i8), + CastTarget::Integer(IntegerKind::I16) => operation.output(input as i16), + CastTarget::Integer(IntegerKind::I32) => operation.output(input as i32), + CastTarget::Integer(IntegerKind::I64) => operation.output(input as i64), + CastTarget::Integer(IntegerKind::I128) => operation.output(input), + CastTarget::Integer(IntegerKind::Isize) => operation.output(input as isize), + CastTarget::Integer(IntegerKind::U8) => operation.output(input as u8), + CastTarget::Integer(IntegerKind::U16) => operation.output(input as u16), + CastTarget::Integer(IntegerKind::U32) => operation.output(input as u32), + CastTarget::Integer(IntegerKind::U64) => operation.output(input as u64), + CastTarget::Integer(IntegerKind::U128) => operation.output(input as u128), + CastTarget::Integer(IntegerKind::Usize) => operation.output(input as usize), + CastTarget::Float(FloatKind::Untyped) => { + operation.output(UntypedFloat::from_fallback(input as FallbackFloat)) + } + CastTarget::Float(FloatKind::F32) => operation.output(input as f32), + CastTarget::Float(FloatKind::F64) => operation.output(input as f64), + CastTarget::Boolean | CastTarget::Char => { + return operation.execution_err("This cast is not supported") + } + CastTarget::String => operation.output(input.to_string()), + CastTarget::Stream => { + operation.output(operation.output(self).into_new_output_stream( + Grouping::Flattened, + StreamOutputBehaviour::Standard, + )?) + } + CastTarget::Group => { + operation.output(operation.output(self).into_new_output_stream( + Grouping::Grouped, + StreamOutputBehaviour::Standard, + )?) + } + }, + }) + } + + pub(super) fn handle_integer_binary_operation( + self, + rhs: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + let lhs = self.parse_fallback()?; + Ok(match operation.operation { + IntegerBinaryOperation::ShiftLeft { .. } => match rhs.value { + ExpressionIntegerValue::Untyped(rhs) => { + operation.output(lhs << rhs.parse_fallback()?) + } + ExpressionIntegerValue::U8(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::U16(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::U32(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::U64(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::U128(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::Usize(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I8(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I16(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I32(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I64(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I128(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::Isize(rhs) => operation.output(lhs << rhs), + }, + IntegerBinaryOperation::ShiftRight { .. } => match rhs.value { + ExpressionIntegerValue::Untyped(rhs) => { + operation.output(lhs >> rhs.parse_fallback()?) + } + ExpressionIntegerValue::U8(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::U16(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::U32(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::U64(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::U128(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::Usize(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I8(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I16(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I32(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I64(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I128(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::Isize(rhs) => operation.output(lhs >> rhs), + }, + }) + } + + pub(super) fn handle_paired_binary_operation( + self, + rhs: Self, + operation: OutputSpanned, + ) -> ExecutionResult { + let lhs = self.parse_fallback()?; + let rhs = rhs.parse_fallback()?; + let overflow_error = || { + format!( + "The untyped integer operation {:?} {} {:?} overflowed in i128 space", + lhs, + operation.symbolic_description(), + rhs + ) + }; + Ok(match operation.operation { + PairedBinaryOperation::Addition { .. } => { + return operation.output_if_some( + lhs.checked_add(rhs).map(Self::from_fallback), + overflow_error, + ) + } + PairedBinaryOperation::Subtraction { .. } => { + return operation.output_if_some( + lhs.checked_sub(rhs).map(Self::from_fallback), + overflow_error, + ) + } + PairedBinaryOperation::Multiplication { .. } => { + return operation.output_if_some( + lhs.checked_mul(rhs).map(Self::from_fallback), + overflow_error, + ) + } + PairedBinaryOperation::Division { .. } => { + return operation.output_if_some( + lhs.checked_div(rhs).map(Self::from_fallback), + overflow_error, + ) + } + PairedBinaryOperation::LogicalAnd { .. } | PairedBinaryOperation::LogicalOr { .. } => { + return operation.unsupported(self); + } + PairedBinaryOperation::Remainder { .. } => { + return operation.output_if_some( + lhs.checked_rem(rhs).map(Self::from_fallback), + overflow_error, + ) + } + PairedBinaryOperation::BitXor { .. } => { + operation.output(Self::from_fallback(lhs ^ rhs)) + } + PairedBinaryOperation::BitAnd { .. } => { + operation.output(Self::from_fallback(lhs & rhs)) + } + PairedBinaryOperation::BitOr { .. } => operation.output(Self::from_fallback(lhs | rhs)), + PairedBinaryOperation::Equal { .. } => operation.output(lhs == rhs), + PairedBinaryOperation::LessThan { .. } => operation.output(lhs < rhs), + PairedBinaryOperation::LessThanOrEqual { .. } => operation.output(lhs <= rhs), + PairedBinaryOperation::NotEqual { .. } => operation.output(lhs != rhs), + PairedBinaryOperation::GreaterThanOrEqual { .. } => operation.output(lhs >= rhs), + PairedBinaryOperation::GreaterThan { .. } => operation.output(lhs > rhs), + }) + } + + pub(crate) fn from_fallback(value: FallbackInteger) -> Self { + Self::new_from_literal(Literal::i128_unsuffixed(value)) + } + + pub(super) fn parse_fallback(&self) -> ExecutionResult { + self.0.base10_digits().parse().map_err(|err| { + self.1.execution_error(format!( + "Could not parse as the default inferred type {}: {}", + core::any::type_name::(), + err + )) + }) + } + + pub(super) fn parse_as(&self) -> ExecutionResult + where + N: FromStr, + N::Err: core::fmt::Display, + { + self.0.base10_digits().parse().map_err(|err| { + self.1.execution_error(format!( + "Could not parse as {}: {}", + core::any::type_name::(), + err + )) + }) + } + + pub(super) fn to_unspanned_literal(&self) -> Literal { + self.0.token() + } +} + +impl ToExpressionValue for UntypedInteger { + fn to_value(mut self, span_range: SpanRange) -> ExpressionValue { + self.1 = span_range; + ExpressionValue::Integer(ExpressionInteger { + value: ExpressionIntegerValue::Untyped(self), + span_range, + }) + } +} + +// We have to use a macro because we don't have checked xx traits :( +macro_rules! impl_int_operations_except_unary { + ( + $($integer_enum_variant:ident($integer_type:ident)),* $(,)? + ) => {$( + impl HasValueType for $integer_type { + fn value_type(&self) -> &'static str { + stringify!($integer_type) + } + } + + impl ToExpressionValue for $integer_type { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Integer(ExpressionInteger { + value: ExpressionIntegerValue::$integer_enum_variant(self), + span_range, + }) + } + } + + impl HandleBinaryOperation for $integer_type { + fn handle_paired_binary_operation(self, rhs: Self, operation: OutputSpanned) -> ExecutionResult { + let lhs = self; + let overflow_error = || format!("The {} operation {:?} {} {:?} overflowed", stringify!($integer_type), lhs, operation.symbolic_description(), rhs); + Ok(match operation.operation { + PairedBinaryOperation::Addition { .. } => return operation.output_if_some(lhs.checked_add(rhs), overflow_error), + PairedBinaryOperation::Subtraction { .. } => return operation.output_if_some(lhs.checked_sub(rhs), overflow_error), + PairedBinaryOperation::Multiplication { .. } => return operation.output_if_some(lhs.checked_mul(rhs), overflow_error), + PairedBinaryOperation::Division { .. } => return operation.output_if_some(lhs.checked_div(rhs), overflow_error), + PairedBinaryOperation::LogicalAnd { .. } + | PairedBinaryOperation::LogicalOr { .. } => return operation.unsupported(self), + PairedBinaryOperation::Remainder { .. } => return operation.output_if_some(lhs.checked_rem(rhs), overflow_error), + PairedBinaryOperation::BitXor { .. } => operation.output(lhs ^ rhs), + PairedBinaryOperation::BitAnd { .. } => operation.output(lhs & rhs), + PairedBinaryOperation::BitOr { .. } => operation.output(lhs | rhs), + PairedBinaryOperation::Equal { .. } => operation.output(lhs == rhs), + PairedBinaryOperation::LessThan { .. } => operation.output(lhs < rhs), + PairedBinaryOperation::LessThanOrEqual { .. } => operation.output(lhs <= rhs), + PairedBinaryOperation::NotEqual { .. } => operation.output(lhs != rhs), + PairedBinaryOperation::GreaterThanOrEqual { .. } => operation.output(lhs >= rhs), + PairedBinaryOperation::GreaterThan { .. } => operation.output(lhs > rhs), + }) + } + + fn handle_integer_binary_operation( + self, + rhs: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + let lhs = self; + Ok(match operation.operation { + IntegerBinaryOperation::ShiftLeft { .. } => { + match rhs.value { + ExpressionIntegerValue::Untyped(rhs) => operation.output(lhs << rhs.parse_fallback()?), + ExpressionIntegerValue::U8(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::U16(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::U32(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::U64(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::U128(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::Usize(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I8(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I16(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I32(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I64(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::I128(rhs) => operation.output(lhs << rhs), + ExpressionIntegerValue::Isize(rhs) => operation.output(lhs << rhs), + } + }, + IntegerBinaryOperation::ShiftRight { .. } => { + match rhs.value { + ExpressionIntegerValue::Untyped(rhs) => operation.output(lhs >> rhs.parse_fallback()?), + ExpressionIntegerValue::U8(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::U16(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::U32(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::U64(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::U128(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::Usize(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I8(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I16(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I32(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I64(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::I128(rhs) => operation.output(lhs >> rhs), + ExpressionIntegerValue::Isize(rhs) => operation.output(lhs >> rhs), + } + }, + }) + } + } + )*}; +} + +macro_rules! impl_unsigned_unary_operations { + ($($integer_type:ident),* $(,)?) => {$( + impl HandleUnaryOperation for $integer_type { + fn handle_unary_operation(self, operation: OutputSpanned) -> ExecutionResult { + Ok(match operation.operation { + UnaryOperation::Neg { .. } + | UnaryOperation::Not { .. } => { + return operation.unsupported(self) + }, + UnaryOperation::Cast { target, .. } => match target { + CastTarget::Integer(IntegerKind::Untyped) => operation.output(UntypedInteger::from_fallback(self as FallbackInteger)), + CastTarget::Integer(IntegerKind::I8) => operation.output(self as i8), + CastTarget::Integer(IntegerKind::I16) => operation.output(self as i16), + CastTarget::Integer(IntegerKind::I32) => operation.output(self as i32), + CastTarget::Integer(IntegerKind::I64) => operation.output(self as i64), + CastTarget::Integer(IntegerKind::I128) => operation.output(self as i128), + CastTarget::Integer(IntegerKind::Isize) => operation.output(self as isize), + CastTarget::Integer(IntegerKind::U8) => operation.output(self as u8), + CastTarget::Integer(IntegerKind::U16) => operation.output(self as u16), + CastTarget::Integer(IntegerKind::U32) => operation.output(self as u32), + CastTarget::Integer(IntegerKind::U64) => operation.output(self as u64), + CastTarget::Integer(IntegerKind::U128) => operation.output(self as u128), + CastTarget::Integer(IntegerKind::Usize) => operation.output(self as usize), + CastTarget::Float(FloatKind::Untyped) => operation.output(UntypedFloat::from_fallback(self as FallbackFloat)), + CastTarget::Float(FloatKind::F32) => operation.output(self as f32), + CastTarget::Float(FloatKind::F64) => operation.output(self as f64), + CastTarget::Boolean | CastTarget::Char => return operation.execution_err("This cast is not supported"), + CastTarget::String => operation.output(self.to_string()), + CastTarget::Stream => operation.output(operation.output(self).into_new_output_stream(Grouping::Flattened, StreamOutputBehaviour::Standard)?), + CastTarget::Group => operation.output(operation.output(self).into_new_output_stream(Grouping::Grouped, StreamOutputBehaviour::Standard)?), + } + }) + } + } + )*}; +} + +macro_rules! impl_signed_unary_operations { + ($($integer_type:ident),* $(,)?) => {$( + impl HandleUnaryOperation for $integer_type { + fn handle_unary_operation(self, operation: OutputSpanned) -> ExecutionResult { + Ok(match operation.operation { + UnaryOperation::Neg { .. } => operation.output(-self), + UnaryOperation::Not { .. } => { + return operation.unsupported(self) + }, + UnaryOperation::Cast { target, .. } => match target { + CastTarget::Integer(IntegerKind::Untyped) => operation.output(UntypedInteger::from_fallback(self as FallbackInteger)), + CastTarget::Integer(IntegerKind::I8) => operation.output(self as i8), + CastTarget::Integer(IntegerKind::I16) => operation.output(self as i16), + CastTarget::Integer(IntegerKind::I32) => operation.output(self as i32), + CastTarget::Integer(IntegerKind::I64) => operation.output(self as i64), + CastTarget::Integer(IntegerKind::I128) => operation.output(self as i128), + CastTarget::Integer(IntegerKind::Isize) => operation.output(self as isize), + CastTarget::Integer(IntegerKind::U8) => operation.output(self as u8), + CastTarget::Integer(IntegerKind::U16) => operation.output(self as u16), + CastTarget::Integer(IntegerKind::U32) => operation.output(self as u32), + CastTarget::Integer(IntegerKind::U64) => operation.output(self as u64), + CastTarget::Integer(IntegerKind::U128) => operation.output(self as u128), + CastTarget::Integer(IntegerKind::Usize) => operation.output(self as usize), + CastTarget::Float(FloatKind::Untyped) => operation.output(UntypedFloat::from_fallback(self as FallbackFloat)), + CastTarget::Float(FloatKind::F32) => operation.output(self as f32), + CastTarget::Float(FloatKind::F64) => operation.output(self as f64), + CastTarget::Boolean | CastTarget::Char => return operation.execution_err("This cast is not supported"), + CastTarget::String => operation.output(self.to_string()), + CastTarget::Stream => operation.output(operation.output(self).into_new_output_stream(Grouping::Flattened, StreamOutputBehaviour::Standard)?), + CastTarget::Group => operation.output(operation.output(self).into_new_output_stream(Grouping::Grouped, StreamOutputBehaviour::Standard)?), + } + }) + } + } + )*}; +} + +impl HandleUnaryOperation for u8 { + fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + Ok(match operation.operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => { + return operation.unsupported(self) + } + UnaryOperation::Cast { target, .. } => match target { + CastTarget::Integer(IntegerKind::Untyped) => { + operation.output(UntypedInteger::from_fallback(self as FallbackInteger)) + } + CastTarget::Integer(IntegerKind::I8) => operation.output(self as i8), + CastTarget::Integer(IntegerKind::I16) => operation.output(self as i16), + CastTarget::Integer(IntegerKind::I32) => operation.output(self as i32), + CastTarget::Integer(IntegerKind::I64) => operation.output(self as i64), + CastTarget::Integer(IntegerKind::I128) => operation.output(self as i128), + CastTarget::Integer(IntegerKind::Isize) => operation.output(self as isize), + CastTarget::Integer(IntegerKind::U8) => operation.output(self), + CastTarget::Integer(IntegerKind::U16) => operation.output(self as u16), + CastTarget::Integer(IntegerKind::U32) => operation.output(self as u32), + CastTarget::Integer(IntegerKind::U64) => operation.output(self as u64), + CastTarget::Integer(IntegerKind::U128) => operation.output(self as u128), + CastTarget::Integer(IntegerKind::Usize) => operation.output(self as usize), + CastTarget::Float(FloatKind::Untyped) => { + operation.output(UntypedFloat::from_fallback(self as FallbackFloat)) + } + CastTarget::Float(FloatKind::F32) => operation.output(self as f32), + CastTarget::Float(FloatKind::F64) => operation.output(self as f64), + CastTarget::Char => operation.output(self as char), + CastTarget::Boolean => { + return operation.execution_err("This cast is not supported") + } + CastTarget::String => operation.output(self.to_string()), + CastTarget::Stream => { + operation.output(operation.output(self).into_new_output_stream( + Grouping::Flattened, + StreamOutputBehaviour::Standard, + )?) + } + CastTarget::Group => { + operation.output(operation.output(self).into_new_output_stream( + Grouping::Grouped, + StreamOutputBehaviour::Standard, + )?) + } + }, + }) + } +} + +impl_int_operations_except_unary!( + U8(u8), + U16(u16), + U32(u32), + U64(u64), + U128(u128), + Usize(usize), + I8(i8), + I16(i16), + I32(i32), + I64(i64), + I128(i128), + Isize(isize), +); + +// U8 has a char cast so is handled separately +impl_unsigned_unary_operations!(u16, u32, u64, u128, usize); +impl_signed_unary_operations!(i8, i16, i32, i64, i128, isize); diff --git a/src/expressions/iterator.rs b/src/expressions/iterator.rs new file mode 100644 index 00000000..0dec18db --- /dev/null +++ b/src/expressions/iterator.rs @@ -0,0 +1,229 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionIterator { + iterator: ExpressionIteratorInner, + #[allow(unused)] + pub(crate) span_range: SpanRange, +} + +impl ExpressionIterator { + pub(crate) fn new_for_array(array: ExpressionArray) -> Self { + Self { + iterator: ExpressionIteratorInner::Array(array.items.into_iter()), + span_range: array.span_range, + } + } + + pub(crate) fn new_for_stream(stream: ExpressionStream) -> Self { + Self { + iterator: ExpressionIteratorInner::Stream(stream.value.into_iter()), + span_range: stream.span_range, + } + } + + pub(crate) fn new_for_range(range: ExpressionRange) -> ExecutionResult { + let iterator = range.inner.into_iterable()?.resolve_iterator()?; + Ok(Self { + iterator: ExpressionIteratorInner::Other(iterator), + span_range: range.span_range, + }) + } + + pub(crate) fn new_custom( + iterator: Box, + span_range: SpanRange, + ) -> Self { + Self { + iterator: ExpressionIteratorInner::Other(iterator), + span_range, + } + } + + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + Ok(match operation.operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => { + return operation.unsupported(self) + } + UnaryOperation::Cast { + target, + target_ident, + .. + } => match target { + CastTarget::Stream => operation.output(self.into_stream_with_grouped_items()?), + CastTarget::Group => operation.output( + operation + .output(self.into_stream_with_grouped_items()?) + .into_new_output_stream( + Grouping::Grouped, + StreamOutputBehaviour::Standard, + )?, + ), + CastTarget::String => operation.output({ + let mut output = String::new(); + self.concat_recursive_into(&mut output, &ConcatBehaviour::standard())?; + output + }), + CastTarget::Boolean + | CastTarget::Char + | CastTarget::Integer(_) + | CastTarget::Float(_) => match self.singleton_value() { + Some(value) => value.handle_unary_operation(operation)?, + None => { + return operation.execution_err(format!( + "Only an iterator with one item can be cast to {}", + target_ident, + )) + } + }, + }, + }) + } + + pub(crate) fn singleton_value(mut self) -> Option { + let first = self.next()?; + if self.next().is_none() { + Some(first) + } else { + None + } + } + + fn into_stream_with_grouped_items(self) -> ExecutionResult { + let mut output = OutputStream::new(); + self.output_grouped_items_to(&mut output)?; + Ok(output) + } + + pub(super) fn output_grouped_items_to(self, output: &mut OutputStream) -> ExecutionResult<()> { + const LIMIT: usize = 10_000; + let span_range = self.span_range; + for (i, item) in self.enumerate() { + if i > LIMIT { + return span_range.execution_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.output_to( + Grouping::Grouped, + output, + StreamOutputBehaviour::PermitArrays, + )?; + } + Ok(()) + } + + pub(crate) fn concat_recursive_into( + self, + output: &mut String, + behaviour: &ConcatBehaviour, + ) -> ExecutionResult<()> { + if behaviour.output_array_structure { + output.push_str("["); + } + let max = self.size_hint().1; + let span_range = self.span_range; + for (i, item) in self.enumerate() { + if i >= behaviour.iterator_limit { + if behaviour.error_after_iterator_limit { + return span_range.execution_err(format!("To protect against infinite loops, only a maximum of {} items can be output to a string from an iterator. Try casting `as stream` to avoid this limit. This can't currently be reconfigured with the iteration limit.", behaviour.iterator_limit)); + } else { + if behaviour.output_array_structure { + match max { + Some(max) => output.push_str(&format!( + ", ..<{} further items>", + max.saturating_sub(i) + )), + None => output.push_str(", .."), + } + } + break; + } + } + if i == 0 { + output.push(' '); + } + if i != 0 && behaviour.output_array_structure { + output.push(','); + } + if i != 0 && behaviour.add_space_between_token_trees { + output.push(' '); + } + item.concat_recursive_into(output, behaviour)?; + } + if behaviour.output_array_structure { + output.push(']'); + } + Ok(()) + } +} + +impl ToExpressionValue for ExpressionIteratorInner { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Iterator(ExpressionIterator { + iterator: self, + span_range, + }) + } +} + +impl ToExpressionValue for Box { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Iterator(ExpressionIterator::new_custom(self, span_range)) + } +} + +impl HasValueType for ExpressionIterator { + fn value_type(&self) -> &'static str { + "iterator" + } +} + +#[derive(Clone)] +enum ExpressionIteratorInner { + Array( as IntoIterator>::IntoIter), + Stream(::IntoIter), + Other(Box), +} + +impl + Clone + 'static> CustomExpressionIterator for T { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +pub(crate) trait CustomExpressionIterator: Iterator { + fn clone_box(&self) -> Box; +} + +impl Clone for Box { + fn clone(&self) -> Self { + (**self).clone_box() + } +} + +impl Iterator for ExpressionIterator { + type Item = ExpressionValue; + + fn next(&mut self) -> Option { + match &mut self.iterator { + ExpressionIteratorInner::Array(iter) => iter.next(), + ExpressionIteratorInner::Stream(iter) => { + let item = iter.next()?; + let span = item.span(); + let stream: OutputStream = item.into(); + Some(stream.coerce_into_value(span.span_range())) + } + ExpressionIteratorInner::Other(iter) => iter.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.iterator { + ExpressionIteratorInner::Array(iter) => iter.size_hint(), + ExpressionIteratorInner::Stream(iter) => iter.size_hint(), + ExpressionIteratorInner::Other(iter) => iter.size_hint(), + } + } +} diff --git a/src/expressions/mod.rs b/src/expressions/mod.rs new file mode 100644 index 00000000..b3a1a0d2 --- /dev/null +++ b/src/expressions/mod.rs @@ -0,0 +1,36 @@ +mod array; +mod boolean; +mod character; +mod evaluation; +mod expression; +mod expression_block; +mod expression_parsing; +mod float; +mod integer; +mod iterator; +mod object; +mod operations; +mod range; +mod stream; +mod string; +mod value; + +pub(crate) use expression::*; +pub(crate) use expression_block::*; +pub(crate) use iterator::*; +pub(crate) use object::*; +pub(crate) use operations::*; +pub(crate) use stream::*; +pub(crate) use value::*; + +// Marked as use for expression sub-modules to use with a `use super::*` statement +use crate::internal_prelude::*; +use array::*; +use boolean::*; +use character::*; +use evaluation::*; +use expression_parsing::*; +use float::*; +use integer::*; +use range::*; +use string::*; diff --git a/src/expressions/object.rs b/src/expressions/object.rs new file mode 100644 index 00000000..b53a5c44 --- /dev/null +++ b/src/expressions/object.rs @@ -0,0 +1,365 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionObject { + pub(crate) entries: BTreeMap, + /// The span range that generated this value. + /// For a complex expression, the start span is the most left part + /// of the expression, and the end span is the most right part. + pub(crate) span_range: SpanRange, +} + +#[derive(Clone)] +pub(crate) struct ObjectEntry { + #[allow(unused)] + pub(crate) key_span: Span, + pub(crate) value: ExpressionValue, +} + +impl ExpressionObject { + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + match operation.operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => operation.unsupported(self), + UnaryOperation::Cast { target, .. } => match target { + CastTarget::String + | CastTarget::Stream + | CastTarget::Group + | CastTarget::Boolean + | CastTarget::Char + | CastTarget::Integer(_) + | CastTarget::Float(_) => operation.unsupported(self), + }, + } + } + + pub(super) fn handle_integer_binary_operation( + self, + _right: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + operation.unsupported(self) + } + + pub(super) fn handle_paired_binary_operation( + self, + _rhs: Self, + operation: OutputSpanned, + ) -> ExecutionResult { + operation.unsupported(self) + } + + pub(super) fn into_indexed( + mut self, + access: IndexAccess, + index: &ExpressionValue, + ) -> ExecutionResult { + let span_range = SpanRange::new_between(self.span_range, access.span_range()); + let key = index.expect_str("An object key")?; + Ok(self.remove_or_none(key, span_range)) + } + + pub(super) fn into_property( + mut self, + access: &PropertyAccess, + ) -> ExecutionResult { + let span_range = SpanRange::new_between(self.span_range, access.span_range()); + let key = access.property.to_string(); + Ok(self.remove_or_none(&key, span_range)) + } + + pub(crate) fn remove_or_none(&mut self, key: &str, span_range: SpanRange) -> ExpressionValue { + match self.entries.remove(key) { + Some(entry) => entry.value.with_span_range(span_range), + None => ExpressionValue::None(span_range), + } + } + + pub(crate) fn remove_no_none( + &mut self, + key: &str, + span_range: SpanRange, + ) -> Option { + match self.entries.remove(key) { + Some(entry) => { + if entry.value.is_none() { + None + } else { + Some(entry.value.with_span_range(span_range)) + } + } + None => None, + } + } + + pub(super) fn index_mut( + &mut self, + access: IndexAccess, + index: &ExpressionValue, + auto_create: bool, + ) -> ExecutionResult<&mut ExpressionValue> { + let index: &str = index.resolve_as()?; + self.mut_entry(index.to_string(), access.span(), auto_create) + } + + pub(super) fn index_ref( + &self, + access: IndexAccess, + index: &ExpressionValue, + ) -> ExecutionResult<&ExpressionValue> { + let key = index.expect_str("An object key")?; + let entry = self.entries.get(key).ok_or_else(|| { + access.execution_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 ExpressionValue> { + self.mut_entry( + access.property.to_string(), + access.property.span(), + auto_create, + ) + } + + pub(super) fn property_ref( + &self, + access: &PropertyAccess, + ) -> ExecutionResult<&ExpressionValue> { + let key = access.property.to_string(); + let entry = self.entries.get(&key).ok_or_else(|| { + access.execution_error(format!("The object does not have a field named `{}`", key)) + })?; + Ok(&entry.value) + } + + fn mut_entry( + &mut self, + key: String, + key_span: Span, + auto_create: bool, + ) -> ExecutionResult<&mut ExpressionValue> { + 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, + value: ExpressionValue::None(key_span.span_range()), + }) + .value + } else { + return key_span.execution_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.output_array_structure { + if self.entries.is_empty() { + output.push_str("{}"); + return Ok(()); + } + output.push('{'); + if behaviour.add_space_between_token_trees { + output.push(' '); + } + } + let mut is_first = true; + for (key, entry) in self.entries { + if !is_first && behaviour.output_array_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.concat_recursive_into(output, behaviour)?; + is_first = false; + } + if behaviour.output_array_structure { + if behaviour.add_space_between_token_trees { + output.push(' '); + } + output.push('}'); + } + Ok(()) + } + + 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: ExpressionValue::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.span_range().execution_err(error_message) + } +} + +impl HasSpanRange for ExpressionObject { + fn span_range(&self) -> SpanRange { + self.span_range + } +} + +impl HasValueType for ExpressionObject { + fn value_type(&self) -> &'static str { + self.entries.value_type() + } +} + +impl HasValueType for BTreeMap { + fn value_type(&self) -> &'static str { + "object" + } +} + +impl ToExpressionValue for BTreeMap { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Object(ExpressionObject { + entries: self, + span_range, + }) + } +} + +#[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>, +} diff --git a/src/expressions/operations.rs b/src/expressions/operations.rs new file mode 100644 index 00000000..947d0ba9 --- /dev/null +++ b/src/expressions/operations.rs @@ -0,0 +1,690 @@ +use super::*; + +pub(super) trait Operation: HasSpanRange { + fn with_output_span_range(&self, output_span_range: SpanRange) -> OutputSpanned<'_, Self> { + OutputSpanned { + output_span_range, + operation: self, + } + } + + fn symbolic_description(&self) -> &'static str; +} + +pub(super) struct OutputSpanned<'a, T: Operation + ?Sized> { + pub(super) output_span_range: SpanRange, + pub(super) operation: &'a T, +} + +impl OutputSpanned<'_, T> { + pub(super) fn symbolic_description(&self) -> &'static str { + self.operation.symbolic_description() + } + + pub(super) fn output(&self, output_value: impl ToExpressionValue) -> ExpressionValue { + output_value.to_value(self.output_span_range) + } + + pub(super) fn output_if_some( + &self, + output_value: Option, + error_message: impl FnOnce() -> String, + ) -> ExecutionResult { + match output_value { + Some(output_value) => Ok(self.output(output_value)), + None => self.operation.execution_err(error_message()), + } + } + + pub(super) fn unsupported(&self, value: impl HasValueType) -> ExecutionResult { + Err(self.operation.execution_error(format!( + "The {} operator is not supported for {} values", + self.operation.symbolic_description(), + value.value_type(), + ))) + } +} + +impl HasSpanRange for OutputSpanned<'_, T> { + fn span_range(&self) -> SpanRange { + // This is used for errors of the operation, so should be targetted + // to the span range of the _operator_, not the output. + self.operation.span_range() + } +} + +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(super) 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 = CastTarget::from_str(target_ident.to_string().as_str()).map_err(|()| { + target_ident.parse_error("This type is not supported in cast expressions") + })?; + + Ok(Self::Cast { + as_token, + target, + target_ident, + }) + } + + pub(super) fn evaluate(self, input: ExpressionValue) -> ExecutionResult { + let mut span_range = input.span_range(); + match &self { + UnaryOperation::Neg { token } => span_range.set_start(token.span), + UnaryOperation::Not { token } => span_range.set_start(token.span), + UnaryOperation::Cast { target_ident, .. } => span_range.set_end(target_ident.span()), + }; + input.handle_unary_operation(self.with_output_span_range(span_range)) + } +} + +#[derive(Copy, Clone)] +pub(super) enum CastTarget { + Integer(IntegerKind), + Float(FloatKind), + Boolean, + String, + Char, + Stream, + Group, +} + +impl FromStr for CastTarget { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "int" | "integer" => CastTarget::Integer(IntegerKind::Untyped), + "u8" => CastTarget::Integer(IntegerKind::U8), + "u16" => CastTarget::Integer(IntegerKind::U16), + "u32" => CastTarget::Integer(IntegerKind::U32), + "u64" => CastTarget::Integer(IntegerKind::U64), + "u128" => CastTarget::Integer(IntegerKind::U128), + "usize" => CastTarget::Integer(IntegerKind::Usize), + "i8" => CastTarget::Integer(IntegerKind::I8), + "i16" => CastTarget::Integer(IntegerKind::I16), + "i32" => CastTarget::Integer(IntegerKind::I32), + "i64" => CastTarget::Integer(IntegerKind::I64), + "i128" => CastTarget::Integer(IntegerKind::I128), + "isize" => CastTarget::Integer(IntegerKind::Isize), + "float" => CastTarget::Float(FloatKind::Untyped), + "f32" => CastTarget::Float(FloatKind::F32), + "f64" => CastTarget::Float(FloatKind::F64), + "bool" => CastTarget::Boolean, + "char" => CastTarget::Char, + "stream" => CastTarget::Stream, + "group" => CastTarget::Group, + "string" => CastTarget::String, + _ => return Err(()), + }) + } +} + +impl CastTarget { + fn symbolic_description(&self) -> &'static str { + match self { + CastTarget::Integer(IntegerKind::Untyped) => "as int", + CastTarget::Integer(IntegerKind::U8) => "as u8", + CastTarget::Integer(IntegerKind::U16) => "as u16", + CastTarget::Integer(IntegerKind::U32) => "as u32", + CastTarget::Integer(IntegerKind::U64) => "as u64", + CastTarget::Integer(IntegerKind::U128) => "as u128", + CastTarget::Integer(IntegerKind::Usize) => "as usize", + CastTarget::Integer(IntegerKind::I8) => "as i8", + CastTarget::Integer(IntegerKind::I16) => "as i16", + CastTarget::Integer(IntegerKind::I32) => "as i32", + CastTarget::Integer(IntegerKind::I64) => "as i64", + CastTarget::Integer(IntegerKind::I128) => "as i128", + CastTarget::Integer(IntegerKind::Isize) => "as isize", + CastTarget::Float(FloatKind::Untyped) => "as float", + CastTarget::Float(FloatKind::F32) => "as f32", + CastTarget::Float(FloatKind::F64) => "as f64", + CastTarget::Boolean => "as bool", + CastTarget::String => "as string", + CastTarget::Char => "as char", + CastTarget::Stream => "as stream", + CastTarget::Group => "as group", + } + } +} + +impl Operation for UnaryOperation { + fn symbolic_description(&self) -> &'static str { + match self { + UnaryOperation::Neg { .. } => "-", + UnaryOperation::Not { .. } => "!", + UnaryOperation::Cast { target, .. } => target.symbolic_description(), + } + } +} + +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, + } + } +} + +pub(super) trait HandleUnaryOperation: Sized { + fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult; +} + +#[derive(Clone)] +pub(crate) enum BinaryOperation { + Paired(PairedBinaryOperation), + Integer(IntegerBinaryOperation), +} + +impl From for BinaryOperation { + fn from(operation: PairedBinaryOperation) -> Self { + Self::Paired(operation) + } +} + +impl From for BinaryOperation { + fn from(operation: IntegerBinaryOperation) -> Self { + Self::Integer(operation) + } +} + +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 + if input.peek(Token![+]) { + Ok(Self::Paired(PairedBinaryOperation::Addition( + input.parse()?, + ))) + } else if input.peek(Token![-]) { + Ok(Self::Paired(PairedBinaryOperation::Subtraction( + input.parse()?, + ))) + } else if input.peek(Token![*]) { + Ok(Self::Paired(PairedBinaryOperation::Multiplication( + input.parse()?, + ))) + } else if input.peek(Token![/]) { + Ok(Self::Paired(PairedBinaryOperation::Division( + input.parse()?, + ))) + } else if input.peek(Token![%]) { + Ok(Self::Paired(PairedBinaryOperation::Remainder( + input.parse()?, + ))) + } else if input.peek(Token![&&]) { + Ok(Self::Paired(PairedBinaryOperation::LogicalAnd( + input.parse()?, + ))) + } else if input.peek(Token![||]) { + Ok(Self::Paired(PairedBinaryOperation::LogicalOr( + input.parse()?, + ))) + } else if input.peek(Token![==]) { + Ok(Self::Paired(PairedBinaryOperation::Equal(input.parse()?))) + } else if input.peek(Token![!=]) { + Ok(Self::Paired(PairedBinaryOperation::NotEqual( + input.parse()?, + ))) + } else if input.peek(Token![>=]) { + Ok(Self::Paired(PairedBinaryOperation::GreaterThanOrEqual( + input.parse()?, + ))) + } else if input.peek(Token![<=]) { + Ok(Self::Paired(PairedBinaryOperation::LessThanOrEqual( + input.parse()?, + ))) + } else if input.peek(Token![<<]) { + Ok(Self::Integer(IntegerBinaryOperation::ShiftLeft( + input.parse()?, + ))) + } else if input.peek(Token![>>]) { + Ok(Self::Integer(IntegerBinaryOperation::ShiftRight( + input.parse()?, + ))) + } else if input.peek(Token![>]) { + Ok(Self::Paired(PairedBinaryOperation::GreaterThan( + input.parse()?, + ))) + } else if input.peek(Token![<]) { + Ok(Self::Paired(PairedBinaryOperation::LessThan( + input.parse()?, + ))) + } else if input.peek(Token![&]) { + Ok(Self::Paired(PairedBinaryOperation::BitAnd(input.parse()?))) + } else if input.peek(Token![|]) { + Ok(Self::Paired(PairedBinaryOperation::BitOr(input.parse()?))) + } else if input.peek(Token![^]) { + Ok(Self::Paired(PairedBinaryOperation::BitXor(input.parse()?))) + } else { + Err(input.error("Expected one of + - * / % && || ^ & | == < <= != >= > << or >>")) + } + } +} + +impl BinaryOperation { + pub(super) fn lazy_evaluate( + &self, + left: &ExpressionValue, + ) -> ExecutionResult> { + match self { + BinaryOperation::Paired(PairedBinaryOperation::LogicalAnd { .. }) => { + let bool = left.clone().expect_bool("The left operand to &&")?; + if !bool.value { + Ok(Some(ExpressionValue::Boolean(bool))) + } else { + Ok(None) + } + } + BinaryOperation::Paired(PairedBinaryOperation::LogicalOr { .. }) => { + let bool = left.clone().expect_bool("The left operand to ||")?; + if bool.value { + Ok(Some(ExpressionValue::Boolean(bool))) + } else { + Ok(None) + } + } + _ => Ok(None), + } + } + + pub(crate) fn evaluate( + &self, + left: ExpressionValue, + right: ExpressionValue, + ) -> ExecutionResult { + let span_range = + SpanRange::new_between(left.span_range().start(), right.span_range().end()); + match self { + BinaryOperation::Paired(operation) => { + let value_pair = left.expect_value_pair(operation, right)?; + value_pair + .handle_paired_binary_operation(operation.with_output_span_range(span_range)) + } + BinaryOperation::Integer(operation) => { + let right = right + .into_integer() + .ok_or_else(|| self.execution_error("The shift amount must be an integer"))?; + left.handle_integer_binary_operation( + right, + operation.with_output_span_range(span_range), + ) + } + } + } +} + +impl HasSpanRange for BinaryOperation { + fn span_range(&self) -> SpanRange { + match self { + BinaryOperation::Paired(op) => op.span_range(), + BinaryOperation::Integer(op) => op.span_range(), + } + } +} + +impl Operation for BinaryOperation { + fn symbolic_description(&self) -> &'static str { + match self { + BinaryOperation::Paired(paired) => paired.symbolic_description(), + BinaryOperation::Integer(integer) => integer.symbolic_description(), + } + } +} + +#[derive(Copy, Clone)] +pub(crate) enum PairedBinaryOperation { + Addition(Token![+]), + Subtraction(Token![-]), + Multiplication(Token![*]), + Division(Token![/]), + Remainder(Token![%]), + LogicalAnd(Token![&&]), + LogicalOr(Token![||]), + BitXor(Token![^]), + BitAnd(Token![&]), + BitOr(Token![|]), + Equal(Token![==]), + LessThan(Token![<]), + LessThanOrEqual(Token![<=]), + NotEqual(Token![!=]), + GreaterThanOrEqual(Token![>=]), + GreaterThan(Token![>]), +} + +impl Operation for PairedBinaryOperation { + fn symbolic_description(&self) -> &'static str { + match self { + PairedBinaryOperation::Addition { .. } => "+", + PairedBinaryOperation::Subtraction { .. } => "-", + PairedBinaryOperation::Multiplication { .. } => "*", + PairedBinaryOperation::Division { .. } => "/", + PairedBinaryOperation::Remainder { .. } => "%", + PairedBinaryOperation::LogicalAnd { .. } => "&&", + PairedBinaryOperation::LogicalOr { .. } => "||", + PairedBinaryOperation::BitXor { .. } => "^", + PairedBinaryOperation::BitAnd { .. } => "&", + PairedBinaryOperation::BitOr { .. } => "|", + PairedBinaryOperation::Equal { .. } => "==", + PairedBinaryOperation::LessThan { .. } => "<", + PairedBinaryOperation::LessThanOrEqual { .. } => "<=", + PairedBinaryOperation::NotEqual { .. } => "!=", + PairedBinaryOperation::GreaterThanOrEqual { .. } => ">=", + PairedBinaryOperation::GreaterThan { .. } => ">", + } + } +} + +impl HasSpanRange for PairedBinaryOperation { + fn span_range(&self) -> SpanRange { + match self { + PairedBinaryOperation::Addition(plus) => plus.span_range(), + PairedBinaryOperation::Subtraction(minus) => minus.span_range(), + PairedBinaryOperation::Multiplication(star) => star.span_range(), + PairedBinaryOperation::Division(slash) => slash.span_range(), + PairedBinaryOperation::Remainder(percent) => percent.span_range(), + PairedBinaryOperation::LogicalAnd(and_and) => and_and.span_range(), + PairedBinaryOperation::LogicalOr(or_or) => or_or.span_range(), + PairedBinaryOperation::BitXor(caret) => caret.span_range(), + PairedBinaryOperation::BitAnd(and) => and.span_range(), + PairedBinaryOperation::BitOr(or) => or.span_range(), + PairedBinaryOperation::Equal(eq_eq) => eq_eq.span_range(), + PairedBinaryOperation::LessThan(lt) => lt.span_range(), + PairedBinaryOperation::LessThanOrEqual(le) => le.span_range(), + PairedBinaryOperation::NotEqual(ne) => ne.span_range(), + PairedBinaryOperation::GreaterThanOrEqual(ge) => ge.span_range(), + PairedBinaryOperation::GreaterThan(gt) => gt.span_range(), + } + } +} + +#[derive(Copy, Clone)] +pub(crate) enum IntegerBinaryOperation { + ShiftLeft(Token![<<]), + ShiftRight(Token![>>]), +} + +impl Operation for IntegerBinaryOperation { + fn symbolic_description(&self) -> &'static str { + match self { + IntegerBinaryOperation::ShiftLeft { .. } => "<<", + IntegerBinaryOperation::ShiftRight { .. } => ">>", + } + } +} + +impl HasSpanRange for IntegerBinaryOperation { + fn span_range(&self) -> SpanRange { + match self { + IntegerBinaryOperation::ShiftLeft(shl) => shl.span_range(), + IntegerBinaryOperation::ShiftRight(shr) => shr.span_range(), + } + } +} + +pub(super) trait HandleBinaryOperation: Sized { + fn handle_paired_binary_operation( + self, + rhs: Self, + operation: OutputSpanned, + ) -> ExecutionResult; + + fn handle_integer_binary_operation( + self, + rhs: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult; +} + +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, Copy)] +pub(crate) enum CompoundAssignmentOperation { + Add(Token![+=]), + Sub(Token![-=]), + Mul(Token![*=]), + Div(Token![/=]), + Rem(Token![%=]), + BitAnd(Token![&=]), + BitOr(Token![|=]), + BitXor(Token![^=]), + Shl(Token![<<=]), + Shr(Token![>>=]), +} + +impl SynParse for CompoundAssignmentOperation { + 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 + if input.peek(Token![+=]) { + Ok(Self::Add(input.parse()?)) + } else if input.peek(Token![-=]) { + Ok(Self::Sub(input.parse()?)) + } else if input.peek(Token![*=]) { + Ok(Self::Mul(input.parse()?)) + } else if input.peek(Token![/=]) { + Ok(Self::Div(input.parse()?)) + } else if input.peek(Token![%=]) { + Ok(Self::Rem(input.parse()?)) + } else if input.peek(Token![&=]) { + Ok(Self::BitAnd(input.parse()?)) + } else if input.peek(Token![|=]) { + Ok(Self::BitOr(input.parse()?)) + } else if input.peek(Token![^=]) { + Ok(Self::BitXor(input.parse()?)) + } else if input.peek(Token![<<=]) { + Ok(Self::Shl(input.parse()?)) + } else if input.peek(Token![>>=]) { + Ok(Self::Shr(input.parse()?)) + } else { + Err(input.error("Expected one of += -= *= /= %= &= |= ^= <<= or >>=")) + } + } +} + +impl CompoundAssignmentOperation { + pub(crate) fn to_binary(self) -> BinaryOperation { + match self { + CompoundAssignmentOperation::Add(token) => { + let token = create_single_token('+', token.spans[0]); + PairedBinaryOperation::Addition(token).into() + } + CompoundAssignmentOperation::Sub(token) => { + let token = create_single_token('-', token.spans[0]); + PairedBinaryOperation::Subtraction(token).into() + } + CompoundAssignmentOperation::Mul(token) => { + let token = create_single_token('*', token.spans[0]); + PairedBinaryOperation::Multiplication(token).into() + } + CompoundAssignmentOperation::Div(token) => { + let token = create_single_token('/', token.spans[0]); + PairedBinaryOperation::Division(token).into() + } + CompoundAssignmentOperation::Rem(token) => { + let token = create_single_token('%', token.spans[0]); + PairedBinaryOperation::Remainder(token).into() + } + CompoundAssignmentOperation::BitAnd(token) => { + let token = create_single_token('&', token.spans[0]); + PairedBinaryOperation::BitAnd(token).into() + } + CompoundAssignmentOperation::BitOr(token) => { + let token = create_single_token('^', token.spans[0]); + PairedBinaryOperation::BitOr(token).into() + } + CompoundAssignmentOperation::BitXor(token) => { + let token = create_single_token('|', token.spans[0]); + PairedBinaryOperation::BitXor(token).into() + } + CompoundAssignmentOperation::Shl(token) => { + let token = create_double_token('<', token.spans[0], '<', token.spans[1]); + IntegerBinaryOperation::ShiftLeft(token).into() + } + CompoundAssignmentOperation::Shr(token) => { + let token = create_double_token('>', token.spans[0], '>', token.spans[1]); + IntegerBinaryOperation::ShiftRight(token).into() + } + } + } +} + +fn create_single_token(char: char, span: Span) -> T { + let stream = Punct::new(char, Spacing::Alone) + .with_span(span) + .to_token_stream(); + T::parse.parse2(stream).unwrap() +} + +fn create_double_token(char1: char, span1: Span, char2: char, span2: Span) -> T { + let mut stream = TokenStream::new(); + Punct::new(char1, Spacing::Alone) + .with_span(span1) + .to_tokens(&mut stream); + Punct::new(char2, Spacing::Alone) + .with_span(span2) + .to_tokens(&mut stream); + T::parse.parse2(stream).unwrap() +} + +impl Operation for CompoundAssignmentOperation { + fn symbolic_description(&self) -> &'static str { + match self { + CompoundAssignmentOperation::Add(_) => "+=", + CompoundAssignmentOperation::Sub(_) => "-=", + CompoundAssignmentOperation::Mul(_) => "*=", + CompoundAssignmentOperation::Div(_) => "/=", + CompoundAssignmentOperation::Rem(_) => "%=", + CompoundAssignmentOperation::BitAnd(_) => "&=", + CompoundAssignmentOperation::BitOr(_) => "|=", + CompoundAssignmentOperation::BitXor(_) => "^=", + CompoundAssignmentOperation::Shl(_) => "<<=", + CompoundAssignmentOperation::Shr(_) => ">>=", + } + } +} + +impl HasSpanRange for CompoundAssignmentOperation { + fn span_range(&self) -> SpanRange { + match self { + CompoundAssignmentOperation::Add(op) => op.span_range(), + CompoundAssignmentOperation::Sub(op) => op.span_range(), + CompoundAssignmentOperation::Mul(op) => op.span_range(), + CompoundAssignmentOperation::Div(op) => op.span_range(), + CompoundAssignmentOperation::Rem(op) => op.span_range(), + CompoundAssignmentOperation::BitAnd(op) => op.span_range(), + CompoundAssignmentOperation::BitOr(op) => op.span_range(), + CompoundAssignmentOperation::BitXor(op) => op.span_range(), + CompoundAssignmentOperation::Shl(op) => op.span_range(), + CompoundAssignmentOperation::Shr(op) => op.span_range(), + } + } +} + +#[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/range.rs b/src/expressions/range.rs new file mode 100644 index 00000000..968df510 --- /dev/null +++ b/src/expressions/range.rs @@ -0,0 +1,436 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionRange { + pub(crate) inner: Box, + /// The span range that generated this value. + /// For a complex expression, the start span is the most left part + /// of the expression, and the end span is the most right part. + pub(crate) span_range: SpanRange, +} + +impl ExpressionRange { + pub(crate) fn concat_recursive_into( + self, + output: &mut String, + behaviour: &ConcatBehaviour, + ) -> ExecutionResult<()> { + match *self.inner { + ExpressionRangeInner::Range { + start_inclusive, + end_exclusive, + .. + } => { + start_inclusive.concat_recursive_into(output, behaviour)?; + output.push_str(".."); + end_exclusive.concat_recursive_into(output, behaviour)?; + } + ExpressionRangeInner::RangeFrom { + start_inclusive, .. + } => { + start_inclusive.concat_recursive_into(output, behaviour)?; + output.push_str(".."); + } + ExpressionRangeInner::RangeTo { end_exclusive, .. } => { + output.push_str(".."); + end_exclusive.concat_recursive_into(output, behaviour)?; + } + ExpressionRangeInner::RangeFull { .. } => { + output.push_str(".."); + } + ExpressionRangeInner::RangeInclusive { + start_inclusive, + end_inclusive, + .. + } => { + start_inclusive.concat_recursive_into(output, behaviour)?; + output.push_str("..="); + end_inclusive.concat_recursive_into(output, behaviour)?; + } + ExpressionRangeInner::RangeToInclusive { end_inclusive, .. } => { + output.push_str("..="); + end_inclusive.concat_recursive_into(output, behaviour)?; + } + } + Ok(()) + } + + pub(crate) fn resolve_to_index_range( + &self, + array: &ExpressionArray, + ) -> ExecutionResult> { + let mut start = 0; + let mut end = array.items.len(); + Ok(match &*self.inner { + ExpressionRangeInner::Range { + start_inclusive, + end_exclusive, + .. + } => { + start = array.resolve_valid_index(start_inclusive, false)?; + end = array.resolve_valid_index(end_exclusive, true)?; + start..end + } + ExpressionRangeInner::RangeFrom { + start_inclusive, .. + } => { + start = array.resolve_valid_index(start_inclusive, false)?; + start..array.items.len() + } + ExpressionRangeInner::RangeTo { end_exclusive, .. } => { + end = array.resolve_valid_index(end_exclusive, true)?; + start..end + } + ExpressionRangeInner::RangeFull { .. } => start..end, + ExpressionRangeInner::RangeInclusive { + start_inclusive, + end_inclusive, + .. + } => { + start = array.resolve_valid_index(start_inclusive, false)?; + // +1 is safe because it must be < array length. + end = array.resolve_valid_index(end_inclusive, false)? + 1; + start..end + } + ExpressionRangeInner::RangeToInclusive { end_inclusive, .. } => { + // +1 is safe because it must be < array length. + end = array.resolve_valid_index(end_inclusive, false)? + 1; + start..end + } + }) + } +} + +impl HasSpanRange for ExpressionRange { + fn span_range(&self) -> SpanRange { + self.span_range + } +} + +impl HasValueType for ExpressionRange { + fn value_type(&self) -> &'static str { + self.inner.value_type() + } +} + +/// A representation of the Rust [range expression]. +/// +/// [range expression]: https://doc.rust-lang.org/reference/expressions/range-expr.html +#[derive(Clone)] +pub(crate) enum ExpressionRangeInner { + /// `start .. end` + Range { + start_inclusive: ExpressionValue, + token: Token![..], + end_exclusive: ExpressionValue, + }, + /// `start ..` + RangeFrom { + start_inclusive: ExpressionValue, + token: Token![..], + }, + /// `.. end` + RangeTo { + token: Token![..], + end_exclusive: ExpressionValue, + }, + /// `..` (used inside arrays) + RangeFull { token: Token![..] }, + /// `start ..= end` + RangeInclusive { + start_inclusive: ExpressionValue, + token: Token![..=], + end_inclusive: ExpressionValue, + }, + /// `..= end` + RangeToInclusive { + token: Token![..=], + end_inclusive: ExpressionValue, + }, +} + +impl ExpressionRangeInner { + pub(super) fn into_iterable(self) -> ExecutionResult> { + Ok(match self { + Self::Range { + start_inclusive, + token, + end_exclusive, + } => IterableExpressionRange::RangeFromTo { + start: start_inclusive, + dots: syn::RangeLimits::HalfOpen(token), + end: end_exclusive, + }, + Self::RangeInclusive { + start_inclusive, + token, + end_inclusive, + } => IterableExpressionRange::RangeFromTo { + start: start_inclusive, + dots: syn::RangeLimits::Closed(token), + end: end_inclusive, + }, + Self::RangeFrom { + start_inclusive, + token, + } => IterableExpressionRange::RangeFrom { + start: start_inclusive, + dots: token, + }, + other => { + return other + .operator_span_range() + .execution_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 HasValueType for ExpressionRangeInner { + fn value_type(&self) -> &'static str { + match self { + Self::Range { .. } => "range start..end", + Self::RangeFrom { .. } => "range start..", + Self::RangeTo { .. } => "range ..end", + Self::RangeFull { .. } => "range ..", + Self::RangeInclusive { .. } => "range start..=end", + Self::RangeToInclusive { .. } => "range ..=end", + } + } +} + +impl ToExpressionValue for ExpressionRangeInner { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Range(ExpressionRange { + inner: Box::new(self), + span_range, + }) + } +} + +pub(super) enum IterableExpressionRange { + // start <= x < end OR start <= x <= end + RangeFromTo { + start: T, + dots: syn::RangeLimits, + end: T, + }, + // start <= x + RangeFrom { + dots: Token![..], + start: T, + }, +} + +impl IterableExpressionRange { + pub(super) fn resolve_iterator(self) -> ExecutionResult> { + match self { + Self::RangeFromTo { start, dots, end } => { + let output_span_range = + SpanRange::new_between(start.span_range().start(), end.span_range().end()); + let pair = start.expect_value_pair(&dots, end)?; + match pair { + ExpressionValuePair::Integer(pair) => match pair { + ExpressionIntegerValuePair::Untyped(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::U8(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::U16(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::U32(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::U64(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::U128(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::Usize(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::I8(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::I16(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::I32(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::I64(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::I128(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + ExpressionIntegerValuePair::Isize(start, end) => { + IterableExpressionRange::RangeFromTo { start, dots, end } + .resolve(output_span_range) + } + }, + ExpressionValuePair::CharPair(start, end) => { + IterableExpressionRange::RangeFromTo { + start: start.value, + dots, + end: end.value, + } + .resolve(output_span_range) + } + _ => dots + .execution_err("The range must be between two integers or two characters"), + } + } + Self::RangeFrom { start, dots } => { + let output_span_range = + SpanRange::new_between(start.span_range().start(), dots.span_range().end()); + match start { + ExpressionValue::Integer(start) => match start.value { + ExpressionIntegerValue::Untyped(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::U8(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::U16(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::U32(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::U64(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::U128(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::Usize(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::I8(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::I16(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::I32(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::I64(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::I128(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + ExpressionIntegerValue::Isize(start) => { + IterableExpressionRange::RangeFrom { start, dots } + .resolve(output_span_range) + } + }, + ExpressionValue::Char(start) => IterableExpressionRange::RangeFrom { + start: start.value, + dots, + } + .resolve(output_span_range), + _ => dots.execution_err("The range must be from an integer or a character"), + } + } + } + } +} + +impl IterableExpressionRange { + fn resolve( + self, + output_span_range: SpanRange, + ) -> ExecutionResult> { + match self { + Self::RangeFromTo { start, dots, end } => { + let start = start.parse_fallback()?; + let end = end.parse_fallback()?; + Ok(match dots { + syn::RangeLimits::HalfOpen { .. } => Box::new((start..end).map(move |x| { + UntypedInteger::from_fallback(x).to_value(output_span_range) + })), + syn::RangeLimits::Closed { .. } => Box::new((start..=end).map(move |x| { + UntypedInteger::from_fallback(x).to_value(output_span_range) + })), + }) + } + Self::RangeFrom { start, .. } => { + let start = start.parse_fallback()?; + Ok(Box::new((start..).map(move |x| { + UntypedInteger::from_fallback(x).to_value(output_span_range) + }))) + } + } + } +} + +macro_rules! define_range_resolvers { + ( + $($the_type:ident),* $(,)? + ) => {$( + impl IterableExpressionRange<$the_type> { + fn resolve(self, output_span_range: SpanRange) -> ExecutionResult> { + match self { + Self::RangeFromTo { start, dots, end } => { + Ok(match dots { + syn::RangeLimits::HalfOpen { .. } => { + Box::new((start..end).map(move |x| x.to_value(output_span_range))) + } + syn::RangeLimits::Closed { .. } => { + Box::new((start..=end).map(move |x| x.to_value(output_span_range))) + } + }) + }, + Self::RangeFrom { start, .. } => { + Ok(Box::new((start..).map(move |x| x.to_value(output_span_range)))) + }, + } + } + } + )*}; +} + +define_range_resolvers! { + u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, char, +} diff --git a/src/expressions/stream.rs b/src/expressions/stream.rs new file mode 100644 index 00000000..1fa44a85 --- /dev/null +++ b/src/expressions/stream.rs @@ -0,0 +1,121 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionStream { + pub(crate) value: OutputStream, + /// The span range that generated this value. + /// For a complex expression, the start span is the most left part + /// of the expression, and the end span is the most right part. + pub(crate) span_range: SpanRange, +} + +impl ExpressionStream { + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + Ok(match operation.operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => { + return operation.unsupported(self) + } + UnaryOperation::Cast { target, .. } => match target { + CastTarget::String => operation.output({ + let mut output = String::new(); + self.concat_recursive_into(&mut output, &ConcatBehaviour::standard()); + output + }), + CastTarget::Stream => operation.output(self.value), + CastTarget::Group => { + operation.output(operation.output(self.value).into_new_output_stream( + Grouping::Grouped, + StreamOutputBehaviour::Standard, + )?) + } + CastTarget::Boolean + | CastTarget::Char + | CastTarget::Integer(_) + | CastTarget::Float(_) => { + let coerced = self.value.coerce_into_value(self.span_range); + if let ExpressionValue::Stream(_) = &coerced { + return operation.unsupported(coerced); + } + coerced.handle_unary_operation(operation)? + } + }, + }) + } + + pub(super) fn handle_integer_binary_operation( + self, + _right: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + operation.unsupported(self) + } + + pub(super) fn handle_paired_binary_operation( + self, + rhs: Self, + operation: OutputSpanned, + ) -> ExecutionResult { + let lhs = self.value; + let rhs = rhs.value; + Ok(match operation.operation { + PairedBinaryOperation::Addition { .. } => operation.output({ + let mut stream = lhs; + rhs.append_cloned_into(&mut stream); + stream + }), + PairedBinaryOperation::Subtraction { .. } + | PairedBinaryOperation::Multiplication { .. } + | PairedBinaryOperation::Division { .. } + | PairedBinaryOperation::LogicalAnd { .. } + | PairedBinaryOperation::LogicalOr { .. } + | PairedBinaryOperation::Remainder { .. } + | PairedBinaryOperation::BitXor { .. } + | PairedBinaryOperation::BitAnd { .. } + | PairedBinaryOperation::BitOr { .. } + | PairedBinaryOperation::Equal { .. } + | PairedBinaryOperation::LessThan { .. } + | PairedBinaryOperation::LessThanOrEqual { .. } + | PairedBinaryOperation::NotEqual { .. } + | PairedBinaryOperation::GreaterThanOrEqual { .. } + | PairedBinaryOperation::GreaterThan { .. } => return operation.unsupported(lhs), + }) + } + + pub(crate) fn concat_recursive_into(self, output: &mut String, behaviour: &ConcatBehaviour) { + if behaviour.output_types_as_commands { + if self.value.is_empty() { + output.push_str("[!stream!]"); + } else { + output.push_str("[!stream! "); + self.value.concat_recursive_into(output, behaviour); + output.push(']'); + } + } else { + self.value.concat_recursive_into(output, behaviour); + } + } +} + +impl HasValueType for ExpressionStream { + fn value_type(&self) -> &'static str { + self.value.value_type() + } +} + +impl HasValueType for OutputStream { + fn value_type(&self) -> &'static str { + "stream" + } +} + +impl ToExpressionValue for OutputStream { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::Stream(ExpressionStream { + value: self, + span_range, + }) + } +} diff --git a/src/expressions/string.rs b/src/expressions/string.rs new file mode 100644 index 00000000..fd56f748 --- /dev/null +++ b/src/expressions/string.rs @@ -0,0 +1,118 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct ExpressionString { + pub(crate) value: String, + /// The span range that generated this value. + /// For a complex expression, the start span is the most left part + /// of the expression, and the end span is the most right part. + pub(super) span_range: SpanRange, +} + +impl ExpressionString { + pub(super) fn for_litstr(lit: syn::LitStr) -> Self { + Self { + value: lit.value(), + span_range: lit.span().span_range(), + } + } + + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + Ok(match operation.operation { + UnaryOperation::Neg { .. } | UnaryOperation::Not { .. } => { + return operation.unsupported(self) + } + UnaryOperation::Cast { target, .. } => match target { + CastTarget::Stream => { + operation.output(operation.output(self.value).into_new_output_stream( + Grouping::Flattened, + StreamOutputBehaviour::Standard, + )?) + } + CastTarget::Group => { + operation.output(operation.output(self.value).into_new_output_stream( + Grouping::Grouped, + StreamOutputBehaviour::Standard, + )?) + } + CastTarget::String => operation.output(self.value), + CastTarget::Boolean + | CastTarget::Char + | CastTarget::Integer(_) + | CastTarget::Float(_) => return operation.unsupported(self), + }, + }) + } + + pub(super) fn handle_integer_binary_operation( + self, + _right: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + operation.unsupported(self) + } + + pub(super) fn handle_paired_binary_operation( + self, + rhs: Self, + operation: OutputSpanned, + ) -> ExecutionResult { + let lhs = self.value; + let rhs = rhs.value; + Ok(match operation.operation { + PairedBinaryOperation::Addition { .. } => operation.output(lhs + &rhs), + PairedBinaryOperation::Subtraction { .. } + | PairedBinaryOperation::Multiplication { .. } + | PairedBinaryOperation::Division { .. } + | PairedBinaryOperation::LogicalAnd { .. } + | PairedBinaryOperation::LogicalOr { .. } + | PairedBinaryOperation::Remainder { .. } + | PairedBinaryOperation::BitXor { .. } + | PairedBinaryOperation::BitAnd { .. } + | PairedBinaryOperation::BitOr { .. } => return operation.unsupported(lhs), + PairedBinaryOperation::Equal { .. } => operation.output(lhs == rhs), + PairedBinaryOperation::LessThan { .. } => operation.output(lhs < rhs), + PairedBinaryOperation::LessThanOrEqual { .. } => operation.output(lhs <= rhs), + PairedBinaryOperation::NotEqual { .. } => operation.output(lhs != rhs), + PairedBinaryOperation::GreaterThanOrEqual { .. } => operation.output(lhs >= rhs), + PairedBinaryOperation::GreaterThan { .. } => operation.output(lhs > rhs), + }) + } + + pub(super) fn to_literal(&self) -> Literal { + Literal::string(&self.value).with_span(self.span_range.join_into_span_else_start()) + } +} + +impl HasValueType for ExpressionString { + fn value_type(&self) -> &'static str { + self.value.value_type() + } +} + +impl HasValueType for String { + fn value_type(&self) -> &'static str { + "string" + } +} + +impl ToExpressionValue for String { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::String(ExpressionString { + value: self, + span_range, + }) + } +} + +impl ToExpressionValue for &str { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::String(ExpressionString { + value: self.to_string(), + span_range, + }) + } +} diff --git a/src/expressions/value.rs b/src/expressions/value.rs new file mode 100644 index 00000000..d7d7f814 --- /dev/null +++ b/src/expressions/value.rs @@ -0,0 +1,844 @@ +use super::*; + +#[derive(Clone)] +pub(crate) enum ExpressionValue { + None(SpanRange), + Integer(ExpressionInteger), + Float(ExpressionFloat), + Boolean(ExpressionBoolean), + String(ExpressionString), + Char(ExpressionChar), + // 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(UnsupportedLiteral), + Array(ExpressionArray), + Object(ExpressionObject), + Stream(ExpressionStream), + Range(ExpressionRange), + Iterator(ExpressionIterator), +} + +pub(crate) trait ToExpressionValue: Sized { + fn to_value(self, span_range: SpanRange) -> ExpressionValue; +} + +impl ToExpressionValue for () { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + ExpressionValue::None(span_range) + } +} + +impl ExpressionValue { + pub(crate) fn for_literal(literal: Literal) -> Self { + // 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().source_parse_as().unwrap()) + } + + pub(crate) fn for_syn_lit(lit: syn::Lit) -> Self { + // https://docs.rs/syn/latest/syn/enum.Lit.html + match lit { + Lit::Int(lit) => match ExpressionInteger::for_litint(&lit) { + Ok(int) => Self::Integer(int), + Err(_) => Self::UnsupportedLiteral(UnsupportedLiteral { + span_range: lit.span().span_range(), + lit: Lit::Int(lit), + }), + }, + Lit::Float(lit) => match ExpressionFloat::for_litfloat(&lit) { + Ok(float) => Self::Float(float), + Err(_) => Self::UnsupportedLiteral(UnsupportedLiteral { + span_range: lit.span().span_range(), + lit: Lit::Float(lit), + }), + }, + Lit::Bool(lit) => Self::Boolean(ExpressionBoolean::for_litbool(lit)), + Lit::Str(lit) => Self::String(ExpressionString::for_litstr(lit)), + Lit::Char(lit) => Self::Char(ExpressionChar::for_litchar(lit)), + other => Self::UnsupportedLiteral(UnsupportedLiteral { + span_range: other.span().span_range(), + lit: other, + }), + } + } + + pub(crate) fn try_transparent_clone( + &self, + new_span_range: SpanRange, + ) -> ExecutionResult { + if !self.kind().supports_transparent_cloning() { + return new_span_range.execution_err(format!( + "An owned value is required, but a reference was received, and {} does not support transparent cloning. You may wish to use .take() or .clone() explicitly.", + self.articled_value_type() + )); + } + Ok(self.clone().with_span_range(new_span_range)) + } + + pub(super) fn expect_value_pair( + self, + operation: &impl Operation, + right: Self, + ) -> ExecutionResult { + Ok(match (self, right) { + (ExpressionValue::Integer(left), ExpressionValue::Integer(right)) => { + let integer_pair = match (left.value, right.value) { + (ExpressionIntegerValue::Untyped(untyped_lhs), rhs) => match rhs { + ExpressionIntegerValue::Untyped(untyped_rhs) => { + ExpressionIntegerValuePair::Untyped(untyped_lhs, untyped_rhs) + } + ExpressionIntegerValue::U8(rhs) => { + ExpressionIntegerValuePair::U8(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::U16(rhs) => { + ExpressionIntegerValuePair::U16(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::U32(rhs) => { + ExpressionIntegerValuePair::U32(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::U64(rhs) => { + ExpressionIntegerValuePair::U64(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::U128(rhs) => { + ExpressionIntegerValuePair::U128(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::Usize(rhs) => { + ExpressionIntegerValuePair::Usize(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::I8(rhs) => { + ExpressionIntegerValuePair::I8(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::I16(rhs) => { + ExpressionIntegerValuePair::I16(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::I32(rhs) => { + ExpressionIntegerValuePair::I32(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::I64(rhs) => { + ExpressionIntegerValuePair::I64(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::I128(rhs) => { + ExpressionIntegerValuePair::I128(untyped_lhs.parse_as()?, rhs) + } + ExpressionIntegerValue::Isize(rhs) => { + ExpressionIntegerValuePair::Isize(untyped_lhs.parse_as()?, rhs) + } + }, + (lhs, ExpressionIntegerValue::Untyped(untyped_rhs)) => match lhs { + ExpressionIntegerValue::Untyped(untyped_lhs) => { + ExpressionIntegerValuePair::Untyped(untyped_lhs, untyped_rhs) + } + ExpressionIntegerValue::U8(lhs) => { + ExpressionIntegerValuePair::U8(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::U16(lhs) => { + ExpressionIntegerValuePair::U16(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::U32(lhs) => { + ExpressionIntegerValuePair::U32(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::U64(lhs) => { + ExpressionIntegerValuePair::U64(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::U128(lhs) => { + ExpressionIntegerValuePair::U128(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::Usize(lhs) => { + ExpressionIntegerValuePair::Usize(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::I8(lhs) => { + ExpressionIntegerValuePair::I8(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::I16(lhs) => { + ExpressionIntegerValuePair::I16(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::I32(lhs) => { + ExpressionIntegerValuePair::I32(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::I64(lhs) => { + ExpressionIntegerValuePair::I64(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::I128(lhs) => { + ExpressionIntegerValuePair::I128(lhs, untyped_rhs.parse_as()?) + } + ExpressionIntegerValue::Isize(lhs) => { + ExpressionIntegerValuePair::Isize(lhs, untyped_rhs.parse_as()?) + } + }, + (ExpressionIntegerValue::U8(lhs), ExpressionIntegerValue::U8(rhs)) => { + ExpressionIntegerValuePair::U8(lhs, rhs) + } + (ExpressionIntegerValue::U16(lhs), ExpressionIntegerValue::U16(rhs)) => { + ExpressionIntegerValuePair::U16(lhs, rhs) + } + (ExpressionIntegerValue::U32(lhs), ExpressionIntegerValue::U32(rhs)) => { + ExpressionIntegerValuePair::U32(lhs, rhs) + } + (ExpressionIntegerValue::U64(lhs), ExpressionIntegerValue::U64(rhs)) => { + ExpressionIntegerValuePair::U64(lhs, rhs) + } + (ExpressionIntegerValue::U128(lhs), ExpressionIntegerValue::U128(rhs)) => { + ExpressionIntegerValuePair::U128(lhs, rhs) + } + (ExpressionIntegerValue::Usize(lhs), ExpressionIntegerValue::Usize(rhs)) => { + ExpressionIntegerValuePair::Usize(lhs, rhs) + } + (ExpressionIntegerValue::I8(lhs), ExpressionIntegerValue::I8(rhs)) => { + ExpressionIntegerValuePair::I8(lhs, rhs) + } + (ExpressionIntegerValue::I16(lhs), ExpressionIntegerValue::I16(rhs)) => { + ExpressionIntegerValuePair::I16(lhs, rhs) + } + (ExpressionIntegerValue::I32(lhs), ExpressionIntegerValue::I32(rhs)) => { + ExpressionIntegerValuePair::I32(lhs, rhs) + } + (ExpressionIntegerValue::I64(lhs), ExpressionIntegerValue::I64(rhs)) => { + ExpressionIntegerValuePair::I64(lhs, rhs) + } + (ExpressionIntegerValue::I128(lhs), ExpressionIntegerValue::I128(rhs)) => { + ExpressionIntegerValuePair::I128(lhs, rhs) + } + (ExpressionIntegerValue::Isize(lhs), ExpressionIntegerValue::Isize(rhs)) => { + ExpressionIntegerValuePair::Isize(lhs, rhs) + } + (left_value, right_value) => { + return operation.execution_err(format!("The {} operator cannot infer a common integer operand type from {} and {}. Consider using `as` to cast to matching types.", operation.symbolic_description(), left_value.value_type(), right_value.value_type())); + } + }; + ExpressionValuePair::Integer(integer_pair) + } + (ExpressionValue::Boolean(left), ExpressionValue::Boolean(right)) => { + ExpressionValuePair::BooleanPair(left, right) + } + (ExpressionValue::Float(left), ExpressionValue::Float(right)) => { + let float_pair = match (left.value, right.value) { + (ExpressionFloatValue::Untyped(untyped_lhs), rhs) => match rhs { + ExpressionFloatValue::Untyped(untyped_rhs) => { + ExpressionFloatValuePair::Untyped(untyped_lhs, untyped_rhs) + } + ExpressionFloatValue::F32(rhs) => { + ExpressionFloatValuePair::F32(untyped_lhs.parse_as()?, rhs) + } + ExpressionFloatValue::F64(rhs) => { + ExpressionFloatValuePair::F64(untyped_lhs.parse_as()?, rhs) + } + }, + (lhs, ExpressionFloatValue::Untyped(untyped_rhs)) => match lhs { + ExpressionFloatValue::Untyped(untyped_lhs) => { + ExpressionFloatValuePair::Untyped(untyped_lhs, untyped_rhs) + } + ExpressionFloatValue::F32(lhs) => { + ExpressionFloatValuePair::F32(lhs, untyped_rhs.parse_as()?) + } + ExpressionFloatValue::F64(lhs) => { + ExpressionFloatValuePair::F64(lhs, untyped_rhs.parse_as()?) + } + }, + (ExpressionFloatValue::F32(lhs), ExpressionFloatValue::F32(rhs)) => { + ExpressionFloatValuePair::F32(lhs, rhs) + } + (ExpressionFloatValue::F64(lhs), ExpressionFloatValue::F64(rhs)) => { + ExpressionFloatValuePair::F64(lhs, rhs) + } + (left_value, right_value) => { + return operation.execution_err(format!("The {} operator cannot infer a common float operand type from {} and {}. Consider using `as` to cast to matching types.", operation.symbolic_description(), left_value.value_type(), right_value.value_type())); + } + }; + ExpressionValuePair::Float(float_pair) + } + (ExpressionValue::String(left), ExpressionValue::String(right)) => { + ExpressionValuePair::StringPair(left, right) + } + (ExpressionValue::Char(left), ExpressionValue::Char(right)) => { + ExpressionValuePair::CharPair(left, right) + } + (ExpressionValue::Array(left), ExpressionValue::Array(right)) => { + ExpressionValuePair::ArrayPair(left, right) + } + (ExpressionValue::Object(left), ExpressionValue::Object(right)) => { + ExpressionValuePair::ObjectPair(left, right) + } + (ExpressionValue::Stream(left), ExpressionValue::Stream(right)) => { + ExpressionValuePair::StreamPair(left, right) + } + (left, right) => { + return operation.execution_err(format!("Cannot infer common type from {} {} {}. Consider using `as` to cast the operands to matching types.", left.value_type(), operation.symbolic_description(), right.value_type())); + } + }) + } + + pub(crate) fn kind(&self) -> ValueKind { + match self { + ExpressionValue::None(_) => ValueKind::None, + ExpressionValue::Integer(_) => ValueKind::Integer, + ExpressionValue::Float(_) => ValueKind::Float, + ExpressionValue::Boolean(_) => ValueKind::Boolean, + ExpressionValue::String(_) => ValueKind::String, + ExpressionValue::Char(_) => ValueKind::Char, + ExpressionValue::Array(_) => ValueKind::Array, + ExpressionValue::Object(_) => ValueKind::Object, + ExpressionValue::Stream(_) => ValueKind::Stream, + ExpressionValue::Range(_) => ValueKind::Range, + ExpressionValue::Iterator(_) => ValueKind::Iterator, + ExpressionValue::UnsupportedLiteral(_) => ValueKind::UnsupportedLiteral, + } + } + + pub(crate) fn is_none(&self) -> bool { + matches!(self, ExpressionValue::None(_)) + } + + pub(crate) fn into_integer(self) -> Option { + match self { + ExpressionValue::Integer(value) => Some(value), + _ => None, + } + } + + pub(crate) fn expect_bool(self, place_descriptor: &str) -> ExecutionResult { + match self { + ExpressionValue::Boolean(value) => Ok(value), + other => other.execution_err(format!( + "{} must be a boolean, but it is {}", + place_descriptor, + other.articled_value_type(), + )), + } + } + + pub(crate) fn expect_integer( + self, + place_descriptor: &str, + ) -> ExecutionResult { + match self { + ExpressionValue::Integer(value) => Ok(value), + other => other.execution_err(format!( + "{} must be an integer, but it is {}", + place_descriptor, + other.articled_value_type(), + )), + } + } + + pub(crate) fn expect_str(&self, place_descriptor: &str) -> ExecutionResult<&str> { + match self { + ExpressionValue::String(value) => Ok(&value.value), + other => other.execution_err(format!( + "{} must be a string, but it is {}", + place_descriptor, + other.articled_value_type(), + )), + } + } + + pub(crate) fn expect_string(self, place_descriptor: &str) -> ExecutionResult { + match self { + ExpressionValue::String(value) => Ok(value), + other => other.execution_err(format!( + "{} must be a string, but it is {}", + place_descriptor, + other.articled_value_type(), + )), + } + } + + pub(crate) fn ref_expect_string( + &self, + place_descriptor: &str, + ) -> ExecutionResult<&ExpressionString> { + match self { + ExpressionValue::String(value) => Ok(value), + other => other.execution_err(format!( + "{} must be a string, but it is {}", + place_descriptor, + other.articled_value_type(), + )), + } + } + + pub(crate) fn expect_array(self, place_descriptor: &str) -> ExecutionResult { + match self { + ExpressionValue::Array(value) => Ok(value), + other => other.execution_err(format!( + "{} must be an array, but it is {}", + place_descriptor, + other.articled_value_type(), + )), + } + } + + pub(crate) fn expect_object(self, place_descriptor: &str) -> ExecutionResult { + match self { + ExpressionValue::Object(value) => Ok(value), + other => other.execution_err(format!( + "{} must be an object, but it is {}", + place_descriptor, + other.articled_value_type(), + )), + } + } + + pub(crate) fn expect_stream(self, place_descriptor: &str) -> ExecutionResult { + match self { + ExpressionValue::Stream(value) => Ok(value), + other => other.execution_err(format!( + "{} must be a stream, but it is {}", + place_descriptor, + other.articled_value_type(), + )), + } + } + + pub(crate) fn expect_any_iterator( + self, + place_descriptor: &str, + ) -> ExecutionResult { + match self { + ExpressionValue::Array(value) => Ok(ExpressionIterator::new_for_array(value)), + ExpressionValue::Stream(value) => Ok(ExpressionIterator::new_for_stream(value)), + ExpressionValue::Iterator(value) => Ok(value), + ExpressionValue::Range(value) => Ok(ExpressionIterator::new_for_range(value)?), + other => other.execution_err(format!( + "{} must be iterable (an array, stream, range or iterator), but it is {}", + place_descriptor, + other.articled_value_type(), + )), + } + } + + pub(super) fn handle_unary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + match self { + ExpressionValue::None(_) => operation.unsupported(self), + ExpressionValue::Integer(value) => value.handle_unary_operation(operation), + ExpressionValue::Float(value) => value.handle_unary_operation(operation), + ExpressionValue::Boolean(value) => value.handle_unary_operation(operation), + ExpressionValue::String(value) => value.handle_unary_operation(operation), + ExpressionValue::Char(value) => value.handle_unary_operation(operation), + ExpressionValue::Stream(value) => value.handle_unary_operation(operation), + ExpressionValue::Array(value) => value.handle_unary_operation(operation), + ExpressionValue::Object(value) => value.handle_unary_operation(operation), + ExpressionValue::Range(range) => { + ExpressionIterator::new_for_range(range)?.handle_unary_operation(operation) + } + ExpressionValue::UnsupportedLiteral(value) => operation.unsupported(value), + ExpressionValue::Iterator(value) => value.handle_unary_operation(operation), + } + } + + pub(super) fn handle_compound_assignment( + &mut self, + operation: &CompoundAssignmentOperation, + right: Self, + source_span_range: SpanRange, + ) -> ExecutionResult<()> { + match (self, operation) { + (ExpressionValue::Stream(left_mut), CompoundAssignmentOperation::Add(_)) => { + let right = right.expect_stream("The target of += on a stream")?; + right.value.append_into(&mut left_mut.value); + left_mut.span_range = source_span_range; + } + (ExpressionValue::Array(left_mut), CompoundAssignmentOperation::Add(_)) => { + let mut right = right.expect_array("The target of += on an array")?; + left_mut.items.append(&mut right.items); + left_mut.span_range = source_span_range; + } + (left_mut, operation) => { + // Fallback to just clone and use the normal operator + let left = left_mut.clone(); + *left_mut = operation + .to_binary() + .evaluate(left, right)? + .with_span_range(source_span_range); + } + } + Ok(()) + } + + pub(super) fn handle_integer_binary_operation( + self, + right: ExpressionInteger, + operation: OutputSpanned, + ) -> ExecutionResult { + match self { + ExpressionValue::None(_) => operation.unsupported(self), + ExpressionValue::Integer(value) => { + value.handle_integer_binary_operation(right, operation) + } + ExpressionValue::Float(value) => { + value.handle_integer_binary_operation(right, operation) + } + ExpressionValue::Boolean(value) => { + value.handle_integer_binary_operation(right, operation) + } + ExpressionValue::String(value) => { + value.handle_integer_binary_operation(right, operation) + } + ExpressionValue::Char(value) => value.handle_integer_binary_operation(right, operation), + ExpressionValue::UnsupportedLiteral(value) => operation.unsupported(value), + ExpressionValue::Array(value) => { + value.handle_integer_binary_operation(right, operation) + } + ExpressionValue::Object(value) => { + value.handle_integer_binary_operation(right, operation) + } + ExpressionValue::Stream(value) => { + value.handle_integer_binary_operation(right, operation) + } + ExpressionValue::Iterator(value) => operation.unsupported(value), + ExpressionValue::Range(value) => operation.unsupported(value), + } + } + + pub(crate) fn into_indexed(self, access: IndexAccess, index: &Self) -> ExecutionResult { + match self { + ExpressionValue::Array(array) => array.into_indexed(access, index), + ExpressionValue::Object(object) => object.into_indexed(access, index), + other => access.execution_err(format!("Cannot index into a {}", other.value_type())), + } + } + + pub(crate) fn index_mut( + &mut self, + access: IndexAccess, + index: &Self, + auto_create: bool, + ) -> ExecutionResult<&mut Self> { + match self { + ExpressionValue::Array(array) => array.index_mut(access, index), + ExpressionValue::Object(object) => object.index_mut(access, index, auto_create), + other => access.execution_err(format!("Cannot index into a {}", other.value_type())), + } + } + + pub(crate) fn index_ref(&self, access: IndexAccess, index: &Self) -> ExecutionResult<&Self> { + match self { + ExpressionValue::Array(array) => array.index_ref(access, index), + ExpressionValue::Object(object) => object.index_ref(access, index), + other => access.execution_err(format!("Cannot index into a {}", other.value_type())), + } + } + + pub(crate) fn into_property(self, access: &PropertyAccess) -> ExecutionResult { + match self { + ExpressionValue::Object(object) => object.into_property(access), + other => access.execution_err(format!( + "Cannot access properties on a {}", + other.value_type() + )), + } + } + + pub(crate) fn property_mut( + &mut self, + access: &PropertyAccess, + auto_create: bool, + ) -> ExecutionResult<&mut Self> { + match self { + ExpressionValue::Object(object) => object.property_mut(access, auto_create), + other => access.execution_err(format!( + "Cannot access properties on a {}", + other.value_type() + )), + } + } + + pub(crate) fn property_ref(&self, access: &PropertyAccess) -> ExecutionResult<&Self> { + match self { + ExpressionValue::Object(object) => object.property_ref(access), + other => access.execution_err(format!( + "Cannot access properties on a {}", + other.value_type() + )), + } + } + + fn span_range_mut(&mut self) -> &mut SpanRange { + match self { + Self::None(span_range) => span_range, + Self::Integer(value) => &mut value.span_range, + Self::Float(value) => &mut value.span_range, + Self::Boolean(value) => &mut value.span_range, + Self::String(value) => &mut value.span_range, + Self::Char(value) => &mut value.span_range, + Self::UnsupportedLiteral(value) => &mut value.span_range, + Self::Array(value) => &mut value.span_range, + Self::Object(value) => &mut value.span_range, + Self::Stream(value) => &mut value.span_range, + Self::Iterator(value) => &mut value.span_range, + Self::Range(value) => &mut value.span_range, + } + } + + pub(crate) fn with_span_range(mut self, source_span_range: SpanRange) -> ExpressionValue { + *self.span_range_mut() = source_span_range; + self + } + + pub(crate) fn into_new_output_stream( + self, + grouping: Grouping, + behaviour: StreamOutputBehaviour, + ) -> ExecutionResult { + Ok(match (self, grouping) { + (Self::Stream(value), Grouping::Flattened) => value.value, + (other, grouping) => { + let mut output = OutputStream::new(); + other.output_to(grouping, &mut output, behaviour)?; + output + } + }) + } + + pub(crate) fn output_to( + &self, + grouping: Grouping, + output: &mut OutputStream, + behaviour: StreamOutputBehaviour, + ) -> 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, behaviour), + Delimiter::None, + self.span_range().join_into_span_else_start(), + )?; + } + Grouping::Flattened => { + self.output_flattened_to(output, behaviour)?; + } + } + Ok(()) + } + + fn output_flattened_to( + &self, + output: &mut OutputStream, + behaviour: StreamOutputBehaviour, + ) -> ExecutionResult<()> { + match self { + Self::None { .. } => {} + Self::Integer(value) => output.push_literal(value.to_literal()), + Self::Float(value) => output.push_literal(value.to_literal()), + Self::Boolean(value) => output.push_ident(value.to_ident()), + Self::String(value) => output.push_literal(value.to_literal()), + Self::Char(value) => output.push_literal(value.to_literal()), + Self::UnsupportedLiteral(literal) => { + output.extend_raw_tokens(literal.lit.to_token_stream()) + } + Self::Object(_) => { + return self.execution_err("Objects cannot be output to a stream"); + } + Self::Array(array) => { + if behaviour.should_output_arrays() { + array.output_grouped_items_to(output)? + } else { + return self.execution_err("Arrays cannot be output to a stream. You likely wish to use the !for! command or if you wish to output every element, use `#(XXX as stream)` to cast the array to a stream."); + } + } + Self::Stream(value) => value.value.append_cloned_into(output), + Self::Iterator(iterator) => { + if behaviour.should_output_iterators() { + iterator.clone().output_grouped_items_to(output)? + } else { + return self.execution_err("Iterators cannot be output to a stream. You likely wish to use the !for! command or if you wish to output every element, use `#(XXX as stream)` to cast the iterator to a stream."); + } + } + Self::Range(range) => { + let iterator = ExpressionIterator::new_for_range(range.clone())?; + if behaviour.should_output_iterators() { + iterator.output_grouped_items_to(output)? + } else { + return self.execution_err("Iterators cannot be output to a stream. You likely wish to use the !for! command or if you wish to output every element, use `#(XXX as stream)` to cast the iterator to a stream."); + } + } + }; + Ok(()) + } + + pub(crate) fn into_debug_string(self) -> ExecutionResult { + self.concat_recursive(&ConcatBehaviour::debug()) + } + + 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 { + ExpressionValue::None { .. } => { + if behaviour.show_none_values { + output.push_str("None"); + } + } + ExpressionValue::Stream(stream) => { + stream.concat_recursive_into(output, behaviour); + } + ExpressionValue::Array(array) => { + array.concat_recursive_into(output, behaviour)?; + } + ExpressionValue::Object(object) => { + object.concat_recursive_into(output, behaviour)?; + } + ExpressionValue::Iterator(iterator) => { + iterator.concat_recursive_into(output, behaviour)?; + } + ExpressionValue::Range(range) => { + range.concat_recursive_into(output, behaviour)?; + } + ExpressionValue::Integer(_) + | ExpressionValue::Float(_) + | ExpressionValue::Char(_) + | ExpressionValue::Boolean(_) + | ExpressionValue::UnsupportedLiteral(_) + | ExpressionValue::String(_) => { + // This isn't the most efficient, but it's less code and debug doesn't need to be super efficient. + let mut stream = OutputStream::new(); + self.output_flattened_to(&mut stream, StreamOutputBehaviour::Standard) + .expect("Non-composite values should all be able to be outputted to a stream"); + stream.concat_recursive_into(output, behaviour); + } + } + Ok(()) + } +} + +impl ToExpressionValue for ExpressionValue { + fn to_value(self, span_range: SpanRange) -> ExpressionValue { + self.with_span_range(span_range) + } +} + +#[derive(Clone, Copy)] +pub(crate) enum StreamOutputBehaviour { + Standard, + PermitArrays, +} + +impl StreamOutputBehaviour { + pub(super) fn should_output_arrays(&self) -> bool { + match self { + Self::Standard => false, + Self::PermitArrays => true, + } + } + + pub(super) fn should_output_iterators(&self) -> bool { + match self { + Self::Standard => false, + Self::PermitArrays => true, + } + } +} + +pub(crate) enum Grouping { + Grouped, + Flattened, +} + +impl HasValueType for ExpressionValue { + fn value_type(&self) -> &'static str { + match self { + Self::None { .. } => "none value", + Self::Integer(value) => value.value_type(), + Self::Float(value) => value.value_type(), + Self::Boolean(value) => value.value_type(), + Self::String(value) => value.value_type(), + Self::Char(value) => value.value_type(), + Self::UnsupportedLiteral(value) => value.value_type(), + Self::Array(value) => value.value_type(), + Self::Object(value) => value.value_type(), + Self::Stream(value) => value.value_type(), + Self::Iterator(value) => value.value_type(), + Self::Range(value) => value.value_type(), + } + } +} + +impl HasSpanRange for ExpressionValue { + fn span_range(&self) -> SpanRange { + match self { + ExpressionValue::None(span_range) => *span_range, + ExpressionValue::Integer(int) => int.span_range, + ExpressionValue::Float(float) => float.span_range, + ExpressionValue::Boolean(bool) => bool.span_range, + ExpressionValue::String(str) => str.span_range, + ExpressionValue::Char(char) => char.span_range, + ExpressionValue::UnsupportedLiteral(lit) => lit.span_range, + ExpressionValue::Array(array) => array.span_range, + ExpressionValue::Object(object) => object.span_range, + ExpressionValue::Stream(stream) => stream.span_range, + ExpressionValue::Iterator(iterator) => iterator.span_range, + ExpressionValue::Range(iterator) => iterator.span_range, + } + } +} + +pub(super) trait HasValueType { + fn value_type(&self) -> &'static str; + + fn articled_value_type(&self) -> String { + let value_type = self.value_type(); + if value_type.is_empty() { + return value_type.to_string(); + } + let first_char = value_type.chars().next().unwrap(); + match first_char { + 'a' | 'e' | 'i' | 'o' | 'u' => format!("an {}", value_type), + _ => format!("a {}", value_type), + } + } +} + +#[derive(Clone)] +pub(crate) struct UnsupportedLiteral { + lit: syn::Lit, + span_range: SpanRange, +} + +impl HasValueType for UnsupportedLiteral { + fn value_type(&self) -> &'static str { + "unsupported literal" + } +} + +pub(super) enum ExpressionValuePair { + Integer(ExpressionIntegerValuePair), + Float(ExpressionFloatValuePair), + BooleanPair(ExpressionBoolean, ExpressionBoolean), + StringPair(ExpressionString, ExpressionString), + CharPair(ExpressionChar, ExpressionChar), + ArrayPair(ExpressionArray, ExpressionArray), + ObjectPair(ExpressionObject, ExpressionObject), + StreamPair(ExpressionStream, ExpressionStream), +} + +impl ExpressionValuePair { + pub(super) fn handle_paired_binary_operation( + self, + operation: OutputSpanned, + ) -> ExecutionResult { + match self { + Self::Integer(pair) => pair.handle_paired_binary_operation(operation), + Self::Float(pair) => pair.handle_paired_binary_operation(operation), + Self::BooleanPair(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::StringPair(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::CharPair(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::ArrayPair(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::ObjectPair(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + Self::StreamPair(lhs, rhs) => lhs.handle_paired_binary_operation(rhs, operation), + } + } +} diff --git a/src/extensions/errors_and_spans.rs b/src/extensions/errors_and_spans.rs new file mode 100644 index 00000000..a96e5d8f --- /dev/null +++ b/src/extensions/errors_and_spans.rs @@ -0,0 +1,367 @@ +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 parse_err(&self, message: impl std::fmt::Display) -> ParseResult { + Err(self.error(message).into()) + } + + fn execution_err(&self, message: impl std::fmt::Display) -> ExecutionResult { + Err(self.error(message).into()) + } + + fn err(&self, message: impl std::fmt::Display) -> syn::Result { + Err(self.error(message)) + } + + fn error(&self, message: impl std::fmt::Display) -> syn::Error; + + fn execution_error(&self, message: impl std::fmt::Display) -> ExecutionInterrupt { + ExecutionInterrupt::Error(self.error(message)) + } + + #[allow(unused)] + fn parse_error(&self, message: impl std::fmt::Display) -> ParseError { + ParseError::Standard(self.error(message)) + } +} + +impl SpanErrorExt for T { + fn 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. +/// +/// See also [`SlowSpanRange`] for the equivalent of [`syn::spanned`]. +pub(crate) trait HasSpanRange { + fn span_range(&self) -> SpanRange; +} + +impl HasSpanRange for T { + fn span_range(&self) -> SpanRange { + SpanRange::new_single(self.span()) + } +} + +impl HasSpanRange for &SpanRange { + fn span_range(&self) -> SpanRange { + **self + } +} + +/// [`syn::spanned`] is potentially unexpectedly expensive, and has the +/// limitation that it uses [`proc_macro::Span::join`] and falls back to the +/// span of the first token when not available. +/// +/// Instead, [`syn::Error`] uses a trick involving a span range. This effectively +/// allows capturing this trick when we're not immediately creating an error. +/// +/// When [`proc_macro::Span::join`] is stabilised and [`syn::spanned`] works, +/// we can swap [`SpanRange`] contents 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; + } + + #[allow(unused)] + pub(crate) fn start(&self) -> Span { + self.start + } + + #[allow(unused)] + pub(crate) fn end(&self) -> Span { + self.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 HasSpan for TokenTree { + fn span(&self) -> Span { + self.span() + } +} + +impl HasSpan for Group { + fn span(&self) -> Span { + self.span() + } +} + +impl HasSpan for DelimSpan { + fn span(&self) -> Span { + self.join() + } +} + +impl HasSpan for Ident { + fn span(&self) -> Span { + self.span() + } +} + +impl HasSpan for Punct { + fn span(&self) -> Span { + self.span() + } +} + +impl HasSpan for Literal { + fn span(&self) -> Span { + self.span() + } +} + +/// [ ... ] +#[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() + } +} + +#[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![|], +} diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs new file mode 100644 index 00000000..346db9de --- /dev/null +++ b/src/extensions/mod.rs @@ -0,0 +1,7 @@ +mod errors_and_spans; +mod parsing; +mod tokens; + +pub(crate) use errors_and_spans::*; +pub(crate) use parsing::*; +pub(crate) use tokens::*; diff --git a/src/extensions/parsing.rs b/src/extensions/parsing.rs new file mode 100644 index 00000000..4943a791 --- /dev/null +++ b/src/extensions/parsing.rs @@ -0,0 +1,251 @@ +use crate::internal_prelude::*; + +pub(crate) trait TokenStreamParseExt: Sized { + fn source_parse_as>(self) -> ParseResult; + fn source_parse_with>( + self, + parser: impl FnOnce(ParseStream) -> Result, + ) -> Result; + + fn interpreted_parse_with>( + self, + parser: impl FnOnce(ParseStream) -> Result, + ) -> Result; +} + +impl TokenStreamParseExt for TokenStream { + fn source_parse_as>(self) -> ParseResult { + self.source_parse_with(T::parse) + } + + fn source_parse_with>( + self, + parser: impl FnOnce(ParseStream) -> Result, + ) -> Result { + parse_with(self, parser) + } + + fn interpreted_parse_with>( + self, + parser: impl FnOnce(ParseStream) -> Result, + ) -> Result { + parse_with(self, parser) + } +} + +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(error.into()), + (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; + #[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 #variable substitution or stream-based command such as [!group! ...]", + } + } + + fn description_of_group(&self) -> &'static str { + match self { + Delimiter::Parenthesis => "(...)", + Delimiter::Brace => "{ ... }", + Delimiter::Bracket => "[...]", + Delimiter::None => "transparent group, from a grouped #variable substitution or stream-based command such as [!group! ...]", + } + } +} + +/// Allows storing a stack of parse buffers for certain parse strategies which require +/// handling multiple groups in parallel. +pub(crate) struct ParseStreamStack<'a, K> { + base: ParseStream<'a, K>, + group_stack: Vec>, +} + +impl<'a, K> ParseStreamStack<'a, K> { + pub(crate) fn new(base: ParseStream<'a, K>) -> Self { + Self { + base, + group_stack: Vec::new(), + } + } + + pub(crate) fn fork_current(&self) -> ParseBuffer<'_, K> { + self.current().fork() + } + + fn current(&self) -> ParseStream<'_, K> { + self.group_stack.last().unwrap_or(self.base) + } + + pub(crate) fn parse_err(&self, message: impl std::fmt::Display) -> ParseResult { + self.current().parse_err(message) + } + + pub(crate) fn parse>(&mut self) -> ParseResult { + self.current().parse() + } + + 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 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), + } + } + + pub(crate) fn parse_and_enter_group(&mut self) -> ParseResult<(Delimiter, DelimSpan)> { + let (delimiter, delim_span, inner) = self.current().parse_any_group()?; + 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 `ParseStreamStack` 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) + }; + self.group_stack.push(inner); + Ok((delimiter, delim_span)) + } + + /// 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`. + /// + /// ### Panics + /// Panics if there is no group available. + pub(crate) fn exit_group(&mut self) { + self.group_stack + .pop() + .expect("finish_group must be paired with push_group"); + } +} + +impl ParseStreamStack<'_, Source> { + pub(crate) fn peek_grammar(&mut self) -> SourcePeekMatch { + self.current().peek_grammar() + } +} + +impl Drop for ParseStreamStack<'_, K> { + fn drop(&mut self) { + while !self.group_stack.is_empty() { + self.exit_group(); + } + } +} diff --git a/src/extensions/tokens.rs b/src/extensions/tokens.rs new file mode 100644 index 00000000..b29e7f0c --- /dev/null +++ b/src/extensions/tokens.rs @@ -0,0 +1,91 @@ +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; +} + +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, + } + } +} + +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..dafebc32 100644 --- a/src/internal_prelude.rs +++ b/src/internal_prelude.rs @@ -1,54 +1,26 @@ pub(crate) use core::iter; +pub(crate) use core::marker::PhantomData; +pub(crate) use core::ops::{Deref, DerefMut}; +pub(crate) use proc_macro2::extra::*; 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 quote::ToTokens; +pub(crate) use std::{ + borrow::Cow, + collections::{BTreeMap, HashMap, HashSet}, + str::FromStr, +}; +pub(crate) use syn::buffer::Cursor; +pub(crate) use syn::ext::IdentExt as SynIdentExt; +pub(crate) use syn::parse::{ + discouraged::Speculative, Parse as SynParse, ParseBuffer as SynParseBuffer, + ParseStream as SynParseStream, Parser as SynParser, +}; +pub(crate) use syn::punctuated::Punctuated; +pub(crate) use syn::{parse_str, Lit, LitFloat, LitInt, Token}; +pub(crate) use syn::{Error as SynError, Result as SynResult}; + +pub(crate) use crate::expressions::*; +pub(crate) use crate::extensions::*; +pub(crate) use crate::interpretation::*; +pub(crate) use crate::misc::*; +pub(crate) use crate::transformation::*; diff --git a/src/interpretation/bindings.rs b/src/interpretation/bindings.rs new file mode 100644 index 00000000..ab3fce77 --- /dev/null +++ b/src/interpretation/bindings.rs @@ -0,0 +1,482 @@ +use super::*; + +use std::cell::*; +use std::rc::Rc; + +pub(super) struct VariableContent { + value: Rc>, + #[allow(unused)] + definition_span_range: SpanRange, +} + +impl VariableContent { + pub(super) fn new(tokens: ExpressionValue, definition_span_range: SpanRange) -> Self { + Self { + value: Rc::new(RefCell::new(tokens)), + definition_span_range, + } + } + + pub(super) fn binding(&self, variable: &(impl IsVariable + ?Sized)) -> VariableBinding { + VariableBinding { + data: self.value.clone(), + variable_span_range: variable.span_range(), + } + } +} + +#[derive(Clone)] +pub(crate) struct VariableBinding { + data: Rc>, + variable_span_range: SpanRange, +} + +impl VariableBinding { + /// Gets the cloned expression value, setting the span range appropriately + /// This only works if the value can be transparently cloned + pub(crate) fn into_transparently_cloned(self) -> ExecutionResult { + self.into_shared()?.transparent_clone() + } + + /// Gets the cloned expression value, setting the span range appropriately + /// This works for any value, but may be more expensive + pub(crate) fn into_expensively_cloned(self) -> ExecutionResult { + Ok(self.into_shared()?.expensive_clone()) + } + + pub(crate) fn into_mut(self) -> ExecutionResult { + MutableValue::new_from_variable(self) + } + + pub(crate) fn into_shared(self) -> ExecutionResult { + SharedValue::new_from_variable(self) + } + + pub(crate) fn into_late_bound(self) -> ExecutionResult { + match self.clone().into_mut() { + Ok(value) => Ok(Place::Mutable { mutable: value }), + Err(ExecutionInterrupt::Error(reason_not_mutable)) => { + // If we get an error with a mutable and shared reference, a mutable reference must already exist. + // We can just propogate the error from taking the shared reference, it should be good enough. + let value = self.into_shared()?; + Ok(Place::Shared { + shared: value, + reason_not_mutable: Some(reason_not_mutable), + }) + } + // Propogate any other errors, these shouldn't happen mind + Err(err) => Err(err), + } + } +} + +/// A rough equivalent of a Rust place (lvalue), as per: +/// https://doc.rust-lang.org/reference/expressions.html#place-expressions-and-value-expressions +/// +/// In preinterpret, references are (currently) only to variables, or sub-values of variables. +/// +/// # Late Binding +/// +/// Sometimes, a value which can be accessed, but we don't yet know *how* we need to access it. +/// In this case, we attempt to load it as a Mutable place, and failing that, as a Shared place. +/// +/// ## Example of requirement +/// For example, if we have `x[a].y(z)`, we first need to resolve the type of `x[a]` to know +/// whether `x[a]` takes 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 Place { + Mutable { + mutable: MutableValue, + }, + Shared { + shared: SharedValue, + reason_not_mutable: Option, + }, +} + +impl HasSpanRange for VariableBinding { + fn span_range(&self) -> SpanRange { + self.variable_span_range + } +} + +pub(crate) type OwnedValue = Owned; + +/// A binding of an owned value along with a span of the whole access. +/// +/// For example, for `x.y[4]`, this would capture both: +/// * The owned value +/// * The lexical span of the tokens `x.y[4]` +pub(crate) struct Owned { + inner: T, + /// The span-range of the current binding to the value. + span_range: SpanRange, +} + +impl Owned { + pub(crate) fn new(inner: T, span_range: SpanRange) -> Self { + Self { inner, span_range } + } + + pub(crate) fn into_inner(self) -> T { + self.inner + } + + pub(crate) fn as_ref(&self) -> &T { + &self.inner + } + + pub(crate) fn as_mut(&mut self) -> &mut T { + &mut self.inner + } + + #[allow(unused)] + pub(crate) fn map(self, value_map: impl FnOnce(T, &SpanRange) -> V) -> Owned { + Owned { + inner: value_map(self.inner, &self.span_range), + span_range: self.span_range, + } + } + + pub(crate) fn try_map( + self, + value_map: impl FnOnce(T, &SpanRange) -> ExecutionResult, + ) -> ExecutionResult> { + Ok(Owned { + inner: value_map(self.inner, &self.span_range)?, + span_range: self.span_range, + }) + } + + pub(crate) fn update_span_range( + self, + span_range_map: impl FnOnce(SpanRange) -> SpanRange, + ) -> Self { + Self { + inner: self.inner, + span_range: span_range_map(self.span_range), + } + } +} + +impl OwnedValue { + pub(crate) fn resolve_indexed( + self, + access: IndexAccess, + index: &ExpressionValue, + ) -> ExecutionResult { + self.update_span_range(|span_range| SpanRange::new_between(span_range, access.span_range())) + .try_map(|value, _| value.into_indexed(access, index)) + } + + pub(crate) fn resolve_property(self, access: &PropertyAccess) -> ExecutionResult { + self.update_span_range(|span_range| SpanRange::new_between(span_range, access.span_range())) + .try_map(|value, _| value.into_property(access)) + } +} + +impl HasSpanRange for Owned { + fn span_range(&self) -> SpanRange { + self.span_range + } +} + +impl From for ExpressionValue { + fn from(value: OwnedValue) -> Self { + value.inner + } +} + +/// NOTE: This should be deleted when we remove the span range from ExpressionValue +impl From for OwnedValue { + fn from(value: ExpressionValue) -> Self { + let span_range = value.span_range(); + Self { + inner: value, + span_range, + } + } +} + +impl WithSpanExt for Owned { + fn with_span(self, span: Span) -> Self { + Self { + inner: self.inner, + span_range: SpanRange::new_single(span), + } + } +} + +pub(crate) type MutableValue = Mutable; + +/// A binding of a unique (mutable) reference to a value +/// (e.g. inside a variable) along with a span of the whole access. +/// +/// For example, for `x.y[4]`, this captures both: +/// * The mutable reference to the location under `x` +/// * The lexical span of the tokens `x.y[4]` +pub(crate) struct Mutable { + mut_cell: MutSubRcRefCell, + span_range: SpanRange, +} + +impl Mutable { + pub(crate) fn into_shared(self) -> Shared { + Shared { + shared_cell: self.mut_cell.into_shared(), + span_range: self.span_range, + } + } + + #[allow(unused)] + pub(crate) fn map( + self, + value_map: impl for<'a> FnOnce(&'a mut T) -> &'a mut V, + ) -> Mutable { + Mutable { + mut_cell: self.mut_cell.map(value_map), + span_range: self.span_range, + } + } + + pub(crate) fn try_map( + self, + value_map: impl for<'a, 'b> FnOnce(&'a mut T, &'b SpanRange) -> ExecutionResult<&'a mut V>, + ) -> ExecutionResult> { + Ok(Mutable { + mut_cell: self + .mut_cell + .try_map(|value| value_map(value, &self.span_range))?, + span_range: self.span_range, + }) + } + + pub(crate) fn update_span_range( + self, + span_range_map: impl FnOnce(SpanRange) -> SpanRange, + ) -> Self { + Self { + mut_cell: self.mut_cell, + span_range: span_range_map(self.span_range), + } + } +} + +impl Mutable { + pub(crate) fn new_from_owned(value: OwnedValue) -> Self { + let span_range = value.span_range; + Self { + // Unwrap is safe because it's a new refcell + mut_cell: MutSubRcRefCell::new(Rc::new(RefCell::new(value.inner))).unwrap(), + span_range, + } + } + + pub(crate) fn transparent_clone(&self) -> ExecutionResult { + let value = self.as_ref().try_transparent_clone(self.span_range)?; + Ok(OwnedValue::new(value, self.span_range)) + } + + fn new_from_variable(reference: VariableBinding) -> ExecutionResult { + Ok(Self { + mut_cell: MutSubRcRefCell::new(reference.data).map_err(|_| { + reference.variable_span_range.execution_error( + "The variable cannot be modified as it is already being modified", + ) + })?, + span_range: reference.variable_span_range, + }) + } + + pub(crate) fn into_stream(self) -> ExecutionResult> { + self.try_map(|value, span_range| match value { + ExpressionValue::Stream(stream) => Ok(&mut stream.value), + _ => span_range.execution_err("The variable is not a stream"), + }) + } + + pub(crate) fn resolve_indexed( + self, + access: IndexAccess, + index: &ExpressionValue, + auto_create: bool, + ) -> ExecutionResult { + self.update_span_range(|span_range| SpanRange::new_between(span_range, access.span_range())) + .try_map(|value, _| value.index_mut(access, index, auto_create)) + } + + pub(crate) fn resolve_property( + self, + access: &PropertyAccess, + auto_create: bool, + ) -> ExecutionResult { + self.update_span_range(|span_range| SpanRange::new_between(span_range, access.span_range())) + .try_map(|value, _| value.property_mut(access, auto_create)) + } + + pub(crate) fn set(&mut self, content: impl ToExpressionValue) { + *self.mut_cell = content.to_value(self.span_range); + } +} + +impl AsMut for Mutable { + fn as_mut(&mut self) -> &mut T { + &mut self.mut_cell + } +} + +impl AsRef for Mutable { + fn as_ref(&self) -> &T { + &self.mut_cell + } +} + +impl HasSpanRange for Mutable { + fn span_range(&self) -> SpanRange { + self.span_range + } +} + +impl WithSpanExt for Mutable { + fn with_span(self, span: Span) -> Self { + Self { + mut_cell: self.mut_cell, + span_range: SpanRange::new_single(span), + } + } +} + +impl Deref for Mutable { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.mut_cell + } +} + +impl DerefMut for Mutable { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.mut_cell + } +} + +pub(crate) type SharedValue = Shared; + +/// A binding of a shared (immutable) reference to a value +/// (e.g. inside a variable) along with a span of the whole access. +/// +/// For example, for `x.y[4]`, this captures both: +/// * The mutable reference to the location under `x` +/// * The lexical span of the tokens `x.y[4]` +pub(crate) struct Shared { + shared_cell: SharedSubRcRefCell, + span_range: SpanRange, +} + +impl Shared { + pub(crate) fn try_map( + self, + value_map: impl for<'a, 'b> FnOnce(&'a T, &'b SpanRange) -> ExecutionResult<&'a V>, + ) -> ExecutionResult> { + Ok(Shared { + shared_cell: self + .shared_cell + .try_map(|value| value_map(value, &self.span_range))?, + span_range: self.span_range, + }) + } + + #[allow(unused)] + pub(crate) fn map(self, value_map: impl FnOnce(&T) -> &V) -> ExecutionResult> { + Ok(Shared { + shared_cell: self.shared_cell.map(value_map), + span_range: self.span_range, + }) + } + + pub(crate) fn update_span_range( + self, + span_range_map: impl FnOnce(SpanRange) -> SpanRange, + ) -> Self { + Self { + shared_cell: self.shared_cell, + span_range: span_range_map(self.span_range), + } + } +} + +impl Shared { + pub(crate) fn new_from_owned(value: OwnedValue) -> Self { + let span_range = value.span_range; + Self { + // Unwrap is safe because it's a new refcell + shared_cell: SharedSubRcRefCell::new(Rc::new(RefCell::new(value.inner))).unwrap(), + span_range, + } + } + + pub(crate) fn transparent_clone(&self) -> ExecutionResult { + let value = self.as_ref().try_transparent_clone(self.span_range)?; + Ok(OwnedValue::new(value, self.span_range)) + } + + pub(crate) fn expensive_clone(&self) -> OwnedValue { + let value = self.as_ref().clone().with_span_range(self.span_range); + OwnedValue::new(value, self.span_range) + } + + fn new_from_variable(reference: VariableBinding) -> ExecutionResult { + Ok(Self { + shared_cell: SharedSubRcRefCell::new(reference.data).map_err(|_| { + reference + .variable_span_range + .execution_error("The variable cannot be read as it is already being modified") + })?, + span_range: reference.variable_span_range, + }) + } + + pub(crate) fn resolve_indexed( + self, + access: IndexAccess, + index: &ExpressionValue, + ) -> ExecutionResult { + self.update_span_range(|old_span| SpanRange::new_between(old_span, access)) + .try_map(|value, _| value.index_ref(access, index)) + } + + pub(crate) fn resolve_property(self, access: &PropertyAccess) -> ExecutionResult { + self.update_span_range(|old_span| SpanRange::new_between(old_span, access.span_range())) + .try_map(|value, _| value.property_ref(access)) + } +} + +impl AsRef for Shared { + fn as_ref(&self) -> &T { + &self.shared_cell + } +} + +impl Deref for Shared { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.shared_cell + } +} + +impl HasSpanRange for Shared { + fn span_range(&self) -> SpanRange { + self.span_range + } +} + +impl WithSpanExt for Shared { + fn with_span(self, span: Span) -> Self { + Self { + shared_cell: self.shared_cell, + span_range: SpanRange::new_single(span), + } + } +} diff --git a/src/interpretation/command.rs b/src/interpretation/command.rs new file mode 100644 index 00000000..685b8257 --- /dev/null +++ b/src/interpretation/command.rs @@ -0,0 +1,465 @@ +use super::commands::*; +use crate::internal_prelude::*; + +#[allow(unused)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum CommandOutputKind { + None, + /// If output to a parent stream, it is flattened + Value, + Ident, + /// If output to a parent stream, it is flattened + Literal, + /// If output to a parent stream, it is flattened + Stream, +} + +pub(crate) trait CommandType { + type OutputKind: OutputKind; +} + +pub(crate) trait OutputKind { + type Output; + fn resolve_enum_kind() -> CommandOutputKind; +} + +struct ExecutionContext<'a> { + interpreter: &'a mut Interpreter, + delim_span: DelimSpan, +} + +trait CommandInvocation { + fn execute_into( + self, + context: ExecutionContext, + output: &mut OutputStream, + ) -> ExecutionResult<()>; + + fn execute_to_value(self, context: ExecutionContext) -> ExecutionResult; +} + +// Using the trick for permitting multiple non-overlapping blanket +// implementations, conditioned on an associated type +trait CommandInvocationAs { + fn execute_into( + self, + context: ExecutionContext, + output: &mut OutputStream, + ) -> ExecutionResult<()>; + + fn execute_to_value(self, context: ExecutionContext) -> ExecutionResult; +} + +impl> CommandInvocation for C { + fn execute_into( + self, + context: ExecutionContext, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + >::execute_into(self, context, output) + } + + fn execute_to_value(self, context: ExecutionContext) -> ExecutionResult { + >::execute_to_value(self, context) + } +} + +//=============== +// OutputKindNone +//=============== + +pub(crate) struct OutputKindNone; +impl OutputKind for OutputKindNone { + type Output = (); + + fn resolve_enum_kind() -> CommandOutputKind { + CommandOutputKind::None + } +} + +pub(crate) trait NoOutputCommandDefinition: + Sized + CommandType +{ + const COMMAND_NAME: &'static str; + fn parse(arguments: CommandArguments) -> ParseResult; + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult<()>; +} + +impl CommandInvocationAs for C { + fn execute_into(self, context: ExecutionContext, _: &mut OutputStream) -> ExecutionResult<()> { + self.execute(context.interpreter)?; + Ok(()) + } + + fn execute_to_value(self, context: ExecutionContext) -> ExecutionResult { + self.execute(context.interpreter)?; + Ok(ExpressionValue::None(context.delim_span.span_range())) + } +} + +//================ +// OutputKindValue +//================ + +pub(crate) struct OutputKindValue; +impl OutputKind for OutputKindValue { + type Output = TokenTree; + + fn resolve_enum_kind() -> CommandOutputKind { + CommandOutputKind::Value + } +} + +pub(crate) trait ValueCommandDefinition: + Sized + CommandType +{ + const COMMAND_NAME: &'static str; + fn parse(arguments: CommandArguments) -> ParseResult; + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult; +} + +impl CommandInvocationAs for C { + fn execute_into( + self, + context: ExecutionContext, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + self.execute(context.interpreter)?.output_to( + Grouping::Grouped, + output, + StreamOutputBehaviour::Standard, + ) + } + + fn execute_to_value(self, context: ExecutionContext) -> ExecutionResult { + self.execute(context.interpreter) + } +} + +//================ +// OutputKindIdent +//================ + +pub(crate) struct OutputKindIdent; +impl OutputKind for OutputKindIdent { + type Output = Ident; + + fn resolve_enum_kind() -> CommandOutputKind { + CommandOutputKind::Ident + } +} + +pub(crate) trait IdentCommandDefinition: + Sized + CommandType +{ + const COMMAND_NAME: &'static str; + fn parse(arguments: CommandArguments) -> ParseResult; + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult; +} + +impl CommandInvocationAs for C { + fn execute_into( + self, + context: ExecutionContext, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + output.push_ident(self.execute(context.interpreter)?); + Ok(()) + } + + fn execute_to_value(self, context: ExecutionContext) -> ExecutionResult { + let span_range = context.delim_span.span_range(); + let mut output = OutputStream::new(); + output.push_ident(self.execute(context.interpreter)?); + Ok(output.to_value(span_range)) + } +} + +//================== +// OutputKindLiteral +//================== + +pub(crate) struct OutputKindLiteral; +impl OutputKind for OutputKindLiteral { + type Output = Literal; + + fn resolve_enum_kind() -> CommandOutputKind { + CommandOutputKind::Literal + } +} + +pub(crate) trait LiteralCommandDefinition: + Sized + CommandType +{ + const COMMAND_NAME: &'static str; + fn parse(arguments: CommandArguments) -> ParseResult; + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult; +} + +impl CommandInvocationAs for C { + fn execute_into( + self, + context: ExecutionContext, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + output.push_literal(self.execute(context.interpreter)?); + Ok(()) + } + + fn execute_to_value(self, context: ExecutionContext) -> ExecutionResult { + let literal = self.execute(context.interpreter)?; + Ok(ExpressionValue::for_literal(literal)) + } +} + +//================= +// OutputKindStream +//================= + +pub(crate) struct OutputKindStream; +impl OutputKind for OutputKindStream { + type Output = (); + + fn resolve_enum_kind() -> CommandOutputKind { + CommandOutputKind::Stream + } +} + +// Control Flow or a command which is unlikely to want grouped output +pub(crate) trait StreamCommandDefinition: + Sized + CommandType +{ + const COMMAND_NAME: &'static str; + fn parse(arguments: CommandArguments) -> ParseResult; + fn execute( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()>; +} + +impl CommandInvocationAs for C { + fn execute_into( + self, + context: ExecutionContext, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + self.execute(context.interpreter, output) + } + + fn execute_to_value(self, context: ExecutionContext) -> ExecutionResult { + let span_range = context.delim_span.span_range(); + let mut output = OutputStream::new(); + >::execute_into(self, context, &mut output)?; + Ok(output.to_value(span_range)) + } +} + +//========================= + +macro_rules! define_command_enums { + ( + $( + $command:ident, + )* + ) => { + #[allow(clippy::enum_variant_names)] + #[derive(Clone, Copy)] + pub(crate) enum CommandKind { + $( + $command, + )* + } + + impl CommandKind { + fn parse_command(&self, arguments: CommandArguments) -> ParseResult { + Ok(match self { + $( + Self::$command => TypedCommand::$command( + $command::parse(arguments)? + ), + )* + }) + } + + pub(crate) fn resolve_output_kind(&self) -> CommandOutputKind { + match self { + $( + Self::$command => <$command as CommandType>::OutputKind::resolve_enum_kind(), + )* + } + } + + pub(crate) fn for_ident(ident: &Ident) -> Option { + Some(match ident.to_string().as_ref() { + $( + $command::COMMAND_NAME => Self::$command, + )* + _ => return None, + }) + } + + const ALL_KIND_NAMES: &'static [&'static str] = &[$($command::COMMAND_NAME,)*]; + + pub(crate) fn list_all() -> String { + // TODO: Separate by group, and add "and" at the end + Self::ALL_KIND_NAMES.join(", ") + } + } + + #[allow(clippy::enum_variant_names)] + #[derive(Clone)] + enum TypedCommand { + $( + $command($command), + )* + } + + impl TypedCommand { + fn execute_into( + self, + context: ExecutionContext, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + match self { + $( + Self::$command(command) => <$command as CommandInvocation>::execute_into(command, context, output), + )* + } + } + + fn execute_to_value(self, context: ExecutionContext) -> ExecutionResult { + match self { + $( + Self::$command(command) => <$command as CommandInvocation>::execute_to_value(command, context), + )* + } + } + } + }; +} + +define_command_enums! { + // Core Commands + SetCommand, + RawCommand, + StreamCommand, + IgnoreCommand, + ReinterpretCommand, + SettingsCommand, + ErrorCommand, + + // 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, + + // Control flow commands + IfCommand, + WhileCommand, + ForCommand, + LoopCommand, + ContinueCommand, + BreakCommand, + + // Token Commands + IsEmptyCommand, + LengthCommand, + GroupCommand, + IntersperseCommand, + SplitCommand, + CommaSplitCommand, + ZipCommand, + ZipTruncatedCommand, + + // Destructuring Commands + ParseCommand, + LetCommand, +} + +#[derive(Clone)] +pub(crate) struct Command { + typed: Box, + brackets: Brackets, +} + +impl Parse for Command { + fn parse(input: ParseStream) -> ParseResult { + let (brackets, content) = input.parse_brackets()?; + content.parse::()?; + let command_name = content.parse_any_ident()?; + let command_kind = match CommandKind::for_ident(&command_name) { + Some(command_kind) => command_kind, + None => command_name.span().err( + 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_name, + ), + )?, + }; + content.parse::()?; + let typed = command_kind.parse_command(CommandArguments::new( + &content, + command_name, + brackets.join(), + ))?; + Ok(Self { + typed: Box::new(typed), + brackets, + }) + } +} + +impl HasSpan for Command { + fn span(&self) -> Span { + self.brackets.join() + } +} + +impl Interpret for Command { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let context = ExecutionContext { + interpreter, + delim_span: self.brackets.delim_span, + }; + self.typed.execute_into(context, output) + } +} + +impl InterpretToValue for Command { + type OutputValue = ExpressionValue; + + fn interpret_to_value( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + let context = ExecutionContext { + interpreter, + delim_span: self.brackets.delim_span, + }; + self.typed.execute_to_value(context) + } +} diff --git a/src/interpretation/command_arguments.rs b/src/interpretation/command_arguments.rs new file mode 100644 index 00000000..90b31e03 --- /dev/null +++ b/src/interpretation/command_arguments.rs @@ -0,0 +1,78 @@ +use crate::internal_prelude::*; + +#[derive(Clone)] +pub(crate) struct CommandArguments<'a> { + parse_stream: ParseStream<'a, Source>, + command_name: Ident, + /// The span of the [ ... ] which contained the command + command_span: Span, +} + +impl<'a> CommandArguments<'a> { + pub(crate) fn new( + parse_stream: ParseStream<'a, Source>, + command_name: Ident, + command_span: Span, + ) -> Self { + Self { + parse_stream, + command_name, + command_span, + } + } + + pub(crate) fn command_span(&self) -> Span { + self.command_span + } + + /// We use this instead of the "unexpected / drop glue" pattern in order to give a better error message + pub(crate) fn assert_empty(&self, error_message: impl std::fmt::Display) -> ParseResult<()> { + if self.parse_stream.is_empty() { + Ok(()) + } else { + self.parse_stream + .parse_err(format!("Unexpected extra tokens. {}", error_message)) + } + } + + pub(crate) fn fully_parse_as(&self) -> ParseResult { + self.fully_parse_or_error(T::parse, T::error_message()) + } + + pub(crate) fn fully_parse_or_error( + &self, + parse_function: impl FnOnce(ParseStream) -> ParseResult, + error_message: impl std::fmt::Display, + ) -> ParseResult { + // In future, when the diagnostic API is stable, + // we can add this context directly onto the command ident... + // Rather than just selectively adding it to the inner-most error. + // + // For now though, we can add additional context to the error message. + // But we can avoid adding this additional context if it's already been added in an + // inner error, because that's likely the correct local context to show. + let parsed = + parse_function(self.parse_stream).add_context_if_error_and_no_context(|| { + format!( + "Occurred whilst parsing [!{}! ...] - {}", + self.command_name, error_message, + ) + })?; + + self.assert_empty(error_message)?; + + Ok(parsed) + } + + pub(crate) fn parse_all_as_source(&self) -> ParseResult { + self.parse_stream.parse_with_context(self.command_span) + } + + pub(crate) fn read_all_as_raw_token_stream(&self) -> TokenStream { + self.parse_stream.parse::().unwrap() + } +} + +pub(crate) trait ArgumentsContent: Parse { + fn error_message() -> String; +} diff --git a/src/interpretation/commands/concat_commands.rs b/src/interpretation/commands/concat_commands.rs new file mode 100644 index 00000000..f31a22c1 --- /dev/null +++ b/src/interpretation/commands/concat_commands.rs @@ -0,0 +1,171 @@ +use crate::internal_prelude::*; + +//======== +// Helpers +//======== + +fn concat_into_string( + input: SourceStream, + interpreter: &mut Interpreter, + conversion_fn: impl Fn(&str) -> String, +) -> ExecutionResult { + let output_span = input.span(); + let concatenated = input + .interpret_to_new_stream(interpreter)? + .concat_recursive(&ConcatBehaviour::standard()); + Ok(conversion_fn(&concatenated).to_value(output_span.span_range())) +} + +fn concat_into_ident( + input: SourceStream, + interpreter: &mut Interpreter, + conversion_fn: impl Fn(&str) -> String, +) -> ExecutionResult { + let output_span = input.span(); + let concatenated = input + .interpret_to_new_stream(interpreter)? + .concat_recursive(&ConcatBehaviour::standard()); + let value = conversion_fn(&concatenated); + let ident = parse_str::(&value) + .map_err(|err| output_span.error(format!("`{}` is not a valid ident: {:?}", value, err,)))? + .with_span(output_span); + Ok(ident) +} + +fn concat_into_literal( + input: SourceStream, + interpreter: &mut Interpreter, + conversion_fn: impl Fn(&str) -> String, +) -> ExecutionResult { + let output_span = input.span(); + let concatenated = input + .interpret_to_new_stream(interpreter)? + .concat_recursive(&ConcatBehaviour::standard()); + let value = conversion_fn(&concatenated); + let literal = Literal::from_str(&value) + .map_err(|err| { + output_span.error(format!("`{}` is not a valid literal: {:?}", value, err,)) + })? + .with_span(output_span); + Ok(literal) +} + +macro_rules! define_string_concat_command { + ( + $command_name:literal => $command:ident: $output_fn:ident($conversion_fn:expr) + ) => { + #[derive(Clone)] + pub(crate) struct $command { + arguments: SourceStream, + } + + impl CommandType for $command { + type OutputKind = OutputKindValue; + } + + impl ValueCommandDefinition for $command { + const COMMAND_NAME: &'static str = $command_name; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + arguments: arguments.parse_all_as_source()?, + }) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + $output_fn(self.arguments, interpreter, $conversion_fn) + } + } + }; +} + +macro_rules! define_ident_concat_command { + ( + $command_name:literal => $command:ident: $output_fn:ident($conversion_fn:expr) + ) => { + #[derive(Clone)] + pub(crate) struct $command { + arguments: SourceStream, + } + + impl CommandType for $command { + type OutputKind = OutputKindIdent; + } + + impl IdentCommandDefinition for $command { + const COMMAND_NAME: &'static str = $command_name; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + arguments: arguments.parse_all_as_source()?, + }) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + $output_fn(self.arguments, interpreter, $conversion_fn) + } + } + }; +} + +macro_rules! define_literal_concat_command { + ( + $command_name:literal => $command:ident: $output_fn:ident($conversion_fn:expr) + ) => { + #[derive(Clone)] + pub(crate) struct $command { + arguments: SourceStream, + } + + impl CommandType for $command { + type OutputKind = OutputKindLiteral; + } + + impl LiteralCommandDefinition for $command { + const COMMAND_NAME: &'static str = $command_name; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + arguments: arguments.parse_all_as_source()?, + }) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + $output_fn(self.arguments, interpreter, $conversion_fn) + } + } + }; +} + +//======================================= +// Concatenating type-conversion commands +//======================================= + +define_string_concat_command!("string" => StringCommand: concat_into_string(|s| s.to_string())); +define_ident_concat_command!("ident" => IdentCommand: concat_into_ident(|s| s.to_string())); +define_ident_concat_command!("ident_camel" => IdentCamelCommand: concat_into_ident(to_upper_camel_case)); +define_ident_concat_command!("ident_snake" => IdentSnakeCommand: concat_into_ident(to_lower_snake_case)); +define_ident_concat_command!("ident_upper_snake" => IdentUpperSnakeCommand: concat_into_ident(to_upper_snake_case)); +define_literal_concat_command!("literal" => LiteralCommand: concat_into_literal(|s| s.to_string())); + +//=========================== +// String conversion commands +//=========================== + +define_string_concat_command!("upper" => UpperCommand: concat_into_string(to_uppercase)); +define_string_concat_command!("lower" => LowerCommand: concat_into_string(to_lowercase)); +// Snake case is typically lower snake case in Rust, so default to that +define_string_concat_command!("snake" => SnakeCommand: concat_into_string(to_lower_snake_case)); +define_string_concat_command!("lower_snake" => LowerSnakeCommand: concat_into_string(to_lower_snake_case)); +define_string_concat_command!("upper_snake" => UpperSnakeCommand: concat_into_string(to_upper_snake_case)); +// 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 +define_string_concat_command!("kebab" => KebabCommand: concat_into_string(to_lower_kebab_case)); +// Upper camel case is the more common casing in Rust, so default to that +define_string_concat_command!("camel" => CamelCommand: concat_into_string(to_upper_camel_case)); +define_string_concat_command!("lower_camel" => LowerCamelCommand: concat_into_string(to_lower_camel_case)); +define_string_concat_command!("upper_camel" => UpperCamelCommand: concat_into_string(to_upper_camel_case)); +define_string_concat_command!("capitalize" => CapitalizeCommand: concat_into_string(capitalize)); +define_string_concat_command!("decapitalize" => DecapitalizeCommand: concat_into_string(decapitalize)); +define_string_concat_command!("title" => TitleCommand: concat_into_string(title_case)); +define_string_concat_command!("insert_spaces" => InsertSpacesCommand: concat_into_string(insert_spaces_between_words)); diff --git a/src/interpretation/commands/control_flow_commands.rs b/src/interpretation/commands/control_flow_commands.rs new file mode 100644 index 00000000..44993341 --- /dev/null +++ b/src/interpretation/commands/control_flow_commands.rs @@ -0,0 +1,299 @@ +use crate::internal_prelude::*; + +#[derive(Clone)] +pub(crate) struct IfCommand { + condition: SourceExpression, + true_code: SourceCodeBlock, + else_ifs: Vec<(SourceExpression, SourceCodeBlock)>, + else_code: Option, +} + +impl CommandType for IfCommand { + type OutputKind = OutputKindStream; +} + +impl StreamCommandDefinition for IfCommand { + const COMMAND_NAME: &'static str = "if"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + let condition = input.parse()?; + let true_code = input.parse()?; + let mut else_ifs = Vec::new(); + let mut else_code = None; + while !input.is_empty() { + input.parse::()?; + if input.peek_ident_matching("elif") { + input.parse_ident_matching("elif")?; + input.parse::()?; + else_ifs.push((input.parse()?, input.parse()?)); + } else { + input.parse_ident_matching("else")?; + input.parse::()?; + else_code = Some(input.parse()?); + break; + } + } + Ok(Self { + condition, + true_code, + else_ifs, + else_code, + }) + }, + "Expected [!if! ... { ... } !else if! ... { ... } !else! ... { ... }]", + ) + } + + fn execute( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let evaluated_condition = self + .condition + .interpret_to_value(interpreter)? + .expect_bool("An if condition")?; + + if evaluated_condition.value { + return self.true_code.interpret_into(interpreter, output); + } + + for (condition, code) in self.else_ifs { + let evaluated_condition = condition + .interpret_to_value(interpreter)? + .expect_bool("An else if condition")?; + + if evaluated_condition.value { + return code.interpret_into(interpreter, output); + } + } + + if let Some(false_code) = self.else_code { + return false_code.interpret_into(interpreter, output); + } + + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct WhileCommand { + condition: SourceExpression, + loop_code: SourceCodeBlock, +} + +impl CommandType for WhileCommand { + type OutputKind = OutputKindStream; +} + +impl StreamCommandDefinition for WhileCommand { + const COMMAND_NAME: &'static str = "while"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + Ok(Self { + condition: input.parse()?, + loop_code: input.parse()?, + }) + }, + "Expected [!while! (condition) { code }]", + ) + } + + fn execute( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let mut iteration_counter = interpreter.start_iteration_counter(&self.loop_code); + loop { + iteration_counter.increment_and_check()?; + + let evaluated_condition = self + .condition + .interpret_to_value(interpreter)? + .expect_bool("A while condition")?; + + if !evaluated_condition.value { + break; + } + + match self + .loop_code + .clone() + .interpret_loop_content_into(interpreter, output)? + { + None => {} + Some(ControlFlowInterrupt::Continue) => continue, + Some(ControlFlowInterrupt::Break) => break, + } + } + + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct LoopCommand { + loop_code: SourceCodeBlock, +} + +impl CommandType for LoopCommand { + type OutputKind = OutputKindStream; +} + +impl StreamCommandDefinition for LoopCommand { + const COMMAND_NAME: &'static str = "loop"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + Ok(Self { + loop_code: input.parse()?, + }) + }, + "Expected [!loop! { ... }]", + ) + } + + fn execute( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let mut iteration_counter = interpreter.start_iteration_counter(&self.loop_code); + + loop { + iteration_counter.increment_and_check()?; + match self + .loop_code + .clone() + .interpret_loop_content_into(interpreter, output)? + { + None => {} + Some(ControlFlowInterrupt::Continue) => continue, + Some(ControlFlowInterrupt::Break) => break, + } + } + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct ForCommand { + destructuring: Pattern, + #[allow(unused)] + in_token: Token![in], + input: SourceExpression, + loop_code: SourceCodeBlock, +} + +impl CommandType for ForCommand { + type OutputKind = OutputKindStream; +} + +impl StreamCommandDefinition for ForCommand { + const COMMAND_NAME: &'static str = "for"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + Ok(Self { + destructuring: input.parse()?, + in_token: input.parse()?, + input: input.parse()?, + loop_code: input.parse()?, + }) + }, + "Expected [!for! in { }]", + ) + } + + fn execute( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let array = self + .input + .interpret_to_value(interpreter)? + .expect_any_iterator("The for loop input")?; + + let mut iteration_counter = interpreter.start_iteration_counter(&self.in_token); + + for item in array { + iteration_counter.increment_and_check()?; + + self.destructuring.handle_destructure(interpreter, item)?; + + match self + .loop_code + .clone() + .interpret_loop_content_into(interpreter, output)? + { + None => {} + Some(ControlFlowInterrupt::Continue) => continue, + Some(ControlFlowInterrupt::Break) => break, + } + } + + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct ContinueCommand { + span: Span, +} + +impl CommandType for ContinueCommand { + type OutputKind = OutputKindNone; +} + +impl NoOutputCommandDefinition for ContinueCommand { + const COMMAND_NAME: &'static str = "continue"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.assert_empty("The !continue! command takes no arguments")?; + Ok(Self { + span: arguments.command_span(), + }) + } + + fn execute(self, _: &mut Interpreter) -> ExecutionResult<()> { + ExecutionResult::Err(ExecutionInterrupt::ControlFlow( + ControlFlowInterrupt::Continue, + self.span, + )) + } +} + +#[derive(Clone)] +pub(crate) struct BreakCommand { + span: Span, +} + +impl CommandType for BreakCommand { + type OutputKind = OutputKindNone; +} + +impl NoOutputCommandDefinition for BreakCommand { + const COMMAND_NAME: &'static str = "break"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.assert_empty("The !break! command takes no arguments")?; + Ok(Self { + span: arguments.command_span(), + }) + } + + fn execute(self, _: &mut Interpreter) -> ExecutionResult<()> { + ExecutionResult::Err(ExecutionInterrupt::ControlFlow( + ControlFlowInterrupt::Break, + self.span, + )) + } +} diff --git a/src/interpretation/commands/core_commands.rs b/src/interpretation/commands/core_commands.rs new file mode 100644 index 00000000..6075626a --- /dev/null +++ b/src/interpretation/commands/core_commands.rs @@ -0,0 +1,376 @@ +use crate::internal_prelude::*; + +#[derive(Clone)] +pub(crate) struct SetCommand { + arguments: SetArguments, +} + +#[allow(unused)] +#[derive(Clone)] +enum SetArguments { + SetVariable { + variable: GroupedVariable, + equals: Token![=], + content: SourceStream, + }, + ExtendVariable { + variable: GroupedVariable, + plus_equals: Token![+=], + content: SourceStream, + }, + SetVariablesEmpty { + variables: Vec, + }, + Discard { + discard: Token![_], + equals: Token![=], + content: SourceStream, + }, +} + +impl CommandType for SetCommand { + type OutputKind = OutputKindNone; +} + +impl NoOutputCommandDefinition for SetCommand { + const COMMAND_NAME: &'static str = "set"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + if input.peek(Token![_]) { + return Ok(SetCommand { + arguments: SetArguments::Discard { + discard: input.parse()?, + equals: input.parse()?, + content: input.parse_with_context(arguments.command_span())?, + }, + }); + } + let variable = input.parse()?; + if input.peek(Token![+=]) { + return Ok(SetCommand { + arguments: SetArguments::ExtendVariable { + variable, + plus_equals: input.parse()?, + content: input.parse_with_context(arguments.command_span())?, + }, + }) + } + if input.peek(Token![=]) { + return Ok(SetCommand { + arguments: SetArguments::SetVariable { + variable, + equals: input.parse()?, + content: input.parse_with_context(arguments.command_span())?, + }, + }) + } + let mut variables = vec![variable]; + loop { + if !input.is_empty() { + input.parse::()?; + } + if input.is_empty() { + return Ok(SetCommand { + arguments: SetArguments::SetVariablesEmpty { variables }, + }); + } + variables.push(input.parse()?); + } + }, + "Expected [!set! #var1 = ...] or [!set! #var1 += ...] or [!set! _ = ...] or [!set! #var1, #var2]", + ) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + match self.arguments { + SetArguments::SetVariable { + variable, content, .. + } => { + let content = content.interpret_to_new_stream(interpreter)?; + variable.define(interpreter, content); + } + SetArguments::ExtendVariable { + variable, content, .. + } => { + let variable_data = variable.binding(interpreter)?; + content.interpret_into( + interpreter, + variable_data.into_mut()?.into_stream()?.as_mut(), + )?; + } + SetArguments::SetVariablesEmpty { variables } => { + for variable in variables { + variable.define(interpreter, OutputStream::new()); + } + } + SetArguments::Discard { content, .. } => { + let _ = content.interpret_to_new_stream(interpreter)?; + } + } + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct RawCommand { + token_stream: TokenStream, +} + +impl CommandType for RawCommand { + type OutputKind = OutputKindStream; +} + +impl StreamCommandDefinition for RawCommand { + const COMMAND_NAME: &'static str = "raw"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + token_stream: arguments.read_all_as_raw_token_stream(), + }) + } + + fn execute( + self, + _interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + output.extend_raw_tokens(self.token_stream); + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct StreamCommand { + inner: SourceStream, +} + +impl CommandType for StreamCommand { + type OutputKind = OutputKindStream; +} + +impl StreamCommandDefinition for StreamCommand { + const COMMAND_NAME: &'static str = "stream"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + inner: arguments.parse_all_as_source()?, + }) + } + + fn execute( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + self.inner.interpret_into(interpreter, output) + } +} + +#[derive(Clone)] +pub(crate) struct IgnoreCommand; + +impl CommandType for IgnoreCommand { + type OutputKind = OutputKindNone; +} + +impl NoOutputCommandDefinition for IgnoreCommand { + const COMMAND_NAME: &'static str = "ignore"; + + fn parse(arguments: CommandArguments) -> ParseResult { + // Avoid a syn parse error by reading all the tokens + let _ = arguments.read_all_as_raw_token_stream(); + Ok(Self) + } + + fn execute(self, _interpreter: &mut Interpreter) -> ExecutionResult<()> { + Ok(()) + } +} + +/// This is temporary until we have a proper implementation of #(...) +#[derive(Clone)] +pub(crate) struct ReinterpretCommand { + content: SourceStream, +} + +impl CommandType for ReinterpretCommand { + type OutputKind = OutputKindStream; +} + +impl StreamCommandDefinition for ReinterpretCommand { + const COMMAND_NAME: &'static str = "reinterpret"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + content: arguments.parse_all_as_source()?, + }) + } + + fn execute( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let command_span = self.content.span(); + let interpreted = self.content.interpret_to_new_stream(interpreter)?; + let source = unsafe { + // RUST-ANALYZER-SAFETY - Can't do much better than this + interpreted.into_token_stream() + }; + let reparsed_source_stream = + source.source_parse_with(|input| SourceStream::parse(input, command_span))?; + reparsed_source_stream.interpret_into(interpreter, output) + } +} + +#[derive(Clone)] +pub(crate) struct SettingsCommand { + inputs: SourceSettingsInputs, +} + +impl CommandType for SettingsCommand { + type OutputKind = OutputKindNone; +} + +define_object_arguments! { + SourceSettingsInputs => SettingsInputs { + required: {}, + optional: { + iteration_limit: DEFAULT_ITERATION_LIMIT_STR ("The new iteration limit"), + } + } +} + +impl NoOutputCommandDefinition for SettingsCommand { + const COMMAND_NAME: &'static str = "settings"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + inputs: arguments.fully_parse_as()?, + }) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + let inputs = self.inputs.interpret_to_value(interpreter)?; + if let Some(limit) = inputs.iteration_limit { + let limit = limit + .expect_integer("The iteration limit")? + .expect_usize()?; + interpreter.set_iteration_limit(Some(limit)); + } + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct ErrorCommand { + inputs: EitherErrorInput, +} + +impl CommandType for ErrorCommand { + type OutputKind = OutputKindNone; +} + +#[derive(Clone)] +enum EitherErrorInput { + Fields(SourceErrorInputs), + JustMessage(SourceStream), +} + +define_object_arguments! { + SourceErrorInputs => ErrorInputs { + required: { + message: r#""...""# ("The error message to display"), + }, + optional: { + spans: "[!stream! $abc]" ("An optional [token stream], to determine where to show the error message"), + } + } +} + +impl NoOutputCommandDefinition for ErrorCommand { + const COMMAND_NAME: &'static str = "error"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + if input.peek(syn::token::Brace) { + Ok(Self { + inputs: EitherErrorInput::Fields(input.parse()?), + }) + } else { + Ok(Self { + inputs: EitherErrorInput::JustMessage( + input.parse_with_context(arguments.command_span())?, + ), + }) + } + }, + format!( + "Expected [!error! \"Expected X, found: \" #world] or [!error! {}]", + SourceErrorInputs::describe_object() + ), + ) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + let fields = match self.inputs { + EitherErrorInput::Fields(error_inputs) => { + error_inputs.interpret_to_value(interpreter)? + } + EitherErrorInput::JustMessage(stream) => { + let error_message = stream + .interpret_to_new_stream(interpreter)? + .concat_recursive(&ConcatBehaviour::standard()); + return Span::call_site().execution_err(error_message); + } + }; + + let error_message = fields.message.expect_string("Error message")?.value; + + let error_span = match fields.spans { + Some(spans) => { + let error_span_stream = spans.expect_stream("The error spans")?.value; + + // Consider the case where preinterpret embeds in a declarative macro, and we have + // an error like this: + // [!error! [!string! "Expected 100, got " $input] [$input]] + // + // 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 = + error_span_stream.into_token_stream_removing_any_transparent_groups(); + if error_span_stream.is_empty() { + Span::call_site().span_range() + } else { + error_span_stream.span_range_from_iterating_over_all_tokens() + } + } + None => Span::call_site().span_range(), + }; + + error_span.execution_err(error_message) + } +} diff --git a/src/interpretation/commands/mod.rs b/src/interpretation/commands/mod.rs new file mode 100644 index 00000000..a88b08ee --- /dev/null +++ b/src/interpretation/commands/mod.rs @@ -0,0 +1,11 @@ +mod concat_commands; +mod control_flow_commands; +mod core_commands; +mod token_commands; +mod transforming_commands; + +pub(crate) use concat_commands::*; +pub(crate) use control_flow_commands::*; +pub(crate) use core_commands::*; +pub(crate) use token_commands::*; +pub(crate) use transforming_commands::*; diff --git a/src/interpretation/commands/token_commands.rs b/src/interpretation/commands/token_commands.rs new file mode 100644 index 00000000..d3c9da3f --- /dev/null +++ b/src/interpretation/commands/token_commands.rs @@ -0,0 +1,586 @@ +use crate::internal_prelude::*; + +#[derive(Clone)] +pub(crate) struct IsEmptyCommand { + arguments: SourceStream, +} + +impl CommandType for IsEmptyCommand { + type OutputKind = OutputKindValue; +} + +impl ValueCommandDefinition for IsEmptyCommand { + const COMMAND_NAME: &'static str = "is_empty"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + arguments: arguments.parse_all_as_source()?, + }) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + let output_span_range = self.arguments.span_range(); + let interpreted = self.arguments.interpret_to_new_stream(interpreter)?; + Ok(interpreted.is_empty().to_value(output_span_range)) + } +} + +#[derive(Clone)] +pub(crate) struct LengthCommand { + arguments: SourceStream, +} + +impl CommandType for LengthCommand { + type OutputKind = OutputKindValue; +} + +impl ValueCommandDefinition for LengthCommand { + const COMMAND_NAME: &'static str = "length"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + arguments: arguments.parse_all_as_source()?, + }) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + let output_span_range = self.arguments.span_range(); + let interpreted = self.arguments.interpret_to_new_stream(interpreter)?; + Ok(interpreted.len().to_value(output_span_range)) + } +} + +#[derive(Clone)] +pub(crate) struct GroupCommand { + arguments: SourceStream, +} + +impl CommandType for GroupCommand { + type OutputKind = OutputKindStream; +} + +impl StreamCommandDefinition for GroupCommand { + const COMMAND_NAME: &'static str = "group"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + arguments: arguments.parse_all_as_source()?, + }) + } + + fn execute( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let span = self.arguments.span(); + output.push_grouped( + |inner| self.arguments.interpret_into(interpreter, inner), + Delimiter::None, + span, + ) + } +} + +#[derive(Clone)] +pub(crate) struct IntersperseCommand { + span: Span, + inputs: SourceIntersperseInputs, +} + +impl CommandType for IntersperseCommand { + type OutputKind = OutputKindValue; +} + +define_object_arguments! { + SourceIntersperseInputs => IntersperseInputs { + required: { + items: r#"["Hello", "World"]"# ("An array or stream (by coerced token-tree) to intersperse"), + separator: "[!stream! ,]" ("The value to add between each item"), + }, + optional: { + add_trailing: "false" ("Whether to add the separator after the last item (default: false)"), + final_separator: "[!stream! or]" ("Define a different final separator (default: same as normal separator)"), + } + } +} + +impl ValueCommandDefinition for IntersperseCommand { + const COMMAND_NAME: &'static str = "intersperse"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + span: arguments.command_span(), + inputs: arguments.fully_parse_as()?, + }) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + let inputs = self.inputs.interpret_to_value(interpreter)?; + let items = inputs.items.expect_any_iterator("The items")?; + let output_span_range = self.span.span_range(); + let add_trailing = match inputs.add_trailing { + Some(add_trailing) => add_trailing.expect_bool("This parameter")?.value, + None => false, + }; + + let mut output = Vec::new(); + + let mut items = items.into_iter().peekable(); + + let mut this_item = match items.next() { + Some(next) => next, + None => return Ok(output.to_value(output_span_range)), + }; + + let mut appender = SeparatorAppender { + separator: inputs.separator, + final_separator: inputs.final_separator, + 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(output.to_value(output_span_range)) + } +} + +struct SeparatorAppender { + separator: ExpressionValue, + 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, +} + +#[derive(Clone)] +pub(crate) struct SplitCommand { + inputs: SourceSplitInputs, +} + +impl CommandType for SplitCommand { + type OutputKind = OutputKindValue; +} + +define_object_arguments! { + SourceSplitInputs => SplitInputs { + required: { + stream: "[!stream! ...] or #var" ("The stream-valued expression to split"), + separator: "[!stream! ::]" ("The token/s to split if they match"), + }, + optional: { + drop_empty_start: "false" ("If true, a leading separator does not yield in an empty item at the start (default: false)"), + drop_empty_middle: "false" ("If true, adjacent separators do not yield an empty item between them (default: false)"), + drop_empty_end: "true" ("If true, a trailing separator does not yield an empty item at the end (default: true)"), + } + } +} + +impl ValueCommandDefinition for SplitCommand { + const COMMAND_NAME: &'static str = "split"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + inputs: arguments.fully_parse_as()?, + }) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + let inputs = self.inputs.interpret_to_value(interpreter)?; + let stream = inputs.stream.expect_stream("The stream input")?; + + let separator = inputs.separator.expect_stream("The separator")?.value; + + let drop_empty_start = match inputs.drop_empty_start { + Some(value) => value.expect_bool("This parameter")?.value, + None => false, + }; + let drop_empty_middle = match inputs.drop_empty_middle { + Some(value) => value.expect_bool("This parameter")?.value, + None => false, + }; + let drop_empty_end = match inputs.drop_empty_end { + Some(value) => value.expect_bool("This parameter")?.value, + None => true, + }; + + handle_split( + interpreter, + stream, + separator.into_exact_stream()?, + drop_empty_start, + drop_empty_middle, + drop_empty_end, + ) + } +} + +#[allow(clippy::too_many_arguments)] +fn handle_split( + interpreter: &mut Interpreter, + input: ExpressionStream, + separator: ExactStream, + drop_empty_start: bool, + drop_empty_middle: bool, + drop_empty_end: bool, +) -> ExecutionResult { + let output_span_range = input.span_range; + unsafe { + // RUST-ANALYZER SAFETY: This is as safe as we can get. + // Typically the separator won't contain none-delimited groups, so we're OK + input.value.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.len() == 0 { + 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.to_value(output_span_range)); + } + return Ok(output.to_value(output_span_range)); + } + + let mut drop_empty_next = drop_empty_start; + while !input.is_empty() { + let separator_fork = input.fork(); + let mut ignored_transformer_output = OutputStream::new(); + if separator + .handle_transform( + &separator_fork, + interpreter, + &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.to_value(output_span_range)); + } + drop_empty_next = drop_empty_middle; + } + if !current_item.is_empty() || !drop_empty_end { + output.push(current_item.to_value(output_span_range)); + } + Ok(output.to_value(output_span_range)) + }) + } +} + +#[derive(Clone)] +pub(crate) struct CommaSplitCommand { + input: SourceStream, +} + +impl CommandType for CommaSplitCommand { + type OutputKind = OutputKindValue; +} + +impl ValueCommandDefinition for CommaSplitCommand { + const COMMAND_NAME: &'static str = "comma_split"; + + fn parse(arguments: CommandArguments) -> ParseResult { + Ok(Self { + input: arguments.parse_all_as_source()?, + }) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + let output_span_range = self.input.span_range(); + let stream = ExpressionStream { + span_range: output_span_range, + value: self.input.interpret_to_new_stream(interpreter)?, + }; + let separator = Punct::new(',', Spacing::Alone) + .with_span(output_span_range.join_into_span_else_start()) + .to_token_stream() + .source_parse_as()?; + + handle_split(interpreter, stream, separator, false, false, true) + } +} + +#[derive(Clone)] +pub(crate) struct ZipCommand { + inputs: SourceExpression, +} + +impl CommandType for ZipCommand { + type OutputKind = OutputKindValue; +} + +impl ValueCommandDefinition for ZipCommand { + const COMMAND_NAME: &'static str = "zip"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + Ok(Self { + inputs: input.parse()?, + }) + }, + "Expected [!zip! [, , ..]] or [!zip! {{ x: , y: , .. }}] for iterable values of the same length. If you instead want to permit different lengths and truncate to the shortest, use `!zip_truncated!` instead.", + ) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + zip_inner( + self.inputs.interpret_to_value(interpreter)?, + interpreter, + true, + ) + } +} + +#[derive(Clone)] +pub(crate) struct ZipTruncatedCommand { + inputs: SourceExpression, +} + +impl CommandType for ZipTruncatedCommand { + type OutputKind = OutputKindValue; +} + +impl ValueCommandDefinition for ZipTruncatedCommand { + const COMMAND_NAME: &'static str = "zip_truncated"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + Ok(Self { + inputs: input.parse()?, + }) + }, + "Expected [!zip_truncated! [, , ..]] or [!zip_truncated! {{ x: , y: , .. }}] for iterable values of possible different lengths (the shortest length will be used). If you want to ensure the lengths are equal, use `!zip!` instead", + ) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult { + zip_inner( + self.inputs.interpret_to_value(interpreter)?, + interpreter, + false, + ) + } +} + +fn zip_inner( + iterators: ExpressionValue, + interpreter: &mut Interpreter, + error_on_length_mismatch: bool, +) -> ExecutionResult { + let output_span_range = iterators.span_range(); + let mut iterators = ZipIterators::from_value(iterators)?; + let mut output = Vec::new(); + + if iterators.len() == 0 { + return Ok(output.to_value(output_span_range)); + } + + 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 output_span_range.execution_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, + output_span_range, + &mut output, + )?; + + Ok(output.to_value(output_span_range)) +} + +enum ZipIterators { + Array(Vec), + Object(Vec<(String, Span, ExpressionIterator)>), +} + +impl ZipIterators { + fn from_value(value: ExpressionValue) -> ExecutionResult { + Ok(match value { + ExpressionValue::Object(object) => { + let span_range = object.span_range; + let entries = object + .entries + .into_iter() + .take(101) + .map(|(k, v)| -> ExecutionResult<_> { + Ok(( + k, + v.key_span, + v.value.expect_any_iterator("A zip iterator")?, + )) + }) + .collect::, _>>()?; + if entries.len() == 101 { + return span_range.execution_err("A maximum of 100 iterators are allowed"); + } + ZipIterators::Object(entries) + } + other => { + let span_range = other.span_range(); + let iterator = other.expect_any_iterator("") + .map_err(|_| span_range.execution_error("Expected an object with iterable values, an array of iterables, or some other iterator of iterables."))?; + let vec = iterator + .take(101) + .map(|x| x.expect_any_iterator("A zip iterator")) + .collect::, _>>()?; + if vec.len() == 101 { + return span_range.execution_err("A maximum of 100 iterators are allowed"); + } + ZipIterators::Array(vec) + } + }) + } + + /// 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, + output_span_range: SpanRange, + output: &mut Vec, + ) -> ExecutionResult<()> { + let mut counter = interpreter.start_iteration_counter(&output_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.to_value(output_span_range)); + } + } + 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.to_value(output_span_range)); + } + } + } + + Ok(()) + } +} diff --git a/src/interpretation/commands/transforming_commands.rs b/src/interpretation/commands/transforming_commands.rs new file mode 100644 index 00000000..77a09ed3 --- /dev/null +++ b/src/interpretation/commands/transforming_commands.rs @@ -0,0 +1,83 @@ +use crate::internal_prelude::*; + +#[derive(Clone)] +pub(crate) struct ParseCommand { + input: SourceExpression, + #[allow(unused)] + with_token: Ident, + transformer: ExplicitTransformStream, +} + +impl CommandType for ParseCommand { + type OutputKind = OutputKindStream; +} + +impl StreamCommandDefinition for ParseCommand { + const COMMAND_NAME: &'static str = "parse"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + Ok(Self { + input: input.parse()?, + with_token: input.parse_ident_matching("with")?, + transformer: input.parse()?, + }) + }, + "Expected [!parse! with ] where:\n* The is some stream-valued expression, such as `#x` or `[!stream! ...]`\n* The is some parser such as @(...)", + ) + } + + fn execute( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let input = self + .input + .interpret_to_value(interpreter)? + .expect_stream("Parse input")? + .value; + self.transformer + .handle_transform_from_stream(input, interpreter, output) + } +} + +#[derive(Clone)] +pub(crate) struct LetCommand { + destructuring: TransformStreamUntilToken, + #[allow(unused)] + equals: Token![=], + arguments: SourceStream, +} + +impl CommandType for LetCommand { + type OutputKind = OutputKindNone; +} + +impl NoOutputCommandDefinition for LetCommand { + const COMMAND_NAME: &'static str = "let"; + + fn parse(arguments: CommandArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + Ok(Self { + destructuring: input.parse()?, + equals: input.parse()?, + arguments: input.parse_with_context(arguments.command_span())?, + }) + }, + "Expected [!let! = ...]", + ) + } + + fn execute(self, interpreter: &mut Interpreter) -> ExecutionResult<()> { + let result_tokens = self.arguments.interpret_to_new_stream(interpreter)?; + let mut ignored_transformer_output = OutputStream::new(); + self.destructuring.handle_transform_from_stream( + result_tokens, + interpreter, + &mut ignored_transformer_output, + ) + } +} diff --git a/src/interpretation/interpret_traits.rs b/src/interpretation/interpret_traits.rs new file mode 100644 index 00000000..37202f90 --- /dev/null +++ b/src/interpretation/interpret_traits.rs @@ -0,0 +1,38 @@ +use crate::internal_prelude::*; + +pub(crate) trait Interpret: Sized { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()>; + + fn interpret_to_new_stream( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + let mut output = OutputStream::new(); + self.interpret_into(interpreter, &mut output)?; + Ok(output) + } +} + +pub(crate) trait InterpretToValue: Sized { + type OutputValue; + + fn interpret_to_value( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult; +} + +impl InterpretToValue for T { + type OutputValue = Self; + + fn interpret_to_value( + self, + _interpreter: &mut Interpreter, + ) -> ExecutionResult { + Ok(self) + } +} diff --git a/src/interpretation/interpreted_stream.rs b/src/interpretation/interpreted_stream.rs new file mode 100644 index 00000000..77ac3239 --- /dev/null +++ b/src/interpretation/interpreted_stream.rs @@ -0,0 +1,578 @@ +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, +} + +#[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)] +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, + } + } +} + +impl OutputStream { + pub(crate) fn new() -> Self { + Self { + segments: vec![], + token_length: 0, + } + } + + 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_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, stream_span_range: SpanRange) -> ExpressionValue { + let parse_result = unsafe { + // RUST-ANALYZER SAFETY: This is actually safe. + self.clone().parse_as::() + }; + match parse_result { + Ok(syn_lit) => ExpressionValue::for_syn_lit(syn_lit), + Err(_) => self.to_value(stream_span_range), + } + } + + /// 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 + /// + /// Annotate usages with // RUST-ANALYZER SAFETY: ... to explain why the use of this function is OK. + pub(crate) unsafe fn parse_with>( + self, + parser: impl FnOnce(ParseStream) -> Result, + ) -> Result { + 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 + /// + /// Annotate usages with // RUST-ANALYZER SAFETY: ... to explain why the use of this function is OK. + pub(crate) unsafe 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 + /// + /// Annotate usages with // RUST-ANALYZER SAFETY: ... to explain why the use of this function is OK. + pub(crate) unsafe fn into_token_stream(self) -> TokenStream { + let mut output = TokenStream::new(); + self.append_to_token_stream(&mut output); + output + } + + unsafe 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 into_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 { + 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)), + } + } + } + OutputSegment::OutputGroup(delimiter, span, interpreted_stream) => { + 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 into_item_vec(self) -> Vec { + let mut output = Vec::with_capacity(self.token_length); + for segment in self.segments { + match segment { + OutputSegment::TokenVec(vec) => { + output.extend(vec.into_iter().map(OutputTokenTree::TokenTree)); + } + OutputSegment::OutputGroup(delimiter, span, interpreted_stream) => { + output.push(OutputTokenTree::OutputGroup( + delimiter, + span, + interpreted_stream, + )); + } + } + } + output + } + + pub(crate) fn concat_recursive(self, behaviour: &ConcatBehaviour) -> String { + let mut output = String::new(); + self.concat_recursive_into(&mut output, behaviour); + output + } + + pub(crate) fn concat_recursive_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 { + 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() { + 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) => { + output.push(punct.as_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 into_exact_stream(self) -> ParseResult { + unsafe { + // RUST-ANALYZER SAFETY: Can't be any safer than this for now + self.parse_as() + } + } +} + +impl IntoIterator for OutputStream { + type IntoIter = std::vec::IntoIter; + type Item = OutputTokenTree; + + fn into_iter(self) -> Self::IntoIter { + self.into_item_vec().into_iter() + } +} + +pub(crate) struct ConcatBehaviour { + pub(crate) add_space_between_token_trees: bool, + pub(crate) output_types_as_commands: bool, + pub(crate) output_array_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, +} + +impl ConcatBehaviour { + pub(crate) fn standard() -> Self { + Self { + add_space_between_token_trees: false, + output_types_as_commands: false, + output_array_structure: false, + unwrap_contents_of_string_like_literals: true, + show_none_values: false, + iterator_limit: 1000, + error_after_iterator_limit: true, + } + } + + pub(crate) fn debug() -> Self { + Self { + add_space_between_token_trees: true, + output_types_as_commands: true, + output_array_structure: true, + unwrap_contents_of_string_like_literals: false, + show_none_values: true, + iterator_limit: 20, + error_after_iterator_limit: false, + } + } + + 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) + } + _ => output.push_str(&literal.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.output_types_as_commands { + if is_empty { + output.push_str("[!group!"); + } else { + output.push_str("[!group! "); + } + inner(output); + output.push(']'); + } else { + inner(output); + } + } + } + } +} + +impl From for OutputStream { + fn from(value: TokenTree) -> Self { + OutputStream::raw(value.into()) + } +} + +// ====================================== +// How syn fits with preinterpret parsing +// ====================================== +// +// TLDR: This is discussed on this syn issue, where David Tolnay suggested +// forking syn to get what we want (i.e. a more versatile TokenBuffer): +// ==> https://github.com/dtolnay/syn/issues/1842 +// +// There are a few places where we support (or might wish to support) parsing +// as part of interpretation: +// * e.g. of a token stream in `SourceValue` +// * e.g. as part of a PARSER, from an OutputStream +// * e.g. of a variable, as part of incremental parsing (while_parse style loops) +// +// I spent quite a while considering whether this could be wrapping a +// `syn::parse::ParseBuffer<'a>` or `syn::buffer::Cursor<'a>`... +// +// Some commands want to performantly parse a variable or other token stream. +// +// Here we want variables to support: +// * Easy appending of tokens +// * Incremental parsing +// +// Ideally we'd want to be able to store a syn::TokenBuffer, and be able to +// append to it, and freely convert it to a syn::ParseStream, possibly even storing +// a cursor position into it. +// +// Unfortunately this isn't at all possible: +// * TokenBuffer appending isn't a thing, you can only create one (recursively) from +// a TokenStream +// * TokenBuffer can't be converted to a ParseStream outside of the syn crate +// * For performance, a cursor stores a pointer into a TokenBuffer, so it can only be +// used against a fixed buffer. +// +// We could probably work around these limitations by sacrificing performance and transforming +// to TokenStream and back, but probably there's a better way. +// +// What we probably want is our own abstraction, likely a fork from `syn`, which supports +// converting a Cursor into an indexed based cursor, which can safely be stored separately +// from the TokenBuffer. +// +// We could use this abstraction for OutputStream; and our variables could store a +// tuple of (IndexCursor, PreinterpretTokenBuffer) + +/// Inspired/ forked from [`syn::buffer::TokenBuffer`], in order to support appending tokens, +/// as per the issue here: https://github.com/dtolnay/syn/issues/1842 +/// +/// 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 +#[allow(unused)] +mod token_buffer { + use super::*; + + /// Inspired by [`syn::buffer::Entry`] + /// Internal type which is used instead of `TokenTree` to represent a token tree + /// within a `TokenBuffer`. + enum TokenBufferEntry { + // Mimicking types from proc-macro. + // Group entries contain the offset to the matching End entry. + Group(Group, usize), + Ident(Ident), + Punct(Punct), + Literal(Literal), + // End entries contain the offset (negative) to the start of the buffer, and + // offset (negative) to the matching Group entry. + End(isize, isize), + } + + /// Inspired by [`syn::buffer::TokenBuffer`], but with the ability to append tokens. + pub(super) struct TokenBuffer { + entries: Vec, + } + + impl TokenBuffer { + pub(crate) fn new(tokens: impl IntoIterator) -> Self { + let mut entries = vec![]; + Self::recursive_new(&mut entries, tokens); + entries.push(TokenBufferEntry::End(-(entries.len() as isize), 0)); + Self { entries } + } + + pub(crate) fn append(&mut self, tokens: impl IntoIterator) { + self.entries.pop(); + Self::recursive_new(&mut self.entries, tokens); + self.entries + .push(TokenBufferEntry::End(-(self.entries.len() as isize), 0)); + } + + fn recursive_new( + entries: &mut Vec, + stream: impl IntoIterator, + ) { + for tt in stream { + match tt { + TokenTree::Ident(ident) => entries.push(TokenBufferEntry::Ident(ident)), + TokenTree::Punct(punct) => entries.push(TokenBufferEntry::Punct(punct)), + TokenTree::Literal(literal) => entries.push(TokenBufferEntry::Literal(literal)), + TokenTree::Group(group) => { + let group_start_index = entries.len(); + entries.push(TokenBufferEntry::End(0, 0)); // we replace this below + Self::recursive_new(entries, group.stream()); + let group_end_index = entries.len(); + let group_offset = group_end_index - group_start_index; + entries.push(TokenBufferEntry::End( + -(group_end_index as isize), + -(group_offset as isize), + )); + entries[group_start_index] = TokenBufferEntry::Group(group, group_offset); + } + } + } + } + } +} diff --git a/src/interpretation/interpreter.rs b/src/interpretation/interpreter.rs new file mode 100644 index 00000000..e79e5dbc --- /dev/null +++ b/src/interpretation/interpreter.rs @@ -0,0 +1,115 @@ +use super::*; + +pub(crate) struct Interpreter { + config: InterpreterConfig, + variable_data: VariableData, +} + +impl Interpreter { + pub(crate) fn new() -> Self { + Self { + config: Default::default(), + variable_data: VariableData::new(), + } + } + + pub(crate) fn define_variable( + &mut self, + variable: &(impl IsVariable + ?Sized), + value: ExpressionValue, + ) { + self.variable_data.define_variable(variable, value) + } + + pub(crate) fn resolve_variable_binding( + &self, + variable: &(impl IsVariable + ?Sized), + make_error: impl FnOnce() -> SynError, + ) -> ExecutionResult { + self.variable_data.resolve_binding(variable, make_error) + } + + 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; + } +} + +struct VariableData { + variable_data: HashMap, +} + +impl VariableData { + fn new() -> Self { + Self { + variable_data: HashMap::new(), + } + } + + fn define_variable(&mut self, variable: &(impl IsVariable + ?Sized), value: ExpressionValue) { + self.variable_data.insert( + variable.get_name(), + VariableContent::new(value, variable.span_range()), + ); + } + + fn resolve_binding( + &self, + variable: &(impl IsVariable + ?Sized), + make_error: impl FnOnce() -> SynError, + ) -> ExecutionResult { + let reference = self + .variable_data + .get(&variable.get_name()) + .ok_or_else(make_error)? + .binding(variable); + Ok(reference) + } +} + +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.execution_err(format!("Iteration limit of {} exceeded.\nIf needed, the limit can be reconfigured with [!settings! {{ iteration_limit: X }}]", 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..3019756c --- /dev/null +++ b/src/interpretation/mod.rs @@ -0,0 +1,22 @@ +mod bindings; +mod command; +mod command_arguments; +mod commands; +mod interpret_traits; +mod interpreted_stream; +mod interpreter; +mod source_code_block; +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::*; +pub(crate) use command::*; +pub(crate) use command_arguments::*; +pub(crate) use interpret_traits::*; +pub(crate) use interpreted_stream::*; +pub(crate) use interpreter::*; +pub(crate) use source_code_block::*; +pub(crate) use source_stream::*; +pub(crate) use variable::*; diff --git a/src/interpretation/source_code_block.rs b/src/interpretation/source_code_block.rs new file mode 100644 index 00000000..d3c2bdbb --- /dev/null +++ b/src/interpretation/source_code_block.rs @@ -0,0 +1,48 @@ +use crate::internal_prelude::*; + +/// A group `{ ... }` representing code which can be interpreted +#[derive(Clone)] +pub(crate) struct SourceCodeBlock { + braces: Braces, + inner: SourceStream, +} + +impl Parse for SourceCodeBlock { + fn parse(input: ParseStream) -> ParseResult { + let (braces, content) = input.parse_braces()?; + let inner = content.parse_with_context(braces.join())?; + Ok(Self { braces, inner }) + } +} + +impl HasSpan for SourceCodeBlock { + fn span(&self) -> Span { + self.braces.span() + } +} + +impl SourceCodeBlock { + pub(crate) fn interpret_loop_content_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult> { + match self.inner.interpret_into(interpreter, output) { + Ok(()) => Ok(None), + Err(ExecutionInterrupt::ControlFlow(control_flow_interrupt, _)) => { + Ok(Some(control_flow_interrupt)) + } + Err(error) => Err(error), + } + } +} + +impl Interpret for SourceCodeBlock { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + self.inner.interpret_into(interpreter, output) + } +} diff --git a/src/interpretation/source_stream.rs b/src/interpretation/source_stream.rs new file mode 100644 index 00000000..06099c2a --- /dev/null +++ b/src/interpretation/source_stream.rs @@ -0,0 +1,150 @@ +use crate::internal_prelude::*; + +/// A parsed stream ready for interpretation +#[derive(Clone)] +pub(crate) struct SourceStream { + items: Vec, + span: Span, +} + +impl ContextualParse for SourceStream { + type Context = Span; + + fn parse(input: ParseStream, span: Self::Context) -> ParseResult { + let mut items = Vec::new(); + while !input.is_empty() { + items.push(input.parse()?); + } + Ok(Self { items, span }) + } +} + +impl Interpret for SourceStream { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + for item in self.items { + item.interpret_into(interpreter, output)?; + } + Ok(()) + } +} + +impl HasSpan for SourceStream { + fn span(&self) -> Span { + self.span + } +} + +#[derive(Clone)] +pub(crate) enum SourceItem { + Command(Command), + Variable(MarkedVariable), + ExpressionBlock(ExpressionBlock), + SourceGroup(SourceGroup), + Punct(Punct), + Ident(Ident), + Literal(Literal), +} + +impl Parse for SourceItem { + fn parse(input: ParseStream) -> ParseResult { + Ok(match input.peek_grammar() { + SourcePeekMatch::Command(_) => SourceItem::Command(input.parse()?), + SourcePeekMatch::Group(_) => SourceItem::SourceGroup(input.parse()?), + SourcePeekMatch::Variable(_) => SourceItem::Variable(input.parse()?), + SourcePeekMatch::ExpressionBlock(_) => SourceItem::ExpressionBlock(input.parse()?), + SourcePeekMatch::ExplicitTransformStream | SourcePeekMatch::Transformer(_) => { + return input.parse_err("Destructurings are not supported here. If this wasn't intended to be a destructuring, replace @ with [!raw! @]"); + } + SourcePeekMatch::AppendVariableParser => { + return input.parse_err("Destructurings are not supported here."); + } + SourcePeekMatch::Punct(_) => SourceItem::Punct(input.parse_any_punct()?), + SourcePeekMatch::Ident(_) => SourceItem::Ident(input.parse_any_ident()?), + SourcePeekMatch::Literal(_) => SourceItem::Literal(input.parse()?), + SourcePeekMatch::End => return input.parse_err("Expected some item."), + }) + } +} + +impl Interpret for SourceItem { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + match self { + SourceItem::Command(command_invocation) => { + command_invocation.interpret_into(interpreter, output)?; + } + SourceItem::Variable(variable) => { + variable.interpret_into(interpreter, output)?; + } + SourceItem::ExpressionBlock(block) => { + block.interpret_into(interpreter, output)?; + } + SourceItem::SourceGroup(group) => { + group.interpret_into(interpreter, output)?; + } + SourceItem::Punct(punct) => output.push_punct(punct), + SourceItem::Ident(ident) => output.push_ident(ident), + SourceItem::Literal(literal) => output.push_literal(literal), + } + Ok(()) + } +} + +impl HasSpanRange for SourceItem { + fn span_range(&self) -> SpanRange { + match self { + SourceItem::Command(command_invocation) => command_invocation.span_range(), + SourceItem::Variable(variable) => variable.span_range(), + SourceItem::ExpressionBlock(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(), + } + } +} + +/// A parsed group ready for interpretation +#[derive(Clone)] +pub(crate) struct SourceGroup { + source_delimiter: Delimiter, + source_delim_span: DelimSpan, + content: SourceStream, +} + +impl Parse for SourceGroup { + fn parse(input: ParseStream) -> ParseResult { + let (delimiter, delim_span, content) = input.parse_any_group()?; + let content = content.parse_with_context(delim_span.join())?; + Ok(Self { + source_delimiter: delimiter, + source_delim_span: delim_span, + content, + }) + } +} + +impl Interpret for SourceGroup { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let inner = self.content.interpret_to_new_stream(interpreter)?; + output.push_new_group(inner, self.source_delimiter, self.source_delim_span.join()); + Ok(()) + } +} + +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..d46efdf5 --- /dev/null +++ b/src/interpretation/variable.rs @@ -0,0 +1,287 @@ +use crate::internal_prelude::*; + +pub(crate) trait IsVariable: HasSpanRange { + fn get_name(&self) -> String; + + fn define(&self, interpreter: &mut Interpreter, value_source: impl ToExpressionValue) { + interpreter.define_variable(self, value_source.to_value(self.span_range())) + } + + fn define_coerced(&self, interpreter: &mut Interpreter, content: OutputStream) { + interpreter.define_variable(self, content.coerce_into_value(self.span_range())) + } + + fn get_cloned_value(&self, interpreter: &Interpreter) -> ExecutionResult { + Ok(self + .binding(interpreter)? + .into_expensively_cloned()? + .into()) + } + + fn substitute_into( + &self, + interpreter: &mut Interpreter, + grouping: Grouping, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + self.binding(interpreter)?.into_shared()?.output_to( + grouping, + output, + StreamOutputBehaviour::Standard, + ) + } + + fn binding(&self, interpreter: &Interpreter) -> ExecutionResult { + interpreter.resolve_variable_binding(self, || { + self.error("The variable does not already exist in the current scope") + }) + } +} + +#[derive(Clone)] +pub(crate) enum MarkedVariable { + Grouped(GroupedVariable), + Flattened(FlattenedVariable), +} + +impl Parse for MarkedVariable { + fn parse(input: ParseStream) -> ParseResult { + if input.peek2(Token![..]) { + Ok(MarkedVariable::Flattened(input.parse()?)) + } else { + Ok(MarkedVariable::Grouped(input.parse()?)) + } + } +} + +impl HasSpanRange for MarkedVariable { + fn span_range(&self) -> SpanRange { + match self { + MarkedVariable::Grouped(variable) => variable.span_range(), + MarkedVariable::Flattened(variable) => variable.span_range(), + } + } +} + +impl HasSpanRange for &MarkedVariable { + fn span_range(&self) -> SpanRange { + ::span_range(self) + } +} + +impl IsVariable for MarkedVariable { + fn get_name(&self) -> String { + match self { + MarkedVariable::Grouped(variable) => variable.get_name(), + MarkedVariable::Flattened(variable) => variable.get_name(), + } + } +} + +impl Interpret for &MarkedVariable { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + match self { + MarkedVariable::Grouped(variable) => variable.interpret_into(interpreter, output), + MarkedVariable::Flattened(variable) => variable.interpret_into(interpreter, output), + } + } +} + +#[derive(Clone)] +pub(crate) struct GroupedVariable { + marker: Token![#], + variable_name: Ident, +} + +impl Parse for GroupedVariable { + fn parse(input: ParseStream) -> ParseResult { + input.try_parse_or_error( + |input| { + Ok(Self { + marker: input.parse()?, + variable_name: input.parse_any_ident()?, + }) + }, + "Expected #variable", + ) + } +} + +impl IsVariable for GroupedVariable { + fn get_name(&self) -> String { + self.variable_name.to_string() + } +} + +impl Interpret for &GroupedVariable { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + self.substitute_into(interpreter, Grouping::Grouped, output) + } +} + +impl InterpretToValue for &GroupedVariable { + type OutputValue = ExpressionValue; + + fn interpret_to_value( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + self.get_cloned_value(interpreter) + } +} + +impl HasSpanRange for GroupedVariable { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.marker.span, self.variable_name.span()) + } +} + +impl HasSpanRange for &GroupedVariable { + fn span_range(&self) -> SpanRange { + ::span_range(self) + } +} + +impl core::fmt::Display for GroupedVariable { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "#{}", self.variable_name) + } +} + +#[derive(Clone)] +pub(crate) struct FlattenedVariable { + marker: Token![#], + #[allow(unused)] + flatten: Token![..], + variable_name: Ident, +} + +impl Parse for FlattenedVariable { + fn parse(input: ParseStream) -> ParseResult { + input.try_parse_or_error( + |input| { + Ok(Self { + marker: input.parse()?, + flatten: input.parse()?, + variable_name: input.parse_any_ident()?, + }) + }, + "Expected #..variable", + ) + } +} + +impl IsVariable for FlattenedVariable { + fn get_name(&self) -> String { + self.variable_name.to_string() + } +} + +impl Interpret for &FlattenedVariable { + fn interpret_into( + self, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + self.substitute_into(interpreter, Grouping::Flattened, output) + } +} + +impl HasSpanRange for FlattenedVariable { + fn span_range(&self) -> SpanRange { + SpanRange::new_between(self.marker.span, self.variable_name.span()) + } +} + +impl HasSpanRange for &FlattenedVariable { + fn span_range(&self) -> SpanRange { + ::span_range(self) + } +} + +impl core::fmt::Display for FlattenedVariable { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "#..{}", self.variable_name) + } +} + +// An identifier for a variable path in an expression +#[derive(Clone)] +pub(crate) struct VariableIdentifier { + pub(crate) ident: Ident, +} + +impl Parse for VariableIdentifier { + fn parse(input: ParseStream) -> ParseResult { + Ok(Self { + ident: input.parse()?, + }) + } +} + +impl IsVariable for VariableIdentifier { + fn get_name(&self) -> String { + self.ident.to_string() + } +} + +impl HasSpan for VariableIdentifier { + fn span(&self) -> Span { + self.ident.span() + } +} + +impl InterpretToValue for &VariableIdentifier { + type OutputValue = ExpressionValue; + + fn interpret_to_value( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + self.get_cloned_value(interpreter) + } +} + +#[derive(Clone)] +pub(crate) struct VariablePattern { + pub(crate) name: Ident, +} + +impl Parse for VariablePattern { + fn parse(input: ParseStream) -> ParseResult { + Ok(Self { + name: input.parse()?, + }) + } +} + +impl IsVariable for VariablePattern { + fn get_name(&self) -> String { + self.name.to_string() + } +} + +impl HasSpan for VariablePattern { + fn span(&self) -> Span { + self.name.span() + } +} + +impl HandleDestructure for VariablePattern { + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: ExpressionValue, + ) -> ExecutionResult<()> { + self.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 7f89bfd6..29d40c23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -510,12 +510,12 @@ //! //! 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; +mod transformation; use internal_prelude::*; @@ -538,11 +538,29 @@ use internal_prelude::*; /// See the [crate-level documentation](crate) for full details. #[proc_macro] pub fn preinterpret(token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream { - interpret(proc_macro2::TokenStream::from(token_stream)) + preinterpret_internal(proc_macro2::TokenStream::from(token_stream)) .unwrap_or_else(|err| err.to_compile_error()) .into() } +fn preinterpret_internal(input: TokenStream) -> SynResult { + let mut interpreter = Interpreter::new(); + + let interpretation_stream = input + .source_parse_with(|input| SourceStream::parse(input, Span::call_site())) + .convert_to_final_result()?; + + let interpreted_stream = interpretation_stream + .interpret_to_new_stream(&mut interpreter) + .convert_to_final_result()?; + + unsafe { + // RUST-ANALYZER-SAFETY: This might drop transparent groups in the output of + // rust-analyzer. There's not much we can do here... + Ok(interpreted_stream.into_token_stream()) + } +} + // 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/errors.rs b/src/misc/errors.rs new file mode 100644 index 00000000..effbc1e6 --- /dev/null +++ b/src/misc/errors.rs @@ -0,0 +1,126 @@ +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 add_context_if_error_and_no_context(self, context: impl FnOnce() -> String) -> Self; + 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 add_context_if_error_and_no_context(self, context: impl FnOnce() -> String) -> Self { + self.map_err(|error| error.add_context_if_none(context())) + } + + fn into_execution_result(self) -> ExecutionResult { + self.map_err(|error| error.into()) + } +} + +#[derive(Debug)] +pub(crate) enum ParseError { + Standard(syn::Error), + Contextual(syn::Error, String), +} + +impl From for ParseError { + fn from(e: syn::Error) -> Self { + ParseError::Standard(e) + } +} + +impl HasSpan for ParseError { + fn span(&self) -> Span { + match self { + ParseError::Standard(e) => e.span(), + ParseError::Contextual(e, _) => e.span(), + } + } +} + +impl ParseError { + /// This is not a `From` because it wants to be explicit + pub(crate) fn convert_to_final_error(self) -> syn::Error { + match self { + ParseError::Standard(e) => e, + ParseError::Contextual(e, message) => e.concat(&format!("\n{}", message)), + } + } + + pub(crate) fn add_context_if_none(self, context: impl std::fmt::Display) -> Self { + match self { + ParseError::Standard(e) => ParseError::Contextual(e, context.to_string()), + other => other, + } + } + + pub(crate) fn context(&self) -> Option<&str> { + match self { + ParseError::Standard(_) => None, + ParseError::Contextual(_, context) => Some(context), + } + } +} + +// 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) 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) enum ExecutionInterrupt { + Error(syn::Error), + DestructureError(ParseError), + ControlFlow(ControlFlowInterrupt, Span), +} + +#[derive(Debug)] +pub(crate) enum ControlFlowInterrupt { + Break, + Continue, +} + +impl From for ExecutionInterrupt { + fn from(e: syn::Error) -> Self { + ExecutionInterrupt::Error(e) + } +} + +impl From for ExecutionInterrupt { + fn from(e: ParseError) -> Self { + ExecutionInterrupt::DestructureError(e) + } +} + +impl ExecutionInterrupt { + pub(crate) fn convert_to_final_error(self) -> syn::Error { + match self { + ExecutionInterrupt::Error(e) => e, + ExecutionInterrupt::DestructureError(e) => e.convert_to_final_error(), + ExecutionInterrupt::ControlFlow(ControlFlowInterrupt::Break, span) => { + syn::Error::new(span, "Break can only be used inside a loop") + } + ExecutionInterrupt::ControlFlow(ControlFlowInterrupt::Continue, span) => { + syn::Error::new(span, "Continue can only be used inside a loop") + } + } + } +} diff --git a/src/misc/field_inputs.rs b/src/misc/field_inputs.rs new file mode 100644 index 00000000..bfffabcb --- /dev/null +++ b/src/misc/field_inputs.rs @@ -0,0 +1,122 @@ +macro_rules! define_object_arguments { + ( + $source:ident => $validated:ident { + required: { + $( + $required_field:ident: $required_example:tt $(($required_description:literal))? + ),* $(,)? + }$(,)? + optional: { + $( + $optional_field:ident: $optional_example:tt $(($optional_description:literal))? + ),* $(,)? + }$(,)? + } + ) => { + #[derive(Clone)] + struct $source { + inner: SourceExpression, + } + + impl ArgumentsContent for $source { + fn error_message() -> String { + format!("Expected: {}", Self::describe_object()) + } + } + + impl Parse for $source { + fn parse(input: ParseStream) -> ParseResult { + Ok(Self { inner: input.parse()? }) + } + } + + impl InterpretToValue for &$source { + type OutputValue = $validated; + + fn interpret_to_value( + self, + interpreter: &mut Interpreter, + ) -> ExecutionResult { + let mut object = self.inner.interpret_to_value(interpreter)? + .expect_object("The arguments")?; + object.validate(&$source::validation())?; + let span_range = object.span_range; + Ok($validated { + $( + $required_field: object.remove_or_none(stringify!($required_field), span_range), + )* + $( + $optional_field: object.remove_no_none(stringify!($optional_field), span_range), + )* + }) + } + } + + struct $validated { + $( + $required_field: ExpressionValue, + )* + $( + $optional_field: Option, + )* + } + + impl $source { + fn describe_object() -> String { + Self::validation().describe_object() + } + + fn validation() -> &'static [(&'static str, FieldDefinition)] { + &Self::VALIDATION + } + + const VALIDATION: [ + (&'static str, FieldDefinition); + count_fields!($($required_field)* $($optional_field)*) + ] = [ + $( + ( + stringify!($required_field), + FieldDefinition { + required: true, + description: optional_else!{ + { $(Some(std::borrow::Cow::Borrowed($required_description)))? } + { None } + }, + example: std::borrow::Cow::Borrowed($required_example), + }, + ), + )* + $( + ( + stringify!($optional_field), + FieldDefinition { + required: false, + description: optional_else!{ + { $(Some(std::borrow::Cow::Borrowed($optional_description)))? } + { None } + }, + example: std::borrow::Cow::Borrowed($optional_example), + }, + ), + )* + ]; + } + }; +} + +pub(crate) use define_object_arguments; + +macro_rules! count_fields { + ($head:tt $($tail:tt)*) => { 1 + count_fields!($($tail)*)}; + () => { 0 } +} + +pub(crate) use count_fields; + +macro_rules! optional_else { + ({$($content:tt)+} {$($else:tt)*}) => { $($content)+ }; + ({} {$($else:tt)*}) => { $($else)* } +} + +pub(crate) use optional_else; diff --git a/src/misc/mod.rs b/src/misc/mod.rs new file mode 100644 index 00000000..eae8d3ee --- /dev/null +++ b/src/misc/mod.rs @@ -0,0 +1,29 @@ +mod errors; +mod field_inputs; +mod mut_rc_ref_cell; +mod parse_traits; +mod string_conversion; + +pub(crate) use errors::*; +pub(crate) use field_inputs::*; +pub(crate) use mut_rc_ref_cell::*; +pub(crate) use parse_traits::*; +pub(crate) use string_conversion::*; + +#[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 +} diff --git a/src/misc/mut_rc_ref_cell.rs b/src/misc/mut_rc_ref_cell.rs new file mode 100644 index 00000000..b602b96f --- /dev/null +++ b/src/misc/mut_rc_ref_cell.rs @@ -0,0 +1,151 @@ +use crate::internal_prelude::*; +use std::cell::*; +use std::rc::Rc; + +/// A mutable reference to a sub-value `U` inside a [`Rc>`]. +/// Only one [`MutSubRcRefCell`] can exist at a time for a given [`Rc>`]. +pub(crate) struct MutSubRcRefCell { + /// 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 MutSubRcRefCell { + 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 { std::mem::transmute::, RefMut<'static, T>>(ref_mut) }, + pointed_at, + }) + } +} + +impl MutSubRcRefCell { + 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 / MutSubRcRefCell 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) + } + } + + pub(crate) fn map(self, f: impl FnOnce(&mut U) -> &mut V) -> MutSubRcRefCell { + MutSubRcRefCell { + ref_mut: RefMut::map(self.ref_mut, f), + 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(MutSubRcRefCell { + ref_mut, + pointed_at: self.pointed_at, + }), + Err(_) => Err(error.unwrap()), + } + } +} + +impl DerefMut for MutSubRcRefCell { + fn deref_mut(&mut self) -> &mut U { + &mut self.ref_mut + } +} + +impl Deref for MutSubRcRefCell { + 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 [`MutSubRcRefCell`] 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 { std::mem::transmute::, Ref<'static, T>>(shared_ref) }, + pointed_at, + }) + } +} + +impl SharedSubRcRefCell { + pub(crate) fn map(self, f: impl FnOnce(&U) -> &V) -> SharedSubRcRefCell { + SharedSubRcRefCell { + shared_ref: Ref::map(self.shared_ref, f), + 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()), + } + } +} + +impl Deref for SharedSubRcRefCell { + type Target = U; + fn deref(&self) -> &U { + &self.shared_ref + } +} diff --git a/src/misc/parse_traits.rs b/src/misc/parse_traits.rs new file mode 100644 index 00000000..986cdd0f --- /dev/null +++ b/src/misc/parse_traits.rs @@ -0,0 +1,397 @@ +use crate::internal_prelude::*; + +// Parsing of source code tokens +// ============================= + +pub(crate) struct Source; + +impl ParseBuffer<'_, Source> { + pub(crate) fn peek_grammar(&self) -> SourcePeekMatch { + detect_preinterpret_grammar(self.cursor()) + } +} + +#[allow(unused)] +pub(crate) enum SourcePeekMatch { + Command(Option), + ExpressionBlock(Grouping), + Variable(Grouping), + AppendVariableParser, + ExplicitTransformStream, + Transformer(Option), + Group(Delimiter), + Ident(Ident), + Punct(Punct), + Literal(Literal), + End, +} + +fn detect_preinterpret_grammar(cursor: syn::buffer::Cursor) -> SourcePeekMatch { + // We have to check groups first, so that we handle transparent groups + // and avoid the self.ignore_none() calls inside cursor + if let Some((next, delimiter, _, _)) = cursor.any_group() { + if delimiter == Delimiter::Bracket { + if let Some((_, next)) = next.punct_matching('!') { + if let Some((ident, next)) = next.ident() { + if next.punct_matching('!').is_some() { + let output_kind = + CommandKind::for_ident(&ident).map(|kind| kind.resolve_output_kind()); + return SourcePeekMatch::Command(output_kind); + } + } + } + } + + // 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); + } + if let Some((_, next)) = cursor.punct_matching('#') { + if next.ident().is_some() { + return SourcePeekMatch::Variable(Grouping::Grouped); + } + if let Some((_, next)) = next.punct_matching('.') { + if let Some((_, next)) = next.punct_matching('.') { + if next.ident().is_some() { + return SourcePeekMatch::Variable(Grouping::Flattened); + } + if next.group_matching(Delimiter::Parenthesis).is_some() { + return SourcePeekMatch::ExpressionBlock(Grouping::Flattened); + } + if let Some((_, next)) = next.punct_matching('>') { + if next.punct_matching('>').is_some() { + return SourcePeekMatch::AppendVariableParser; + } + } + } + } + if let Some((_, next)) = next.punct_matching('>') { + if next.punct_matching('>').is_some() { + return SourcePeekMatch::AppendVariableParser; + } + } + if next.group_matching(Delimiter::Parenthesis).is_some() { + return SourcePeekMatch::ExpressionBlock(Grouping::Grouped); + } + } + + if let Some((_, next)) = cursor.punct_matching('@') { + if let Some((_, _, _)) = next.group_matching(Delimiter::Parenthesis) { + // @(...) or @(_ = ...) or @(#x = ...) + return SourcePeekMatch::ExplicitTransformStream; + } + if let Some((ident, _)) = next.ident() { + let name = ident.to_string(); + if name.to_uppercase() == name { + return SourcePeekMatch::Transformer(TransformerKind::for_ident(&ident)); + } + } + if let Some((_, next, _)) = next.group_matching(Delimiter::Bracket) { + if let Some((ident, _)) = next.ident() { + let name = ident.to_string(); + if name.to_uppercase() == name { + return SourcePeekMatch::Transformer(TransformerKind::for_ident(&ident)); + } + } + } + } + + 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 ParseBuffer<'_, Output> { + pub(crate) fn peek_grammar(&self) -> OutputPeekMatch { + match self.cursor().token_tree() { + Some((TokenTree::Ident(ident), _)) => OutputPeekMatch::Ident(ident), + Some((TokenTree::Punct(punct), _)) => OutputPeekMatch::Punct(punct), + Some((TokenTree::Literal(literal), _)) => OutputPeekMatch::Literal(literal), + Some((TokenTree::Group(group), _)) => OutputPeekMatch::Group(group.delimiter()), + None => OutputPeekMatch::End, + } + } +} + +#[allow(unused)] +pub(crate) enum OutputPeekMatch { + Group(Delimiter), + Ident(Ident), + Punct(Punct), + Literal(Literal), + End, +} + +// Generic parsing +// =============== + +pub(crate) trait Parse: Sized { + fn parse(input: ParseStream) -> ParseResult; +} + +pub(crate) trait ContextualParse: Sized { + type Context; + + fn parse(input: ParseStream, context: Self::Context) -> ParseResult; +} + +impl Parse for T { + fn parse(input: ParseStream) -> ParseResult { + Ok(T::parse(&input.inner)?) + } +} + +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>(&self) -> ParseResult { + T::parse(self) + } + + pub(crate) fn parse_with_context>( + &self, + context: T::Context, + ) -> ParseResult { + T::parse(self, context) + } + + pub fn parse_terminated, P: syn::parse::Parse>( + &'a self, + ) -> ParseResult> { + Ok(Punctuated::parse_terminated_with(&self.inner, |inner| { + ParseStream::::from(inner) + .parse() + .map_err(|err| err.convert_to_final_error()) + })?) + } + + pub(crate) fn call) -> 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.error(message).into()) + } + + pub(crate) fn parse_any_punct(&self) -> ParseResult { + // Annoyingly, ' behaves weirdly in syn, so we need to handle it + 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(|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.span().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.span().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.span().error(format!("expected {}", content))) + })?) + } + + 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() + } +} 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/src/transformation/exact_stream.rs b/src/transformation/exact_stream.rs new file mode 100644 index 00000000..432ebb8a --- /dev/null +++ b/src/transformation/exact_stream.rs @@ -0,0 +1,194 @@ +use crate::internal_prelude::*; + +// An ExactStream is a transformer created with @[EXACT ...]. +// It has subtly different behaviour to a transform stream, but they can be nested inside each other. +// +// It differs in two ways: +// * Commands and Outputs are interpreted, and then matched from the parse stream +// * Each consumed item is output as-is + +pub(crate) type ExactStream = ExactSegment; + +#[derive(Clone)] +pub(crate) struct ExactSegment { + stop_condition: PhantomData, + inner: Vec, +} + +impl, K> Parse for ExactSegment +where + ExactItem: Parse, +{ + fn parse(input: ParseStream) -> ParseResult { + let mut inner = vec![]; + while !C::should_stop(input) { + inner.push(ExactItem::parse(input)?); + } + Ok(Self { + stop_condition: PhantomData, + inner, + }) + } +} + +impl ExactSegment { + pub(crate) fn len(&self) -> usize { + self.inner.len() + } +} + +impl HandleTransformation for ExactSegment { + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + for item in self.inner.iter() { + item.handle_transform(input, interpreter, output)?; + } + Ok(()) + } +} + +/// This must match item-by-item. +#[derive(Clone)] +pub(crate) enum ExactItem { + TransformStreamInput(ExplicitTransformStream), + Transformer(Transformer), + ExactCommandOutput(Command), + ExactVariableOutput(MarkedVariable), + ExactExpressionBlock(ExpressionBlock), + ExactPunct(Punct), + ExactIdent(Ident), + ExactLiteral(Literal), + ExactGroup(ExactGroup), +} + +impl Parse for ExactItem { + fn parse(input: ParseStream) -> ParseResult { + Ok(match input.peek_grammar() { + SourcePeekMatch::Command(_) => Self::ExactCommandOutput(input.parse()?), + SourcePeekMatch::Variable(_) => Self::ExactVariableOutput(input.parse()?), + SourcePeekMatch::ExpressionBlock(_) => Self::ExactExpressionBlock(input.parse()?), + SourcePeekMatch::AppendVariableParser => { + return input + .parse_err("Append variable bindings are not supported in an EXACT stream") + } + SourcePeekMatch::ExplicitTransformStream => Self::TransformStreamInput(input.parse()?), + SourcePeekMatch::Transformer(_) => Self::Transformer(input.parse()?), + SourcePeekMatch::Group(_) => Self::ExactGroup(input.parse()?), + SourcePeekMatch::Ident(_) => Self::ExactIdent(input.parse_any_ident()?), + SourcePeekMatch::Punct(_) => Self::ExactPunct(input.parse_any_punct()?), + SourcePeekMatch::Literal(_) => Self::ExactLiteral(input.parse()?), + SourcePeekMatch::End => return input.parse_err("Unexpected end"), + }) + } +} + +impl Parse for ExactItem { + fn parse(input: ParseStream) -> ParseResult { + Ok(match input.peek_grammar() { + OutputPeekMatch::Group(_) => Self::ExactGroup(input.parse()?), + OutputPeekMatch::Ident(_) => Self::ExactIdent(input.parse_any_ident()?), + OutputPeekMatch::Punct(_) => Self::ExactPunct(input.parse_any_punct()?), + OutputPeekMatch::Literal(_) => Self::ExactLiteral(input.parse()?), + OutputPeekMatch::End => return input.parse_err("Unexpected end"), + }) + } +} + +impl HandleTransformation for ExactItem { + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + match self { + ExactItem::TransformStreamInput(transform_stream_input) => { + transform_stream_input.handle_transform(input, interpreter, output)?; + } + ExactItem::Transformer(transformer) => { + transformer.handle_transform(input, interpreter, output)?; + } + ExactItem::ExactCommandOutput(command) => { + command + .clone() + .interpret_to_new_stream(interpreter)? + .into_exact_stream()? + .handle_transform(input, interpreter, output)?; + } + ExactItem::ExactVariableOutput(variable) => { + variable + .interpret_to_new_stream(interpreter)? + .into_exact_stream()? + .handle_transform(input, interpreter, output)?; + } + ExactItem::ExactExpressionBlock(expression_block) => { + expression_block + .interpret_to_new_stream(interpreter)? + .into_exact_stream()? + .handle_transform(input, interpreter, output)?; + } + ExactItem::ExactPunct(punct) => { + output.push_punct(input.parse_punct_matching(punct.as_char())?); + } + ExactItem::ExactIdent(ident) => { + output.push_ident(input.parse_ident_matching(&ident.to_string())?); + } + ExactItem::ExactLiteral(literal) => { + output.push_literal(input.parse_literal_matching(&literal.to_string())?); + } + ExactItem::ExactGroup(exact_group) => { + exact_group.handle_transform(input, interpreter, output)? + } + } + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct ExactGroup { + delimiter: Delimiter, + inner: ExactStream, +} + +impl Parse for ExactGroup +where + ExactStream: Parse, +{ + fn parse(input: ParseStream) -> ParseResult { + let (delimiter, _, content) = input.parse_any_group()?; + Ok(Self { + delimiter, + inner: content.parse()?, + }) + } +} + +impl HandleTransformation for ExactGroup { + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + 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 self.delimiter == Delimiter::None { + self.inner.handle_transform(input, interpreter, output) + } else { + let (source_span, inner_source) = input.parse_specific_group(self.delimiter)?; + output.push_grouped( + |inner_output| { + self.inner + .handle_transform(&inner_source, interpreter, inner_output) + }, + self.delimiter, + source_span.join(), + ) + } + } +} diff --git a/src/transformation/fields.rs b/src/transformation/fields.rs new file mode 100644 index 00000000..1323334b --- /dev/null +++ b/src/transformation/fields.rs @@ -0,0 +1,176 @@ +use crate::internal_prelude::*; +use std::collections::{BTreeMap, BTreeSet, HashSet}; + +#[allow(unused)] +pub(crate) struct FieldsParseDefinition { + new_builder: T, + field_definitions: FieldDefinitions, +} + +#[allow(unused)] +impl FieldsParseDefinition { + pub(crate) fn new(new_builder: T) -> Self { + Self { + new_builder, + field_definitions: FieldDefinitions(BTreeMap::new()), + } + } + + pub(crate) fn add_required_field + 'static>( + self, + field_name: &str, + example: &str, + explanation: Option<&str>, + set: impl Fn(&mut T, F) + 'static, + ) -> Self { + self.add_field(field_name, example, explanation, true, F::parse, set) + } + + pub(crate) fn add_optional_field + 'static>( + self, + field_name: &str, + example: &str, + explanation: Option<&str>, + set: impl Fn(&mut T, F) + 'static, + ) -> Self { + self.add_field(field_name, example, explanation, false, F::parse, set) + } + + pub(crate) fn add_field( + mut self, + field_name: &str, + example: &str, + explanation: Option<&str>, + is_required: bool, + parse: impl Fn(ParseStream) -> ParseResult + 'static, + set: impl Fn(&mut T, F) + 'static, + ) -> Self { + if self + .field_definitions + .0 + .insert( + field_name.to_string(), + FieldParseDefinition { + is_required, + example: example.into(), + explanation: explanation.map(|s| s.to_string()), + parse_and_set: Box::new(move |builder, content| { + let value = parse(content)?; + set(builder, value); + Ok(()) + }), + }, + ) + .is_some() + { + panic!("Duplicate field name: {field_name:?}"); + } + self + } + + pub(crate) fn create_syn_parser( + self, + error_span_range: SpanRange, + ) -> impl FnOnce(SynParseStream) -> ParseResult { + fn inner( + input: ParseStream, + new_builder: T, + field_definitions: &FieldDefinitions, + error_span_range: SpanRange, + ) -> ParseResult { + let mut builder = new_builder; + let (_, content) = input.parse_braces()?; + + let mut required_field_names: BTreeSet<_> = field_definitions + .0 + .iter() + .filter_map( + |(field_name, field_definition)| match field_definition.is_required { + true => Some(field_name.clone()), + false => None, + }, + ) + .collect(); + let mut seen_field_names = HashSet::new(); + + while !content.is_empty() { + let field_name = content.parse::()?; + let field_name_value = field_name.to_string(); + if !seen_field_names.insert(field_name_value.clone()) { + return field_name.parse_err("Duplicate field name"); + } + required_field_names.remove(field_name_value.as_str()); + let _ = content.parse::()?; + let field_definition = field_definitions + .0 + .get(field_name_value.as_str()) + .ok_or_else(|| field_name.error("Unsupported field name".to_string()))?; + (field_definition.parse_and_set)(&mut builder, &content)?; + if !content.is_empty() { + content.parse::()?; + } + } + + if !required_field_names.is_empty() { + return error_span_range.parse_err(format!( + "Missing required fields: {missing_fields:?}", + missing_fields = required_field_names, + )); + } + + Ok(builder) + } + move |input: SynParseStream| { + inner( + input.into(), + self.new_builder, + &self.field_definitions, + error_span_range, + ) + .add_context_if_error_and_no_context(|| self.field_definitions.error_message()) + } + } +} + +struct FieldDefinitions(BTreeMap>); + +impl FieldDefinitions { + fn error_message(&self) -> String { + use std::fmt::Write; + let mut message = "Expected: {\n".to_string(); + for (field_name, field_definition) in &self.0 { + write!( + &mut message, + " {}{}: {}", + field_name, + if field_definition.is_required { + "" + } else { + "?" + }, + field_definition.example, + ) + .unwrap(); + match field_definition.explanation { + Some(ref explanation) => writeln!( + &mut message, + ", // {explanation}", + explanation = explanation, + ) + .unwrap(), + None => writeln!(&mut message, ",").unwrap(), + } + } + message.push('}'); + message + } +} + +#[allow(unused)] +struct FieldParseDefinition { + is_required: bool, + example: String, + explanation: Option, + #[allow(clippy::type_complexity)] + parse_and_set: Box) -> ParseResult<()>>, +} diff --git a/src/transformation/mod.rs b/src/transformation/mod.rs new file mode 100644 index 00000000..1c02bdf7 --- /dev/null +++ b/src/transformation/mod.rs @@ -0,0 +1,20 @@ +mod exact_stream; +mod fields; +mod parse_utilities; +mod patterns; +mod transform_stream; +mod transformation_traits; +mod transformer; +mod transformers; +mod variable_parser; + +pub(crate) use exact_stream::*; +#[allow(unused)] +pub(crate) use fields::*; +pub(crate) use parse_utilities::*; +pub(crate) use patterns::*; +pub(crate) use transform_stream::*; +pub(crate) use transformation_traits::*; +pub(crate) use transformer::*; +pub(crate) use transformers::*; +pub(crate) use variable_parser::*; diff --git a/src/transformation/parse_utilities.rs b/src/transformation/parse_utilities.rs new file mode 100644 index 00000000..8d1290c0 --- /dev/null +++ b/src/transformation/parse_utilities.rs @@ -0,0 +1,134 @@ +use crate::internal_prelude::*; + +/// Designed to instruct the parser to stop parsing when a certain condition is met +pub(crate) trait StopCondition: Clone { + fn should_stop(input: ParseStream) -> bool; +} + +#[derive(Clone)] +pub(crate) struct UntilEnd; +impl StopCondition for UntilEnd { + fn should_stop(input: ParseStream) -> bool { + input.is_empty() + } +} + +pub(crate) trait PeekableToken: Clone { + fn peek(input: ParseStream) -> bool; +} + +// This is going through such pain to ensure we stay in the public API of syn +// We'd like to be able to use `input.peek::()` for some syn::token::Token +// but that's just not an API they support for some reason +macro_rules! impl_peekable_token { + ($(Token![$token:tt]),* $(,)?) => { + $( + impl PeekableToken for Token![$token] { + fn peek(input: ParseStream) -> bool { + input.peek(Token![$token]) + } + } + )* + }; +} + +impl_peekable_token! { + Token![=], + Token![in], +} + +#[derive(Clone)] +pub(crate) struct UntilToken { + token: PhantomData, +} + +impl, K> StopCondition for UntilToken { + fn should_stop(input: ParseStream) -> bool { + input.is_empty() || T::peek(input) + } +} + +/// Designed to automatically discover (via peeking) the next token, to limit the extent of a +/// match such as `#..x`. +#[derive(Clone)] +pub(crate) enum ParseUntil { + End, + Group(Delimiter), + Ident(Ident), + Punct(Punct), + Literal(Literal), +} + +impl ParseUntil { + /// Peeks the next token, to discover what we should parse next + pub(crate) fn peek_flatten_limit>( + input: ParseStream, + ) -> ParseResult { + if C::should_stop(input) { + return Ok(ParseUntil::End); + } + Ok(match input.peek_grammar() { + SourcePeekMatch::Command(_) + | SourcePeekMatch::Variable(_) + | SourcePeekMatch::Transformer(_) + | SourcePeekMatch::ExplicitTransformStream + | SourcePeekMatch::AppendVariableParser + | SourcePeekMatch::ExpressionBlock(_) => { + return input + .span() + .parse_err("This cannot follow a flattened variable binding"); + } + SourcePeekMatch::Group(delimiter) => ParseUntil::Group(delimiter), + SourcePeekMatch::Ident(ident) => ParseUntil::Ident(ident), + SourcePeekMatch::Literal(literal) => ParseUntil::Literal(literal), + SourcePeekMatch::Punct(punct) => ParseUntil::Punct(punct), + SourcePeekMatch::End => ParseUntil::End, + }) + } + + pub(crate) fn handle_parse_into( + &self, + input: ParseStream, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + match self { + ParseUntil::End => output.extend_raw_tokens(input.parse::()?), + ParseUntil::Group(delimiter) => { + while !input.is_empty() { + if input.peek_specific_group(*delimiter) { + return Ok(()); + } + output.push_raw_token_tree(input.parse()?); + } + } + ParseUntil::Ident(ident) => { + let content = ident.to_string(); + while !input.is_empty() { + if input.peek_ident_matching(&content) { + return Ok(()); + } + output.push_raw_token_tree(input.parse()?); + } + } + ParseUntil::Punct(punct) => { + let punct = punct.as_char(); + while !input.is_empty() { + if input.peek_punct_matching(punct) { + return Ok(()); + } + output.push_raw_token_tree(input.parse()?); + } + } + ParseUntil::Literal(literal) => { + let content = literal.to_string(); + while !input.is_empty() { + if input.peek_literal_matching(&content) { + return Ok(()); + } + output.push_raw_token_tree(input.parse()?); + } + } + } + Ok(()) + } +} diff --git a/src/transformation/patterns.rs b/src/transformation/patterns.rs new file mode 100644 index 00000000..eb4397da --- /dev/null +++ b/src/transformation/patterns.rs @@ -0,0 +1,254 @@ +use crate::internal_prelude::*; + +pub(crate) trait HandleDestructure { + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: ExpressionValue, + ) -> ExecutionResult<()>; +} + +#[derive(Clone)] +pub(crate) enum Pattern { + Variable(VariablePattern), + Array(ArrayPattern), + Object(ObjectPattern), + Stream(ExplicitTransformStream), + DotDot(Token![..]), + #[allow(unused)] + Discarded(Token![_]), +} + +impl Parse for Pattern { + fn parse(input: ParseStream) -> 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(syn::token::Brace) { + Ok(Pattern::Object(input.parse()?)) + } else if lookahead.peek(Token![@]) { + Ok(Pattern::Stream(input.parse()?)) + } else if lookahead.peek(Token![_]) { + Ok(Pattern::Discarded(input.parse()?)) + } else if lookahead.peek(Token![..]) { + Ok(Pattern::DotDot(input.parse()?)) + } else if input.peek(Token![#]) { + input.parse_err("Use `var` instead of `#var` in a destructuring") + } else { + Err(lookahead.error().into()) + } + } +} + +impl HandleDestructure for Pattern { + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: ExpressionValue, + ) -> 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::DotDot(token) => token.execution_err("This cannot be used here"), + Pattern::Discarded(_) => Ok(()), + } + } +} + +#[derive(Clone)] +pub struct ArrayPattern { + #[allow(unused)] + brackets: Brackets, + items: Punctuated, +} + +impl Parse for ArrayPattern { + fn parse(input: ParseStream) -> ParseResult { + let (brackets, inner) = input.parse_brackets()?; + Ok(Self { + brackets, + items: inner.parse_terminated()?, + }) + } +} + +impl HandleDestructure for ArrayPattern { + /// See also `ArrayAssigneeStackFrame` in `evaluation.rs` + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: ExpressionValue, + ) -> ExecutionResult<()> { + let array = value.expect_array("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 { + Pattern::DotDot(dot_dot) => { + if has_seen_dot_dot { + return dot_dot.execution_err("Only one .. is allowed in an array pattern"); + } + has_seen_dot_dot = true; + } + _ => { + 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.execution_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.execution_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(()) + } +} + +#[derive(Clone)] +pub struct ObjectPattern { + #[allow(unused)] + braces: Braces, + entries: Punctuated, +} + +impl Parse for ObjectPattern { + fn parse(input: ParseStream) -> ParseResult { + let (braces, inner) = input.parse_braces()?; + Ok(Self { + braces, + entries: inner.parse_terminated()?, + }) + } +} + +impl HandleDestructure for ObjectPattern { + /// See also `ObjectAssigneeStackFrame` in `evaluation.rs` + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: ExpressionValue, + ) -> ExecutionResult<()> { + let object = value.expect_object("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.execution_err(format!("The key `{}` has already used", key)); + } + let value = value_map + .remove(&key) + .map(|entry| entry.value) + .unwrap_or_else(|| ExpressionValue::None(key_span.span_range())); + already_used_keys.insert(key); + pattern.handle_destructure(interpreter, value)?; + } + Ok(()) + } +} + +#[derive(Clone)] +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 Parse for ObjectEntry { + fn parse(input: ParseStream) -> 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 { + name: field.clone(), + }); + 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\"]: `") + } + } +} diff --git a/src/transformation/transform_stream.rs b/src/transformation/transform_stream.rs new file mode 100644 index 00000000..f262c523 --- /dev/null +++ b/src/transformation/transform_stream.rs @@ -0,0 +1,282 @@ +use crate::internal_prelude::*; + +pub(crate) type TransformStream = TransformSegment; +pub(crate) type TransformStreamUntilToken = TransformSegment>; + +#[derive(Clone)] +pub(crate) struct TransformSegment { + stop_condition: PhantomData, + inner: Vec, +} + +impl> Parse for TransformSegment { + fn parse(input: ParseStream) -> ParseResult { + let mut inner = vec![]; + while !C::should_stop(input) { + inner.push(TransformItem::parse_until::(input)?); + } + Ok(Self { + stop_condition: PhantomData, + inner, + }) + } +} + +impl HandleTransformation for TransformSegment { + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + for item in self.inner.iter() { + item.handle_transform(input, interpreter, output)?; + } + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) enum TransformItem { + Command(Command), + Variable(VariableParserKind), + ExpressionBlock(ExpressionBlock), + Transformer(Transformer), + TransformStreamInput(ExplicitTransformStream), + ExactPunct(Punct), + ExactIdent(Ident), + ExactLiteral(Literal), + ExactGroup(TransformGroup), +} + +impl TransformItem { + /// We provide a stop condition so that some of the items can know when to stop consuming greedily - + /// notably the flattened command. This allows [!let! #..x = Hello => World] to parse as setting + /// `x` to `Hello => World` rather than having `#..x` peeking to see it is "up to =" and then only + /// parsing `Hello` into `x`. + pub(crate) fn parse_until>( + input: ParseStream, + ) -> ParseResult { + Ok(match input.peek_grammar() { + SourcePeekMatch::Command(_) => Self::Command(input.parse()?), + SourcePeekMatch::Variable(_) | SourcePeekMatch::AppendVariableParser => { + Self::Variable(VariableParserKind::parse_until::(input)?) + } + SourcePeekMatch::ExpressionBlock(_) => Self::ExpressionBlock(input.parse()?), + SourcePeekMatch::Group(_) => Self::ExactGroup(input.parse()?), + SourcePeekMatch::ExplicitTransformStream => Self::TransformStreamInput(input.parse()?), + SourcePeekMatch::Transformer(_) => Self::Transformer(input.parse()?), + SourcePeekMatch::Punct(_) => Self::ExactPunct(input.parse_any_punct()?), + SourcePeekMatch::Literal(_) => Self::ExactLiteral(input.parse()?), + SourcePeekMatch::Ident(_) => Self::ExactIdent(input.parse_any_ident()?), + SourcePeekMatch::End => return input.parse_err("Unexpected end"), + }) + } +} + +impl HandleTransformation for TransformItem { + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + match self { + TransformItem::Variable(variable) => { + variable.handle_transform(input, interpreter, output)?; + } + TransformItem::Command(command) => { + command.clone().interpret_into(interpreter, output)?; + } + TransformItem::Transformer(transformer) => { + transformer.handle_transform(input, interpreter, output)?; + } + TransformItem::TransformStreamInput(stream) => { + stream.handle_transform(input, interpreter, output)?; + } + TransformItem::ExpressionBlock(block) => { + block.interpret_into(interpreter, output)?; + } + TransformItem::ExactPunct(punct) => { + input.parse_punct_matching(punct.as_char())?; + } + TransformItem::ExactIdent(ident) => { + input.parse_ident_matching(&ident.to_string())?; + } + TransformItem::ExactLiteral(literal) => { + input.parse_literal_matching(&literal.to_string())?; + } + TransformItem::ExactGroup(group) => { + group.handle_transform(input, interpreter, output)?; + } + } + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct TransformGroup { + delimiter: Delimiter, + inner: TransformStream, +} + +impl Parse for TransformGroup { + fn parse(input: ParseStream) -> ParseResult { + let (delimiter, _, content) = input.parse_any_group()?; + Ok(Self { + delimiter, + inner: content.parse()?, + }) + } +} + +impl HandleTransformation for TransformGroup { + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + 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 ...]` transformer. + if self.delimiter == Delimiter::None { + self.inner.handle_transform(input, interpreter, output) + } else { + let (_, inner) = input.parse_specific_group(self.delimiter)?; + self.inner.handle_transform(&inner, interpreter, output) + } + } +} + +#[derive(Clone)] +pub(crate) struct ExplicitTransformStream { + #[allow(unused)] + transformer_token: Token![@], + #[allow(unused)] + parentheses: Parentheses, + arguments: ExplicitTransformStreamArguments, +} + +#[derive(Clone)] +pub(crate) enum ExplicitTransformStreamArguments { + Output { + content: TransformStream, + }, + StoreToVariable { + variable: GroupedVariable, + #[allow(unused)] + equals: Token![=], + content: TransformStream, + }, + ExtendToVariable { + variable: GroupedVariable, + #[allow(unused)] + plus_equals: Token![+=], + content: TransformStream, + }, + Discard { + #[allow(unused)] + discard: Token![_], + #[allow(unused)] + equals: Token![=], + content: TransformStream, + }, +} + +impl Parse for ExplicitTransformStreamArguments { + fn parse(input: ParseStream) -> ParseResult { + if input.peek(Token![_]) { + return Ok(Self::Discard { + discard: input.parse()?, + equals: input.parse()?, + content: input.parse()?, + }); + } + if input.peek(Token![#]) { + let variable = input.parse()?; + let lookahead = input.lookahead1(); + if lookahead.peek(Token![=]) { + return Ok(Self::StoreToVariable { + variable, + equals: input.parse()?, + content: input.parse()?, + }); + } + if lookahead.peek(Token![+=]) { + return Ok(Self::ExtendToVariable { + variable, + plus_equals: input.parse()?, + content: input.parse()?, + }); + } + Err(lookahead.error())?; + } + Ok(Self::Output { + content: input.parse()?, + }) + } +} + +impl Parse for ExplicitTransformStream { + fn parse(input: ParseStream) -> ParseResult { + let transformer_token = input.parse()?; + let (parentheses, content) = input.parse_parentheses()?; + + Ok(Self { + transformer_token, + parentheses, + arguments: content.parse()?, + }) + } +} + +impl HandleTransformation for ExplicitTransformStream { + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + match &self.arguments { + ExplicitTransformStreamArguments::Output { content } => { + content.handle_transform(input, interpreter, output)?; + } + ExplicitTransformStreamArguments::StoreToVariable { + variable, content, .. + } => { + let mut new_output = OutputStream::new(); + content.handle_transform(input, interpreter, &mut new_output)?; + variable.define(interpreter, new_output); + } + ExplicitTransformStreamArguments::ExtendToVariable { + variable, content, .. + } => { + let reference = variable.binding(interpreter)?; + content.handle_transform( + input, + interpreter, + reference.into_mut()?.into_stream()?.as_mut(), + )?; + } + ExplicitTransformStreamArguments::Discard { content, .. } => { + let mut discarded = OutputStream::new(); + content.handle_transform(input, interpreter, &mut discarded)?; + } + } + Ok(()) + } +} + +impl HandleDestructure for ExplicitTransformStream { + fn handle_destructure( + &self, + interpreter: &mut Interpreter, + value: ExpressionValue, + ) -> ExecutionResult<()> { + let stream = value.expect_stream("The destructure source")?; + let mut discarded = OutputStream::new(); + self.handle_transform_from_stream(stream.value, interpreter, &mut discarded)?; + Ok(()) + } +} diff --git a/src/transformation/transformation_traits.rs b/src/transformation/transformation_traits.rs new file mode 100644 index 00000000..6d964469 --- /dev/null +++ b/src/transformation/transformation_traits.rs @@ -0,0 +1,24 @@ +use crate::internal_prelude::*; + +pub(crate) trait HandleTransformation { + fn handle_transform_from_stream( + &self, + input: OutputStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + unsafe { + // RUST-ANALYZER-SAFETY: ...this isn't generally safe... + // We should only do this when we know that either the input or parser doesn't require + // analysis of nested None-delimited groups. + input.parse_with(|input| self.handle_transform(input, interpreter, output)) + } + } + + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()>; +} diff --git a/src/transformation/transformer.rs b/src/transformation/transformer.rs new file mode 100644 index 00000000..34b777ff --- /dev/null +++ b/src/transformation/transformer.rs @@ -0,0 +1,231 @@ +use crate::internal_prelude::*; + +pub(crate) trait TransformerDefinition: Clone { + const TRANSFORMER_NAME: &'static str; + fn parse(arguments: TransformerArguments) -> ParseResult; + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()>; +} + +#[derive(Clone)] +pub(crate) struct TransformerArguments<'a> { + parse_stream: ParseStream<'a, Source>, + transformer_name: Ident, + full_span: Span, +} + +#[allow(unused)] +impl<'a> TransformerArguments<'a> { + pub(crate) fn new( + parse_stream: ParseStream<'a, Source>, + transformer_name: Ident, + full_span: Span, + ) -> Self { + Self { + parse_stream, + transformer_name, + full_span, + } + } + + pub(crate) fn full_span(&self) -> Span { + self.full_span + } + + /// We use this instead of the "unexpected / drop glue" pattern in order to give a better error message + pub(crate) fn assert_empty(&self, error_message: impl std::fmt::Display) -> ParseResult<()> { + if self.parse_stream.is_empty() { + Ok(()) + } else { + self.full_span.parse_err(error_message) + } + } + + pub(crate) fn fully_parse_no_error_override>(&self) -> ParseResult { + self.parse_stream.parse() + } + + pub(crate) fn fully_parse_as(&self) -> ParseResult { + self.fully_parse_or_error(T::parse, T::error_message()) + } + + pub(crate) fn fully_parse_or_error( + &self, + parse_function: impl FnOnce(ParseStream) -> ParseResult, + error_message: impl std::fmt::Display, + ) -> ParseResult { + // In future, when the diagnostic API is stable, + // we can add this context directly onto the transformer ident... + // Rather than just selectively adding it to the inner-most error. + // + // For now though, we can add additional context to the error message. + // But we can avoid adding this additional context if it's already been added in an + // inner error, because that's likely the correct local context to show. + let parsed = + parse_function(self.parse_stream).add_context_if_error_and_no_context(|| { + format!( + "Occurred whilst parsing @[{} ...] - {}", + self.transformer_name, error_message, + ) + })?; + + self.assert_empty(error_message)?; + + Ok(parsed) + } +} + +#[derive(Clone)] +pub(crate) struct Transformer { + #[allow(unused)] + transformer_token: Token![@], + instance: NamedTransformer, + #[allow(unused)] + source_brackets: Option, +} + +impl Parse for Transformer { + fn parse(input: ParseStream) -> ParseResult { + let transformer_token = input.parse()?; + + let (name, arguments) = if input.cursor().ident().is_some() { + let ident = input.parse_any_ident()?; + (ident, None) + } else if input.peek_specific_group(Delimiter::Bracket) { + let (brackets, content) = input.parse_brackets()?; + let ident = content.parse_any_ident()?; + (ident, Some((content, brackets))) + } else { + return input.parse_err("Expected @TRANSFORMER or @[TRANSFORMER ...arguments...]"); + }; + + let transformer_kind = match TransformerKind::for_ident(&name) { + Some(transformer_kind) => transformer_kind, + None => name.span().err( + format!( + "Expected `@NAME` or `@[NAME ...arguments...]` for NAME one of: {}.\nIf this wasn't intended to be a named transformer, you can work around this by replacing the @ with @(_ = @[EXACT [!raw! @]])", + TransformerKind::list_all(), + ), + )?, + }; + + match arguments { + Some((buffer, brackets)) => { + let arguments = TransformerArguments::new(&buffer, name, brackets.join()); + Ok(Self { + transformer_token, + instance: transformer_kind.parse_instance(arguments)?, + source_brackets: Some(brackets), + }) + } + None => { + let span = name.span(); + let instance = TokenStream::new() + .source_parse_with(|parse_stream| { + let arguments = TransformerArguments::new(parse_stream, name.clone(), span); + transformer_kind.parse_instance(arguments) + }) + .map_err(|original_error| { + let replacement = span.parse_error( + format!("This transformer requires arguments, and should be invoked as @[{} ...]", name), + ); + if let Some(context) = original_error.context() { + replacement.add_context_if_none(context) + } else { + replacement + } + })?; + Ok(Self { + transformer_token, + instance, + source_brackets: None, + }) + } + } + } +} + +impl HandleTransformation for Transformer { + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + self.instance.handle_transform(input, interpreter, output) + } +} + +macro_rules! define_transformers { + ( + $( + $transformer:ident, + )* + ) => { + #[allow(clippy::enum_variant_names)] + #[derive(Clone, Copy)] + pub(crate) enum TransformerKind { + $( + $transformer, + )* + } + + impl TransformerKind { + fn parse_instance(&self, arguments: TransformerArguments) -> ParseResult { + Ok(match self { + $( + Self::$transformer => NamedTransformer::$transformer( + $transformer::parse(arguments)? + ), + )* + }) + } + + pub(crate) fn for_ident(ident: &Ident) -> Option { + Some(match ident.to_string().as_ref() { + $( + $transformer::TRANSFORMER_NAME => Self::$transformer, + )* + _ => return None, + }) + } + + const ALL_KIND_NAMES: &'static [&'static str] = &[$($transformer::TRANSFORMER_NAME,)*]; + + pub(crate) fn list_all() -> String { + // TODO: Add "and" at the end + Self::ALL_KIND_NAMES.join(", ") + } + } + + #[derive(Clone)] + #[allow(clippy::enum_variant_names)] + pub(crate) enum NamedTransformer { + $( + $transformer($transformer), + )* + } + + impl NamedTransformer { + fn handle_transform(&self, input: ParseStream, interpreter: &mut Interpreter, output: &mut OutputStream) -> ExecutionResult<()> { + match self { + $( + Self::$transformer(transformer) => transformer.handle_transform(input, interpreter, output), + )* + } + } + } + }; +} + +define_transformers! { + IdentTransformer, + LiteralTransformer, + PunctTransformer, + GroupTransformer, + ExactTransformer, +} diff --git a/src/transformation/transformers.rs b/src/transformation/transformers.rs new file mode 100644 index 00000000..2fe3a07e --- /dev/null +++ b/src/transformation/transformers.rs @@ -0,0 +1,130 @@ +use crate::internal_prelude::*; + +#[derive(Clone)] +pub(crate) struct IdentTransformer; + +impl TransformerDefinition for IdentTransformer { + const TRANSFORMER_NAME: &'static str = "IDENT"; + + fn parse(arguments: TransformerArguments) -> ParseResult { + arguments.fully_parse_or_error(|_| Ok(Self), "Expected @IDENT or @[IDENT]") + } + + fn handle_transform( + &self, + input: ParseStream, + _: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + if input.cursor().ident().is_some() { + output.push_ident(input.parse_any_ident()?); + Ok(()) + } else { + input.parse_err("Expected an ident")? + } + } +} + +#[derive(Clone)] +pub(crate) struct LiteralTransformer; + +impl TransformerDefinition for LiteralTransformer { + const TRANSFORMER_NAME: &'static str = "LITERAL"; + + fn parse(arguments: TransformerArguments) -> ParseResult { + arguments.fully_parse_or_error(|_| Ok(Self), "Expected @LITERAL or @[LITERAL]") + } + + fn handle_transform( + &self, + input: ParseStream, + _: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + if input.cursor().literal().is_some() { + output.push_literal(input.parse()?); + Ok(()) + } else { + input.parse_err("Expected a literal")? + } + } +} + +#[derive(Clone)] +pub(crate) struct PunctTransformer; + +impl TransformerDefinition for PunctTransformer { + const TRANSFORMER_NAME: &'static str = "PUNCT"; + + fn parse(arguments: TransformerArguments) -> ParseResult { + arguments.fully_parse_or_error(|_| Ok(Self), "Expected @PUNCT or @[PUNCT]") + } + + fn handle_transform( + &self, + input: ParseStream, + _: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + if input.cursor().any_punct().is_some() { + output.push_punct(input.parse_any_punct()?); + Ok(()) + } else { + input.parse_err("Expected a punct")? + } + } +} + +#[derive(Clone)] +pub(crate) struct GroupTransformer { + inner: TransformStream, +} + +impl TransformerDefinition for GroupTransformer { + const TRANSFORMER_NAME: &'static str = "GROUP"; + + fn parse(arguments: TransformerArguments) -> ParseResult { + Ok(Self { + inner: arguments.fully_parse_no_error_override()?, + }) + } + + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + let (_, inner) = input.parse_transparent_group()?; + self.inner.handle_transform(&inner, interpreter, output) + } +} + +#[derive(Clone)] +pub(crate) struct ExactTransformer { + stream: ExactStream, +} + +impl TransformerDefinition for ExactTransformer { + const TRANSFORMER_NAME: &'static str = "EXACT"; + + fn parse(arguments: TransformerArguments) -> ParseResult { + arguments.fully_parse_or_error( + |input| { + Ok(Self { + stream: ExactStream::parse(input)?, + }) + }, + "Expected @[EXACT ... interpretable input to be matched exactly ...]", + ) + } + + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + output: &mut OutputStream, + ) -> ExecutionResult<()> { + self.stream.handle_transform(input, interpreter, output) + } +} diff --git a/src/transformation/variable_parser.rs b/src/transformation/variable_parser.rs new file mode 100644 index 00000000..b65eea6c --- /dev/null +++ b/src/transformation/variable_parser.rs @@ -0,0 +1,272 @@ +use crate::internal_prelude::*; + +/// We have the following write modes: +/// * `#x` - Reads a token tree, writes a stream (opposite of #x) +/// * `#..x` - Reads a stream, writes a stream (opposite of #..x) +/// +/// And the following append modes: +/// * `#>>x` - Reads a token tree, appends a token tree (opposite of for #y in #x) +/// * `#>>..x` - Reads a token tree, appends a stream (i.e. flatten it if it's a group) +/// * `#..>>x` - Reads a stream, appends a group (opposite of for #y in #x) +/// * `#..>>..x` - Reads a stream, appends a stream +#[derive(Clone)] +#[allow(unused)] +pub(crate) enum VariableParserKind { + /// #x - Reads a token tree, writes a stream (opposite of #x) + Grouped { marker: Token![#], name: Ident }, + /// #..x - Reads a stream, writes a stream (opposite of #..x) + Flattened { + marker: Token![#], + flatten: Token![..], + name: Ident, + until: ParseUntil, + }, + /// #>>x - Reads a token tree, appends the token tree (allows reading with !for! #y in #x) + GroupedAppendGrouped { + marker: Token![#], + append: Token![>>], + name: Ident, + }, + /// #>>..x - Reads a token tree, appends it flattened if its a group + GroupedAppendFlattened { + marker: Token![#], + append: Token![>>], + flattened_write: Token![..], + name: Ident, + }, + /// #..>>x - Reads a stream, appends a group (allows reading with !for! #y in #x) + FlattenedAppendGrouped { + marker: Token![#], + flattened_read: Token![..], + append: Token![>>], + name: Ident, + until: ParseUntil, + }, + /// #..>>..x - Reads a stream, appends a stream + FlattenedAppendFlattened { + marker: Token![#], + flattened_read: Token![..], + append: Token![>>], + flattened_write: Token![..], + name: Ident, + until: ParseUntil, + }, +} + +impl VariableParserKind { + #[allow(unused)] + pub(crate) fn parse_only_unflattened_input(input: ParseStream) -> ParseResult { + let variable: VariableParserKind = Self::parse_until::(input)?; + if variable.is_flattened_input() { + return variable + .span_range() + .parse_err("A flattened input variable is not supported here"); + } + Ok(variable) + } + + pub(crate) fn parse_until>( + input: ParseStream, + ) -> ParseResult { + let marker = input.parse()?; + if input.peek(Token![..]) { + let flatten = input.parse()?; + if input.peek(Token![>>]) { + let append = input.parse()?; + if input.peek(Token![..]) { + let flattened_write = input.parse()?; + let name = input.parse()?; + let until = ParseUntil::peek_flatten_limit::(input)?; + return Ok(Self::FlattenedAppendFlattened { + marker, + flattened_read: flatten, + append, + flattened_write, + name, + until, + }); + } + let name = input.parse()?; + let until = ParseUntil::peek_flatten_limit::(input)?; + return Ok(Self::FlattenedAppendGrouped { + marker, + flattened_read: flatten, + append, + name, + until, + }); + } + let name = input.parse()?; + let until = ParseUntil::peek_flatten_limit::(input)?; + return Ok(Self::Flattened { + marker, + flatten, + name, + until, + }); + } + if input.peek(Token![>>]) { + let append = input.parse()?; + if input.peek(Token![..]) { + let flattened_write = input.parse()?; + let name = input.parse()?; + return Ok(Self::GroupedAppendFlattened { + marker, + append, + flattened_write, + name, + }); + } + let name = input.parse()?; + return Ok(Self::GroupedAppendGrouped { + marker, + append, + name, + }); + } + let name = input.parse()?; + Ok(Self::Grouped { marker, name }) + } +} + +impl IsVariable for VariableParserKind { + fn get_name(&self) -> String { + let name_ident = match self { + VariableParserKind::Grouped { name, .. } => name, + VariableParserKind::Flattened { name, .. } => name, + VariableParserKind::GroupedAppendGrouped { name, .. } => name, + VariableParserKind::GroupedAppendFlattened { name, .. } => name, + VariableParserKind::FlattenedAppendGrouped { name, .. } => name, + VariableParserKind::FlattenedAppendFlattened { name, .. } => name, + }; + name_ident.to_string() + } +} + +impl HasSpanRange for VariableParserKind { + fn span_range(&self) -> SpanRange { + let (marker, name) = match self { + VariableParserKind::Grouped { marker, name, .. } => (marker, name), + VariableParserKind::Flattened { marker, name, .. } => (marker, name), + VariableParserKind::GroupedAppendGrouped { marker, name, .. } => (marker, name), + VariableParserKind::GroupedAppendFlattened { marker, name, .. } => (marker, name), + VariableParserKind::FlattenedAppendGrouped { marker, name, .. } => (marker, name), + VariableParserKind::FlattenedAppendFlattened { marker, name, .. } => (marker, name), + }; + SpanRange::new_between(marker.span, name.span()) + } +} + +impl VariableParserKind { + pub(crate) fn is_flattened_input(&self) -> bool { + matches!( + self, + VariableParserKind::Flattened { .. } + | VariableParserKind::FlattenedAppendGrouped { .. } + | VariableParserKind::FlattenedAppendFlattened { .. } + ) + } +} + +impl HandleTransformation for VariableParserKind { + fn handle_transform( + &self, + input: ParseStream, + interpreter: &mut Interpreter, + _: &mut OutputStream, + ) -> ExecutionResult<()> { + match self { + VariableParserKind::Grouped { .. } => { + let content = input.parse::()?.into_interpreted(); + self.define_coerced(interpreter, content); + } + VariableParserKind::Flattened { until, .. } => { + let mut content = OutputStream::new(); + until.handle_parse_into(input, &mut content)?; + self.define_coerced(interpreter, content); + } + VariableParserKind::GroupedAppendGrouped { .. } => { + let reference = self.binding(interpreter)?; + input + .parse::()? + .push_as_token_tree(reference.into_mut()?.into_stream()?.as_mut()); + } + VariableParserKind::GroupedAppendFlattened { .. } => { + let reference = self.binding(interpreter)?; + input + .parse::()? + .flatten_into(reference.into_mut()?.into_stream()?.as_mut()); + } + VariableParserKind::FlattenedAppendGrouped { marker, until, .. } => { + let reference = self.binding(interpreter)?; + reference.into_mut()?.into_stream()?.as_mut().push_grouped( + |inner| until.handle_parse_into(input, inner), + Delimiter::None, + marker.span, + )?; + } + VariableParserKind::FlattenedAppendFlattened { until, .. } => { + let reference = self.binding(interpreter)?; + until.handle_parse_into(input, reference.into_mut()?.into_stream()?.as_mut())?; + } + } + Ok(()) + } +} + +enum ParsedTokenTree { + NoneGroup(Group), + Ident(Ident), + Punct(Punct), + Literal(Literal), +} + +impl ParsedTokenTree { + fn into_interpreted(self) -> OutputStream { + match self { + ParsedTokenTree::NoneGroup(group) => OutputStream::raw(group.stream()), + ParsedTokenTree::Ident(ident) => OutputStream::raw(ident.to_token_stream()), + ParsedTokenTree::Punct(punct) => OutputStream::raw(punct.to_token_stream()), + ParsedTokenTree::Literal(literal) => OutputStream::raw(literal.to_token_stream()), + } + } + + fn push_as_token_tree(self, output: &mut OutputStream) { + match self { + ParsedTokenTree::NoneGroup(group) => { + output.push_raw_token_tree(TokenTree::Group(group)) + } + ParsedTokenTree::Ident(ident) => output.push_ident(ident), + ParsedTokenTree::Punct(punct) => output.push_punct(punct), + ParsedTokenTree::Literal(literal) => output.push_literal(literal), + } + } + + fn flatten_into(self, output: &mut OutputStream) { + match self { + ParsedTokenTree::NoneGroup(group) => output.extend_raw_tokens(group.stream()), + ParsedTokenTree::Ident(ident) => output.push_ident(ident), + ParsedTokenTree::Punct(punct) => output.push_punct(punct), + ParsedTokenTree::Literal(literal) => output.push_literal(literal), + } + } +} + +impl Parse for ParsedTokenTree { + fn parse(input: ParseStream) -> ParseResult { + Ok(match input.parse::()? { + TokenTree::Group(group) if group.delimiter() == Delimiter::None => { + ParsedTokenTree::NoneGroup(group) + } + TokenTree::Group(group) => { + return group + .delim_span() + .open() + .parse_err("Expected a group with transparent delimiters"); + } + TokenTree::Ident(ident) => ParsedTokenTree::Ident(ident), + TokenTree::Punct(punct) => ParsedTokenTree::Punct(punct), + TokenTree::Literal(literal) => ParsedTokenTree::Literal(literal), + }) + } +} 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..273fb4ae --- /dev/null +++ b/tests/compilation_failures/complex/nested.rs @@ -0,0 +1,13 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!if! true { + [!if! true { + [!if! true { + [!error! { + // Missing message + }] + }] + }] + }]); +} \ 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..2d08d99a --- /dev/null +++ b/tests/compilation_failures/complex/nested.stderr @@ -0,0 +1,15 @@ +error: Expected: + { + // The error message to display + message: "...", + // An optional [token stream], to determine where to show the error message + spans?: [!stream! $abc], + } + The following required field/s are missing: message + --> tests/compilation_failures/complex/nested.rs:7:26 + | +7 | [!error! { + | __________________________^ +8 | | // Missing message +9 | | }] + | |_________________^ 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..a8844658 --- /dev/null +++ b/tests/compilation_failures/control_flow/break_outside_a_loop.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!(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..a134bbda --- /dev/null +++ b/tests/compilation_failures/control_flow/break_outside_a_loop.stderr @@ -0,0 +1,5 @@ +error: Break can only be used inside a loop + --> tests/compilation_failures/control_flow/break_outside_a_loop.rs:4:23 + | +4 | preinterpret!(1 + [!break!] 2); + | ^^^^^^^^^ 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..f3275414 --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_outside_a_loop.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!(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..c486fb6c --- /dev/null +++ b/tests/compilation_failures/control_flow/continue_outside_a_loop.stderr @@ -0,0 +1,5 @@ +error: Continue can only be used inside a loop + --> tests/compilation_failures/control_flow/continue_outside_a_loop.rs:4:23 + | +4 | preinterpret!(1 + [!continue!] 2); + | ^^^^^^^^^^^^ 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..04d06ac5 --- /dev/null +++ b/tests/compilation_failures/control_flow/error_after_continue.rs @@ -0,0 +1,19 @@ +use preinterpret::*; + +fn main() { + preinterpret!( + #(let x = 0) + [!while! true { + #(x += 1) + [!if! x == 3 { + [!continue!] + } !elif! x >= 3 { + // This checks that the "continue" flag is consumed, + // and future errors propagate correctly. + [!error! { + message: "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..b98f5442 --- /dev/null +++ b/tests/compilation_failures/control_flow/error_after_continue.stderr @@ -0,0 +1,13 @@ +error: And now we error + --> tests/compilation_failures/control_flow/error_after_continue.rs:4:5 + | +4 | / preinterpret!( +5 | | #(let x = 0) +6 | | [!while! true { +7 | | #(x += 1) +... | +17 | | }] +18 | | ); + | |_____^ + | + = note: this error originates in the macro `preinterpret` (in Nightly builds, run with -Z macro-backtrace for more info) 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..ad2ad9de --- /dev/null +++ b/tests/compilation_failures/control_flow/while_infinite_loop.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!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..a9138322 --- /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 [!settings! { iteration_limit: X }] + --> tests/compilation_failures/control_flow/while_infinite_loop.rs:4:33 + | +4 | preinterpret!([!while! true {}]); + | ^^ diff --git a/tests/compilation_failures/core/error_invalid_structure.rs b/tests/compilation_failures/core/error_invalid_structure.rs new file mode 100644 index 00000000..f5b1aa27 --- /dev/null +++ b/tests/compilation_failures/core/error_invalid_structure.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!error! { }]); +} \ No newline at end of file diff --git a/tests/compilation_failures/core/error_invalid_structure.stderr b/tests/compilation_failures/core/error_invalid_structure.stderr new file mode 100644 index 00000000..aa2d9809 --- /dev/null +++ b/tests/compilation_failures/core/error_invalid_structure.stderr @@ -0,0 +1,12 @@ +error: Expected: + { + // The error message to display + message: "...", + // An optional [token stream], to determine where to show the error message + spans?: [!stream! $abc], + } + The following required field/s are missing: message + --> tests/compilation_failures/core/error_invalid_structure.rs:4:28 + | +4 | preinterpret!([!error! { }]); + | ^^^ 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..8c4aca90 --- /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) => {preinterpret!{ + [!if! ($input1 != $input2) { + [!error! "Expected " $input1 " to equal " $input2] + }] + }}; +} + +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..332f8824 --- /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) => {preinterpret!{ + | ____________________________________________^ +5 | | [!if! ($input1 != $input2) { +6 | | [!error! "Expected " $input1 " to equal " $input2] +7 | | }] +8 | | }}; + | |_____^ +... +12 | assert_literals_eq!(102, 64); + | ---------------------------- in this macro invocation + | + = note: this error originates in the macro `preinterpret` 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..2471f8e9 --- /dev/null +++ b/tests/compilation_failures/core/error_no_span.rs @@ -0,0 +1,15 @@ +use preinterpret::*; + +macro_rules! assert_literals_eq_no_spans { + ($input1:literal and $input2:literal) => {preinterpret!{ + [!if! ($input1 != $input2) { + [!error! { + message: [!string! "Expected " $input1 " to equal " $input2], + }] + }] + }}; +} + +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..f29628b2 --- /dev/null +++ b/tests/compilation_failures/core/error_no_span.stderr @@ -0,0 +1,17 @@ +error: Expected 102 to equal 64 + --> tests/compilation_failures/core/error_no_span.rs:4:47 + | +4 | ($input1:literal and $input2:literal) => {preinterpret!{ + | _______________________________________________^ +5 | | [!if! ($input1 != $input2) { +6 | | [!error! { +7 | | message: [!string! "Expected " $input1 " to equal " $input2], +8 | | }] +9 | | }] +10 | | }}; + | |_____^ +... +14 | assert_literals_eq_no_spans!(102 and 64); + | ---------------------------------------- in this macro invocation + | + = note: this error originates in the macro `preinterpret` 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..ac86a85b --- /dev/null +++ b/tests/compilation_failures/core/error_span_multiple.rs @@ -0,0 +1,16 @@ +use preinterpret::*; + +macro_rules! assert_literals_eq { + ($input1:literal and $input2:literal) => {preinterpret!{ + [!if! ($input1 != $input2) { + [!error! { + message: [!string! "Expected " $input1 " to equal " $input2], + spans: [!stream! $input1, $input2], + }] + }] + }}; +} + +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..33d69e86 --- /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:15:25 + | +15 | 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..36832d75 --- /dev/null +++ b/tests/compilation_failures/core/error_span_repeat.rs @@ -0,0 +1,17 @@ +use preinterpret::*; + +macro_rules! assert_input_length_of_3 { + ($($input:literal)+) => {preinterpret!{ + [!set! #input_length = [!length! $($input)+]]; + [!if! input_length != 3 { + [!error! { + message: [!string! "Expected 3 inputs, got " #input_length], + spans: [!stream! $($input)+], + }] + }] + }}; +} + +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..93939f2f --- /dev/null +++ b/tests/compilation_failures/core/error_span_repeat.stderr @@ -0,0 +1,10 @@ +error: Cannot infer common type from stream != untyped integer. Consider using `as` to cast the operands to matching types. + --> tests/compilation_failures/core/error_span_repeat.rs:6:28 + | +6 | [!if! input_length != 3 { + | ^^ +... +16 | assert_input_length_of_3!(42 101 666 1024); + | ------------------------------------------ in this macro invocation + | + = note: this error originates in the macro `assert_input_length_of_3` (in Nightly builds, run with -Z macro-backtrace for more info) 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..a4cfad38 --- /dev/null +++ b/tests/compilation_failures/core/error_span_single.rs @@ -0,0 +1,16 @@ +use preinterpret::*; + +macro_rules! assert_is_100 { + ($input:literal) => {preinterpret!{ + [!if! ($input != 100) { + [!error! { + message: [!string! "Expected 100, got " $input], + spans: [!stream! $input], + }] + }] + }}; +} + +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..476323e0 --- /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:15:20 + | +15 | assert_is_100!(5); + | ^ diff --git a/tests/compilation_failures/core/extend_flattened_variable.rs b/tests/compilation_failures/core/extend_flattened_variable.rs new file mode 100644 index 00000000..b8004fe0 --- /dev/null +++ b/tests/compilation_failures/core/extend_flattened_variable.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + preinterpret! { + [!set! #variable = 1] + [!set! #..variable += 2] + } +} \ No newline at end of file diff --git a/tests/compilation_failures/core/extend_flattened_variable.stderr b/tests/compilation_failures/core/extend_flattened_variable.stderr new file mode 100644 index 00000000..28fcacf2 --- /dev/null +++ b/tests/compilation_failures/core/extend_flattened_variable.stderr @@ -0,0 +1,6 @@ +error: Expected #variable + Occurred whilst parsing [!set! ...] - Expected [!set! #var1 = ...] or [!set! #var1 += ...] or [!set! _ = ...] or [!set! #var1, #var2] + --> tests/compilation_failures/core/extend_flattened_variable.rs:6:16 + | +6 | [!set! #..variable += 2] + | ^ 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..eafb285c --- /dev/null +++ b/tests/compilation_failures/core/extend_non_existing_variable.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + preinterpret! { + [!set! #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..50d3ba93 --- /dev/null +++ b/tests/compilation_failures/core/extend_non_existing_variable.stderr @@ -0,0 +1,5 @@ +error: The variable does not already exist in the current scope + --> tests/compilation_failures/core/extend_non_existing_variable.rs:5:16 + | +5 | [!set! #variable += 2] + | ^^^^^^^^^ diff --git a/tests/compilation_failures/core/extend_variable_and_then_extend_it_again.rs b/tests/compilation_failures/core/extend_variable_and_then_extend_it_again.rs new file mode 100644 index 00000000..2019d567 --- /dev/null +++ b/tests/compilation_failures/core/extend_variable_and_then_extend_it_again.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + preinterpret! { + [!set! #variable = Hello] + [!set! #variable += World [!set! #variable += !]] + } +} \ No newline at end of file diff --git a/tests/compilation_failures/core/extend_variable_and_then_extend_it_again.stderr b/tests/compilation_failures/core/extend_variable_and_then_extend_it_again.stderr new file mode 100644 index 00000000..738eacd7 --- /dev/null +++ b/tests/compilation_failures/core/extend_variable_and_then_extend_it_again.stderr @@ -0,0 +1,5 @@ +error: The variable cannot be modified as it is already being modified + --> tests/compilation_failures/core/extend_variable_and_then_extend_it_again.rs:6:42 + | +6 | [!set! #variable += World [!set! #variable += !]] + | ^^^^^^^^^ diff --git a/tests/compilation_failures/core/extend_variable_and_then_read_it.rs b/tests/compilation_failures/core/extend_variable_and_then_read_it.rs new file mode 100644 index 00000000..b016d73a --- /dev/null +++ b/tests/compilation_failures/core/extend_variable_and_then_read_it.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + preinterpret! { + [!set! #variable = Hello] + [!set! #variable += World #variable] + } +} \ No newline at end of file diff --git a/tests/compilation_failures/core/extend_variable_and_then_read_it.stderr b/tests/compilation_failures/core/extend_variable_and_then_read_it.stderr new file mode 100644 index 00000000..38bfe725 --- /dev/null +++ b/tests/compilation_failures/core/extend_variable_and_then_read_it.stderr @@ -0,0 +1,5 @@ +error: The variable cannot be read as it is already being modified + --> tests/compilation_failures/core/extend_variable_and_then_read_it.rs:6:35 + | +6 | [!set! #variable += World #variable] + | ^^^^^^^^^ diff --git a/tests/compilation_failures/core/extend_variable_and_then_set_it.rs b/tests/compilation_failures/core/extend_variable_and_then_set_it.rs new file mode 100644 index 00000000..ca180ac7 --- /dev/null +++ b/tests/compilation_failures/core/extend_variable_and_then_set_it.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + preinterpret! { + [!set! #variable = Hello] + [!set! #variable += World #(variable = [!stream! Hello2])] + } +} \ No newline at end of file diff --git a/tests/compilation_failures/core/extend_variable_and_then_set_it.stderr b/tests/compilation_failures/core/extend_variable_and_then_set_it.stderr new file mode 100644 index 00000000..793c8290 --- /dev/null +++ b/tests/compilation_failures/core/extend_variable_and_then_set_it.stderr @@ -0,0 +1,5 @@ +error: The variable cannot be modified as it is already being modified + --> tests/compilation_failures/core/extend_variable_and_then_set_it.rs:6:37 + | +6 | [!set! #variable += World #(variable = [!stream! Hello2])] + | ^^^^^^^^ diff --git a/tests/compilation_failures/core/set_flattened_variable.rs b/tests/compilation_failures/core/set_flattened_variable.rs new file mode 100644 index 00000000..f1264dcf --- /dev/null +++ b/tests/compilation_failures/core/set_flattened_variable.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + preinterpret! { + [!set! #..variable = 1] + } +} \ No newline at end of file diff --git a/tests/compilation_failures/core/set_flattened_variable.stderr b/tests/compilation_failures/core/set_flattened_variable.stderr new file mode 100644 index 00000000..c26ff84e --- /dev/null +++ b/tests/compilation_failures/core/set_flattened_variable.stderr @@ -0,0 +1,6 @@ +error: Expected #variable + Occurred whilst parsing [!set! ...] - Expected [!set! #var1 = ...] or [!set! #var1 += ...] or [!set! _ = ...] or [!set! #var1, #var2] + --> tests/compilation_failures/core/set_flattened_variable.rs:5:16 + | +5 | [!set! #..variable = 1] + | ^ 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..406635e9 --- /dev/null +++ b/tests/compilation_failures/core/settings_update_iteration_limit.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + preinterpret!{ + [!settings! { 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..49f34ac4 --- /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 [!settings! { iteration_limit: X }] + --> tests/compilation_failures/core/settings_update_iteration_limit.rs:6:17 + | +6 | [!loop! {}] + | ^^ 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..6532d39d --- /dev/null +++ b/tests/compilation_failures/expressions/add_float_and_int.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..cf52f4cc --- /dev/null +++ b/tests/compilation_failures/expressions/add_float_and_int.stderr @@ -0,0 +1,5 @@ +error: Cannot infer common type from untyped float + untyped integer. Consider using `as` to cast the operands to matching types. + --> tests/compilation_failures/expressions/add_float_and_int.rs:5:15 + | +5 | #(1.2 + 1) + | ^ 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..62895890 --- /dev/null +++ b/tests/compilation_failures/expressions/array_missing_comma.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #([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..ffc29e16 --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_1.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..2a86b522 --- /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:15 + | +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..b10f9848 --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_2.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..552f5916 --- /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:15 + | +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..d03f3c4f --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_element_mismatch_3.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..873bd7d4 --- /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:15 + | +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..35ef4683 --- /dev/null +++ b/tests/compilation_failures/expressions/array_pattern_destructure_multiple_dot_dots.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..7569b8ee --- /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:26 + | +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..3ee71560 --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_1.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #([_, _, _] = [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..fde742e8 --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_2.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #([_] = [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..4afec6f3 --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_element_mismatch_3.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #([_, _, .., _] = [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..559db0a6 --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_multiple_dot_dots.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #([_, .., _, .., _] = [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..e6c1fa0a --- /dev/null +++ b/tests/compilation_failures/expressions/array_place_destructure_multiple_muts.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..fe0f4c69 --- /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:5:33 + | +5 | #(let arr = [0, 1]; arr[arr[1]] = 3) + | ^^^ diff --git a/tests/compilation_failures/expressions/cannot_output_array_to_stream.rs b/tests/compilation_failures/expressions/cannot_output_array_to_stream.rs new file mode 100644 index 00000000..a33b5c7e --- /dev/null +++ b/tests/compilation_failures/expressions/cannot_output_array_to_stream.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #([1, 2, 3]) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/cannot_output_array_to_stream.stderr b/tests/compilation_failures/expressions/cannot_output_array_to_stream.stderr new file mode 100644 index 00000000..58705eb1 --- /dev/null +++ b/tests/compilation_failures/expressions/cannot_output_array_to_stream.stderr @@ -0,0 +1,5 @@ +error: Arrays cannot be output to a stream. You likely wish to use the !for! command or if you wish to output every element, use `#(XXX as stream)` to cast the array to a stream. + --> tests/compilation_failures/expressions/cannot_output_array_to_stream.rs:5:11 + | +5 | #([1, 2, 3]) + | ^^^^^^^^^ diff --git a/tests/compilation_failures/expressions/cannot_output_object_to_stream.rs b/tests/compilation_failures/expressions/cannot_output_object_to_stream.rs new file mode 100644 index 00000000..e2fc037d --- /dev/null +++ b/tests/compilation_failures/expressions/cannot_output_object_to_stream.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #({ hello: "world" }) + }; +} \ No newline at end of file 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..38bdb2fc --- /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:11 + | +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..744fc192 --- /dev/null +++ b/tests/compilation_failures/expressions/cast_int_to_bool.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..1a036393 --- /dev/null +++ b/tests/compilation_failures/expressions/cast_int_to_bool.stderr @@ -0,0 +1,5 @@ +error: This cast is not supported + --> tests/compilation_failures/expressions/cast_int_to_bool.rs:5:13 + | +5 | #(1 as bool) + | ^^ diff --git a/tests/compilation_failures/expressions/code_blocks_are_not_reevaluated.rs b/tests/compilation_failures/expressions/code_blocks_are_not_reevaluated.rs new file mode 100644 index 00000000..b5541945 --- /dev/null +++ b/tests/compilation_failures/expressions/code_blocks_are_not_reevaluated.rs @@ -0,0 +1,13 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #( + let indirect = [!raw! [!error! "This was a re-evaluation"]]; + // We don't get a re-evaluation. Instead, we get a parse error, because we end up + // with let _ = [!error! "This was a re-evaluation"]; which is a parse error in + // normal rust land. + #(indirect) + ) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/code_blocks_are_not_reevaluated.stderr b/tests/compilation_failures/expressions/code_blocks_are_not_reevaluated.stderr new file mode 100644 index 00000000..d561ddf6 --- /dev/null +++ b/tests/compilation_failures/expressions/code_blocks_are_not_reevaluated.stderr @@ -0,0 +1,5 @@ +error: expected one of `(`, `[`, or `{`, found `"This was a re-evaluation"` + --> tests/compilation_failures/expressions/code_blocks_are_not_reevaluated.rs:6:44 + | +6 | let indirect = [!raw! [!error! "This was a re-evaluation"]]; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected one of `(`, `[`, or `{` 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..d4e08b5c --- /dev/null +++ b/tests/compilation_failures/expressions/compare_int_and_float.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..738a561c --- /dev/null +++ b/tests/compilation_failures/expressions/compare_int_and_float.stderr @@ -0,0 +1,5 @@ +error: Cannot infer common type from untyped integer < untyped float. Consider using `as` to cast the operands to matching types. + --> tests/compilation_failures/expressions/compare_int_and_float.rs:5:13 + | +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..4d6162cf --- /dev/null +++ b/tests/compilation_failures/expressions/debug_method.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..fc932ac4 --- /dev/null +++ b/tests/compilation_failures/expressions/debug_method.stderr @@ -0,0 +1,5 @@ +error: [1, 2] + --> tests/compilation_failures/expressions/debug_method.rs:5:27 + | +5 | #(let x = [1, 2]; 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..845bf0d3 --- /dev/null +++ b/tests/compilation_failures/expressions/discard_in_value_position.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(_) + }; +} \ 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..4acb8839 --- /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/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..c6cb2193 --- /dev/null +++ b/tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.rs @@ -0,0 +1,10 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #( + 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..b2ef1fc9 --- /dev/null +++ b/tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.stderr @@ -0,0 +1,5 @@ +error: A statement ending with ; must not return a value. If you wish to explicitly discard the expression's result, use `let _ = ...;` + --> tests/compilation_failures/expressions/expression_block_semicolon_expressions_cannot_return_value.rs:6:13 + | +6 | 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..6cc0b2ce --- /dev/null +++ b/tests/compilation_failures/expressions/fix_me_negative_max_int_fails.rs @@ -0,0 +1,12 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret! { + // 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..20765e62 --- /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 unsupported literal values + --> tests/compilation_failures/expressions/fix_me_negative_max_int_fails.rs:8:11 + | +8 | #(-128i8) + | ^ diff --git a/tests/compilation_failures/expressions/flattened_variables_in_expressions.rs b/tests/compilation_failures/expressions/flattened_variables_in_expressions.rs new file mode 100644 index 00000000..8c14c97c --- /dev/null +++ b/tests/compilation_failures/expressions/flattened_variables_in_expressions.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret! { + #(partial_sum = [!stream! + 2]; 5 #..partial_sum) + }; +} diff --git a/tests/compilation_failures/expressions/flattened_variables_in_expressions.stderr b/tests/compilation_failures/expressions/flattened_variables_in_expressions.stderr new file mode 100644 index 00000000..861019a5 --- /dev/null +++ b/tests/compilation_failures/expressions/flattened_variables_in_expressions.stderr @@ -0,0 +1,5 @@ +error: Expected an operator to continue the expression, or ; to mark the end of the expression statement + --> tests/compilation_failures/expressions/flattened_variables_in_expressions.rs:5:43 + | +5 | #(partial_sum = [!stream! + 2]; 5 #..partial_sum) + | ^ diff --git a/tests/compilation_failures/expressions/grouped_variable_with_incomplete_expression.rs b/tests/compilation_failures/expressions/grouped_variable_with_incomplete_expression.rs new file mode 100644 index 00000000..28cfa92d --- /dev/null +++ b/tests/compilation_failures/expressions/grouped_variable_with_incomplete_expression.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(x = [!stream! + 1]; 1 x) + }; +} \ No newline at end of file diff --git a/tests/compilation_failures/expressions/grouped_variable_with_incomplete_expression.stderr b/tests/compilation_failures/expressions/grouped_variable_with_incomplete_expression.stderr new file mode 100644 index 00000000..5c70238a --- /dev/null +++ b/tests/compilation_failures/expressions/grouped_variable_with_incomplete_expression.stderr @@ -0,0 +1,5 @@ +error: Expected an operator to continue the expression, or ; to mark the end of the expression statement + --> tests/compilation_failures/expressions/grouped_variable_with_incomplete_expression.rs:5:33 + | +5 | #(x = [!stream! + 1]; 1 x) + | ^ 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..85e1d62b --- /dev/null +++ b/tests/compilation_failures/expressions/index_into_discarded_place.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(_[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..7403ee1e --- /dev/null +++ b/tests/compilation_failures/expressions/index_into_discarded_place.stderr @@ -0,0 +1,5 @@ +error: This expression cannot be resolved into a memory location. + --> 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..28dfe336 --- /dev/null +++ b/tests/compilation_failures/expressions/invalid_binary_operator.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret! { + #(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..ac1b8398 --- /dev/null +++ b/tests/compilation_failures/expressions/invalid_binary_operator.stderr @@ -0,0 +1,5 @@ +error: Expected an operator to continue the expression, or ; to mark the end of the expression statement + --> tests/compilation_failures/expressions/invalid_binary_operator.rs:5:14 + | +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..96f34295 --- /dev/null +++ b/tests/compilation_failures/expressions/invalid_unary_operator.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret! { + #(^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..c2abe00b --- /dev/null +++ b/tests/compilation_failures/expressions/invalid_unary_operator.stderr @@ -0,0 +1,5 @@ +error: Expected ! or - + --> 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..8b966558 --- /dev/null +++ b/tests/compilation_failures/expressions/large_range_to_string.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #((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..54e7f415 --- /dev/null +++ b/tests/compilation_failures/expressions/large_range_to_string.stderr @@ -0,0 +1,5 @@ +error: This type is not supported in cast expressions + --> tests/compilation_failures/expressions/large_range_to_string.rs:5:25 + | +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..3c3b3251 --- /dev/null +++ b/tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #( + 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..03395987 --- /dev/null +++ b/tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.stderr @@ -0,0 +1,5 @@ +error: A mutable reference is required, but an owned value was received, this indicates a possible bug as the updated value won't be accessible. To proceed regardless, use `.as_mut()` to get a mutable reference. + --> tests/compilation_failures/expressions/late_bound_owned_to_mutable copy.rs:7:13 + | +7 | "b".swap(a); + | ^^^ diff --git a/tests/compilation_failures/expressions/no_output_commands_in_expressions.rs b/tests/compilation_failures/expressions/no_output_commands_in_expressions.rs new file mode 100644 index 00000000..df4e28d6 --- /dev/null +++ b/tests/compilation_failures/expressions/no_output_commands_in_expressions.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret! { + #( 5 + [!set! #x = 2] 2) + }; +} diff --git a/tests/compilation_failures/expressions/no_output_commands_in_expressions.stderr b/tests/compilation_failures/expressions/no_output_commands_in_expressions.stderr new file mode 100644 index 00000000..24d51ad0 --- /dev/null +++ b/tests/compilation_failures/expressions/no_output_commands_in_expressions.stderr @@ -0,0 +1,5 @@ +error: Expected an operator to continue the expression, or ; to mark the end of the expression statement + --> tests/compilation_failures/expressions/no_output_commands_in_expressions.rs:5:31 + | +5 | #( 5 + [!set! #x = 2] 2) + | ^ 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..c5b1a96c --- /dev/null +++ b/tests/compilation_failures/expressions/object_block_confusion.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..eb1662f0 --- /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:21 + | +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..fe915557 --- /dev/null +++ b/tests/compilation_failures/expressions/object_field_duplication.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #({ 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..632455b3 --- /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:29 + | +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..9dc192eb --- /dev/null +++ b/tests/compilation_failures/expressions/object_incorrect_comma.rs @@ -0,0 +1,8 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + 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..84263409 --- /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:14 + | +6 | #({ a; b: 1 }) + | ^ 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..83d17569 --- /dev/null +++ b/tests/compilation_failures/expressions/object_pattern_destructuring_repeated_field.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..333ab2ce --- /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:20 + | +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..f61105a9 --- /dev/null +++ b/tests/compilation_failures/expressions/object_pattern_destructuring_wrong_type.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..745b31c5 --- /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 must be an object, but it is an untyped integer + --> tests/compilation_failures/expressions/object_pattern_destructuring_wrong_type.rs:5:23 + | +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..c569e426 --- /dev/null +++ b/tests/compilation_failures/expressions/object_place_destructuring_repeated_field.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #( + 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..4ae8a188 --- /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:7:18 + | +7 | { 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..9b2d32fc --- /dev/null +++ b/tests/compilation_failures/expressions/object_place_destructuring_wrong_type.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #( + 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..435db860 --- /dev/null +++ b/tests/compilation_failures/expressions/object_place_destructuring_wrong_type.stderr @@ -0,0 +1,5 @@ +error: The value destructured as an object must be an object, but it is an array + --> tests/compilation_failures/expressions/object_place_destructuring_wrong_type.rs:7:21 + | +7 | { 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..8681de36 --- /dev/null +++ b/tests/compilation_failures/expressions/owned_to_mutable.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #( + 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..23e92cb5 --- /dev/null +++ b/tests/compilation_failures/expressions/owned_to_mutable.stderr @@ -0,0 +1,5 @@ +error: A mutable reference is required, but an owned value was received, this indicates a possible bug as the updated value won't be accessible. To proceed regardless, use `.as_mut()` to get a mutable reference. + --> tests/compilation_failures/expressions/owned_to_mutable.rs:7:20 + | +7 | a.swap("b"); + | ^^^ 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..1e1231fd --- /dev/null +++ b/tests/compilation_failures/expressions/tuple_syntax_helpful_error.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + let _ = preinterpret!{ + #(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..fe284cb9 --- /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:21 + | +5 | #(let x = (1, 2)) + | ^ diff --git a/tests/compilation_failures/tokens/intersperse_stream_input_variable_issue.rs b/tests/compilation_failures/tokens/intersperse_stream_input_variable_issue.rs new file mode 100644 index 00000000..e8d5ffa7 --- /dev/null +++ b/tests/compilation_failures/tokens/intersperse_stream_input_variable_issue.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +fn main() { + preinterpret! { + [!set! #x = 1 2] + [!intersperse! { + items: #..x, + separator: [] + }] + } +} \ No newline at end of file diff --git a/tests/compilation_failures/tokens/intersperse_stream_input_variable_issue.stderr b/tests/compilation_failures/tokens/intersperse_stream_input_variable_issue.stderr new file mode 100644 index 00000000..3c756a08 --- /dev/null +++ b/tests/compilation_failures/tokens/intersperse_stream_input_variable_issue.stderr @@ -0,0 +1,15 @@ +error: In an expression, the #.. variable prefix is not allowed. The # prefix should only be used when embedding a variable into an output sream. + Occurred whilst parsing [!intersperse! ...] - Expected: { + // An array or stream (by coerced token-tree) to intersperse + items: ["Hello", "World"], + // The value to add between each item + separator: [!stream! ,], + // 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?: [!stream! or], + } + --> tests/compilation_failures/tokens/intersperse_stream_input_variable_issue.rs:7:20 + | +7 | items: #..x, + | ^ diff --git a/tests/compilation_failures/tokens/zip_different_length_streams.rs b/tests/compilation_failures/tokens/zip_different_length_streams.rs new file mode 100644 index 00000000..8f1c809d --- /dev/null +++ b/tests/compilation_failures/tokens/zip_different_length_streams.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + preinterpret! { + [!zip! [["A", "B", "C"], [1, 2, 3, 4]]] + } +} \ No newline at end of file diff --git a/tests/compilation_failures/tokens/zip_different_length_streams.stderr b/tests/compilation_failures/tokens/zip_different_length_streams.stderr new file mode 100644 index 00000000..671b7a37 --- /dev/null +++ b/tests/compilation_failures/tokens/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/tokens/zip_different_length_streams.rs:5:16 + | +5 | [!zip! [["A", "B", "C"], [1, 2, 3, 4]]] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/tokens/zip_no_input.rs b/tests/compilation_failures/tokens/zip_no_input.rs new file mode 100644 index 00000000..739dd4ff --- /dev/null +++ b/tests/compilation_failures/tokens/zip_no_input.rs @@ -0,0 +1,7 @@ +use preinterpret::*; + +fn main() { + preinterpret! { + [!zip!] + } +} \ No newline at end of file diff --git a/tests/compilation_failures/tokens/zip_no_input.stderr b/tests/compilation_failures/tokens/zip_no_input.stderr new file mode 100644 index 00000000..de103dd5 --- /dev/null +++ b/tests/compilation_failures/tokens/zip_no_input.stderr @@ -0,0 +1,6 @@ +error: Expected an expression + Occurred whilst parsing [!zip! ...] - Expected [!zip! [, , ..]] or [!zip! {{ x: , y: , .. }}] for iterable values of the same length. If you instead want to permit different lengths and truncate to the shortest, use `!zip_truncated!` instead. + --> tests/compilation_failures/tokens/zip_no_input.rs:5:15 + | +5 | [!zip!] + | ^ diff --git a/tests/compilation_failures/transforming/append_before_set.rs b/tests/compilation_failures/transforming/append_before_set.rs new file mode 100644 index 00000000..0f3e1c7c --- /dev/null +++ b/tests/compilation_failures/transforming/append_before_set.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! #>>x = Hello]); +} \ No newline at end of file diff --git a/tests/compilation_failures/transforming/append_before_set.stderr b/tests/compilation_failures/transforming/append_before_set.stderr new file mode 100644 index 00000000..0a5734ae --- /dev/null +++ b/tests/compilation_failures/transforming/append_before_set.stderr @@ -0,0 +1,5 @@ +error: The variable does not already exist in the current scope + --> tests/compilation_failures/transforming/append_before_set.rs:4:26 + | +4 | preinterpret!([!let! #>>x = Hello]); + | ^^^^ diff --git a/tests/compilation_failures/transforming/destructure_with_ident_flattened_variable.rs b/tests/compilation_failures/transforming/destructure_with_ident_flattened_variable.rs new file mode 100644 index 00000000..46c13177 --- /dev/null +++ b/tests/compilation_failures/transforming/destructure_with_ident_flattened_variable.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! @(#..x = @IDENT) = Hello]); +} diff --git a/tests/compilation_failures/transforming/destructure_with_ident_flattened_variable.stderr b/tests/compilation_failures/transforming/destructure_with_ident_flattened_variable.stderr new file mode 100644 index 00000000..ce302ffb --- /dev/null +++ b/tests/compilation_failures/transforming/destructure_with_ident_flattened_variable.stderr @@ -0,0 +1,6 @@ +error: Expected #variable + Occurred whilst parsing [!let! ...] - Expected [!let! = ...] + --> tests/compilation_failures/transforming/destructure_with_ident_flattened_variable.rs:4:28 + | +4 | preinterpret!([!let! @(#..x = @IDENT) = Hello]); + | ^ diff --git a/tests/compilation_failures/transforming/destructure_with_literal_flattened_variable.rs b/tests/compilation_failures/transforming/destructure_with_literal_flattened_variable.rs new file mode 100644 index 00000000..bb13d834 --- /dev/null +++ b/tests/compilation_failures/transforming/destructure_with_literal_flattened_variable.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! @(#..x = @LITERAL) = "Hello"]); +} diff --git a/tests/compilation_failures/transforming/destructure_with_literal_flattened_variable.stderr b/tests/compilation_failures/transforming/destructure_with_literal_flattened_variable.stderr new file mode 100644 index 00000000..f114e8dd --- /dev/null +++ b/tests/compilation_failures/transforming/destructure_with_literal_flattened_variable.stderr @@ -0,0 +1,6 @@ +error: Expected #variable + Occurred whilst parsing [!let! ...] - Expected [!let! = ...] + --> tests/compilation_failures/transforming/destructure_with_literal_flattened_variable.rs:4:28 + | +4 | preinterpret!([!let! @(#..x = @LITERAL) = "Hello"]); + | ^ diff --git a/tests/compilation_failures/transforming/destructure_with_outputting_command.rs b/tests/compilation_failures/transforming/destructure_with_outputting_command.rs new file mode 100644 index 00000000..41748784 --- /dev/null +++ b/tests/compilation_failures/transforming/destructure_with_outputting_command.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! [!group! Output] = [!group! Output]]); +} diff --git a/tests/compilation_failures/transforming/destructure_with_outputting_command.stderr b/tests/compilation_failures/transforming/destructure_with_outputting_command.stderr new file mode 100644 index 00000000..6393fdbf --- /dev/null +++ b/tests/compilation_failures/transforming/destructure_with_outputting_command.stderr @@ -0,0 +1,5 @@ +error: unexpected token + --> tests/compilation_failures/transforming/destructure_with_outputting_command.rs:4:54 + | +4 | preinterpret!([!let! [!group! Output] = [!group! Output]]); + | ^^^^^^ diff --git a/tests/compilation_failures/transforming/destructure_with_punct_flattened_variable.rs b/tests/compilation_failures/transforming/destructure_with_punct_flattened_variable.rs new file mode 100644 index 00000000..89341d19 --- /dev/null +++ b/tests/compilation_failures/transforming/destructure_with_punct_flattened_variable.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! @(#..x = @PUNCT) = @]); +} diff --git a/tests/compilation_failures/transforming/destructure_with_punct_flattened_variable.stderr b/tests/compilation_failures/transforming/destructure_with_punct_flattened_variable.stderr new file mode 100644 index 00000000..66a8a803 --- /dev/null +++ b/tests/compilation_failures/transforming/destructure_with_punct_flattened_variable.stderr @@ -0,0 +1,6 @@ +error: Expected #variable + Occurred whilst parsing [!let! ...] - Expected [!let! = ...] + --> tests/compilation_failures/transforming/destructure_with_punct_flattened_variable.rs:4:28 + | +4 | preinterpret!([!let! @(#..x = @PUNCT) = @]); + | ^ diff --git a/tests/compilation_failures/transforming/double_flattened_variable.rs b/tests/compilation_failures/transforming/double_flattened_variable.rs new file mode 100644 index 00000000..4177821e --- /dev/null +++ b/tests/compilation_failures/transforming/double_flattened_variable.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! #..x #..y = Hello World]); +} \ No newline at end of file diff --git a/tests/compilation_failures/transforming/double_flattened_variable.stderr b/tests/compilation_failures/transforming/double_flattened_variable.stderr new file mode 100644 index 00000000..35c196e9 --- /dev/null +++ b/tests/compilation_failures/transforming/double_flattened_variable.stderr @@ -0,0 +1,6 @@ +error: This cannot follow a flattened variable binding + Occurred whilst parsing [!let! ...] - Expected [!let! = ...] + --> tests/compilation_failures/transforming/double_flattened_variable.rs:4:31 + | +4 | preinterpret!([!let! #..x #..y = Hello World]); + | ^ diff --git a/tests/compilation_failures/transforming/invalid_content_too_long.rs b/tests/compilation_failures/transforming/invalid_content_too_long.rs new file mode 100644 index 00000000..20ac159b --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_too_long.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! Hello World = Hello World!!!]); +} diff --git a/tests/compilation_failures/transforming/invalid_content_too_long.stderr b/tests/compilation_failures/transforming/invalid_content_too_long.stderr new file mode 100644 index 00000000..81df09f7 --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_too_long.stderr @@ -0,0 +1,5 @@ +error: unexpected token + --> tests/compilation_failures/transforming/invalid_content_too_long.rs:4:51 + | +4 | preinterpret!([!let! Hello World = Hello World!!!]); + | ^ diff --git a/tests/compilation_failures/transforming/invalid_content_too_short.rs b/tests/compilation_failures/transforming/invalid_content_too_short.rs new file mode 100644 index 00000000..081ed084 --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_too_short.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! Hello World = Hello]); +} diff --git a/tests/compilation_failures/transforming/invalid_content_too_short.stderr b/tests/compilation_failures/transforming/invalid_content_too_short.stderr new file mode 100644 index 00000000..ede603a2 --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_too_short.stderr @@ -0,0 +1,7 @@ +error: expected World + --> tests/compilation_failures/transforming/invalid_content_too_short.rs:4:5 + | +4 | preinterpret!([!let! Hello World = Hello]); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `preinterpret` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/compilation_failures/transforming/invalid_content_wrong_group.rs b/tests/compilation_failures/transforming/invalid_content_wrong_group.rs new file mode 100644 index 00000000..28819476 --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_wrong_group.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! (#..x) = [Hello World]]); +} diff --git a/tests/compilation_failures/transforming/invalid_content_wrong_group.stderr b/tests/compilation_failures/transforming/invalid_content_wrong_group.stderr new file mode 100644 index 00000000..ded3e105 --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_wrong_group.stderr @@ -0,0 +1,5 @@ +error: Expected ( + --> tests/compilation_failures/transforming/invalid_content_wrong_group.rs:4:35 + | +4 | preinterpret!([!let! (#..x) = [Hello World]]); + | ^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/transforming/invalid_content_wrong_group_2.rs b/tests/compilation_failures/transforming/invalid_content_wrong_group_2.rs new file mode 100644 index 00000000..025d39fe --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_wrong_group_2.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! (!group! #..x) = [Hello World]]); +} diff --git a/tests/compilation_failures/transforming/invalid_content_wrong_group_2.stderr b/tests/compilation_failures/transforming/invalid_content_wrong_group_2.stderr new file mode 100644 index 00000000..01c68b55 --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_wrong_group_2.stderr @@ -0,0 +1,5 @@ +error: Expected ( + --> tests/compilation_failures/transforming/invalid_content_wrong_group_2.rs:4:43 + | +4 | preinterpret!([!let! (!group! #..x) = [Hello World]]); + | ^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/transforming/invalid_content_wrong_ident.rs b/tests/compilation_failures/transforming/invalid_content_wrong_ident.rs new file mode 100644 index 00000000..959e900f --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_wrong_ident.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! Hello World = Hello Earth]); +} diff --git a/tests/compilation_failures/transforming/invalid_content_wrong_ident.stderr b/tests/compilation_failures/transforming/invalid_content_wrong_ident.stderr new file mode 100644 index 00000000..117f2c59 --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_wrong_ident.stderr @@ -0,0 +1,5 @@ +error: expected World + --> tests/compilation_failures/transforming/invalid_content_wrong_ident.rs:4:46 + | +4 | preinterpret!([!let! Hello World = Hello Earth]); + | ^^^^^ diff --git a/tests/compilation_failures/transforming/invalid_content_wrong_punct.rs b/tests/compilation_failures/transforming/invalid_content_wrong_punct.rs new file mode 100644 index 00000000..ab6bf1d4 --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_wrong_punct.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! Hello _ World = Hello World]); +} diff --git a/tests/compilation_failures/transforming/invalid_content_wrong_punct.stderr b/tests/compilation_failures/transforming/invalid_content_wrong_punct.stderr new file mode 100644 index 00000000..f8647cfa --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_content_wrong_punct.stderr @@ -0,0 +1,5 @@ +error: expected _ + --> tests/compilation_failures/transforming/invalid_content_wrong_punct.rs:4:48 + | +4 | preinterpret!([!let! Hello _ World = Hello World]); + | ^^^^^ diff --git a/tests/compilation_failures/transforming/invalid_group_content_too_long.rs b/tests/compilation_failures/transforming/invalid_group_content_too_long.rs new file mode 100644 index 00000000..b3553ecc --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_group_content_too_long.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! Group: (Hello World) = Group: (Hello World!!!)]); +} diff --git a/tests/compilation_failures/transforming/invalid_group_content_too_long.stderr b/tests/compilation_failures/transforming/invalid_group_content_too_long.stderr new file mode 100644 index 00000000..8593afc8 --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_group_content_too_long.stderr @@ -0,0 +1,5 @@ +error: unexpected token, expected `)` + --> tests/compilation_failures/transforming/invalid_group_content_too_long.rs:4:68 + | +4 | preinterpret!([!let! Group: (Hello World) = Group: (Hello World!!!)]); + | ^ diff --git a/tests/compilation_failures/transforming/invalid_group_content_too_short.rs b/tests/compilation_failures/transforming/invalid_group_content_too_short.rs new file mode 100644 index 00000000..22f8edce --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_group_content_too_short.rs @@ -0,0 +1,5 @@ +use preinterpret::*; + +fn main() { + preinterpret!([!let! Group: (Hello World!!) = Group: (Hello World)]); +} diff --git a/tests/compilation_failures/transforming/invalid_group_content_too_short.stderr b/tests/compilation_failures/transforming/invalid_group_content_too_short.stderr new file mode 100644 index 00000000..4a4624bb --- /dev/null +++ b/tests/compilation_failures/transforming/invalid_group_content_too_short.stderr @@ -0,0 +1,5 @@ +error: expected ! + --> tests/compilation_failures/transforming/invalid_group_content_too_short.rs:4:58 + | +4 | preinterpret!([!let! Group: (Hello World!!) = Group: (Hello World)]); + | ^^^^^^^^^^^^^ diff --git a/tests/compilation_failures/transforming/mistaken_at_symbol.rs b/tests/compilation_failures/transforming/mistaken_at_symbol.rs new file mode 100644 index 00000000..4a6e0c1e --- /dev/null +++ b/tests/compilation_failures/transforming/mistaken_at_symbol.rs @@ -0,0 +1,11 @@ +use preinterpret::*; + +struct I; + +fn main() { + preinterpret!( + match I { + x @ I => x, + } + ); +} \ No newline at end of file diff --git a/tests/compilation_failures/transforming/mistaken_at_symbol.stderr b/tests/compilation_failures/transforming/mistaken_at_symbol.stderr new file mode 100644 index 00000000..e22d76c6 --- /dev/null +++ b/tests/compilation_failures/transforming/mistaken_at_symbol.stderr @@ -0,0 +1,5 @@ +error: Destructurings are not supported here. If this wasn't intended to be a destructuring, replace @ with [!raw! @] + --> tests/compilation_failures/transforming/mistaken_at_symbol.rs:8:15 + | +8 | x @ I => x, + | ^ diff --git a/tests/simple_test.rs b/tests/complex.rs similarity index 58% rename from tests/simple_test.rs rename to tests/complex.rs index 2775098b..f8c4398c 100644 --- a/tests/simple_test.rs +++ b/tests/complex.rs @@ -1,8 +1,11 @@ -use preinterpret::preinterpret; +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; preinterpret! { [!set! #bytes = 32] [!set! #postfix = Hello World #bytes] + [!set! #some_symbols = and some symbols such as [!raw! #] and #123] [!set! #MyRawVar = [!raw! Test no #str [!ident! replacement]]] [!ignore! non - sensical !code :D - ignored (!)] struct MyStruct; @@ -12,6 +15,17 @@ preinterpret! { const SNAKE_CASE: &str = [!snake! MyVar]; } +#[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; diff --git a/tests/control_flow.rs b/tests/control_flow.rs new file mode 100644 index 00000000..277e62c5 --- /dev/null +++ b/tests/control_flow.rs @@ -0,0 +1,102 @@ +#![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() { + preinterpret_assert_eq!([!if! (1 == 2) { "YES" } !else! { "NO" }], "NO"); + preinterpret_assert_eq!({ + #(let x = 1 == 2) + [!if! x { "YES" } !else! { "NO" }] + }, "NO"); + preinterpret_assert_eq!({ + #(let x = 1; let y = 2) + [!if! x == y { "YES" } !else! { "NO" }] + }, "NO"); + preinterpret_assert_eq!({ + 0 + [!if! true { + 1 }] + }, 1); + preinterpret_assert_eq!({ + 0 + [!if! false { + 1 }] + }, 0); + preinterpret_assert_eq!({ + [!if! false { + 1 + } !elif! false { + 2 + } !elif! true { + 3 + } !else! { + 4 + }] + }, 3); +} + +#[test] +fn test_while() { + preinterpret_assert_eq!({ + #(let x = 0) + [!while! x < 5 { #(x += 1) }] + #x + }, 5); +} + +#[test] +fn test_loop_continue_and_break() { + preinterpret_assert_eq!( + { + #(let x = 0) + [!loop! { + #(x += 1) + [!if! x >= 10 { [!break!] }] + }] + #x + }, + 10 + ); + preinterpret_assert_eq!( + { + [!string! [!for! x in 65..75 { + [!if! x % 2 == 0 { [!continue!] }] + #(x as u8 as char) + }]] + }, + "ACEGI" + ); +} + +#[test] +fn test_for() { + preinterpret_assert_eq!( + { + [!string! [!for! x in 65..70 { + #(x as u8 as char) + }]] + }, + "ABCDE" + ); + preinterpret_assert_eq!( + { + [!string! [!for! @((#x,)) in [!stream! (a,) (b,) (c,)] { + #x + [!if! [!string! #x] == "b" { [!break!] }] + }]] + }, + "ab" + ); +} diff --git a/tests/core.rs b/tests/core.rs new file mode 100644 index 00000000..bd08d8bd --- /dev/null +++ b/tests/core.rs @@ -0,0 +1,132 @@ +use preinterpret::preinterpret; + +#[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_set() { + preinterpret_assert_eq!({ + [!set! #output = "Hello World!"] + #output + }, "Hello World!"); + preinterpret_assert_eq!({ + [!set! #hello = "Hello"] + [!set! #world = "World"] + [!set! #output = #hello " " #world "!"] + [!set! #output = [!string! #output]] + #output + }, "Hello World!"); +} + +#[test] +fn test_raw() { + preinterpret_assert_eq!( + { [!string! [!raw! #variable and [!command!] are not interpreted or error]] }, + "#variableand[!command!]arenotinterpretedorerror" + ); +} + +#[test] +fn test_extend() { + preinterpret_assert_eq!( + { + [!set! #variable = "Hello"] + [!set! #variable += " World!"] + [!string! #variable] + }, + "Hello World!" + ); + preinterpret_assert_eq!( + { + #(let i = 1) + [!set! #output =] + [!while! i <= 4 { + [!set! #output += #i] + [!if! i <= 3 { + [!set! #output += ", "] + }] + #(i += 1) + }] + [!string! #output] + }, + "1, 2, 3, 4" + ); +} + +#[test] +fn test_ignore() { + preinterpret_assert_eq!({ + [!set! #x = false] + [!ignore! [!set! #x = true] nothing is interpreted. Everything is ignored...] + #x + }, false); +} + +#[test] +fn test_empty_set() { + preinterpret_assert_eq!({ + [!set! #x] + [!set! #x += "hello"] + #x + }, "hello"); + preinterpret_assert_eq!({ + [!set! #x, #y] + [!set! #x += "hello"] + [!set! #y += "world"] + [!string! #x " " #y] + }, "hello world"); + preinterpret_assert_eq!({ + [!set! #x, #y, #z,] + [!set! #x += "hello"] + [!set! #y += "world"] + [!string! #x " " #y #z] + }, "hello world"); +} + +#[test] +fn test_discard_set() { + preinterpret_assert_eq!({ + [!set! #x = false] + [!set! _ = [!set! #x = true] things _are_ interpreted, but the result is ignored...] + #x + }, true); +} + +#[test] +fn test_debug() { + // It keeps the semantic punctuation spacing intact + // (e.g. it keeps 'a and >> together) + preinterpret_assert_eq!( + #( + [!stream! impl<'a, T> MyStruct<'a, T> { + pub fn new() -> Self { + !($crate::Test::CONSTANT >> 5 > 1) + } + }].debug_string() + ), + "[!stream! impl < 'a , T > MyStruct < 'a , T > { pub fn new () -> Self { ! ($ crate :: Test :: CONSTANT >> 5 > 1) } }]" + ); + // It shows transparent groups + // NOTE: The output code can't be used directly as preinterpret input + // because it doesn't stick [!raw! ...] around things which could be confused + // for the preinterpret grammar. Perhaps it could/should in future. + preinterpret_assert_eq!( + #( + let x = [!stream! Hello (World)]; + [!stream! #x [!raw! #test] "and" [!raw! ##] #..x].debug_string() + ), + r###"[!stream! [!group! Hello (World)] # test "and" ## Hello (World)]"### + ); +} diff --git a/tests/expressions.rs b/tests/expressions.rs new file mode 100644 index 00000000..cc38c942 --- /dev/null +++ b/tests/expressions.rs @@ -0,0 +1,518 @@ +#[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() { + preinterpret_assert_eq!(#(!!(!!(true))), true); + preinterpret_assert_eq!(#(1 + 5), 6u8); + preinterpret_assert_eq!(#(1 + 5), 6i128); + preinterpret_assert_eq!(#("Hello" + " " + "World!"), "Hello World!"); + preinterpret_assert_eq!(#(1 + 5u16), 6u16); + preinterpret_assert_eq!(#(127i8 + (-127i8) + (-127i8)), -127i8); + preinterpret_assert_eq!(#(3.0 + 3.2), 6.2); + preinterpret_assert_eq!(#(3.6 + 3999999999999999992.0), 3.6 + 3999999999999999992.0); + preinterpret_assert_eq!(#(-3.2), -3.2); + preinterpret_assert_eq!(#(true && true || false), true); + preinterpret_assert_eq!(#(true || false && false), true); // The && has priority + preinterpret_assert_eq!(#(true | false & false), true); // The & has priority + preinterpret_assert_eq!(#(true as u32 + 2), 3); + preinterpret_assert_eq!(#(3.57 as int + 1), 4u32); + preinterpret_assert_eq!(#(3.57 as int + 1), 4u64); + preinterpret_assert_eq!(#(0b1000 & 0b1101), 0b1000); + preinterpret_assert_eq!(#(0b1000 | 0b1101), 0b1101); + preinterpret_assert_eq!(#(0b1000 ^ 0b1101), 0b101); + preinterpret_assert_eq!(#(5 << 2), 20); + preinterpret_assert_eq!(#(5 >> 1), 2); + preinterpret_assert_eq!(#(123 == 456), false); + preinterpret_assert_eq!(#(123 < 456), true); + preinterpret_assert_eq!(#(123 <= 456), true); + preinterpret_assert_eq!(#(123 != 456), true); + preinterpret_assert_eq!(#(123 >= 456), false); + preinterpret_assert_eq!(#(123 > 456), false); + preinterpret_assert_eq!(#(let six_as_sum = 3 + 3; six_as_sum * six_as_sum), 36); + preinterpret_assert_eq!(#( + let partial_sum = [!stream! + 2]; + ([!stream! #([!stream! 5] + partial_sum) =] + [!reinterpret! [!raw! #](5 #..partial_sum)]).debug_string() + ), "[!stream! [!group! 5 + 2] = [!group! 7]]"); + preinterpret_assert_eq!(#(1 + (1..2) as int), 2); + preinterpret_assert_eq!(#("hello" == "world"), false); + preinterpret_assert_eq!(#("hello" == "hello"), true); + preinterpret_assert_eq!(#('A' as u8 == 65), true); + preinterpret_assert_eq!(#(65u8 as char == 'A'), true); + preinterpret_assert_eq!(#('A' == 'A'), true); + preinterpret_assert_eq!(#('A' == 'B'), false); + preinterpret_assert_eq!(#('A' < 'B'), true); + preinterpret_assert_eq!(#("Zoo" > "Aardvark"), true); + preinterpret_assert_eq!( + #(("Hello" as stream + "World" as stream + (1 + 1) as stream + (1 + 1) as group).debug_string()), + r#"[!stream! "Hello" "World" 2 [!group! 2]]"# + ); + preinterpret_assert_eq!( + #([1, 2, 1 + 2, 4].debug_string()), + "[1, 2, 3, 4]" + ); + preinterpret_assert_eq!( + #(([1 + (3 + 4), [5,] + [], [[6, 7],]] + [123]).debug_string()), + "[8, [5], [[6, 7]], 123]" + ); + preinterpret_assert_eq!( + #(("Hello" as stream + "World" as stream + (1 + 1) as stream + (1 + 1) as group).debug_string()), + r#"[!stream! "Hello" "World" 2 [!group! 2]]"# + ); +} + +#[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 + preinterpret_assert_eq!(#(1 + -(1) + (2 + 4) * 3 - 9), 9); + // (true > true) > true => false > true => false + preinterpret_assert_eq!(#(true > true > true), false); + // (5 - 2) - 1 => 3 - 1 => 2 + preinterpret_assert_eq!(#(5 - 2 - 1), 2); + // ((3 * 3 - 4) < (3 << 1)) && true => 5 < 6 => true + preinterpret_assert_eq!(#(3 * 3 - 4 < 3 << 1 && true), true); +} + +#[test] +#[allow(clippy::zero_prefixed_literal)] +fn test_very_long_expression_works() { + preinterpret_assert_eq!( + { + [!settings! { + iteration_limit: 100000, + }] + #(let expression = [!stream! 0] + [!for! _ in 0..100000 { + 1 }]) + [!reinterpret! [!raw! #](#expression)] + }, + 100000 + ); +} + +#[test] +fn boolean_operators_short_circuit() { + // && short-circuits if first operand is false + preinterpret_assert_eq!( + #( + let is_lazy = true; + let _ = false && #(is_lazy = false; true); + is_lazy + ), + true + ); + // || short-circuits if first operand is true + preinterpret_assert_eq!( + #( + let is_lazy = true; + let _ = true || #(is_lazy = false; true); + is_lazy + ), + true + ); + // For comparison, the & operator does _not_ short-circuit + preinterpret_assert_eq!( + #( + let is_lazy = true; + let _ = false & #(is_lazy = false; true); + is_lazy + ), + false + ); +} + +#[test] +fn assign_works() { + preinterpret_assert_eq!( + #( + let x = 5 + 5; + x.debug_string() + ), + "10" + ); + preinterpret_assert_eq!( + #( + 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. + preinterpret_assert_eq!( + #( + let x = 2; + x += x; + x + ), + 4 + ); +} + +#[test] +fn test_range() { + preinterpret_assert_eq!( + #([!intersperse! { + items: -2..5, + separator: [" "], + }] as string), + "-2 -1 0 1 2 3 4" + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: -2..=5, + separator: " ", + }] as stream as string), + "-2 -1 0 1 2 3 4 5" + ); + preinterpret_assert_eq!( + { + #(let x = 2) + #([!intersperse! { + items: (x + x)..=5, + separator: " ", + }] as stream as string) + }, + "4 5" + ); + preinterpret_assert_eq!( + { + #([!intersperse! { + items: 8..=5, + separator: " ", + }] as stream as string) + }, + "" + ); + preinterpret_assert_eq!({ [!string! #(('a'..='f') as stream)] }, "abcdef"); + preinterpret_assert_eq!( + #((-1i8..3i8).debug_string()), + "-1i8..3i8" + ); + + // Large ranges are allowed, but are subject to limits at iteration time + preinterpret_assert_eq!(#((0..10000).debug_string()), "0..10000"); + preinterpret_assert_eq!(#((..5 + 5).debug_string()), "..10"); + preinterpret_assert_eq!(#((..=9).debug_string()), "..=9"); + preinterpret_assert_eq!(#((..).debug_string()), ".."); + preinterpret_assert_eq!(#([.., ..].debug_string()), "[.., ..]"); + preinterpret_assert_eq!(#([..[1, 2..], ..].debug_string()), "[..[1, 2..], ..]"); + preinterpret_assert_eq!(#((4 + 7..=10).debug_string()), "11..=10"); + preinterpret_assert_eq!( + [!for! i in 0..10000000 { + [!if! i == 5 { + [!string! #i] + [!break!] + }] + }], + "5" + ); + // preinterpret_assert_eq!(#((0..10000 as iterator).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 test_array_indexing() { + preinterpret_assert_eq!( + #(let x = [1, 2, 3]; x[1]), 2 + ); + preinterpret_assert_eq!( + #(let x = [1, 2, 3]; x[x[0] + x[x[1] - 1] - 1]), 3 + ); + // And setting indices... + preinterpret_assert_eq!( + #( + let x = [0, 0, 0]; + x[0] = 2; + (x[1 + 1]) = x[0]; + x.debug_string() + ), + "[2, 0, 2]" + ); + // And ranges in value position + preinterpret_assert_eq!( + #( + let x = [1, 2, 3, 4, 5]; + x.take()[..].debug_string() + ), + "[1, 2, 3, 4, 5]" + ); + preinterpret_assert_eq!( + #( + let x = [1, 2, 3, 4, 5]; + x.take()[0..0].debug_string() + ), + "[]" + ); + preinterpret_assert_eq!( + #( + let x = [1, 2, 3, 4, 5]; + x.take()[2..=2].debug_string() + ), + "[3]" + ); + preinterpret_assert_eq!( + #( + let x = [1, 2, 3, 4, 5]; + x.take()[..=2].debug_string() + ), + "[1, 2, 3]" + ); + preinterpret_assert_eq!( + #( + let x = [1, 2, 3, 4, 5]; + x.take()[..4].debug_string() + ), + "[1, 2, 3, 4]" + ); + preinterpret_assert_eq!( + #( + let x = [1, 2, 3, 4, 5]; + x.take()[2..].debug_string() + ), + "[3, 4, 5]" + ); +} + +#[test] +fn test_array_place_destructurings() { + // And array destructuring + preinterpret_assert_eq!( + #( + let a = 0; let b = 0; let c = 0; + let x = [1, 2, 3, 4, 5]; + [a, b, _, _, c] = x.take(); + [a, b, c].debug_string() + ), + "[1, 2, 5]" + ); + preinterpret_assert_eq!( + #( + let a = 0; let b = 0; let c = 0; + let x = [1, 2, 3, 4, 5]; + [a, b, c, ..] = x.take(); + [a, b, c].debug_string() + ), + "[1, 2, 3]" + ); + preinterpret_assert_eq!( + #( + let a = 0; let b = 0; let c = 0; + let x = [1, 2, 3, 4, 5]; + [.., a, b] = x.take(); + [a, b, c].debug_string() + ), + "[4, 5, 0]" + ); + preinterpret_assert_eq!( + #( + let a = 0; let b = 0; let c = 0; + let x = [1, 2, 3, 4, 5]; + [a, .., b, c] = x.take(); + [a, b, c].debug_string() + ), + "[1, 4, 5]" + ); + // Nested places + preinterpret_assert_eq!( + #( + 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.debug_string() + ), + "[[4, 5], 1]" + ); + // Misc + preinterpret_assert_eq!( + #( + 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.take(), b, c].debug_string() + ), + "[[0, 2, 4, 0, 0], 2, None]" + ); + // This test demonstrates that the right side executes first. + // This aligns with the rust behaviour. + preinterpret_assert_eq!( + #( + let a = [0, 0]; + let b = 0; + a[b] += #(b += 1; 5); + a.debug_string() + ), + "[0, 5]" + ); + // This test demonstrates that the assignee operation is executed + // incrementally, to align with the rust behaviour. + preinterpret_assert_eq!( + #( + 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[#(arr[0] = 5; 1)]] = [1, 1]; + arr[0] + ), + 5 + ); +} + +#[test] +fn test_array_pattern_destructurings() { + // And array destructuring + preinterpret_assert_eq!( + #( + let [a, b, _, _, c] = [1, 2, 3, 4, 5]; + [a, b, c].debug_string() + ), + "[1, 2, 5]" + ); + preinterpret_assert_eq!( + #( + let [a, b, c, ..] = [1, 2, 3, 4, 5]; + [a, b, c].debug_string() + ), + "[1, 2, 3]" + ); + preinterpret_assert_eq!( + #( + let [.., a, b] = [1, 2, 3, 4, 5]; + [a, b].debug_string() + ), + "[4, 5]" + ); + preinterpret_assert_eq!( + #( + let [a, .., b, c] = [1, 2, 3, 4, 5]; + [a, b, c].debug_string() + ), + "[1, 4, 5]" + ); + preinterpret_assert_eq!( + #( + let [a, .., b, c] = [[1, "a"], 2, 3, 4, 5]; + [a.take(), b, c].debug_string() + ), + r#"[[1, "a"], 4, 5]"# + ); +} + +#[test] +fn test_objects() { + preinterpret_assert_eq!( + #( + 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.debug_string() + ), + r#"{ a: {}, b: "Hello", hello: 1, world: 2, ["x y z"]: 4, y: 5, ["z\" test"]: {} }"# + ); + preinterpret_assert_eq!( + #( + { prop1: 1 }["prop1"].debug_string() + ), + r#"1"# + ); + preinterpret_assert_eq!( + #( + { prop1: 1 }["prop2"].debug_string() + ), + r#"None"# + ); + preinterpret_assert_eq!( + #( + { prop1: 1 }.prop1.debug_string() + ), + r#"1"# + ); + preinterpret_assert_eq!( + #( + let a; + let b; + let z; + { a, y: [_, b], z } = { a: 1, y: [5, 7] }; + { a, b, z }.debug_string() + ), + r#"{ a: 1, b: 7, z: None }"# + ); + preinterpret_assert_eq!( + #( + let { a, y: [_, b], ["c"]: c, [r#"two "words"#]: x, z } = { a: 1, y: [5, 7], ["two \"words"]: {}, }; + { a, b, c, x: x.take(), z }.debug_string() + ), + r#"{ a: 1, b: 7, c: None, x: {}, z: None }"# + ); +} + +#[test] +fn test_method_calls() { + preinterpret_assert_eq!( + #( + let x = [1, 2, 3]; + x.len() + [!stream! "Hello" world].len() + ), + 2 + 3 + ); + preinterpret_assert_eq!( + #( + let x = [1, 2, 3]; + x.push(5); + x.push(2); + x.debug_string() + ), + "[1, 2, 3, 5, 2]" + ); + // Push returns None + preinterpret_assert_eq!( + #([1, 2, 3].as_mut().push(4).debug_string()), + "None" + ); + // Converting to mut and then to shared works + preinterpret_assert_eq!( + #([].as_mut().len().debug_string()), + "0usize" + ); + preinterpret_assert_eq!( + #( + let x = [1, 2, 3]; + let y = x.take(); + // x is now None + x.debug_string() + " - " + y.debug_string() + ), + "None - [1, 2, 3]" + ); + preinterpret_assert_eq!( + #( + let a = "a"; + let b = "b"; + a.swap(b); + [!string! #a " - " #b] + ), + "b - a" + ); +} diff --git a/tests/helpers/prelude.rs b/tests/helpers/prelude.rs new file mode 100644 index 00000000..41836121 --- /dev/null +++ b/tests/helpers/prelude.rs @@ -0,0 +1,26 @@ +// 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 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 + } +} + +macro_rules! preinterpret_assert_eq { + (#($($input:tt)*), $($output:tt)*) => { + assert_eq!(preinterpret!(#($($input)*)), $($output)*); + }; + ($input:tt, $($output:tt)*) => { + assert_eq!(preinterpret!($input), $($output)*); + }; +} + +pub(crate) use preinterpret_assert_eq; diff --git a/tests/literal.rs b/tests/literal.rs index 0f9ad07d..8acebd35 100644 --- a/tests/literal.rs +++ b/tests/literal.rs @@ -1,46 +1,42 @@ -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_literal() { - my_assert_eq!([!literal! '"' hello World! "\""], "helloWorld!"); + preinterpret_assert_eq!([!literal! '"' hello World! "\""], "helloWorld!"); } #[test] fn test_byte_string_literal() { - my_assert_eq!([!literal! b '"' hello World! "\""], b"helloWorld!"); + preinterpret_assert_eq!([!literal! b '"' hello World! "\""], b"helloWorld!"); } #[test] fn test_c_string_literal() { - my_assert_eq!([!literal! c '"' hello World! "\""], c"helloWorld!"); + preinterpret_assert_eq!([!literal! c '"' hello World! "\""], 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); + preinterpret_assert_eq!([!literal! "123" 456], 123456); + preinterpret_assert_eq!([!literal! 456u "32"], 456); + preinterpret_assert_eq!([!literal! 000 u64], 0); } #[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); + preinterpret_assert_eq!([!literal! 0 . 123], 0.123); + preinterpret_assert_eq!([!literal! 677f32], 677f32); + preinterpret_assert_eq!([!literal! "12" 9f64], 129f64); } #[test] fn test_character() { - my_assert_eq!([!literal! "'" 7 "'"], '7'); + preinterpret_assert_eq!([!literal! "'" 7 "'"], '7'); } #[test] fn test_byte_character() { - my_assert_eq!([!literal! "b'a'"], b'a'); + preinterpret_assert_eq!([!literal! "b'a'"], b'a'); } diff --git a/tests/string.rs b/tests/string.rs index c9102b59..c3ebecfa 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -1,295 +1,291 @@ -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! "真是难以置信!"], "真是难以置信!"); + preinterpret_assert_eq!([!string! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "my_MixedCaseSTRINGWhichis #awesome -whatdo youthink?"); + preinterpret_assert_eq!([!string! UPPER], "UPPER"); + preinterpret_assert_eq!([!string! lower], "lower"); + preinterpret_assert_eq!([!string! lower_snake_case], "lower_snake_case"); + preinterpret_assert_eq!([!string! UPPER_SNAKE_CASE], "UPPER_SNAKE_CASE"); + preinterpret_assert_eq!([!string! lowerCamelCase], "lowerCamelCase"); + preinterpret_assert_eq!([!string! UpperCamelCase], "UpperCamelCase"); + preinterpret_assert_eq!([!string! Capitalized], "Capitalized"); + preinterpret_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."); + preinterpret_assert_eq!([!string! "hello_w🌎rld"], "hello_w🌎rld"); + preinterpret_assert_eq!([!string! "kebab-case"], "kebab-case"); + preinterpret_assert_eq!([!string! "~~h4xx0rZ <3 1337c0de"], "~~h4xx0rZ <3 1337c0de"); + preinterpret_assert_eq!([!string! PostgreSQLConnection], "PostgreSQLConnection"); + preinterpret_assert_eq!([!string! PostgreSqlConnection], "PostgreSqlConnection"); + preinterpret_assert_eq!([!string! "U+000A LINE FEED (LF)"], "U+000A LINE FEED (LF)"); + preinterpret_assert_eq!([!string! "\nThis\r\n is a\tmulti-line\nstring"], "\nThis\r\n is a\tmulti-line\nstring"); + preinterpret_assert_eq!([!string! " lots of _ space and _whacky |c$ara_cte>>rs|"], " lots of _ space and _whacky |c$ara_cte>>rs|"); + preinterpret_assert_eq!([!string! "über CöÖl"], "über CöÖl"); + preinterpret_assert_eq!([!string! "◌̈ubër Cöol"], "◌̈ubër Cöol"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!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! "真是难以置信!"], "真是难以置信!"); + preinterpret_assert_eq!([!upper! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "MY_MIXEDCASESTRINGWHICHIS #AWESOME -WHATDO YOUTHINK?"); + preinterpret_assert_eq!([!upper! UPPER], "UPPER"); + preinterpret_assert_eq!([!upper! lower], "LOWER"); + preinterpret_assert_eq!([!upper! lower_snake_case], "LOWER_SNAKE_CASE"); + preinterpret_assert_eq!([!upper! UPPER_SNAKE_CASE], "UPPER_SNAKE_CASE"); + preinterpret_assert_eq!([!upper! lowerCamelCase], "LOWERCAMELCASE"); + preinterpret_assert_eq!([!upper! UpperCamelCase], "UPPERCAMELCASE"); + preinterpret_assert_eq!([!upper! Capitalized], "CAPITALIZED"); + preinterpret_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."); + preinterpret_assert_eq!([!upper! "hello_w🌎rld"], "HELLO_W🌎RLD"); + preinterpret_assert_eq!([!upper! "kebab-case"], "KEBAB-CASE"); + preinterpret_assert_eq!([!upper! "~~h4xx0rZ <3 1337c0de"], "~~H4XX0RZ <3 1337C0DE"); + preinterpret_assert_eq!([!upper! PostgreSQLConnection], "POSTGRESQLCONNECTION"); + preinterpret_assert_eq!([!upper! PostgreSqlConnection], "POSTGRESQLCONNECTION"); + preinterpret_assert_eq!([!upper! "U+000A LINE FEED (LF)"], "U+000A LINE FEED (LF)"); + preinterpret_assert_eq!([!upper! "\nThis\r\n is a\tmulti-line\nstring"], "\nTHIS\r\n IS A\tMULTI-LINE\nSTRING"); + preinterpret_assert_eq!([!upper! " lots of _ space and _whacky |c$ara_cte>>rs|"], " LOTS OF _ SPACE AND _WHACKY |C$ARA_CTE>>RS|"); + preinterpret_assert_eq!([!upper! "über CöÖl"], "ÜBER CÖÖL"); + preinterpret_assert_eq!([!upper! "◌̈ubër Cöol"], "◌̈UBËR CÖOL"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!upper! "真是难以置信!"], "真是难以置信!"); } #[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! "真是难以置信!"], "真是难以置信!"); + preinterpret_assert_eq!([!lower! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "my_mixedcasestringwhichis #awesome -whatdo youthink?"); + preinterpret_assert_eq!([!lower! UPPER], "upper"); + preinterpret_assert_eq!([!lower! lower], "lower"); + preinterpret_assert_eq!([!lower! lower_snake_case], "lower_snake_case"); + preinterpret_assert_eq!([!lower! UPPER_SNAKE_CASE], "upper_snake_case"); + preinterpret_assert_eq!([!lower! lowerCamelCase], "lowercamelcase"); + preinterpret_assert_eq!([!lower! UpperCamelCase], "uppercamelcase"); + preinterpret_assert_eq!([!lower! Capitalized], "capitalized"); + preinterpret_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."); + preinterpret_assert_eq!([!lower! "hello_w🌎rld"], "hello_w🌎rld"); + preinterpret_assert_eq!([!lower! "kebab-case"], "kebab-case"); + preinterpret_assert_eq!([!lower! "~~h4xx0rZ <3 1337c0de"], "~~h4xx0rz <3 1337c0de"); + preinterpret_assert_eq!([!lower! PostgreSQLConnection], "postgresqlconnection"); + preinterpret_assert_eq!([!lower! PostgreSqlConnection], "postgresqlconnection"); + preinterpret_assert_eq!([!lower! "U+000A LINE FEED (LF)"], "u+000a line feed (lf)"); + preinterpret_assert_eq!([!lower! "\nThis\r\n is a\tmulti-line\nstring"], "\nthis\r\n is a\tmulti-line\nstring"); + preinterpret_assert_eq!([!lower! " lots of _ space and _whacky |c$ara_cte>>rs|"], " lots of _ space and _whacky |c$ara_cte>>rs|"); + preinterpret_assert_eq!([!lower! "über CöÖl"], "über cööl"); + preinterpret_assert_eq!([!lower! "◌̈ubër Cööl"], "◌̈ubër cööl"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!lower! "真是难以置信!"], "真是难以置信!"); } #[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! "真是难以置信!"], "真是难以置信"); + preinterpret_assert_eq!([!snake! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "my_mixed_case_string_whichis_awesome_whatdo_youthink"); + preinterpret_assert_eq!([!snake! UPPER], "upper"); + preinterpret_assert_eq!([!snake! lower], "lower"); + preinterpret_assert_eq!([!snake! lower_snake_case], "lower_snake_case"); + preinterpret_assert_eq!([!snake! UPPER_SNAKE_CASE], "upper_snake_case"); + preinterpret_assert_eq!([!snake! lowerCamelCase], "lower_camel_case"); + preinterpret_assert_eq!([!snake! UpperCamelCase], "upper_camel_case"); + preinterpret_assert_eq!([!snake! Capitalized], "capitalized"); + preinterpret_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"); + preinterpret_assert_eq!([!snake! "hello_w🌎rld"], "hello_w_rld"); + preinterpret_assert_eq!([!snake! "kebab-case"], "kebab_case"); + preinterpret_assert_eq!([!snake! "~~h4xx0rZ <3 1337c0de"], "h4xx0r_z_3_1337c0de"); + preinterpret_assert_eq!([!snake! PostgreSQLConnection], "postgre_sql_connection"); + preinterpret_assert_eq!([!snake! PostgreSqlConnection], "postgre_sql_connection"); + preinterpret_assert_eq!([!snake! "U+000A LINE FEED (LF)"], "u_000a_line_feed_lf"); + preinterpret_assert_eq!([!snake! "\nThis\r\n is a\tmulti-line\nstring"], "this_is_a_multi_line_string"); + preinterpret_assert_eq!([!snake! " lots of _ space and _whacky |c$ara_cte>>rs|"], "lots_of_space_and_whacky_c_ara_cte_rs"); + preinterpret_assert_eq!([!snake! "über CöÖl"], "über_cö_öl"); + preinterpret_assert_eq!([!snake! "◌̈ubër Cööl"], "ube_r_cööl"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!snake! "真是难以置信!"], "真是难以置信"); } #[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! "真是难以置信!"], "真是难以置信"); + preinterpret_assert_eq!([!upper_snake! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "MY_MIXED_CASE_STRING_WHICHIS_AWESOME_WHATDO_YOUTHINK"); + preinterpret_assert_eq!([!upper_snake! UPPER], "UPPER"); + preinterpret_assert_eq!([!upper_snake! lower], "LOWER"); + preinterpret_assert_eq!([!upper_snake! lower_snake_case], "LOWER_SNAKE_CASE"); + preinterpret_assert_eq!([!upper_snake! UPPER_SNAKE_CASE], "UPPER_SNAKE_CASE"); + preinterpret_assert_eq!([!upper_snake! lowerCamelCase], "LOWER_CAMEL_CASE"); + preinterpret_assert_eq!([!upper_snake! UpperCamelCase], "UPPER_CAMEL_CASE"); + preinterpret_assert_eq!([!upper_snake! Capitalized], "CAPITALIZED"); + preinterpret_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"); + preinterpret_assert_eq!([!upper_snake! "hello_w🌎rld"], "HELLO_W_RLD"); + preinterpret_assert_eq!([!upper_snake! "kebab-case"], "KEBAB_CASE"); + preinterpret_assert_eq!([!upper_snake! "~~h4xx0rZ <3 1337c0de"], "H4XX0R_Z_3_1337C0DE"); + preinterpret_assert_eq!([!upper_snake! PostgreSQLConnection], "POSTGRE_SQL_CONNECTION"); + preinterpret_assert_eq!([!upper_snake! PostgreSqlConnection], "POSTGRE_SQL_CONNECTION"); + preinterpret_assert_eq!([!upper_snake! "U+000A LINE FEED (LF)"], "U_000A_LINE_FEED_LF"); + preinterpret_assert_eq!([!upper_snake! "\nThis\r\n is a\tmulti-line\nstring"], "THIS_IS_A_MULTI_LINE_STRING"); + preinterpret_assert_eq!([!upper_snake! " lots of _ space and _whacky |c$ara_cte>>rs|"], "LOTS_OF_SPACE_AND_WHACKY_C_ARA_CTE_RS"); + preinterpret_assert_eq!([!upper_snake! "über CöÖl"], "ÜBER_CÖ_ÖL"); + preinterpret_assert_eq!([!upper_snake! "◌̈ubër Cöol"], "UBE_R_CÖOL"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!upper_snake! "真是难以置信!"], "真是难以置信"); } #[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! "真是难以置信!"], "真是难以置信"); + preinterpret_assert_eq!([!kebab! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "my-mixed-case-string-whichis-awesome-whatdo-youthink"); + preinterpret_assert_eq!([!kebab! UPPER], "upper"); + preinterpret_assert_eq!([!kebab! lower], "lower"); + preinterpret_assert_eq!([!kebab! lower_snake_case], "lower-snake-case"); + preinterpret_assert_eq!([!kebab! UPPER_SNAKE_CASE], "upper-snake-case"); + preinterpret_assert_eq!([!kebab! lowerCamelCase], "lower-camel-case"); + preinterpret_assert_eq!([!kebab! UpperCamelCase], "upper-camel-case"); + preinterpret_assert_eq!([!kebab! Capitalized], "capitalized"); + preinterpret_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"); + preinterpret_assert_eq!([!kebab! "hello_w🌎rld"], "hello-w-rld"); + preinterpret_assert_eq!([!kebab! "kebab-case"], "kebab-case"); + preinterpret_assert_eq!([!kebab! "~~h4xx0rZ <3 1337c0de"], "h4xx0r-z-3-1337c0de"); + preinterpret_assert_eq!([!kebab! PostgreSQLConnection], "postgre-sql-connection"); + preinterpret_assert_eq!([!kebab! PostgreSqlConnection], "postgre-sql-connection"); + preinterpret_assert_eq!([!kebab! "U+000A LINE FEED (LF)"], "u-000a-line-feed-lf"); + preinterpret_assert_eq!([!kebab! "\nThis\r\n is a\tmulti-line\nstring"], "this-is-a-multi-line-string"); + preinterpret_assert_eq!([!kebab! " lots of _ space and _whacky |c$ara_cte>>rs|"], "lots-of-space-and-whacky-c-ara-cte-rs"); + preinterpret_assert_eq!([!kebab! "über CöÖl"], "über-cö-öl"); + preinterpret_assert_eq!([!kebab! "◌̈ubër Cööl"], "ube-r-cööl"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!kebab! "真是难以置信!"], "真是难以置信"); } #[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! "真是难以置信!"], "真是难以置信"); + preinterpret_assert_eq!([!lower_camel! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "myMixedCaseStringWhichisAwesomeWhatdoYouthink"); + preinterpret_assert_eq!([!lower_camel! UPPER], "upper"); + preinterpret_assert_eq!([!lower_camel! lower], "lower"); + preinterpret_assert_eq!([!lower_camel! lower_snake_case], "lowerSnakeCase"); + preinterpret_assert_eq!([!lower_camel! UPPER_SNAKE_CASE], "upperSnakeCase"); + preinterpret_assert_eq!([!lower_camel! lowerCamelCase], "lowerCamelCase"); + preinterpret_assert_eq!([!lower_camel! UpperCamelCase], "upperCamelCase"); + preinterpret_assert_eq!([!lower_camel! Capitalized], "capitalized"); + preinterpret_assert_eq!([!lower_camel! "THEY SAID: A quick brown fox jumps over the lazy dog."], "theySaidAQuickBrownFoxJumpsOverTheLazyDog"); + preinterpret_assert_eq!([!lower_camel! "hello_w🌎rld"], "helloWRld"); + preinterpret_assert_eq!([!lower_camel! "kebab-case"], "kebabCase"); + preinterpret_assert_eq!([!lower_camel! "~~h4xx0rZ <3 1337c0de"], "h4xx0rZ31337c0de"); + preinterpret_assert_eq!([!lower_camel! PostgreSQLConnection], "postgreSqlConnection"); + preinterpret_assert_eq!([!lower_camel! PostgreSqlConnection], "postgreSqlConnection"); + preinterpret_assert_eq!([!lower_camel! "U+000A LINE FEED (LF)"], "u000aLineFeedLf"); + preinterpret_assert_eq!([!lower_camel! "\nThis\r\n is a\tmulti-line\nstring"], "thisIsAMultiLineString"); + preinterpret_assert_eq!([!lower_camel! " lots of _ space and _whacky |c$ara_cte>>rs|"], "lotsOfSpaceAndWhackyCAraCteRs"); + preinterpret_assert_eq!([!lower_camel! "über CöÖl"], "überCöÖl"); + preinterpret_assert_eq!([!lower_camel! "◌̈ubër Cööl"], "ubeRCööl"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!lower_camel! "真是难以置信!"], "真是难以置信"); } #[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! "真是难以置信!"], "真是难以置信"); + preinterpret_assert_eq!([!camel! my_ MixedCase STRING Which is " #awesome " - what "do you" think?], "MyMixedCaseStringWhichisAwesomeWhatdoYouthink"); + preinterpret_assert_eq!([!camel! UPPER], "Upper"); + preinterpret_assert_eq!([!camel! lower], "Lower"); + preinterpret_assert_eq!([!camel! lower_snake_case], "LowerSnakeCase"); + preinterpret_assert_eq!([!camel! UPPER_SNAKE_CASE], "UpperSnakeCase"); + preinterpret_assert_eq!([!camel! lowerCamelCase], "LowerCamelCase"); + preinterpret_assert_eq!([!camel! UpperCamelCase], "UpperCamelCase"); + preinterpret_assert_eq!([!camel! Capitalized], "Capitalized"); + preinterpret_assert_eq!([!camel! "THEY SAID: A quick brown fox jumps over the lazy dog."], "TheySaidAQuickBrownFoxJumpsOverTheLazyDog"); + preinterpret_assert_eq!([!camel! "hello_w🌎rld"], "HelloWRld"); + preinterpret_assert_eq!([!camel! "kebab-case"], "KebabCase"); + preinterpret_assert_eq!([!camel! "~~h4xx0rZ <3 1337c0de"], "H4xx0rZ31337c0de"); + preinterpret_assert_eq!([!camel! PostgreSQLConnection], "PostgreSqlConnection"); + preinterpret_assert_eq!([!camel! PostgreSqlConnection], "PostgreSqlConnection"); + preinterpret_assert_eq!([!camel! "U+000A LINE FEED (LF)"], "U000aLineFeedLf"); + preinterpret_assert_eq!([!camel! "\nThis\r\n is a\tmulti-line\nstring"], "ThisIsAMultiLineString"); + preinterpret_assert_eq!([!camel! " lots of _ space and _whacky |c$ara_cte>>rs|"], "LotsOfSpaceAndWhackyCAraCteRs"); + preinterpret_assert_eq!([!camel! "über CöÖl"], "ÜberCöÖl"); + preinterpret_assert_eq!([!camel! "◌̈ubër Cööl"], "UbeRCööl"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!camel! "真是难以置信!"], "真是难以置信"); } #[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! "真是难以置信!"], "真是难以置信!"); + preinterpret_assert_eq!([!capitalize! my_ MixedCase STRING Which is " #awesome " - what do you think?], "My_MixedCaseSTRINGWhichis #awesome -whatdoyouthink?"); + preinterpret_assert_eq!([!capitalize! UPPER], "UPPER"); + preinterpret_assert_eq!([!capitalize! lower], "Lower"); + preinterpret_assert_eq!([!capitalize! lower_snake_case], "Lower_snake_case"); + preinterpret_assert_eq!([!capitalize! UPPER_SNAKE_CASE], "UPPER_SNAKE_CASE"); + preinterpret_assert_eq!([!capitalize! lowerCamelCase], "LowerCamelCase"); + preinterpret_assert_eq!([!capitalize! UpperCamelCase], "UpperCamelCase"); + preinterpret_assert_eq!([!capitalize! Capitalized], "Capitalized"); + preinterpret_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."); + preinterpret_assert_eq!([!capitalize! "hello_w🌎rld"], "Hello_w🌎rld"); + preinterpret_assert_eq!([!capitalize! "kebab-case"], "Kebab-case"); + preinterpret_assert_eq!([!capitalize! "~~h4xx0rZ <3 1337c0de"], "~~H4xx0rZ <3 1337c0de"); + preinterpret_assert_eq!([!capitalize! PostgreSQLConnection], "PostgreSQLConnection"); + preinterpret_assert_eq!([!capitalize! PostgreSqlConnection], "PostgreSqlConnection"); + preinterpret_assert_eq!([!capitalize! "U+000A LINE FEED (LF)"], "U+000A LINE FEED (LF)"); + preinterpret_assert_eq!([!capitalize! "\nThis\r\n is a\tmulti-line\nstring"], "\nThis\r\n is a\tmulti-line\nstring"); + preinterpret_assert_eq!([!capitalize! " lots of _ space and _whacky |c$ara_cte>>rs|"], " Lots of _ space and _whacky |c$ara_cte>>rs|"); + preinterpret_assert_eq!([!capitalize! "über CöÖl"], "Über CöÖl"); + preinterpret_assert_eq!([!capitalize! "◌̈ubër Cööl"], "◌̈Ubër Cööl"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!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! "真是难以置信!"], "真是难以置信!"); + preinterpret_assert_eq!([!decapitalize! my_ MixedCase STRING Which is " #awesome " - what do you think?], "my_MixedCaseSTRINGWhichis #awesome -whatdoyouthink?"); + preinterpret_assert_eq!([!decapitalize! UPPER], "uPPER"); + preinterpret_assert_eq!([!decapitalize! lower], "lower"); + preinterpret_assert_eq!([!decapitalize! lower_snake_case], "lower_snake_case"); + preinterpret_assert_eq!([!decapitalize! UPPER_SNAKE_CASE], "uPPER_SNAKE_CASE"); + preinterpret_assert_eq!([!decapitalize! lowerCamelCase], "lowerCamelCase"); + preinterpret_assert_eq!([!decapitalize! UpperCamelCase], "upperCamelCase"); + preinterpret_assert_eq!([!decapitalize! Capitalized], "capitalized"); + preinterpret_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."); + preinterpret_assert_eq!([!decapitalize! "hello_w🌎rld"], "hello_w🌎rld"); + preinterpret_assert_eq!([!decapitalize! "kebab-case"], "kebab-case"); + preinterpret_assert_eq!([!decapitalize! "~~h4xx0rZ <3 1337c0de"], "~~h4xx0rZ <3 1337c0de"); + preinterpret_assert_eq!([!decapitalize! PostgreSQLConnection], "postgreSQLConnection"); + preinterpret_assert_eq!([!decapitalize! PostgreSqlConnection], "postgreSqlConnection"); + preinterpret_assert_eq!([!decapitalize! "U+000A LINE FEED (LF)"], "u+000A LINE FEED (LF)"); + preinterpret_assert_eq!([!decapitalize! "\nThis\r\n is a\tmulti-line\nstring"], "\nthis\r\n is a\tmulti-line\nstring"); + preinterpret_assert_eq!([!decapitalize! " lots of _ space and _whacky |c$ara_cte>>rs|"], " lots of _ space and _whacky |c$ara_cte>>rs|"); + preinterpret_assert_eq!([!decapitalize! "über CöÖl"], "über CöÖl"); + preinterpret_assert_eq!([!decapitalize! "◌̈ubër Cööl"], "◌̈ubër Cööl"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!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! "真是难以置信!"], "真是难以置信"); + preinterpret_assert_eq!([!title! my_ MixedCase STRING Which is " #awesome " - what do you think?], "My Mixed Case String Whichis Awesome Whatdoyouthink"); + preinterpret_assert_eq!([!title! UPPER], "Upper"); + preinterpret_assert_eq!([!title! lower], "Lower"); + preinterpret_assert_eq!([!title! lower_snake_case], "Lower Snake Case"); + preinterpret_assert_eq!([!title! UPPER_SNAKE_CASE], "Upper Snake Case"); + preinterpret_assert_eq!([!title! lowerCamelCase], "Lower Camel Case"); + preinterpret_assert_eq!([!title! UpperCamelCase], "Upper Camel Case"); + preinterpret_assert_eq!([!title! Capitalized], "Capitalized"); + preinterpret_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"); + preinterpret_assert_eq!([!title! "hello_w🌎rld"], "Hello W Rld"); + preinterpret_assert_eq!([!title! "kebab-case"], "Kebab Case"); + preinterpret_assert_eq!([!title! "~~h4xx0rZ <3 1337c0de"], "H4xx0r Z 3 1337c0de"); + preinterpret_assert_eq!([!title! PostgreSQLConnection], "Postgre Sql Connection"); + preinterpret_assert_eq!([!title! PostgreSqlConnection], "Postgre Sql Connection"); + preinterpret_assert_eq!([!title! "U+000A LINE FEED (LF)"], "U 000a Line Feed Lf"); + preinterpret_assert_eq!([!title! "\nThis\r\n is a\tmulti-line\nstring"], "This Is A Multi Line String"); + preinterpret_assert_eq!([!title! " lots of _ space and _whacky |c$ara_cte>>rs|"], "Lots Of Space And Whacky C Ara Cte Rs"); + preinterpret_assert_eq!([!title! "über CöÖl"], "Über Cö Öl"); + preinterpret_assert_eq!([!title! "◌̈ubër Cööl"], "Ube R Cööl"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!title! "真是难以置信!"], "真是难以置信"); } #[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! "真是难以置信!"], "真是难以置信"); + preinterpret_assert_eq!([!insert_spaces! my_ MixedCase STRING Which is " #awesome " - what do you think?], "my Mixed Case STRING Whichis awesome whatdoyouthink"); + preinterpret_assert_eq!([!insert_spaces! UPPER], "UPPER"); + preinterpret_assert_eq!([!insert_spaces! lower], "lower"); + preinterpret_assert_eq!([!insert_spaces! lower_snake_case], "lower snake case"); + preinterpret_assert_eq!([!insert_spaces! UPPER_SNAKE_CASE], "UPPER SNAKE CASE"); + preinterpret_assert_eq!([!insert_spaces! lowerCamelCase], "lower Camel Case"); + preinterpret_assert_eq!([!insert_spaces! UpperCamelCase], "Upper Camel Case"); + preinterpret_assert_eq!([!insert_spaces! Capitalized], "Capitalized"); + preinterpret_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"); + preinterpret_assert_eq!([!insert_spaces! "hello_w🌎rld"], "hello w rld"); + preinterpret_assert_eq!([!insert_spaces! "kebab-case"], "kebab case"); + preinterpret_assert_eq!([!insert_spaces! "~~h4xx0rZ <3 1337c0de"], "h4xx0r Z 3 1337c0de"); + preinterpret_assert_eq!([!insert_spaces! PostgreSQLConnection], "Postgre SQL Connection"); + preinterpret_assert_eq!([!insert_spaces! PostgreSqlConnection], "Postgre Sql Connection"); + preinterpret_assert_eq!([!insert_spaces! "U+000A LINE FEED (LF)"], "U 000A LINE FEED LF"); + preinterpret_assert_eq!([!insert_spaces! "\nThis\r\n is a\tmulti-line\nstring"], "This is a multi line string"); + preinterpret_assert_eq!([!insert_spaces! " lots of _ space and _whacky |c$ara_cte>>rs|"], "lots of space and whacky c ara cte rs"); + preinterpret_assert_eq!([!insert_spaces! "über CöÖl"], "über Cö Öl"); + preinterpret_assert_eq!([!insert_spaces! "◌̈ubër Cööl"], "ube r Cööl"); // The ë (and only the e) uses a post-fix combining character + preinterpret_assert_eq!([!insert_spaces! "真是难以置信!"], "真是难以置信"); } diff --git a/tests/tokens.rs b/tests/tokens.rs new file mode 100644 index 00000000..69b02644 --- /dev/null +++ b/tests/tokens.rs @@ -0,0 +1,402 @@ +#[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/tokens/*.rs"); +} + +#[test] +fn test_empty_stream_is_empty() { + preinterpret_assert_eq!({ + [!stream!] "hello" [!stream!] [!stream!] + }, "hello"); + preinterpret_assert_eq!([!is_empty!], true); + preinterpret_assert_eq!([!is_empty! [!stream!]], true); + preinterpret_assert_eq!([!is_empty! [!stream!] [!stream!]], true); + preinterpret_assert_eq!([!is_empty! Not Empty], false); + preinterpret_assert_eq!({ + [!set! #x =] + [!is_empty! #..x] + }, true); + preinterpret_assert_eq!({ + [!set! #x =] + [!set! #x = #x is no longer empty] + [!is_empty! #x] + }, false); +} + +#[test] +fn test_length_and_group() { + preinterpret_assert_eq!({ + [!length! "hello" World] + }, 2); + preinterpret_assert_eq!({ [!length! ("hello" World)] }, 1); + preinterpret_assert_eq!({ [!length! [!group! "hello" World]] }, 1); + preinterpret_assert_eq!({ + [!set! #x = Hello "World" (1 2 3 4 5)] + [!length! #..x] + }, 3); + preinterpret_assert_eq!({ + [!set! #x = Hello "World" (1 2 3 4 5)] + [!length! [!group! #..x]] + }, 1); +} + +#[test] +fn test_intersperse() { + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! Hello World], + separator: [", "], + }] as string), + "Hello, World" + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! Hello World], + separator: [!stream! _ "and" _], + }] as stream as string), + "Hello_and_World" + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! Hello World], + separator: [!stream! _ "and" _], + add_trailing: true, + }] as stream as string), + "Hello_and_World_and_" + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! The Quick Brown Fox], + separator: [!stream!], + }] as stream as string), + "TheQuickBrownFox" + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! The Quick Brown Fox], + separator: [!stream! ,], + add_trailing: true, + }] as stream as string), + "The,Quick,Brown,Fox," + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! Red Green Blue], + separator: [!stream! ", "], + final_separator: [!stream! " and "], + }] as stream as string), + "Red, Green and Blue" + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! Red Green Blue], + separator: [!stream! ", "], + add_trailing: true, + final_separator: [!stream! " and "], + }] as stream as string), + "Red, Green, Blue and " + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream!], + separator: [!stream! ", "], + add_trailing: true, + final_separator: [!stream! " and "], + }] as stream as string), + "" + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! SingleItem], + separator: [!stream! ","], + final_separator: [!stream! "!"], + }] as stream as string), + "SingleItem" + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! SingleItem], + separator: [!stream! ","], + final_separator: [!stream! "!"], + add_trailing: true, + }] as stream as string), + "SingleItem!" + ); + preinterpret_assert_eq!( + #([!intersperse! { + items: [!stream! SingleItem], + separator: [!stream! ","], + add_trailing: true, + }] as stream as string), + "SingleItem," + ); +} + +#[test] +fn complex_cases_for_intersperse_and_input_types() { + // Variable containing stream be used for items + preinterpret_assert_eq!({ + [!set! #items = 0 1 2 3] + #([!intersperse! { + items: items, + separator: [!stream! _], + }] as stream as string) + }, "0_1_2_3"); + // Variable containing iterable can be used for items + preinterpret_assert_eq!({ + #(let items = 0..4) + #([!intersperse! { + items: items, + separator: [!stream! _], + }] as stream as string) + }, "0_1_2_3"); + // #(...) block returning token stream (from variable) + preinterpret_assert_eq!({ + [!set! #items = 0 1 2 3] + #([!intersperse! { + items: #(items), + separator: ["_"], + }] as string) + }, "0_1_2_3"); + // #(...) block returning array + preinterpret_assert_eq!( + #([!intersperse! { + items: #([0, 1, 2, 3]), + separator: ["_"], + }] as string), + "0_1_2_3" + ); + // Stream containing two groups + preinterpret_assert_eq!( + #( + let items = [!stream! 0 1]; + [!intersperse! { + items: [!stream! #items #items], // [!stream! [!group! 0 1] [!group! 0 1]] + separator: [!stream! _], + }] as string + ), + "01_01", + ); + // Commands can be used, if they return a valid iterable (e.g. a stream) + preinterpret_assert_eq!( + #([!intersperse! { + items: [!if! false { 0 1 } !else! { 2 3 }], + separator: [!stream! _], + }] as stream as string), + "2_3" + ); + // All inputs can be variables + // Inputs can be in any order + preinterpret_assert_eq!( + #( + let people = [!stream! Anna Barbara Charlie]; + let separator = [", "]; + let final_separator = [" and "]; + let add_trailing = false; + [!intersperse! { + separator: separator.take(), + final_separator: final_separator.take(), + add_trailing: add_trailing, + items: people, + }] as string + ), + "Anna, Barbara and Charlie" + ); + // Add trailing is executed even if it's irrelevant because there are no items + preinterpret_assert_eq!( + #( + let x = "NOT_EXECUTED"; + let _ = [!intersperse! { + items: [], + separator: [], + add_trailing: #( + x = "EXECUTED"; + false + ), + }]; + x + ), + "EXECUTED", + ); +} + +#[test] +fn test_split() { + // Empty separators are allowed, and split on every token + // In this case, drop_empty_start / drop_empty_end are ignored + preinterpret_assert_eq!( + #( + [!split! { + stream: [!stream! A::B], + separator: [!stream!], + }].debug_string() + ), + "[[!stream! A], [!stream! :], [!stream! :], [!stream! B]]" + ); + // Double separators are allowed + preinterpret_assert_eq!( + #( + [!split! { + stream: [!stream! A::B::C], + separator: [!stream! ::], + }].debug_string() + ), + "[[!stream! A], [!stream! B], [!stream! C]]" + ); + // Trailing separator is ignored by default + preinterpret_assert_eq!( + #( + [!split! { + stream: [!stream! Pizza, Mac and Cheese, Hamburger,], + separator: [!stream! ,], + }].debug_string() + ), + "[[!stream! Pizza], [!stream! Mac and Cheese], [!stream! Hamburger]]" + ); + // By default, empty groups are included except at the end + preinterpret_assert_eq!( + #( + ([!split! { + stream: [!stream! ::A::B::::C::], + separator: [!stream! ::], + }] as stream).debug_string() + ), + "[!stream! [!group!] [!group! A] [!group! B] [!group!] [!group! C]]" + ); + // Stream and separator are both interpreted + preinterpret_assert_eq!( + #( + let x = [!stream! ;]; + ([!split! { + stream: [!stream! ;A;;B;C;D #..x E;], + separator: x, + drop_empty_start: true, + drop_empty_middle: true, + drop_empty_end: true, + }] as stream).debug_string() + ), + "[!stream! [!group! A] [!group! B] [!group! C] [!group! D] [!group! E]]"); + // Drop empty false works + preinterpret_assert_eq!( + #( + let x = [!stream! ;]; + let output = [!split! { + stream: [!stream! ;A;;B;C;D #..x E;], + separator: x, + drop_empty_start: false, + drop_empty_middle: false, + drop_empty_end: false, + }] as stream; + output.debug_string() + ), + "[!stream! [!group!] [!group! A] [!group!] [!group! B] [!group! C] [!group! D] [!group! E] [!group!]]" + ); + // Drop empty middle works + preinterpret_assert_eq!( + #( + [!split! { + stream: [!stream! ;A;;B;;;;E;], + separator: [!stream! ;], + drop_empty_start: false, + drop_empty_middle: true, + drop_empty_end: false, + }].debug_string() + ), + "[[!stream!], [!stream! A], [!stream! B], [!stream! E], [!stream!]]" + ); +} + +#[test] +fn test_comma_split() { + preinterpret_assert_eq!( + #([!comma_split! Pizza, Mac and Cheese, Hamburger,].debug_string()), + "[[!stream! Pizza], [!stream! Mac and Cheese], [!stream! Hamburger]]" + ); +} + +#[test] +fn test_zip() { + preinterpret_assert_eq!( + #([!zip! [[!stream! Hello "Goodbye"], ["World", "Friend"]]].debug_string()), + r#"[[[!stream! Hello], "World"], ["Goodbye", "Friend"]]"#, + ); + preinterpret_assert_eq!( + #( + let countries = [!stream! "France" "Germany" "Italy"]; + let flags = [!stream! "🇫🇷" "🇩🇪" "🇮🇹"]; + let capitals = [!stream! "Paris" "Berlin" "Rome"]; + [!zip! [countries, flags, capitals]].debug_string() + ), + r#"[["France", "🇫🇷", "Paris"], ["Germany", "🇩🇪", "Berlin"], ["Italy", "🇮🇹", "Rome"]]"#, + ); + preinterpret_assert_eq!( + #( + let longer = [!stream! A B C D]; + let shorter = [1, 2, 3]; + [!zip_truncated! [longer, shorter.take()]].debug_string() + ), + r#"[[[!stream! A], 1], [[!stream! B], 2], [[!stream! C], 3]]"#, + ); + preinterpret_assert_eq!( + #( + let letters = [!stream! A B C]; + let numbers = [1, 2, 3]; + [!zip! [letters, numbers.take()]].debug_string() + ), + r#"[[[!stream! A], 1], [[!stream! B], 2], [[!stream! C], 3]]"#, + ); + preinterpret_assert_eq!( + #( + let letters = [!stream! A B C]; + let numbers = [1, 2, 3]; + let combined = [letters, numbers.take()]; + [!zip! combined.take()].debug_string() + ), + r#"[[[!stream! A], 1], [[!stream! B], 2], [[!stream! C], 3]]"#, + ); + preinterpret_assert_eq!( + #( + [!set! #letters = A B C]; + let numbers = [1, 2, 3]; + [!zip! { number: numbers.take(), letter: letters }].debug_string() + ), + r#"[{ letter: [!stream! A], number: 1 }, { letter: [!stream! B], number: 2 }, { letter: [!stream! C], number: 3 }]"#, + ); + preinterpret_assert_eq!(#([!zip![]].debug_string()), r#"[]"#); + preinterpret_assert_eq!(#([!zip! {}].debug_string()), r#"[]"#); +} + +#[test] +fn test_zip_with_for() { + preinterpret_assert_eq!( + { + [!set! #countries = France Germany Italy] + #(let flags = ["🇫🇷", "🇩🇪", "🇮🇹"]) + [!set! #capitals = "Paris" "Berlin" "Rome"] + #(let facts = []) + [!for! [country, flag, capital] in [!zip! [countries, flags.take(), capitals]] { + #(facts.push([!string! "=> The capital of " #country " is " #capital " and its flag is " #flag])) + }] + + #("The facts are:\n" + [!intersperse! { + items: facts.take(), + separator: ["\n"], + }] as 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 🇮🇹 +"#, + ); +} diff --git a/tests/transforming.rs b/tests/transforming.rs new file mode 100644 index 00000000..3fec1a34 --- /dev/null +++ b/tests/transforming.rs @@ -0,0 +1,200 @@ +#[path = "helpers/prelude.rs"] +mod prelude; +use prelude::*; + +#[test] +#[cfg_attr(miri, ignore = "incompatible with miri")] +fn test_transfoming_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/transforming/*.rs"); +} + +#[test] +fn test_variable_parsing() { + preinterpret_assert_eq!({ + [!let! = ] + [!string! #inner] + }, "Beautiful"); + preinterpret_assert_eq!({ + [!let! #..inner = ] + [!string! #inner] + }, ""); + preinterpret_assert_eq!({ + [!let! #..x = Hello => World] + [!string! #x] + }, "Hello=>World"); + preinterpret_assert_eq!({ + [!let! Hello #..x!! = Hello => World!!] + [!string! #x] + }, "=>World"); + preinterpret_assert_eq!({ + [!let! Hello #..x World = Hello => World] + [!string! #x] + }, "=>"); + preinterpret_assert_eq!({ + [!let! Hello #..x World = Hello And Welcome To The Wonderful World] + [!string! #x] + }, "AndWelcomeToTheWonderful"); + preinterpret_assert_eq!({ + [!let! Hello #..x "World"! = Hello World And Welcome To The Wonderful "World"!] + [!string! #x] + }, "WorldAndWelcomeToTheWonderful"); + preinterpret_assert_eq!({ + [!let! #..x (#..y) = Why Hello (World)] + [!string! "#x = " #x "; #y = " #y] + }, "#x = WhyHello; #y = World"); + preinterpret_assert_eq!({ + [!set! #x =] + [!let! + #>>x // Matches one tt and appends it: Why + #..>>x // Matches stream until (, appends it grouped: [!group! Hello Everyone] + ( + #>>..x // Matches one tt and appends it flattened: This is an exciting adventure + #..>>..x // Matches stream and appends it flattened: do you agree ? + ) + = Why Hello Everyone ([!group! This is an exciting adventure] do you agree?)] + #(x.debug_string()) + }, "[!stream! Why [!group! Hello Everyone] This is an exciting adventure do you agree ?]"); +} + +#[test] +fn test_explicit_transform_stream() { + // It's not very exciting + preinterpret!([!let! @(Hello World) = Hello World]); + preinterpret!([!let! Hello @(World) = Hello World]); + preinterpret!([!let! @(Hello @(World)) = Hello World]); +} + +#[test] +fn test_ident_transformer() { + preinterpret_assert_eq!({ + [!let! The "quick" @(#x = @IDENT) fox "jumps" = The "quick" brown fox "jumps"] + [!string! #x] + }, "brown"); + preinterpret_assert_eq!({ + [!set! #x =] + [!let! The quick @(#x += @IDENT) fox jumps @(#x += @IDENT) the lazy dog = The quick brown fox jumps over the lazy dog] + #(x.debug_string()) + }, "[!stream! brown over]"); +} + +#[test] +fn test_literal_transformer() { + preinterpret_assert_eq!({ + [!let! The "quick" @(#x = @LITERAL) fox "jumps" = The "quick" "brown" fox "jumps"] + #x + }, "brown"); + // Lots of literals + preinterpret_assert_eq!({ + [!set! #x] + [!let! @LITERAL @LITERAL @LITERAL @(#x += @LITERAL) @LITERAL @(#x += @LITERAL @LITERAL) = "Hello" 9 3.4 'c' 41u16 0b1010 r#"123"#] + #(x.debug_string()) + }, "[!stream! 'c' 0b1010 r#\"123\"#]"); +} + +#[test] +fn test_punct_transformer() { + preinterpret_assert_eq!({ + [!let! The "quick" brown fox "jumps" @(#x = @PUNCT) = The "quick" brown fox "jumps"!] + #(x.debug_string()) + }, "[!stream! !]"); + // Test for ' which is treated weirdly by syn / rustc + preinterpret_assert_eq!({ + [!let! The "quick" fox isn 't brown and doesn @(#x = @PUNCT) t "jump" = The "quick" fox isn 't brown and doesn 't "jump"] + #(x.debug_string()) + }, "[!stream! ']"); + // Lots of punctuation, most of it ignored + preinterpret_assert_eq!({ + [!set! #x =] + [!let! @PUNCT @PUNCT @PUNCT @PUNCT @(#x += @PUNCT) @PUNCT @PUNCT @PUNCT @PUNCT @PUNCT @(#x += @PUNCT) @PUNCT @PUNCT @PUNCT = # ! $$ % ^ & * + = | @ : ;] + #(x.debug_string()) + }, "[!stream! % |]"); +} + +#[test] +fn test_group_transformer() { + preinterpret_assert_eq!({ + [!let! The "quick" @[GROUP brown #x] "jumps" = The "quick" [!group! brown fox] "jumps"] + #(x.debug_string()) + }, "[!stream! fox]"); + preinterpret_assert_eq!({ + [!set! #x = "hello" "world"] + [!let! I said @[GROUP #..y]! = I said #x!] + #(y.debug_string()) + }, "[!stream! \"hello\" \"world\"]"); + // ... which is equivalent to this: + preinterpret_assert_eq!({ + [!set! #x = "hello" "world"] + [!let! I said #y! = I said #x!] + #(y.debug_string()) + }, "[!stream! \"hello\" \"world\"]"); +} + +#[test] +fn test_none_output_commands_mid_parse() { + preinterpret_assert_eq!({ + [!let! The "quick" @(#x = @LITERAL) fox [!let! #y = #x] @(#x = @IDENT) = The "quick" "brown" fox jumps] + [!string! "#x = " #(x.debug_string()) "; #y = "#(y.debug_string())] + }, "#x = [!stream! jumps]; #y = \"brown\""); +} + +#[test] +fn test_raw_content_in_exact_transformer() { + preinterpret_assert_eq!({ + [!set! #x = true] + [!let! The @[EXACT [!raw! #x]] = The [!raw! #] x] + #x + }, true); +} + +#[test] +fn test_exact_transformer() { + // EXACT works + preinterpret_assert_eq!({ + [!set! #x = true] + [!let! The @[EXACT #..x] = The true] + #x + }, true); + // EXACT is evaluated at execution time + preinterpret_assert_eq!({ + [!set! #x =] + [!let! The #>>..x fox is #>>..x. It 's super @[EXACT #..x]. = The brown fox is brown. It 's super brown brown.] + true + }, true); +} + +#[test] +fn test_parse_command_and_exact_transformer() { + // The output stream is additive + preinterpret_assert_eq!( + #([!parse! [!stream! Hello World] with @(@IDENT @IDENT)].debug_string()), + "[!stream! Hello World]" + ); + // Substreams redirected to a variable are not included in the output + preinterpret_assert_eq!( + #( + [!parse! [!stream! The quick brown fox] with @( + @[EXACT The] quick @IDENT @(#x = @IDENT) + )].debug_string() + ), + "[!stream! The brown]" + ); + // This tests that: + // * Can nest EXACT and transform streams + // * Can discard output with @(_ = ...) + // * That EXACT ignores none-delimited groups, to make it more intuitive + preinterpret_assert_eq!( + #( + [!set! #x = [!group! fox]]; + [!parse! [!stream! The quick brown fox is a fox - right?!] with @( + // The outputs are only from the EXACT transformer + The quick @(_ = @IDENT) @[EXACT #x @(_ = @IDENT a) #..x - right?!] + )].debug_string() + ), + "[!stream! fox fox - right ?!]" + ); +}