├── .clippy.toml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── cli ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src │ ├── common.rs │ ├── library.rs │ ├── main.rs │ └── repl.rs └── tests │ ├── non_interactive.rs │ ├── repl.rs │ └── snapshots │ ├── README.md │ ├── ast.svg │ ├── errors-ast.svg │ ├── errors-basic.svg │ ├── errors-call-trace.svg │ ├── errors-complex-call-trace.svg │ ├── errors-int.svg │ ├── errors-native-call-trace.svg │ ├── errors-typing-multiple.svg │ ├── errors-typing.svg │ ├── functions.svg │ ├── repl │ ├── README.md │ ├── basics.svg │ ├── dump.svg │ ├── errors-command.svg │ ├── errors-recovery.svg │ ├── errors-var.svg │ ├── help.svg │ ├── incomplete.svg │ └── type.svg │ ├── simple.svg │ ├── std-objects-with-types.svg │ └── std-objects.svg ├── deny.toml ├── e2e-tests ├── no-std │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── memory.x │ └── src │ │ └── main.rs └── wasm │ ├── Cargo.toml │ ├── README.md │ ├── src │ └── lib.rs │ └── test.js ├── eval ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches │ └── interpreter.rs ├── examples │ ├── cyclic_group.rs │ ├── dsa.script │ ├── el_gamal.rs │ ├── elgamal.script │ ├── owned_module.rs │ ├── rfold.script │ └── schnorr.script ├── src │ ├── arith │ │ ├── bigint.rs │ │ ├── generic.rs │ │ ├── mod.rs │ │ └── modular.rs │ ├── compiler │ │ ├── captures.rs │ │ ├── expr.rs │ │ └── mod.rs │ ├── env │ │ ├── mod.rs │ │ └── variable_map.rs │ ├── error.rs │ ├── exec │ │ ├── command.rs │ │ ├── mod.rs │ │ ├── module_id.rs │ │ └── registers.rs │ ├── fns │ │ ├── array.rs │ │ ├── assertions.rs │ │ ├── flow.rs │ │ ├── mod.rs │ │ ├── std.rs │ │ └── wrapper │ │ │ ├── mod.rs │ │ │ └── traits.rs │ ├── lib.rs │ └── values │ │ ├── function.rs │ │ ├── mod.rs │ │ ├── object.rs │ │ ├── ops.rs │ │ └── tuple.rs └── tests │ ├── check_readme.rs │ ├── integration │ ├── custom_cmp.rs │ ├── functions.rs │ ├── hof.rs │ ├── integers.rs │ ├── main.rs │ └── objects.rs │ └── version_match.rs ├── parser ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ └── complex_c.rs ├── src │ ├── ast │ │ ├── expr.rs │ │ ├── lvalue.rs │ │ └── mod.rs │ ├── error.rs │ ├── grammars │ │ ├── mod.rs │ │ └── traits.rs │ ├── lib.rs │ ├── ops.rs │ ├── parser │ │ ├── expr.rs │ │ ├── helpers.rs │ │ ├── lvalue.rs │ │ ├── mod.rs │ │ └── tests │ │ │ ├── basics.rs │ │ │ ├── features.rs │ │ │ ├── function.rs │ │ │ ├── mod.rs │ │ │ ├── object.rs │ │ │ ├── order.rs │ │ │ ├── tuple.rs │ │ │ └── type_cast.rs │ └── spans.rs └── tests │ ├── check_readme.rs │ └── version_match.rs └── typing ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── num_or_bytes.rs └── strings.rs ├── src ├── arith │ ├── constraints.rs │ ├── mod.rs │ └── substitutions │ │ ├── fns.rs │ │ ├── mod.rs │ │ └── tests.rs ├── ast │ ├── conversion.rs │ ├── mod.rs │ └── tests.rs ├── defs.rs ├── env │ ├── mod.rs │ └── processor.rs ├── error │ ├── kind.rs │ ├── mod.rs │ ├── op_errors.rs │ └── path.rs ├── lib.rs ├── types │ ├── fn_type.rs │ ├── mod.rs │ ├── object.rs │ ├── quantifier.rs │ └── tuple.rs └── visit.rs └── tests ├── check_readme.rs ├── integration ├── annotations.rs ├── basics.rs ├── errors │ ├── annotations.rs │ ├── mod.rs │ ├── multiple.rs │ ├── object.rs │ └── recovery.rs ├── examples │ ├── dsa.script │ ├── elgamal.script │ ├── mod.rs │ ├── quick_sort.script │ ├── rfold.script │ └── schnorr.script ├── length_eqs.rs ├── main.rs └── object.rs └── version_match.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | # Minimum supported Rust version. Should be consistent with CI and mentions 2 | # in crate READMEs. 3 | msrv = "1.70" 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{rs,script}] 12 | indent_size = 4 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Use this template for reporting issues 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Bug report 10 | 11 | 12 | 13 | ### Steps to reproduce 14 | 15 | 16 | 17 | ### Expected behavior 18 | 19 | 20 | 21 | ### Environment 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Use this template to request features 4 | title: '' 5 | labels: feat 6 | assignees: '' 7 | --- 8 | 9 | ## Feature request 10 | 11 | 12 | 13 | ### Why? 14 | 15 | 16 | 17 | ### Alternatives 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "03:00" 8 | groups: 9 | dev-dependencies: 10 | dependency-type: "development" 11 | minor-changes: 12 | update-types: 13 | - "minor" 14 | - "patch" 15 | open-pull-requests-limit: 10 16 | assignees: 17 | - slowli 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What? 2 | 3 | 4 | 5 | 6 | 7 | ## Why? 8 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | target 3 | # IDE 4 | .idea 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `arithmetic-parser` 2 | 3 | This project welcomes contribution from everyone, which can take form of suggestions / feature requests, bug reports, or pull requests. 4 | This document provides guidance how best to contribute. 5 | 6 | ## Bug reports and feature requests 7 | 8 | For bugs or when asking for help, please use the bug issue template and include enough details so that your observations 9 | can be reproduced. 10 | 11 | For feature requests, please use the feature request issue template and describe the intended use case(s) and motivation 12 | to go for them. If possible, include your ideas how to implement the feature, potential alternatives and disadvantages. 13 | 14 | ## Pull requests 15 | 16 | Please use the pull request template when submitting a PR. List the major goal(s) achieved by the PR 17 | and describe the motivation behind it. If applicable, like to the related issue(s). 18 | 19 | Optimally, you should check locally that the CI checks pass before submitting the PR. Checks included in the CI 20 | include: 21 | 22 | - Formatting using `cargo fmt --all -- --config imports_granularity=Crate --config group_imports=StdExternalCrate` 23 | - Linting using `cargo clippy` 24 | - Linting the dependency graph using [`cargo deny`](https://crates.io/crates/cargo-deny) 25 | - Running the test suite using `cargo test` 26 | 27 | A complete list of checks can be viewed in [the CI workflow file](.github/workflows/ci.yml). The checks are run 28 | on the latest stable Rust version. 29 | 30 | ### MSRV checks 31 | 32 | A part of the CI assertions is the minimum supported Rust version (MSRV). If this check fails, consult the error messages. Depending on 33 | the error (e.g., whether it is caused by a newer language feature used in the PR code, or in a dependency), 34 | you might want to rework the PR, get rid of the offending dependency, or bump the MSRV; don't hesitate to consult the maintainers. 35 | 36 | ### No-std support checks 37 | 38 | Another part of the CI assertions is no-std compatibility of the project. To check it locally, install a no-std Rust target 39 | (CI uses `thumbv7m-none-eabi`) and build the project libraries for it. Keep in mind that no-std compatibility may be broken 40 | by dependencies. 41 | 42 | ## Code of Conduct 43 | 44 | Be polite and respectful. 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | # Libraries 4 | "parser", 5 | "eval", 6 | "typing", 7 | # CLI app 8 | "cli", 9 | # E2E tests 10 | "e2e-tests/wasm", 11 | "e2e-tests/no-std", 12 | ] 13 | resolver = "2" 14 | 15 | [workspace.package] 16 | version = "0.4.0-beta.1" 17 | authors = ["Alex Ostrovski "] 18 | edition = "2021" 19 | rust-version = "1.70" 20 | license = "MIT OR Apache-2.0" 21 | repository = "https://github.com/slowli/arithmetic-parser" 22 | 23 | [workspace.dependencies] 24 | # External dependencies 25 | anyhow = { version = "1.0.95", default-features = false } 26 | assert_matches = "1.3.0" 27 | bitflags = "2.6.0" 28 | clap = "4.5.23" 29 | codespan = "0.11.1" 30 | codespan-reporting = "0.11.1" 31 | cortex-m = "0.7" 32 | cortex-m-rt = "0.7" 33 | cortex-m-semihosting = "0.5" 34 | criterion = "0.5.0" 35 | embedded-alloc = "0.6.0" 36 | glass_pumpkin = "1.7.0" 37 | hashbrown = "0.15" 38 | hex = "0.4.2" 39 | nom = { version = "7", default-features = false, features = ["alloc"] } 40 | nom_locate = { version = "4", default-features = false, features = ["alloc"] } 41 | num-bigint = "0.4.6" 42 | num-complex = "0.4.6" 43 | num-traits = { version = "0.2.19", default-features = false } 44 | once_cell = { version = "1.20.2", default-features = false } 45 | panic-halt = "1.0.0" 46 | pulldown-cmark = "0.12.2" 47 | rand = "0.8.3" 48 | rand_chacha = { version = "0.3.1", default-features = false } 49 | rustyline = "14.0.0" 50 | sha2 = "0.10.0" 51 | static_assertions = "1.1.0" 52 | term-transcript = "=0.4.0-beta.1" 53 | textwrap = "0.16.1" 54 | typed-arena = "2.0.1" 55 | unindent = "0.2.3" 56 | version-sync = "0.9" 57 | wasm-bindgen = "0.2.99" 58 | 59 | # Workspace dependencies 60 | arithmetic-parser = { version = "=0.4.0-beta.1", path = "parser", default-features = false } 61 | arithmetic-eval = { version = "=0.4.0-beta.1", path = "eval", default-features = false } 62 | arithmetic-typing = { version = "=0.4.0-beta.1", path = "typing" } 63 | 64 | # Speed up big integer crates. 65 | [profile.dev.package.num-bigint] 66 | opt-level = 2 67 | [profile.dev.package.glass_pumpkin] 68 | opt-level = 2 69 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2021-current Developers of arithmetic-parser 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flexible Arithmetic Parser and Interpreter 2 | 3 | [![Build Status](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml) 4 | [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/arithmetic-parser#license) 5 | 6 | This repository contains a versatile parser for arithmetic expressions 7 | which allows customizing literal definitions, type annotations and several other aspects of parsing. 8 | The repository also contains several auxiliary crates (for example, a simple interpreter). 9 | 10 | ## Contents 11 | 12 | - [`arithmetic-parser`](parser) is the core parsing library. 13 | - [`arithmetic-eval`](eval) is a simple interpreter that could be used on parsed expressions 14 | in *some* cases. See the crate docs for more details on its limitations. 15 | - [`arithmetic-typing`](typing) is Hindley–Milner type inference for parsed expressions. 16 | - [`arithmetic-parser-cli`](cli) is the CLI / REPL for the library. 17 | 18 | ## Why? 19 | 20 | - The parser is designed to be reusable and customizable for simple scripting use cases. 21 | For example, it's used to dynamically define and process complex-valued functions 22 | in a [Julia set renderer](https://github.com/slowli/julia-set-rs). 23 | Customization specifically extends to literals; e.g., it is possible 24 | to have a single numeric literal / primitive type. 25 | - Interpreter and type inference are natural complementary tools for the parser 26 | that allow evaluating parsed ASTs and reasoning about their correctness. Again, 27 | it is possible to fully customize primitive types and their mapping from literals, 28 | as well as semantics of arithmetic ops and (in case of typing) constraints they put on 29 | operands. 30 | - Type inference is a challenging (and therefore interesting!) problem given the requirements 31 | (being able to work without any explicit type annotations). 32 | 33 | ## Project status 🚧 34 | 35 | Early-stage; quite a bit of functionality is lacking, especially in interpreter and typing. 36 | As an example, method resolution is a mess (methods are just syntax sugar for functions). 37 | 38 | ## Alternatives / similar tools 39 | 40 | - Scripting languages like [Rhai](https://rhai.rs/book/) and [Gluon](https://gluon-lang.org/) 41 | are significantly more mature and have a sizable standard library, but are less customizable. 42 | E.g., there is a pre-determined set of primitive types with unchangeable semantics and type constraints. 43 | Rhai also does not have parser / interpreter separation or type inference support, 44 | and Gluon's syntax is a bit academic at times. 45 | 46 | ## Contributing 47 | 48 | All contributions are welcome! See [the contributing guide](CONTRIBUTING.md) to help 49 | you get involved. 50 | 51 | ## License 52 | 53 | All code in this repository is licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) 54 | or [MIT license](LICENSE-MIT) at your option. 55 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arithmetic-parser-cli" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | readme = "README.md" 9 | description = "CLI / REPL for arithmetic expressions." 10 | 11 | [[bin]] 12 | name = "arithmetic-parser" 13 | path = "src/main.rs" 14 | 15 | [dependencies] 16 | anyhow.workspace = true 17 | clap = { workspace = true, features = ["derive", "env", "wrap_help"] } 18 | codespan.workspace = true 19 | codespan-reporting.workspace = true 20 | num-complex.workspace = true 21 | num-traits.workspace = true 22 | rustyline.workspace = true 23 | textwrap.workspace = true 24 | unindent.workspace = true 25 | 26 | arithmetic-parser = { workspace = true, default-features = true } 27 | arithmetic-eval = { workspace = true, default-features = true, features = ["complex"] } 28 | arithmetic-typing.workspace = true 29 | 30 | [dev-dependencies] 31 | assert_matches.workspace = true 32 | term-transcript.workspace = true 33 | -------------------------------------------------------------------------------- /cli/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /cli/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # CLI / REPL for Arithmetic Parser 2 | 3 | [![Build Status](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml) 4 | [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/arithmetic-parser#license) 5 | 6 | CLI and REPL for parsing and evaluating arithmetic expressions 7 | that uses [`arithmetic-parser`](../parser) and [`arithmetic-eval`](../eval) internally. 8 | Supports integer, modular, real and complex-valued arithmetic. 9 | Each arithmetic is supplied with all standard functions from the `arithmetic-eval` crate 10 | (`map`, `assert` and so on) and some functions / constants specific to the number type. 11 | 12 | ![REPL example](tests/snapshots/repl/basics.svg) 13 | 14 | ## Usage 15 | 16 | **Tip.** Run the binary with `--help` flag to find out more details. 17 | 18 | ### Parsing 19 | 20 | Use the `--ast` flag to output the AST of the expression. The AST is output 21 | in the standard Rust debug format. 22 | 23 | ### Evaluating 24 | 25 | Without the `--ast` or `--interactive` flags, the command evaluates 26 | the provided expression in the selected arithmetic. 27 | 28 | ### REPL 29 | 30 | With the `--interactive` / `-i` flag, the command works as REPL, allowing 31 | to iteratively evaluate expressions. 32 | 33 | ### Minimum supported Rust version 34 | 35 | The crate supports the latest stable Rust version. It may support previous stable Rust versions, 36 | but this is not guaranteed. 37 | 38 | ## License 39 | 40 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) 41 | or [MIT license](LICENSE-MIT) at your option. 42 | 43 | Unless you explicitly state otherwise, any contribution intentionally submitted 44 | for inclusion in `arithmetic-parser-cli` by you, as defined in the Apache-2.0 license, 45 | shall be dual licensed as above, without any additional terms or conditions. 46 | -------------------------------------------------------------------------------- /cli/src/repl.rs: -------------------------------------------------------------------------------- 1 | //! REPL for arithmetic expressions. 2 | 3 | use std::io; 4 | 5 | use arithmetic_eval::Environment; 6 | use arithmetic_typing::TypeEnvironment; 7 | use codespan_reporting::term::termcolor::ColorChoice; 8 | use rustyline::{error::ReadlineError, Editor}; 9 | 10 | use crate::{ 11 | common::{Env, ParseAndEvalResult}, 12 | library::ReplLiteral, 13 | }; 14 | 15 | fn into_io_error(err: ReadlineError) -> io::Error { 16 | match err { 17 | ReadlineError::Io(err) => err, 18 | other => io::Error::new(io::ErrorKind::Other, other), 19 | } 20 | } 21 | 22 | pub fn repl( 23 | env: Environment, 24 | type_env: Option, 25 | color_choice: ColorChoice, 26 | ) -> io::Result<()> { 27 | let mut rl = Editor::<(), _>::new().map_err(into_io_error)?; 28 | let mut env = Env::new(env, type_env, color_choice); 29 | env.print_greeting()?; 30 | 31 | let mut snippet = String::new(); 32 | let mut prompt = ">>> "; 33 | 34 | loop { 35 | let line = rl.readline(prompt); 36 | match line { 37 | Ok(line) => { 38 | snippet.push_str(&line); 39 | let result = env.parse_and_eval(&snippet, true)?; 40 | match result { 41 | ParseAndEvalResult::Ok(_) => { 42 | prompt = ">>> "; 43 | snippet.clear(); 44 | rl.add_history_entry(line).map_err(into_io_error)?; 45 | } 46 | ParseAndEvalResult::Incomplete => { 47 | prompt = "... "; 48 | snippet.push('\n'); 49 | rl.add_history_entry(line).map_err(into_io_error)?; 50 | } 51 | ParseAndEvalResult::Errored => { 52 | prompt = ">>> "; 53 | snippet.clear(); 54 | } 55 | } 56 | } 57 | 58 | Err(ReadlineError::Interrupted) => { 59 | println!("Bye"); 60 | break Ok(()); 61 | } 62 | 63 | Err(ReadlineError::Eof) => { 64 | break Ok(()); 65 | } 66 | 67 | Err(err) => panic!("Error reading command: {err}"), 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cli/tests/non_interactive.rs: -------------------------------------------------------------------------------- 1 | //! E2E tests for a non-interactive binary usage. 2 | 3 | // Some tests use multi-line formatting, which is awkward to achieve on Windows. 4 | #![cfg(unix)] 5 | 6 | use term_transcript::{ 7 | svg::{ScrollOptions, Template, TemplateOptions}, 8 | test::{MatchKind, TestConfig}, 9 | ShellOptions, 10 | }; 11 | 12 | fn test_config() -> TestConfig { 13 | let shell_options = ShellOptions::default() 14 | .with_env("COLOR", "always") 15 | .with_cargo_path(); 16 | TestConfig::new(shell_options).with_match_kind(MatchKind::Precise) 17 | } 18 | 19 | fn scroll_template() -> Template { 20 | Template::new(TemplateOptions { 21 | scroll: Some(ScrollOptions::default()), 22 | ..TemplateOptions::default() 23 | }) 24 | } 25 | 26 | #[test] 27 | fn successful_execution_for_ast() { 28 | test_config().with_template(scroll_template()).test( 29 | "tests/snapshots/ast.svg", 30 | ["arithmetic-parser ast -a u64 '1 + 2 * 3'"], 31 | ); 32 | } 33 | 34 | #[test] 35 | fn successful_execution_for_arithmetics() { 36 | test_config().test( 37 | "tests/snapshots/simple.svg", 38 | [ 39 | "arithmetic-parser eval -a u64 '1 + 2 * 3'", 40 | "arithmetic-parser eval -a i64 '1 - 2 * 3'", 41 | "arithmetic-parser eval -a u128 '2 ^ 71 - 1'", 42 | "arithmetic-parser eval -a u64 --wrapping '1 - 2 + 3'", 43 | "arithmetic-parser eval -a f32 '1 + 3 / 2'", 44 | "arithmetic-parser eval -a c64 '2 / (1 - i)'", 45 | "arithmetic-parser eval -a u64/11 '5 / 9'", 46 | ], 47 | ); 48 | } 49 | 50 | #[test] 51 | fn evaluating_functions() { 52 | test_config().test( 53 | "tests/snapshots/functions.svg", 54 | [ 55 | "arithmetic-parser eval 'if(5 > 3, 1, -1)'", 56 | "arithmetic-parser eval 'if'", 57 | "arithmetic-parser eval 'is_positive = |x| x > 0; is_positive'", 58 | ], 59 | ); 60 | } 61 | 62 | #[test] 63 | fn using_std_objects() { 64 | test_config().test( 65 | "tests/snapshots/std-objects.svg", 66 | [ 67 | "arithmetic-parser eval '{Array.map}((1, 2, 3), Num.sin)'", 68 | "arithmetic-parser eval -a u64 '(33, 6, 15).fold(0, Num.xor)'", 69 | "arithmetic-parser eval '{ map } = Array; map((1, -2), |x| x + 1)'", 70 | ], 71 | ); 72 | } 73 | 74 | #[test] 75 | fn using_std_objects_with_typing() { 76 | test_config().test( 77 | "tests/snapshots/std-objects-with-types.svg", 78 | [ 79 | "arithmetic-parser eval --types '{Array.map}((1, 2, 3), Num.sin)'", 80 | "arithmetic-parser eval --types -a u64 \\\n '(33, 6, 15).fold(0, Num.xor)'", 81 | "arithmetic-parser eval --types \\\n '{ map } = Array; map((1, -2), |x| x + 1)'", 82 | ], 83 | ); 84 | } 85 | 86 | #[test] 87 | fn syntax_errors() { 88 | test_config().with_template(scroll_template()).test( 89 | "tests/snapshots/errors-ast.svg", 90 | [ 91 | "arithmetic-parser ast 'let x = 5'", 92 | "arithmetic-parser eval 'let x = 5'", 93 | "arithmetic-parser ast 'x = {'", 94 | "arithmetic-parser eval 'x = {'", 95 | ], 96 | ); 97 | } 98 | 99 | #[test] 100 | fn eval_integer_errors() { 101 | test_config().test( 102 | "tests/snapshots/errors-int.svg", 103 | [ 104 | "arithmetic-parser eval -a u64 '1 - 3 + 5'", 105 | "arithmetic-parser eval -a i64 '20 ^ 20'", 106 | "arithmetic-parser eval -a i128 '10 ^ -3'", 107 | ], 108 | ); 109 | } 110 | 111 | #[test] 112 | fn eval_basic_errors() { 113 | test_config().test( 114 | "tests/snapshots/errors-basic.svg", 115 | [ 116 | "arithmetic-parser eval '1 + 2 * x'", 117 | "arithmetic-parser eval 'if(2 > 1, 3)'", 118 | "arithmetic-parser eval 'assert_eq(1 + 2, 3 / 2)'", 119 | ], 120 | ); 121 | } 122 | 123 | #[test] 124 | fn error_with_call_trace() { 125 | test_config().test( 126 | "tests/snapshots/errors-call-trace.svg", 127 | ["arithmetic-parser eval '\n \ 128 | is_positive = |x| x > 0;\n \ 129 | is_positive(3) && !is_positive((1, 2))'"], 130 | ); 131 | } 132 | 133 | #[test] 134 | fn error_with_complex_call_trace() { 135 | test_config().test( 136 | "tests/snapshots/errors-complex-call-trace.svg", 137 | ["arithmetic-parser eval '\n \ 138 | double = |x| x * 2;\n \ 139 | quadruple = |x| double(double(x));\n \ 140 | quadruple(true)'"], 141 | ); 142 | } 143 | 144 | #[test] 145 | fn error_with_call_complex_call_trace_and_native_fns() { 146 | test_config().test( 147 | "tests/snapshots/errors-native-call-trace.svg", 148 | ["arithmetic-parser eval '\n \ 149 | all = |array, pred| array.fold(true, |acc, x| acc && pred(x));\n \ 150 | all((1, 2, Array.map), |x| 0 < x)'"], 151 | ); 152 | } 153 | 154 | #[test] 155 | fn typing_errors_simple() { 156 | test_config().test( 157 | "tests/snapshots/errors-typing.svg", 158 | [ 159 | "arithmetic-parser eval --types '(1, 2, 3).map(|x| x, 1)'", 160 | "arithmetic-parser eval --types '\n \ 161 | all = |array, pred| array.fold(true, |acc, x| acc && pred(x));\n \ 162 | all((1, 2, Array.map), |x| 0 < x)'", 163 | ], 164 | ); 165 | } 166 | 167 | #[test] 168 | fn multiple_typing_errors() { 169 | test_config().test( 170 | "tests/snapshots/errors-typing-multiple.svg", 171 | ["arithmetic-parser eval --types '(1, (2, 3)).filter(|x| x + 1)'"], 172 | ); 173 | } 174 | -------------------------------------------------------------------------------- /cli/tests/repl.rs: -------------------------------------------------------------------------------- 1 | //! E2E tests for interactive binary usage. 2 | 3 | use std::{process::Command, time::Duration}; 4 | 5 | use term_transcript::{ 6 | svg::{ScrollOptions, Template, TemplateOptions}, 7 | test::{MatchKind, TestConfig}, 8 | ShellOptions, UserInput, 9 | }; 10 | 11 | const PATH_TO_BIN: &str = env!("CARGO_BIN_EXE_arithmetic-parser"); 12 | 13 | fn scroll_template() -> Template { 14 | Template::new(TemplateOptions { 15 | scroll: Some(ScrollOptions::default()), 16 | window_frame: true, 17 | ..TemplateOptions::default() 18 | }) 19 | } 20 | 21 | fn test_config(with_types: bool) -> TestConfig { 22 | let mut command = Command::new(PATH_TO_BIN); 23 | command.arg("eval"); 24 | if with_types { 25 | command.arg("--types"); 26 | } 27 | command.arg("-a").arg("f64").arg("-i"); 28 | 29 | let shell_options = ShellOptions::new(command) 30 | .with_env("COLOR", "always") 31 | .with_io_timeout(Duration::from_millis(250)) 32 | .with_init_timeout(Duration::from_secs(1)); 33 | 34 | // `codespan_reporting` uses some different colors on Windows: 35 | // https://docs.rs/codespan-reporting/0.11.1/codespan_reporting/term/struct.Styles.html; 36 | // hence, we check only text correspondence on Windows. 37 | let match_kind = if cfg!(windows) { 38 | MatchKind::TextOnly 39 | } else { 40 | MatchKind::Precise 41 | }; 42 | TestConfig::new(shell_options).with_match_kind(match_kind) 43 | } 44 | 45 | // Helper commands to create `UserInput`s. 46 | 47 | #[inline] 48 | fn repl(input: &str) -> UserInput { 49 | UserInput::repl(input) 50 | } 51 | 52 | #[inline] 53 | fn cont(input: &str) -> UserInput { 54 | UserInput::repl_continuation(input) 55 | } 56 | 57 | #[test] 58 | fn repl_basics() { 59 | test_config(false).with_template(scroll_template()).test( 60 | "tests/snapshots/repl/basics.svg", 61 | vec![ 62 | repl("1 + 2*3"), 63 | repl("all = |array, pred| array.fold(true, |acc, x| acc && pred(x));"), 64 | repl("all"), 65 | repl("all((1, 2, 5), |x| 0 < x)"), 66 | repl("all((1, -2, 5), |x| 0 < x)"), 67 | repl("all((1, 2, 5, Array.map), |x| 0 < x)"), 68 | ], 69 | ); 70 | } 71 | 72 | #[test] 73 | fn incomplete_statements() { 74 | test_config(false).test( 75 | "tests/snapshots/repl/incomplete.svg", 76 | vec![ 77 | repl("sum = |...xs| {"), 78 | cont(" xs.fold(0, |acc, x| acc + x)"), 79 | cont("};"), 80 | repl("sum(3, -5, 1)"), 81 | repl("x = 1; /* Comment starts"), 82 | cont("You can put anything within a comment, really"), 83 | cont("Comment ends */ x"), 84 | ], 85 | ); 86 | } 87 | 88 | #[test] 89 | fn undefined_var_error() { 90 | test_config(false).test( 91 | "tests/snapshots/repl/errors-var.svg", 92 | vec![repl("foo(3)"), repl("foo = |x| x + 1;"), repl("foo(3)")], 93 | ); 94 | } 95 | 96 | #[test] 97 | fn getting_help() { 98 | test_config(false).test("tests/snapshots/repl/help.svg", vec![repl(".help")]); 99 | } 100 | 101 | #[test] 102 | fn dumping_vars() { 103 | test_config(false).test( 104 | "tests/snapshots/repl/dump.svg", 105 | vec![repl("xs = (1, 2, || PI + 3);"), repl(".dump")], 106 | ); 107 | } 108 | 109 | #[test] 110 | fn unknown_command() { 111 | test_config(false).test( 112 | "tests/snapshots/repl/errors-command.svg", 113 | vec![repl(".exit")], 114 | ); 115 | } 116 | 117 | #[test] 118 | fn variable_type() { 119 | test_config(true).test( 120 | "tests/snapshots/repl/type.svg", 121 | vec![ 122 | repl("tuple = (1, #{ x: 3, y: 4 });"), 123 | repl(".type tuple"), 124 | repl("all = |xs, pred| xs.fold(true, |acc, x| acc && pred(x));"), 125 | repl(".type all"), 126 | repl(".type non_existing_var"), 127 | ], 128 | ); 129 | } 130 | 131 | #[test] 132 | fn error_recovery() { 133 | test_config(true).test( 134 | "tests/snapshots/repl/errors-recovery.svg", 135 | vec![ 136 | repl("x = 1; y = !x;"), 137 | repl("x // Should be defined since an error occurs in a later stmt"), 138 | repl("y"), 139 | ], 140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /cli/tests/snapshots/README.md: -------------------------------------------------------------------------------- 1 | # Snapshots for `arithmetic-parser` CLI 2 | 3 | This document lists all snapshots used in E2E testing the `arithmetic-parser` CLI, 4 | with a brief explanation what each snapshot does. 5 | 6 | Snapshots for REPL mode (`eval -i` command) are described in [a separate file](repl/README.md). 7 | 8 | ## Basics 9 | 10 | ### AST parsing 11 | 12 | ![AST parsing](ast.svg) 13 | 14 | The `ast` command outputs the parsed AST of the arithmetic block 15 | (of which expression is a partial case). So far, this is just a pretty-printed 16 | `Debug` implementation for the `Block` data type. 17 | 18 | ### Simple expressions 19 | 20 | ![Simple expressions](simple.svg) 21 | 22 | Expressions / blocks can be evaluated with the `eval` command. 23 | 24 | ### Functions 25 | 26 | ![Functions](functions.svg) 27 | 28 | Functions are first-class! Beside being called, functions can be used as values 29 | themselves. 30 | 31 | ### Standard objects 32 | 33 | ![Standard objects](std-objects.svg) 34 | 35 | Standard functions are grouped in `Array` and `Num` objects. Functions 36 | in these objects can be "quoted" by enclosing access to them in a `{}` block, 37 | e.g., `{Array.map}(xs, |x| x + 1)` or `xs.{Array.map}(|x| x + 1)`. 38 | 39 | ## AST errors 40 | 41 | ![AST errors](errors-ast.svg) 42 | 43 | If parsing an expression / block fails, this is the kind of errors that will be returned, 44 | both for `ast` and `eval` commands. 45 | 46 | ## Evaluation errors 47 | 48 | ### Basic evaluation errors 49 | 50 | ![Basic evaluation errors](errors-basic.svg) 51 | 52 | Some errors, such as missing variables, or incorrect function calls, are common 53 | to all supported arithmetics. 54 | 55 | ### Integer errors 56 | 57 | ![Integer errors](errors-int.svg) 58 | 59 | The integer arithmetics are checked by default, meaning that some operations will 60 | lead to an error (rather than a panic or an incorrect result). 61 | 62 | ### Errors with call trace 63 | 64 | ![Error with simple call trace](errors-call-trace.svg) 65 | 66 | ![Error with complex call trace](errors-complex-call-trace.svg) 67 | 68 | If an error occurs within an interpreted function, the call trace will be displayed. 69 | 70 | ![Error with native call trace](errors-native-call-trace.svg) 71 | 72 | This includes the case when some functions in the call chain are native 73 | (like `fold` in the snapshot above). 74 | 75 | ## Typing errors 76 | 77 | ![Typing errors](errors-typing.svg) 78 | 79 | If the `--types` flag is enabled, typing checks will be performed before evaluation. 80 | The report spans are WIP, but in most cases, it is possible to get a general idea 81 | of the error cause. 82 | 83 | ![Typing checks are exhaustive](errors-typing-multiple.svg) 84 | 85 | Unlike with the "raw" evaluation mode, typing errors are *exhaustive*; the typing 86 | logic will attempt to find all errors in the evaluated code. 87 | -------------------------------------------------------------------------------- /cli/tests/snapshots/errors-basic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval '1 + 2 * x'
72 |
error[EVAL]: Variable `x` is not defined
 73 |   ┌─ Snippet #1:1:9
 74 |   
 75 | 1  1 + 2 * x
 76 |            ^ Undefined variable occurrence
 77 | 
78 |
$ arithmetic-parser eval 'if(2 > 1, 3)'
79 |
error[EVAL]: Mismatch between the number of arguments in the function definition
and its call
80 | ┌─ Snippet #1:1:1 81 | 82 | 1 if(2 > 1, 3) 83 | ^^^^^^^^^^^^ Called with 2 arg(s) here 84 |
85 |
$ arithmetic-parser eval 'assert_eq(1 + 2, 3 / 2)'
86 |
error[EVAL]: Equality assertion failed
 87 |   ┌─ Snippet #1:1:1
 88 |   
 89 | 1  assert_eq(1 + 2, 3 / 2)
 90 |    ^^^^^^^^^^^^^^^^^^^^^^^
 91 |                   
 92 |                   Has value: 1.5
 93 |             Has value: 3
 94 |    Failed call
 95 | 
96 |
97 |
98 |
99 |
100 | -------------------------------------------------------------------------------- /cli/tests/snapshots/errors-call-trace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval '
72 |   is_positive = |x| x > 0;
73 |   is_positive(3) && !is_positive((1, 2))'
74 |
error[EVAL]: Value is not comparable
75 |   ┌─ Snippet #1:2:21
76 |   
77 | 2    is_positive = |x| x > 0;
78 |                    ----^----
79 |                       
80 |                       Cannot be compared
81 |                    The error occurred in function `is_positive`
82 | 3    is_positive(3) && !is_positive((1, 2))
83 |                         ------------------- Call at depth 1
84 |   
85 |   = Only primitive values can be compared; complex values cannot
86 | 
87 |
88 |
89 |
90 |
91 | -------------------------------------------------------------------------------- /cli/tests/snapshots/errors-complex-call-trace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval '
72 |   double = |x| x * 2;
73 |   quadruple = |x| double(double(x));
74 |   quadruple(true)'
75 |
error[EVAL]: Unexpected operand type for multiplication
76 |   ┌─ Snippet #1:2:16
77 |   
78 | 2    double = |x| x * 2;
79 |               ----^----
80 |                  
81 |                  Operand of wrong type
82 |               The error occurred in function `double`
83 | 3    quadruple = |x| double(double(x));
84 |                             --------- Call at depth 1
85 | 4    quadruple(true)
86 |      --------------- Call at depth 2
87 |   
88 |   = Operands of binary arithmetic ops must be primitive values or tuples /
89 |     objects consisting of primitive values
90 | 
91 |
92 |
93 |
94 |
95 | -------------------------------------------------------------------------------- /cli/tests/snapshots/errors-int.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval -a u64 '1 - 3 + 5'
72 |
error[EVAL]: Arithmetic error
73 |   ┌─ Snippet #1:1:1
74 |   
75 | 1  1 - 3 + 5
76 |    ^^^^^ integer overflow or underflow
77 | 
78 |
$ arithmetic-parser eval -a i64 '20 ^ 20'
79 |
error[EVAL]: Arithmetic error
80 |   ┌─ Snippet #1:1:1
81 |   
82 | 1  20 ^ 20
83 |    ^^^^^^^ integer overflow or underflow
84 | 
85 |
$ arithmetic-parser eval -a i128 '10 ^ -3'
86 |
error[EVAL]: Arithmetic error
87 |   ┌─ Snippet #1:1:1
88 |   
89 | 1  10 ^ -3
90 |    ^^^^^^^ exponent is too large or negative
91 | 
92 |
93 |
94 |
95 |
96 | -------------------------------------------------------------------------------- /cli/tests/snapshots/errors-native-call-trace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval '
72 |   all = |array, pred| array.fold(true, |acc, x| acc && pred(x));
73 |   all((1, 2, Array.map), |x| 0 < x)'
74 |
error[EVAL]: Value is not comparable
75 |   ┌─ Snippet #1:3:34
76 |   
77 | 2    all = |array, pred| array.fold(true, |acc, x| acc && pred(x));
78 |                          -----------------------------------------
79 |                                                          
80 |                                                          Call at depth 1
81 |                          Call at depth 2
82 | 3    all((1, 2, Array.map), |x| 0 < x)
83 |      -------------------------------^-
84 |                                   
85 |                                   Cannot be compared
86 |                            The error occurred in function `pred`
87 |      Call at depth 3
88 |   
89 |   = Only primitive values can be compared; complex values cannot
90 | 
91 |
92 |
93 |
94 |
95 | -------------------------------------------------------------------------------- /cli/tests/snapshots/errors-typing-multiple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval --types '(1, (2, 3)).filter(|x| x + 1)'
72 |
error[TYPE]: Type `(Num, Num)` is not assignable to type `Num`
73 |   ┌─ Snippet #1:1:5
74 |   
75 | 1  (1, (2, 3)).filter(|x| x + 1)
76 |        ^^^^^^ Error occurred here
77 | 
78 | error[TYPE]: Type `Num` is not assignable to type `Bool`
79 |   ┌─ Snippet #1:1:20
80 |   
81 | 1  (1, (2, 3)).filter(|x| x + 1)
82 |                       ^^^^^^^^^ Error occurred here
83 | 
84 |
85 |
86 |
87 |
88 | -------------------------------------------------------------------------------- /cli/tests/snapshots/errors-typing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval --types '(1, 2, 3).map(|x| x, 1)'
72 |
error[TYPE]: Function expects 2 args, but is called with 3 args
73 |   ┌─ Snippet #1:1:1
74 |   
75 | 1  (1, 2, 3).map(|x| x, 1)
76 |    ^^^^^^^^^^^^^^^^^^^^^^^ Error occurred here
77 | 
78 |
$ arithmetic-parser eval --types '
79 |   all = |array, pred| array.fold(true, |acc, x| acc && pred(x));
80 |   all((1, 2, Array.map), |x| 0 < x)'
81 |
error[TYPE]: Type `(['T; N], ('T) -> 'U) -> ['U; N]` is not assignable to type `
Num`
82 | ┌─ Snippet #1:3:14 83 | 84 | 3 all((1, 2, Array.map), |x| 0 < x) 85 | ^^^^^^^^^ Error occurred here 86 |
87 |
88 |
89 |
90 |
91 | -------------------------------------------------------------------------------- /cli/tests/snapshots/functions.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval 'if(5 > 3, 1, -1)'
72 |
1
73 |
$ arithmetic-parser eval 'if'
74 |
(native fn)
75 |
$ arithmetic-parser eval 'is_positive = |x| x > 0; is_positive'
76 |
fn(1 arg)
77 |
78 |
79 |
80 |
81 | -------------------------------------------------------------------------------- /cli/tests/snapshots/repl/README.md: -------------------------------------------------------------------------------- 1 | # Snapshots for `arithmetic-parser` REPL 2 | 3 | This document lists all snapshots used in E2E testing the `arithmetic-parser` CLI 4 | in the REPL mode, with a brief explanation what each snapshot does. 5 | The REPL mode is activated using the `eval` command with the `--interactive` / `-i` flag. 6 | 7 | Snapshots for the non-interactive mode are described [separately](../README.md). 8 | 9 | ## Basics 10 | 11 | ![Basic commands](basics.svg) 12 | 13 | The REPL can be used to evaluate blocks, which includes defining new vars in the global scope. 14 | 15 | ### Multi-line statements 16 | 17 | ![Multi-line statements](incomplete.svg) 18 | 19 | Like "grown-up" REPLs, `arithmetic-parser` supports multi-line statements 20 | (including multi-line comments). 21 | 22 | ### Dumping vars 23 | 24 | ![Dumping vars](dump.svg) 25 | 26 | `.dump` command can be used to dump user-defined or all variables from the global scope. 27 | 28 | ### Typing 29 | 30 | ![Typing information](type.svg) 31 | 32 | If launched with the `--types` flag, the REPL first checks all statements with the type checker. 33 | It is also possible to output variable types with the `.type` command. 34 | 35 | ### Getting help 36 | 37 | ![Getting help](help.svg) 38 | 39 | `.help` command outputs basic information about REPL usage. 40 | 41 | ## Errors 42 | 43 | ### Undefined var 44 | 45 | ![Undefined variable](errors-var.svg) 46 | 47 | REPL highlights the undefined var. Naturally, it is possible to define a var afterwards 48 | and re-run the command. 49 | 50 | ### Unknown command 51 | 52 | ![Unknown command](errors-command.svg) 53 | 54 | Incorrect commands (a statement starting with `.`) are handled as well. 55 | 56 | ### Error recovery 57 | 58 | ![Error recovery](errors-recovery.svg) 59 | 60 | If an error occurs during type checks or evaluation, preceding statements still influence 61 | the global scope. 62 | -------------------------------------------------------------------------------- /cli/tests/snapshots/repl/dump.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
>>> xs = (1, 2, || PI + 3);
72 |
73 |
>>> .dump
74 |
xs = (
75 |   1,
76 |   2,
77 |   fn(0 args)[
78 |     PI = 3.141592653589793
79 |   ]
80 | )
81 |
82 |
83 |
84 |
85 | -------------------------------------------------------------------------------- /cli/tests/snapshots/repl/errors-command.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
>>> .exit
72 |
error[CMD]: Unknown command
73 |   ┌─ Snippet #1:1:1
74 |   
75 | 1  .exit
76 |    ^^^^^ Use `.help` to find out commands
77 | 
78 |
79 |
80 |
81 |
82 | -------------------------------------------------------------------------------- /cli/tests/snapshots/repl/errors-recovery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
>>> x = 1; y = !x;
72 |
error[TYPE]: Type `Num` is not assignable to type `Bool`
73 |   ┌─ Snippet #1:1:12
74 |   
75 | 1  x = 1; y = !x;
76 |               ^^ Error occurred here
77 | 
78 |
>>> x // Should be defined since an error occurs in a later stmt
79 |
1
80 |
>>> y
81 |
error[TYPE]: Variable `y` is not defined
82 |   ┌─ Snippet #3:1:1
83 |   
84 | 1  y
85 |    ^ Error occurred here
86 | 
87 |
88 |
89 |
90 |
91 | -------------------------------------------------------------------------------- /cli/tests/snapshots/repl/errors-var.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
>>> foo(3)
72 |
error[EVAL]: Variable `foo` is not defined
73 |   ┌─ Snippet #1:1:1
74 |   
75 | 1  foo(3)
76 |    ^^^ Undefined variable occurrence
77 | 
78 |
>>> foo = |x| x + 1;
79 |
80 |
>>> foo(3)
81 |
4
82 |
83 |
84 |
85 |
86 | -------------------------------------------------------------------------------- /cli/tests/snapshots/repl/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
>>> .help
72 |
REPL supports functions, blocks, methods, comparisons, etc.
73 | Syntax is similar to Rust; see `arithmetic-parser` docs for details.
74 | Use Ctrl+C / Cmd+C to exit the REPL.
75 | 
76 | EXAMPLE
77 | Input each line separately.
78 | 
79 |     sins = (1, 2, 3).map(sin); sins
80 |     min_sin = sins.fold(INF, min); min_sin
81 |     assert(min_sin > 0);
82 | 
83 | COMMANDS
84 | Several commands are supported. All commands start with a dot '.'.
85 | 
86 |     .help     Displays help.
87 |     .dump     Outputs all defined variables. Use '.dump all' to include
88 |               built-in vars.
89 |     .type     Outputs type of a variable. Requires `--types` flag.
90 |     .clear    Resets the interpreter state to the original one.
91 | 
92 |
93 |
94 |
95 |
96 | -------------------------------------------------------------------------------- /cli/tests/snapshots/repl/incomplete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
>>> sum = |...xs| {
72 |
73 |
...   xs.fold(0, |acc, x| acc + x)
74 |
75 |
... };
76 |
77 |
>>> sum(3, -5, 1)
78 |
-1
79 |
>>> x = 1; /* Comment starts
80 |
81 |
... You can put anything within a comment, really
82 |
83 |
... Comment ends */ x
84 |
1
85 |
86 |
87 |
88 |
89 | -------------------------------------------------------------------------------- /cli/tests/snapshots/repl/type.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
>>> tuple = (1, #{ x: 3, y: 4 });
72 |
73 |
>>> .type tuple
74 |
(Num, { x: Num, y: Num })
75 |
>>> all = |xs, pred| xs.fold(true, |acc, x| acc && pred(x));
76 |
77 |
>>> .type all
78 |
(['T; N], ('T) -> Bool) -> Bool
79 |
>>> .type non_existing_var
80 |
error[CMD]: Variable `non_existing_var` is not defined
81 |   ┌─ Snippet #5:1:7
82 |   
83 | 1  .type non_existing_var
84 |          ^^^^^^^^^^^^^^^^ Undefined variable
85 | 
86 |
87 |
88 |
89 |
90 | -------------------------------------------------------------------------------- /cli/tests/snapshots/simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval -a u64 '1 + 2 * 3'
72 |
7
73 |
$ arithmetic-parser eval -a i64 '1 - 2 * 3'
74 |
-5
75 |
$ arithmetic-parser eval -a u128 '2 ^ 71 - 1'
76 |
2361183241434822606847
77 |
$ arithmetic-parser eval -a u64 --wrapping '1 - 2 + 3'
78 |
2
79 |
$ arithmetic-parser eval -a f32 '1 + 3 / 2'
80 |
2.5
81 |
$ arithmetic-parser eval -a c64 '2 / (1 - i)'
82 |
1+1i
83 |
$ arithmetic-parser eval -a u64/11 '5 / 9'
84 |
3
85 |
86 |
87 |
88 |
89 | -------------------------------------------------------------------------------- /cli/tests/snapshots/std-objects-with-types.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval --types '{Array.map}((1, 2, 3), Num.sin)'
72 |
(
73 |   0.84147096,
74 |   0.9092974,
75 |   0.14112
76 | )
77 |
$ arithmetic-parser eval --types -a u64 \
78 |   '(33, 6, 15).fold(0, Num.xor)'
79 |
40
80 |
$ arithmetic-parser eval --types \
81 |   '{ map } = Array; map((1, -2), |x| x + 1)'
82 |
(
83 |   2,
84 |   -1
85 | )
86 |
87 |
88 |
89 |
90 | -------------------------------------------------------------------------------- /cli/tests/snapshots/std-objects.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | 70 |
71 |
$ arithmetic-parser eval '{Array.map}((1, 2, 3), Num.sin)'
72 |
(
73 |   0.84147096,
74 |   0.9092974,
75 |   0.14112
76 | )
77 |
$ arithmetic-parser eval -a u64 '(33, 6, 15).fold(0, Num.xor)'
78 |
40
79 |
$ arithmetic-parser eval '{ map } = Array; map((1, -2), |x| x + 1)'
80 |
(
81 |   2,
82 |   -1
83 | )
84 |
85 |
86 |
87 |
88 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # `cargo-deny` configuration. 2 | 3 | [output] 4 | feature-depth = 1 5 | 6 | [advisories] 7 | db-urls = ["https://github.com/rustsec/advisory-db"] 8 | yanked = "deny" 9 | 10 | [licenses] 11 | allow = [ 12 | # Permissive open-source licenses 13 | "MIT", 14 | "Apache-2.0", 15 | "Zlib", 16 | "BSL-1.0", 17 | "Unicode-3.0", 18 | ] 19 | confidence-threshold = 0.8 20 | 21 | [bans] 22 | multiple-versions = "deny" 23 | wildcards = "deny" 24 | allow-wildcard-paths = true 25 | skip-tree = [ 26 | # `cortex-m` crates (which are only used in the no-std test crate) have some outdated deps. 27 | { name = "cortex-m", version = "^0.7" }, 28 | { name = "cortex-m-rt", version = "^0.7" }, 29 | # Used by some less frequently updated crates; since it only provides WinAPI declarations, 30 | # multiple versions should be OK (?). 31 | { name = "windows-sys", version = "^0.52" }, 32 | ] 33 | 34 | [sources] 35 | unknown-registry = "deny" 36 | unknown-git = "deny" 37 | -------------------------------------------------------------------------------- /e2e-tests/no-std/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.thumbv7m-none-eabi] 2 | runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel" 3 | 4 | [build] 5 | target = "thumbv7m-none-eabi" 6 | 7 | [profile.release] 8 | opt-level = "z" # Optimize for size, rather than speed 9 | lto = true 10 | codegen-units = 1 11 | -------------------------------------------------------------------------------- /e2e-tests/no-std/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arithmetic-parser-nostd" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | description = "Testing usability of `arithmetic-parser` in no-std env" 9 | publish = false 10 | 11 | [dependencies] 12 | # Cortex-M dependencies 13 | cortex-m = { workspace = true, features = ["critical-section-single-core"] } 14 | cortex-m-rt.workspace = true 15 | cortex-m-semihosting.workspace = true 16 | embedded-alloc.workspace = true 17 | panic-halt.workspace = true 18 | 19 | rand_chacha.workspace = true 20 | 21 | # Arithmetic dependencies 22 | arithmetic-parser.workspace = true 23 | arithmetic-eval = { workspace = true, features = ["hashbrown"] } 24 | -------------------------------------------------------------------------------- /e2e-tests/no-std/README.md: -------------------------------------------------------------------------------- 1 | # Testing `arithmetic-*` Crates in `no_std` Env 2 | 3 | This simple crate tests that `arithmetic-parser` / `arithmetic-eval` crates build 4 | and can be used in a `no_std` environment (namely, an [ARM Cortex-M3] microcontroller). 5 | It requires a nightly toolchain. The `release` profile must be used 6 | to not overflow the available microcontroller flash memory. 7 | 8 | ## Usage 9 | 10 | Beside using a real microcontroller, the crate can be tested with [qemu]. 11 | In fact, Cargo is configured to run qemu when using `cargo run`. 12 | 13 | 1. Install a recent nightly Rust toolchain and the `thumbv7m-none-eabi` target 14 | for it. 15 | 2. Install qemu. In Linux, qemu is often included as a system package, so 16 | this step can be as simple as `sudo apt-get install qemu-system-arm`. 17 | 3. Compile and run the app on qemu using `cd $crate_dir && cargo run --release`, 18 | where `$crate_dir` is the directory containing this README. 19 | Switching to the crate directory is necessary to use the [Cargo config](.cargo/config.toml), 20 | which sets up necessary `rustc` flags and the qemu wrapper for `cargo run`. 21 | 22 | [ARM Cortex-M3]: https://en.wikipedia.org/wiki/ARM_Cortex-M#Cortex-M3 23 | [qemu]: https://www.qemu.org/ 24 | -------------------------------------------------------------------------------- /e2e-tests/no-std/build.rs: -------------------------------------------------------------------------------- 1 | //! Copies `memory.x` declaration to where the linker is guaranteed to see it. 2 | //! Taken from https://github.com/rust-embedded/cortex-m-quickstart with minor changes. 3 | 4 | use std::{env, fs, path::PathBuf}; 5 | 6 | fn main() { 7 | // Put `memory.x` in our output directory and ensure it's on the linker search path. 8 | let out = PathBuf::from(env::var_os("OUT_DIR").unwrap()); 9 | fs::write(out.join("memory.x"), include_bytes!("memory.x")) 10 | .expect("Failed copying `memory.x` declaration"); 11 | println!("cargo:rustc-link-search={}", out.display()); 12 | println!("cargo:rerun-if-changed=memory.x"); 13 | 14 | // `--nmagic` is required if memory section addresses are not aligned to 0x10000, 15 | // for example the FLASH and RAM sections in your `memory.x`. 16 | // See https://github.com/rust-embedded/cortex-m-quickstart/pull/95 17 | println!("cargo:rustc-link-arg=--nmagic"); 18 | // Set the linker script to the one provided by cortex-m-rt. 19 | println!("cargo:rustc-link-arg=-Tlink.x"); 20 | } 21 | -------------------------------------------------------------------------------- /e2e-tests/no-std/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | FLASH : ORIGIN = 0x00000000, LENGTH = 256K 4 | RAM : ORIGIN = 0x20000000, LENGTH = 64K 5 | } 6 | -------------------------------------------------------------------------------- /e2e-tests/no-std/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Test no-std application for arithmetic parser / interpreter. 2 | 3 | #![no_std] 4 | #![no_main] 5 | 6 | extern crate alloc; 7 | 8 | use alloc::vec::Vec; 9 | use core::cell::RefCell; 10 | 11 | use arithmetic_eval::{ 12 | arith::CheckedArithmetic, 13 | env::{Assertions, Prelude}, 14 | fns, CallContext, Environment, EvalResult, ExecutableModule, NativeFn, SpannedValue, Value, 15 | }; 16 | use arithmetic_parser::grammars::{NumGrammar, Parse, Untyped}; 17 | use cortex_m_rt::entry; 18 | use cortex_m_semihosting::{debug, hprintln, syscall}; 19 | use embedded_alloc::LlffHeap as Heap; 20 | #[cfg(target_arch = "arm")] 21 | use panic_halt as _; 22 | use rand_chacha::{ 23 | rand_core::{RngCore, SeedableRng}, 24 | ChaChaRng, 25 | }; 26 | 27 | #[global_allocator] 28 | static ALLOCATOR: Heap = Heap::empty(); 29 | 30 | const HEAP_SIZE: usize = 49_152; 31 | 32 | const MINMAX_SCRIPT: &str = " 33 | minmax = |xs| fold(xs, #{ min: MAX_VALUE, max: MIN_VALUE }, |acc, x| #{ 34 | min: if(x < acc.min, x, acc.min), 35 | max: if(x > acc.max, x, acc.max), 36 | }); 37 | xs = dbg(array(10, |_| rand_num())); 38 | { min, max } = dbg(minmax(xs)); 39 | assert(fold(xs, true, |acc, x| acc && x >= min && x <= max)); 40 | "; 41 | 42 | /// Analogue of `arithmetic_eval::fns::Dbg` that writes to the semihosting interface. 43 | struct Dbg; 44 | 45 | impl NativeFn for Dbg { 46 | fn evaluate( 47 | &self, 48 | mut args: Vec>, 49 | ctx: &mut CallContext<'_, i32>, 50 | ) -> EvalResult { 51 | ctx.check_args_count(&args, 1)?; 52 | let arg = args.pop().unwrap(); 53 | 54 | hprintln!( 55 | "[{line}:{col}] {val}", 56 | line = arg.location_line(), 57 | col = arg.get_column(), 58 | val = arg.extra 59 | ); 60 | Ok(arg.extra) 61 | } 62 | } 63 | 64 | fn main_inner() { 65 | let module = { 66 | let block = Untyped::>::parse_statements(MINMAX_SCRIPT).unwrap(); 67 | ExecutableModule::new("minmax", &block).unwrap() 68 | }; 69 | 70 | let epoch_seconds = unsafe { syscall!(TIME) }; 71 | // Using a timestamp as an RNG seed is unsecure and done for simplicity only. 72 | // Modern bare metal envs come with a hardware RNG peripheral that should be used instead. 73 | let rng = ChaChaRng::seed_from_u64(epoch_seconds as u64); 74 | let rng = RefCell::new(rng); 75 | let rand_num = Value::wrapped_fn(move || rng.borrow_mut().next_u32() as i32); 76 | 77 | let mut env = Environment::with_arithmetic(::new()); 78 | let prelude = Prelude::iter() 79 | .chain(Assertions::iter()) 80 | .filter(|(name, _)| module.is_import(name)); 81 | env.extend(prelude); 82 | env.insert_native_fn("dbg", Dbg) 83 | .insert_native_fn("array", fns::Array) 84 | .insert_native_fn("fold", fns::Fold) 85 | .insert("rand_num", rand_num) 86 | .insert("MIN_VALUE", Value::Prim(i32::MIN)) 87 | .insert("MAX_VALUE", Value::Prim(i32::MAX)); 88 | 89 | module.with_env(&env).unwrap().run().unwrap(); 90 | } 91 | 92 | #[entry] 93 | fn main() -> ! { 94 | let start = cortex_m_rt::heap_start() as usize; 95 | unsafe { 96 | ALLOCATOR.init(start, HEAP_SIZE); 97 | } 98 | 99 | main_inner(); 100 | 101 | debug::exit(debug::EXIT_SUCCESS); 102 | unreachable!("Program must exit by this point"); 103 | } 104 | -------------------------------------------------------------------------------- /e2e-tests/wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arithmetic-parser-wasm" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | description = "Testing usability of `arithmetic-parser` & `arithmetic-eval` in WASM" 9 | publish = false 10 | 11 | [lib] 12 | crate-type = ["cdylib", "rlib"] 13 | 14 | [dependencies] 15 | # WASM glue 16 | wasm-bindgen = { workspace = true, features = ["serde-serialize"] } 17 | 18 | arithmetic-parser.workspace = true 19 | arithmetic-eval = { workspace = true, default-features = true } 20 | -------------------------------------------------------------------------------- /e2e-tests/wasm/README.md: -------------------------------------------------------------------------------- 1 | # Testing Usability of `arithmetic-*` Crates in WASM 2 | 3 | This simple crate tests that `arithmetic-parser` and `arithmetic-eval` build 4 | and can be used in WASM. 5 | 6 | ## Compiling 7 | 8 | 1. Install WASM target for Rust via `rustup`: `rustup target add wasm32-unknown-unknown`. 9 | 2. Install [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer/). 10 | 3. Install [Node](https://nodejs.org/). 11 | 4. Switch to the directory with this README and run `wasm-pack build --target nodejs`. 12 | 5. Run the testing script: `node test.js`. 13 | -------------------------------------------------------------------------------- /e2e-tests/wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(clippy::unused_unit)] // produced by `wasm_bindgen` macro 3 | 4 | extern crate alloc; 5 | 6 | use alloc::string::ToString; 7 | use core::f64::consts as f64_consts; 8 | 9 | use arithmetic_eval::{ 10 | env::{Assertions, Environment, Prelude}, 11 | exec::WildcardId, 12 | fns, ExecutableModule, Value, 13 | }; 14 | use arithmetic_parser::grammars::{F64Grammar, Parse, Untyped}; 15 | use wasm_bindgen::prelude::*; 16 | 17 | #[wasm_bindgen] 18 | extern "C" { 19 | pub type Error; 20 | 21 | #[wasm_bindgen(constructor)] 22 | fn new(message: &str) -> Error; 23 | } 24 | 25 | #[allow(clippy::type_complexity)] 26 | fn initialize_env(env: &mut Environment) { 27 | const CONSTANTS: &[(&str, f64)] = &[ 28 | ("E", f64_consts::E), 29 | ("PI", f64_consts::PI), 30 | ("Inf", f64::INFINITY), 31 | ]; 32 | 33 | const UNARY_FNS: &[(&str, fn(f64) -> f64)] = &[ 34 | // Rounding functions. 35 | ("floor", f64::floor), 36 | ("ceil", f64::ceil), 37 | ("round", f64::round), 38 | ("frac", f64::fract), 39 | // Exponential functions. 40 | ("exp", f64::exp), 41 | ("ln", f64::ln), 42 | ("sinh", f64::sinh), 43 | ("cosh", f64::cosh), 44 | ("tanh", f64::tanh), 45 | ("asinh", f64::asinh), 46 | ("acosh", f64::acosh), 47 | ("atanh", f64::atanh), 48 | // Trigonometric functions. 49 | ("sin", f64::sin), 50 | ("cos", f64::cos), 51 | ("tan", f64::tan), 52 | ("asin", f64::asin), 53 | ("acos", f64::acos), 54 | ("atan", f64::atan), 55 | ]; 56 | 57 | const BINARY_FNS: &[(&str, fn(f64, f64) -> f64)] = &[ 58 | ("min", |x, y| if x < y { x } else { y }), 59 | ("max", |x, y| if x > y { x } else { y }), 60 | ]; 61 | 62 | for (name, c) in CONSTANTS { 63 | env.insert(name, Value::Prim(*c)); 64 | } 65 | for (name, unary_fn) in UNARY_FNS { 66 | env.insert_native_fn(name, fns::Unary::new(*unary_fn)); 67 | } 68 | for (name, binary_fn) in BINARY_FNS { 69 | env.insert_native_fn(name, fns::Binary::new(*binary_fn)); 70 | } 71 | } 72 | 73 | fn into_js_error(err: impl ToString) -> JsValue { 74 | Error::new(&err.to_string()).into() 75 | } 76 | 77 | #[wasm_bindgen] 78 | pub fn evaluate(program: &str) -> Result { 79 | let block = Untyped::::parse_statements(program).map_err(into_js_error)?; 80 | let module = ExecutableModule::new(WildcardId, &block).map_err(into_js_error)?; 81 | 82 | let mut env = Environment::new(); 83 | env.extend(Prelude::iter().chain(Assertions::iter())); 84 | initialize_env(&mut env); 85 | 86 | let value = module 87 | .with_env(&env) 88 | .map_err(into_js_error)? 89 | .run() 90 | .map_err(into_js_error)?; 91 | match value { 92 | Value::Prim(number) => Ok(JsValue::from(number)), 93 | Value::Bool(flag) => Ok(JsValue::from(flag)), 94 | _ => Err(Error::new("returned value is not presentable").into()), 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /e2e-tests/wasm/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { strict: assert } = require('assert'); 4 | const { evaluate } = require('./pkg'); 5 | 6 | // Normal cases. 7 | const evaluated = evaluate(` 8 | // The interpreter supports all parser features, including 9 | // function definitions, tuples and blocks. 10 | order = |x, y| (min(x, y), max(x, y)); 11 | assert_eq(order(0.5, -1), (-1, 0.5)); 12 | (_, M) = order(3^2, { x = 3; x + 5 }); 13 | M`); 14 | assert.equal(evaluated, 9); 15 | 16 | const evaluatedFlag = evaluate(` 17 | max_value = |...xs| { 18 | xs.fold(-Inf, |acc, x| if(x > acc, x, acc)) 19 | }; 20 | max_value(1, -2, 7, 2, 5) == 7 && max_value(3, -5, 9) == 9 21 | `); 22 | assert(evaluatedFlag); 23 | 24 | // Parse errors. 25 | assert.throws(() => evaluate('1 +'), /1:4: Unfinished arithmetic expression/); 26 | 27 | // Evaluation errors. 28 | assert.throws(() => evaluate('2 + test(1, 2)'), /1:5: Variable `test` is not defined/); 29 | -------------------------------------------------------------------------------- /eval/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arithmetic-eval" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "README.md" 10 | description = "Simple interpreter for arithmetic expressions." 11 | categories = ["mathematics", "compilers", "no-std"] 12 | keywords = ["interpreter", "arithmetic", "scripting", "language"] 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | # Set `docsrs` to enable unstable `doc(cfg(...))` attributes. 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | [dependencies] 20 | # Public dependencies (present in the public API). 21 | arithmetic-parser = { workspace = true, default-features = false } 22 | anyhow.workspace = true 23 | num-bigint = { workspace = true, optional = true } 24 | num-complex = { workspace = true, optional = true } 25 | num-traits.workspace = true 26 | 27 | # Private dependencies. 28 | hashbrown = { workspace = true, optional = true } 29 | once_cell.workspace = true 30 | 31 | [dev-dependencies] 32 | anyhow = { workspace = true, features = ["default"] } 33 | assert_matches.workspace = true 34 | criterion.workspace = true 35 | glass_pumpkin.workspace = true 36 | nom.workspace = true 37 | pulldown-cmark.workspace = true 38 | sha2.workspace = true 39 | rand.workspace = true 40 | static_assertions.workspace = true 41 | typed-arena.workspace = true 42 | version-sync.workspace = true 43 | 44 | [features] 45 | default = ["std"] 46 | # Enables support of types from `std`, such as the `Error` trait. 47 | std = ["anyhow/std", "num-traits/std", "arithmetic-parser/std"] 48 | # Enables support of grammars with complex-valued literals. 49 | complex = ["std", "num-complex", "arithmetic-parser/num-complex"] 50 | # Enables support of grammars with arbitrary-precision integers. 51 | bigint = ["num-bigint", "arithmetic-parser/num-bigint"] 52 | 53 | [[bench]] 54 | name = "interpreter" 55 | harness = false 56 | path = "benches/interpreter.rs" 57 | 58 | [[example]] 59 | name = "owned_module" 60 | path = "examples/owned_module.rs" 61 | required-features = ["std"] 62 | 63 | [[example]] 64 | name = "el_gamal" 65 | path = "examples/el_gamal.rs" 66 | required-features = ["std", "bigint", "num-bigint/rand"] 67 | 68 | [[example]] 69 | name = "cyclic_group" 70 | path = "examples/cyclic_group.rs" 71 | required-features = ["std", "bigint", "num-bigint/rand"] 72 | -------------------------------------------------------------------------------- /eval/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /eval/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /eval/README.md: -------------------------------------------------------------------------------- 1 | # Simple Arithmetic Interpreter 2 | 3 | [![Build Status](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml) 4 | [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/arithmetic-parser#license) 5 | ![rust 1.70+ required](https://img.shields.io/badge/rust-1.70+-blue.svg) 6 | ![no_std supported](https://img.shields.io/badge/no__std-tested-green.svg) 7 | 8 | **Links:** [![Docs on docs.rs](https://img.shields.io/docsrs/arithmetic-eval)](https://docs.rs/arithmetic-eval/) 9 | [![crate docs (master)](https://img.shields.io/badge/master-yellow.svg?label=docs)](https://slowli.github.io/arithmetic-parser/arithmetic_eval/) 10 | 11 | This library provides a simple interpreter, which can be used for some grammars 12 | recognized by [`arithmetic-parser`], e.g., integer-, real-, complex-valued and modular arithmetic. 13 | (Both built-in integer types and big integers from [`num-bigint`] are supported.) 14 | The interpreter provides support for native functions, 15 | which allows to overcome some syntax limitations (e.g., the lack of control flow 16 | can be solved with native `if` / `while` functions). Native functions and opaque reference types 17 | allow effectively embedding the interpreter into larger Rust applications. 18 | 19 | The interpreter is somewhat opinionated on how to interpret language features 20 | (e.g., in terms of arithmetic ops for tuple / object arguments). 21 | On the other hand, handling primitive types is fully customizable, just like their parsing 22 | in `arithmetic-parser`. 23 | The primary goal is to be intuitive for simple grammars (such as the aforementioned 24 | real-valued arithmetic). 25 | 26 | The interpreter is quite slow – 1–2 orders of magnitude slower than native arithmetic. 27 | 28 | ## Usage 29 | 30 | Add this to your `Crate.toml`: 31 | 32 | ```toml 33 | [dependencies] 34 | arithmetic-eval = "0.4.0-beta.1" 35 | ``` 36 | 37 | ### Script samples 38 | 39 | A simple script relying entirely on standard functions. 40 | 41 | ```text 42 | minmax = |...xs| xs.fold(#{ min: INF, max: -INF }, |acc, x| #{ 43 | min: if(x < acc.min, x, acc.min), 44 | max: if(x > acc.max, x, acc.max), 45 | }); 46 | assert_eq(minmax(3, 7, 2, 4).min, 2); 47 | assert_eq(minmax(5, -4, 6, 9, 1), #{ min: -4, max: 9 }); 48 | ``` 49 | 50 | Recursive quick sort implementation: 51 | 52 | ```text 53 | sort = defer(|sort| { 54 | // `defer` allows to define a function recursively 55 | |xs| { 56 | if(xs == (), || (), || { 57 | (pivot, ...rest) = xs; 58 | lesser_part = sort(rest.filter(|x| x < pivot)); 59 | greater_part = sort(rest.filter(|x| x >= pivot)); 60 | lesser_part.push(pivot).merge(greater_part) 61 | })() 62 | } 63 | }); 64 | 65 | assert_eq(sort((1, 7, -3, 2, -1, 4, 2)), (-3, -1, 1, 2, 2, 4, 7)); 66 | 67 | // Generate a larger array to sort. `rand_num` is a custom native function 68 | // that generates random numbers in the specified range. 69 | xs = sort(array(1000, |_| rand_num(0, 100))); 70 | // Check that elements in `xs` are monotonically non-decreasing. 71 | { sorted } = xs.fold( 72 | #{ prev: -1, sorted: true }, 73 | |{ prev, sorted }, x| #{ 74 | prev: x, 75 | sorted: sorted && prev <= x 76 | }, 77 | ); 78 | assert(sorted); 79 | ``` 80 | 81 | Defining a type: 82 | 83 | ```text 84 | Vector = { 85 | len = |{ x, y }| sqrt(x * x + y * y); 86 | scale = |self| if(self.x == 0 && self.y == 0, 87 | || self, 88 | || self / self.len(), 89 | )(); 90 | #{ len, scale } 91 | }; 92 | 93 | assert_close(#{ x: 3, y: 4 }.{Vector.len}(), 5); 94 | // ...is same as 95 | assert_close({Vector.len}(#{ x: 3, y: 4 }), 5); 96 | scaled = #{ x: 3, y: -4 }.{Vector.scale}(); 97 | assert_close(scaled.x, 0.6); 98 | assert_close(scaled.y, -0.8); 99 | ``` 100 | 101 | Please see the crate docs and [examples](examples) for more examples. 102 | 103 | ## See also 104 | 105 | - [`arithmetic-typing`] is a type checker / inference tool for ASTs evaluated 106 | by this crate. 107 | 108 | ## License 109 | 110 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) 111 | or [MIT license](LICENSE-MIT) at your option. 112 | 113 | Unless you explicitly state otherwise, any contribution intentionally submitted 114 | for inclusion in `arithmetic-eval` by you, as defined in the Apache-2.0 license, 115 | shall be dual licensed as above, without any additional terms or conditions. 116 | 117 | [`arithmetic-parser`]: https://crates.io/crates/arithmetic-parser 118 | [`arithmetic-typing`]: https://crates.io/crates/arithmetic-typing 119 | [`num-bigint`]: https://crates.io/crates/num-bigint 120 | [Schnorr signatures]: https://en.wikipedia.org/wiki/Schnorr_signature 121 | -------------------------------------------------------------------------------- /eval/examples/dsa.script: -------------------------------------------------------------------------------- 1 | //! DSA signatures on a prime-order cyclic group. 2 | 3 | dbg(GEN, ORDER); 4 | 5 | PublicKey = #{ 6 | verify: |self, message, { r, s }| { 7 | (u1, u2) = (hash_to_scalar(message) / s, r / s); 8 | (GEN ^ u1 * self ^ u2).to_scalar() == r 9 | }, 10 | }; 11 | 12 | SecretKey = #{ 13 | sign: |self, message| { 14 | k = rand_scalar(); 15 | r = (GEN ^ k).to_scalar(); 16 | s = (hash_to_scalar(message) + self * r) / k; 17 | #{ r, s } 18 | }, 19 | public_key: |self| GEN ^ self, 20 | }; 21 | 22 | gen = || { 23 | sk = rand_scalar(); 24 | #{ sk, pk: {SecretKey.public_key}(sk) } 25 | }; 26 | 27 | // Test! 28 | { sk, pk } = gen(); 29 | { pk: other_pk } = gen(); 30 | 31 | while(5, |i| i != 0, |i| { 32 | message = rand_scalar(); 33 | dbg(message); 34 | signature = sk.{SecretKey.sign}(message); 35 | dbg(signature); 36 | 37 | assert(pk.{PublicKey.verify}(message, signature)); 38 | assert(!other_pk.{PublicKey.verify}(message, signature)); 39 | assert(!pk.{PublicKey.verify}(rand_scalar(), signature)); 40 | 41 | i - 1 42 | }); 43 | -------------------------------------------------------------------------------- /eval/examples/el_gamal.rs: -------------------------------------------------------------------------------- 1 | //! Showcases modular arithmetic by implementing a toy version of ElGamal encryption. 2 | //! 3 | //! See the `cyclic_group` example for a more complex usage of the crate. 4 | //! 5 | //! ⚠ This implementation is NOT SECURE (e.g., in terms of side-channel attacks) 6 | //! and should be viewed only as a showcase of the crate abilities. 7 | 8 | use std::cell::RefCell; 9 | 10 | use arithmetic_eval::{ 11 | arith::{ArithmeticExt, ModularArithmetic}, 12 | env::{Assertions, Prelude}, 13 | fns, Environment, ExecutableModule, Value, 14 | }; 15 | use arithmetic_parser::grammars::{NumGrammar, Parse, Untyped}; 16 | use glass_pumpkin::safe_prime; 17 | use num_bigint::{BigUint, RandBigInt}; 18 | use rand::thread_rng; 19 | 20 | // NB: this is nowhere near a secure value (~2,048 bits). 21 | const BIT_LENGTH: usize = 256; 22 | 23 | /// Finds a generator of a prime-order multiplicative subgroup in integers modulo `modulus` 24 | /// (which is guaranteed to be a safe prime). Per Fermat's little theorem, a square of any 25 | /// number is guaranteed to be in the group, thus it will be a generator. 26 | fn find_generator(modulus: &BigUint) -> BigUint { 27 | let two = BigUint::from(2_u32); 28 | let random_value = thread_rng().gen_biguint_range(&two, modulus); 29 | random_value.modpow(&two, modulus) 30 | } 31 | 32 | const EL_GAMAL_ENCRYPTION: &str = include_str!("elgamal.script"); 33 | 34 | fn main() -> anyhow::Result<()> { 35 | let el_gamal_encryption = 36 | Untyped::>::parse_statements(EL_GAMAL_ENCRYPTION)?; 37 | let el_gamal_encryption = ExecutableModule::new("el_gamal", &el_gamal_encryption)?; 38 | 39 | // Run the compiled module with different groups. 40 | for i in 0..5 { 41 | println!("\nRunning sample #{i}"); 42 | 43 | let modulus = safe_prime::new(BIT_LENGTH)?; 44 | println!("Generated safe prime: {modulus}"); 45 | 46 | let prime_subgroup_order: BigUint = &modulus >> 1; 47 | let order_value = Value::Prim(prime_subgroup_order.clone()); 48 | let generator = find_generator(&modulus); 49 | let arithmetic = ModularArithmetic::new(modulus).without_comparisons(); 50 | 51 | let rng = RefCell::new(thread_rng()); 52 | let two = BigUint::from(2_u32); 53 | let rand_scalar = Value::wrapped_fn(move || { 54 | rng.borrow_mut() 55 | .gen_biguint_range(&two, &prime_subgroup_order) 56 | }); 57 | 58 | let mut env = Environment::with_arithmetic(arithmetic); 59 | env.extend(Prelude::iter().chain(Assertions::iter())); 60 | env.insert_native_fn("dbg", fns::Dbg) 61 | .insert("GEN", Value::Prim(generator)) 62 | .insert("ORDER", order_value) 63 | .insert("rand_scalar", rand_scalar); 64 | 65 | el_gamal_encryption.with_env(&env)?.run()?; 66 | } 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /eval/examples/elgamal.script: -------------------------------------------------------------------------------- 1 | //! El-Gamal encryption on a prime-order cyclic group. 2 | 3 | dbg(GEN, ORDER); 4 | 5 | PublicKey = #{ 6 | encrypt: |self, message| { 7 | r = rand_scalar(); 8 | shared_secret = self ^ r; 9 | #{ R: GEN ^ r, B: message * shared_secret } 10 | }, 11 | }; 12 | 13 | SecretKey = #{ 14 | decrypt: |self, { R, B }| { 15 | shared_secret = R ^ self; 16 | B / shared_secret 17 | }, 18 | public_key: |self| GEN ^ self, 19 | }; 20 | 21 | gen = || { 22 | sk = rand_scalar(); 23 | #{ sk, pk: {SecretKey.public_key}(sk) } 24 | }; 25 | 26 | // Test! 27 | { sk, pk } = gen(); 28 | 29 | while(5, |i| i != 0, |i| { 30 | message = GEN ^ rand_scalar(); 31 | dbg(message); 32 | encrypted = pk.{PublicKey.encrypt}(message); 33 | dbg(encrypted); 34 | assert_eq(sk.{SecretKey.decrypt}(encrypted), message); 35 | 36 | i - 1 37 | }); 38 | 39 | // Advanced testing making use of partial homomorphicity of encryption. 40 | ONE = GEN ^ 0; 41 | encrypt_and_combine = |pk, messages| { 42 | messages.map(|msg| pk.{PublicKey.encrypt}(msg)).fold( 43 | #{ R: ONE, B: ONE }, 44 | |enc_x, enc_y| enc_x * enc_y, 45 | ) 46 | }; 47 | 48 | messages = (1, 2, 3, 4, 5).map(|_| GEN ^ rand_scalar()); 49 | assert_eq( 50 | sk.{SecretKey.decrypt}(encrypt_and_combine(pk, messages)), 51 | messages.fold(ONE, |acc, msg| acc * msg) 52 | ); 53 | -------------------------------------------------------------------------------- /eval/examples/owned_module.rs: -------------------------------------------------------------------------------- 1 | //! Shows how to use owned modules. 2 | 3 | use arithmetic_eval::{ 4 | env::{Assertions, Environment, Prelude}, 5 | fns, ErrorKind, ExecutableModule, Value, 6 | }; 7 | use arithmetic_parser::{ 8 | grammars::{F64Grammar, MockTypes, Parse, WithMockedTypes}, 9 | BinaryOp, 10 | }; 11 | use assert_matches::assert_matches; 12 | 13 | /// We need to process some type annotations, but don't want to depend 14 | /// on the typing crate for that. Hence, we define a grammar that gobbles up the exact 15 | /// type annotations used in the script. 16 | struct MockedTypesList; 17 | 18 | impl MockTypes for MockedTypesList { 19 | const MOCKED_TYPES: &'static [&'static str] = &["Num", "[_]", "any"]; 20 | } 21 | 22 | type Grammar = WithMockedTypes; 23 | 24 | fn create_module( 25 | module_name: &'static str, 26 | program: &str, 27 | ) -> anyhow::Result> { 28 | let block = Grammar::parse_statements(program)?; 29 | Ok(ExecutableModule::new(module_name, &block)?) 30 | } 31 | 32 | fn main() -> anyhow::Result<()> { 33 | let mut env = Environment::new(); 34 | env.extend(Prelude::iter().chain(Assertions::iter())); 35 | env.insert_native_fn("fold", fns::Fold) 36 | .insert_native_fn("push", fns::Push) 37 | .insert("INF", Value::Prim(f64::INFINITY)); 38 | 39 | let sum_module = { 40 | let dynamic_program = String::from("|...vars| fold(vars, 0, |acc, x| acc + x)"); 41 | create_module("sum", &dynamic_program)? 42 | // Ensure that the program is indeed dropped by using a separate scope. 43 | }; 44 | 45 | // The code is dropped here, but the module is still usable. 46 | let sum_fn = sum_module.with_env(&env)?.run()?; 47 | assert!(sum_fn.is_function()); 48 | 49 | // Let's import the function into another module and check that it works. 50 | let test_module = create_module("test", "sum(1, 2, -5)")?; 51 | let mut non_static_env = env.clone(); 52 | non_static_env.insert("sum", sum_fn); 53 | let sum_value = test_module.with_env(&non_static_env)?.run()?; 54 | assert_eq!(sum_value, Value::Prim(-2.0)); // 1 + 2 - 5 55 | 56 | // Errors are handled as well. 57 | let bogus_module = create_module("bogus", "sum(1, true, -5)")?; 58 | 59 | let err = bogus_module.with_env(&non_static_env)?.run().unwrap_err(); 60 | println!("Expected error:\n{:#}", err); 61 | assert_matches!( 62 | err.source().kind(), 63 | ErrorKind::UnexpectedOperand { op } if *op == BinaryOp::Add.into() 64 | ); 65 | 66 | // Naturally, spans in the stripped module do not retain refs to source code, 67 | // but rather contain info sufficient to be recoverable. 68 | assert_eq!( 69 | err.source().location().in_module().to_string("call"), 70 | "call at 1:40" 71 | ); 72 | 73 | // Importing into a stripped module also works. Let's redefine the `fold` import. 74 | let fold_program = include_str!("rfold.script"); 75 | let fold_program = String::from(fold_program); 76 | let fold_module = create_module("rfold", &fold_program)?; 77 | let rfold_fn = fold_module.with_env(&env)?.run()?; 78 | 79 | env.insert("fold", rfold_fn); 80 | let rfold_sum = sum_module.with_env(&env)?.run()?; 81 | env.insert("sum", rfold_sum); 82 | let sum_value = test_module.with_env(&env)?.run()?; 83 | assert_eq!(sum_value, Value::Prim(-2.0)); 84 | 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /eval/examples/rfold.script: -------------------------------------------------------------------------------- 1 | //! Module defining a right-fold function. 2 | 3 | rfold = |xs, acc, fn| { 4 | (_, acc) = while( 5 | (xs, acc), 6 | |(xs, _)| xs as [_] != (), 7 | |(xs, acc)| { 8 | (...head: Num, tail: Num) = xs as any; 9 | (head, fn(acc, tail)) 10 | }, 11 | ); 12 | acc 13 | }; 14 | 15 | // Check that `rfold` works with different accumulator types. 16 | folded = rfold((1, 2, 3), () as [_], push); 17 | assert_eq(folded, (3, 2, 1)); 18 | 19 | rfold((4, 5, 6), true, |acc, x| acc && x > 0); 20 | 21 | (min, max) = rfold((1, 2, 3, 4), (INF, -INF), |(min, max), x| { 22 | min = if(x < min, x, min); 23 | max = if(x > max, x, max); 24 | (min, max) 25 | }); 26 | 27 | // Export the `rfold` function. 28 | rfold 29 | -------------------------------------------------------------------------------- /eval/examples/schnorr.script: -------------------------------------------------------------------------------- 1 | //! Schnorr signatures on a prime-order cyclic group. 2 | 3 | dbg(GEN, ORDER); 4 | 5 | PublicKey = #{ 6 | verify: |self, message, { e, s }| { 7 | R = GEN ^ s * self ^ e; 8 | e == hash_to_scalar(R, message) 9 | }, 10 | }; 11 | 12 | SecretKey = #{ 13 | sign: |self, message| { 14 | r = rand_scalar(); 15 | R = GEN ^ r; 16 | e = hash_to_scalar(R, message); 17 | #{ e, s: r - self * e } 18 | }, 19 | public_key: |self| GEN ^ self, 20 | }; 21 | 22 | gen = || { 23 | sk = rand_scalar(); 24 | #{ sk, pk: {SecretKey.public_key}(sk) } 25 | }; 26 | 27 | // Test! 28 | { sk, pk } = gen(); 29 | { pk: other_pk } = gen(); 30 | 31 | while(5, |i| i != 0, |i| { 32 | message = rand_scalar(); 33 | dbg(message); 34 | signature = sk.{SecretKey.sign}(message); 35 | dbg(signature); 36 | 37 | assert(pk.{PublicKey.verify}(message, signature)); 38 | assert(!other_pk.{PublicKey.verify}(message, signature)); 39 | assert(!pk.{PublicKey.verify}(rand_scalar(), signature)); 40 | 41 | i - 1 42 | }); 43 | -------------------------------------------------------------------------------- /eval/src/env/variable_map.rs: -------------------------------------------------------------------------------- 1 | //! Standard collections of variables. 2 | 3 | use core::{cmp::Ordering, fmt}; 4 | 5 | use crate::{fns, Object, Value}; 6 | 7 | /// Commonly used constants and functions from the [`fns` module](fns). 8 | /// 9 | /// # Contents 10 | /// 11 | /// - `true` and `false` Boolean constants. 12 | /// - Deferred initialization: [`defer`](fns::Defer). 13 | /// - Control flow functions: [`if`](fns::If), [`while`](fns::While). 14 | /// - Array functions: [`all`](fns::All), [`any`](fns::Any), [`filter`](fns::Filter), [`fold`](fns::Fold), 15 | /// [`map`](fns::Map), [`merge`](fns::Merge), [`push`](fns::Push). Available both as free functions 16 | /// and as fields in an `Array` object. 17 | #[derive(Debug, Clone, Copy, Default)] 18 | pub struct Prelude; 19 | 20 | impl Prelude { 21 | /// Creates an iterator over contained values and the corresponding names. 22 | pub fn iter() -> impl Iterator)> 23 | where 24 | T: 'static + Clone, 25 | { 26 | let array_object: Object = Self::array_functions::().collect(); 27 | [ 28 | ("false", Value::Bool(false)), 29 | ("true", Value::Bool(true)), 30 | ("defer", Value::native_fn(fns::Defer)), 31 | ("if", Value::native_fn(fns::If)), 32 | ("while", Value::native_fn(fns::While)), 33 | ] 34 | .into_iter() 35 | .chain(Self::array_functions::()) 36 | .chain([("Array", array_object.into())]) 37 | } 38 | 39 | fn array_functions() -> impl Iterator)> 40 | where 41 | T: 'static + Clone, 42 | { 43 | [ 44 | ("all", Value::native_fn(fns::All)), 45 | ("any", Value::native_fn(fns::Any)), 46 | ("filter", Value::native_fn(fns::Filter)), 47 | ("fold", Value::native_fn(fns::Fold)), 48 | ("map", Value::native_fn(fns::Map)), 49 | ("merge", Value::native_fn(fns::Merge)), 50 | ("push", Value::native_fn(fns::Push)), 51 | ] 52 | .into_iter() 53 | } 54 | } 55 | 56 | /// Container for assertion functions: `assert`, `assert_eq` and `assert_fails`. 57 | #[derive(Debug, Clone, Copy, Default)] 58 | pub struct Assertions; 59 | 60 | impl Assertions { 61 | /// Creates an iterator over contained values and the corresponding names. 62 | pub fn iter() -> impl Iterator)> 63 | where 64 | T: 'static + Clone + fmt::Display, 65 | { 66 | [ 67 | ("assert", Value::native_fn(fns::Assert)), 68 | ("assert_eq", Value::native_fn(fns::AssertEq)), 69 | ( 70 | "assert_fails", 71 | Value::native_fn(fns::AssertFails::default()), 72 | ), 73 | ] 74 | .into_iter() 75 | } 76 | } 77 | 78 | /// Container with the comparison functions: `cmp`, `min` and `max`. 79 | #[derive(Debug, Clone, Copy, Default)] 80 | pub struct Comparisons; 81 | 82 | impl Comparisons { 83 | /// Creates an iterator over contained values and the corresponding names. 84 | pub fn iter() -> impl Iterator)> { 85 | [ 86 | ("LESS", Value::opaque_ref(Ordering::Less)), 87 | ("EQUAL", Value::opaque_ref(Ordering::Equal)), 88 | ("GREATER", Value::opaque_ref(Ordering::Greater)), 89 | ("cmp", Value::native_fn(fns::Compare::Raw)), 90 | ("min", Value::native_fn(fns::Compare::Min)), 91 | ("max", Value::native_fn(fns::Compare::Max)), 92 | ] 93 | .into_iter() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /eval/src/exec/command.rs: -------------------------------------------------------------------------------- 1 | //! Executable `Command` and its building blocks. 2 | 3 | use arithmetic_parser::{BinaryOp, Location, LvalueLen, UnaryOp}; 4 | 5 | use crate::alloc::{String, Vec}; 6 | 7 | /// Pointer to a register or constant. 8 | #[derive(Debug)] 9 | pub(crate) enum Atom { 10 | Constant(T), 11 | Register(usize), 12 | Void, 13 | } 14 | 15 | impl Clone for Atom { 16 | fn clone(&self) -> Self { 17 | match self { 18 | Self::Constant(literal) => Self::Constant(literal.clone()), 19 | Self::Register(index) => Self::Register(*index), 20 | Self::Void => Self::Void, 21 | } 22 | } 23 | } 24 | 25 | pub(crate) type LocatedAtom = Location>; 26 | 27 | #[derive(Debug, Clone)] 28 | pub(crate) enum FieldName { 29 | Index(usize), 30 | Name(String), 31 | } 32 | 33 | /// Atomic operation on registers and/or constants. 34 | #[derive(Debug, Clone)] 35 | pub(crate) enum CompiledExpr { 36 | Atom(Atom), 37 | Tuple(Vec>), 38 | Object(Vec<(String, Atom)>), 39 | Unary { 40 | op: UnaryOp, 41 | inner: LocatedAtom, 42 | }, 43 | Binary { 44 | op: BinaryOp, 45 | lhs: LocatedAtom, 46 | rhs: LocatedAtom, 47 | }, 48 | FieldAccess { 49 | receiver: LocatedAtom, 50 | field: FieldName, 51 | }, 52 | FunctionCall { 53 | name: LocatedAtom, 54 | // Original function name if it is a proper variable name. 55 | original_name: Option, 56 | args: Vec>, 57 | }, 58 | DefineFunction { 59 | ptr: usize, 60 | captures: Vec>, 61 | // Original capture names. 62 | capture_names: Vec, 63 | }, 64 | } 65 | 66 | /// Commands for a primitive register VM used to execute compiled programs. 67 | #[derive(Debug, Clone)] 68 | pub(crate) enum Command { 69 | /// Create a new register and push the result of the specified computation there. 70 | Push(CompiledExpr), 71 | 72 | /// Destructure a tuple value. This will push `start_len` starting elements from the tuple, 73 | /// the middle of the tuple (as a tuple), and `end_len` ending elements from the tuple 74 | /// as new registers, in this order. 75 | Destructure { 76 | /// Index of the register with the value. 77 | source: usize, 78 | /// Number of starting arguments to place in separate registers. 79 | start_len: usize, 80 | /// Number of ending arguments to place in separate registers. 81 | end_len: usize, 82 | /// Acceptable length(s) of the source. 83 | lvalue_len: LvalueLen, 84 | /// Does `lvalue_len` should be checked? When destructuring arguments for functions, 85 | /// this check was performed previously. 86 | unchecked: bool, 87 | }, 88 | 89 | /// Copies the source register into the destination. The destination register must exist. 90 | Copy { source: usize, destination: usize }, 91 | 92 | /// Annotates a register as containing the specified variable. 93 | Annotate { register: usize, name: String }, 94 | 95 | /// Signals that the following commands are executed in the inner scope. 96 | StartInnerScope, 97 | /// Signals that the following commands are executed in the global scope. 98 | EndInnerScope, 99 | /// Signals to truncate registers to the specified number. 100 | TruncateRegisters(usize), 101 | } 102 | 103 | pub(crate) type LocatedCommand = Location>; 104 | -------------------------------------------------------------------------------- /eval/src/exec/module_id.rs: -------------------------------------------------------------------------------- 1 | //! Module ID. 2 | 3 | use core::{ 4 | any::{Any, TypeId}, 5 | fmt, 6 | }; 7 | 8 | /// Identifier of an [`ExecutableModule`](crate::ExecutableModule). This is usually a "small" type, 9 | /// such as an integer or a string. 10 | /// 11 | /// The ID is provided when [creating](crate::ExecutableModule::new()) a module. It is displayed 12 | /// in error messages (using `Display::fmt`). `ModuleId` is also associated with some types 13 | /// (e.g., [`InterpretedFn`] and [`LocationInModule`]), which allows to obtain module info. 14 | /// This can be particularly useful for outputting rich error information. 15 | /// 16 | /// A `ModuleId` can be downcast to a specific type, similarly to [`Any`]. 17 | /// 18 | /// [`InterpretedFn`]: crate::InterpretedFn 19 | /// [`LocationInModule`]: crate::error::LocationInModule 20 | pub trait ModuleId: Any + fmt::Display + Send + Sync {} 21 | 22 | impl dyn ModuleId { 23 | /// Returns `true` if the boxed type is the same as `T`. 24 | /// 25 | /// This method is effectively a carbon copy of [`::is`]. Such a copy is necessary 26 | /// because `&dyn ModuleId` cannot be converted to `&dyn Any`, despite `ModuleId` having `Any` 27 | /// as a super-trait. 28 | /// 29 | /// [`::is`]: https://doc.rust-lang.org/std/any/trait.Any.html#method.is 30 | #[inline] 31 | pub fn is(&self) -> bool { 32 | let t = TypeId::of::(); 33 | let concrete = self.type_id(); 34 | t == concrete 35 | } 36 | 37 | /// Returns a reference to the boxed value if it is of type `T`, or `None` if it isn't. 38 | /// 39 | /// This method is effectively a carbon copy of [`::downcast_ref`]. Such a copy 40 | /// is necessary because `&dyn ModuleId` cannot be converted to `&dyn Any`, despite `ModuleId` 41 | /// having `Any` as a super-trait. 42 | /// 43 | /// [`::downcast_ref`]: https://doc.rust-lang.org/std/any/trait.Any.html#method.downcast_ref 44 | pub fn downcast_ref(&self) -> Option<&T> { 45 | if self.is::() { 46 | // SAFETY: Same code as for `::downcast_ref()`. 47 | unsafe { Some(&*(self as *const dyn ModuleId).cast::()) } 48 | } else { 49 | None 50 | } 51 | } 52 | } 53 | 54 | impl fmt::Debug for dyn ModuleId { 55 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 56 | write!(formatter, "ModuleId({self})") 57 | } 58 | } 59 | 60 | impl ModuleId for &'static str {} 61 | 62 | /// Module identifier that has a single possible value, which is displayed as `*`. 63 | /// 64 | /// This type is a `ModuleId`-compatible replacement of `()`; `()` does not implement `Display` 65 | /// and thus cannot implement `ModuleId` directly. 66 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] 67 | pub struct WildcardId; 68 | 69 | impl ModuleId for WildcardId {} 70 | 71 | impl fmt::Display for WildcardId { 72 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 73 | formatter.write_str("*") 74 | } 75 | } 76 | 77 | /// Indexed module ID containing a prefix part (e.g., `snippet`). 78 | /// 79 | /// The ID is `Display`ed as `{prefix} #{index + 1}`: 80 | /// 81 | /// ``` 82 | /// # use arithmetic_eval::exec::IndexedId; 83 | /// let module_id = IndexedId::new("snippet", 4); 84 | /// assert_eq!(module_id.to_string(), "snippet #5"); 85 | /// ``` 86 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 87 | pub struct IndexedId { 88 | /// Prefix that can identify the nature of the module, such as `snippet`. 89 | pub prefix: &'static str, 90 | /// 0-based index of the module. 91 | pub index: usize, 92 | } 93 | 94 | impl IndexedId { 95 | /// Creates a new ID instance. 96 | pub const fn new(prefix: &'static str, index: usize) -> Self { 97 | Self { prefix, index } 98 | } 99 | } 100 | 101 | impl fmt::Display for IndexedId { 102 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 103 | write!(formatter, "{} #{}", self.prefix, self.index + 1) 104 | } 105 | } 106 | 107 | impl ModuleId for IndexedId {} 108 | -------------------------------------------------------------------------------- /eval/src/fns/flow.rs: -------------------------------------------------------------------------------- 1 | //! Flow control functions. 2 | 3 | use crate::{ 4 | alloc::{vec, Vec}, 5 | error::AuxErrorInfo, 6 | fns::extract_fn, 7 | CallContext, ErrorKind, EvalResult, NativeFn, SpannedValue, Value, 8 | }; 9 | 10 | /// `if` function that eagerly evaluates "if" / "else" terms. 11 | /// 12 | /// # Type 13 | /// 14 | /// (using [`arithmetic-typing`](https://docs.rs/arithmetic-typing/) notation) 15 | /// 16 | /// ```text 17 | /// (Bool, 'T, 'T) -> 'T 18 | /// ``` 19 | /// 20 | /// # Examples 21 | /// 22 | /// ``` 23 | /// # use arithmetic_parser::grammars::{F32Grammar, Parse, Untyped}; 24 | /// # use arithmetic_eval::{fns, Environment, ExecutableModule, Value}; 25 | /// # fn main() -> anyhow::Result<()> { 26 | /// let program = "x = 3; if(x == 2, -1, x + 1)"; 27 | /// let program = Untyped::::parse_statements(program)?; 28 | /// let module = ExecutableModule::new("test_if", &program)?; 29 | /// 30 | /// let mut env = Environment::new(); 31 | /// env.insert_native_fn("if", fns::If); 32 | /// assert_eq!(module.with_env(&env)?.run()?, Value::Prim(4.0)); 33 | /// # Ok(()) 34 | /// # } 35 | /// ``` 36 | /// 37 | /// You can also use the lazy evaluation by returning a function and evaluating it 38 | /// afterwards: 39 | /// 40 | /// ``` 41 | /// # use arithmetic_parser::grammars::{F32Grammar, Parse, Untyped}; 42 | /// # use arithmetic_eval::{fns, Environment, ExecutableModule, Value}; 43 | /// # fn main() -> anyhow::Result<()> { 44 | /// let program = "x = 3; if(x == 2, || -1, || x + 1)()"; 45 | /// let program = Untyped::::parse_statements(program)?; 46 | /// let module = ExecutableModule::new("test_if", &program)?; 47 | /// 48 | /// let mut env = Environment::new(); 49 | /// env.insert_native_fn("if", fns::If); 50 | /// assert_eq!(module.with_env(&env)?.run()?, Value::Prim(4.0)); 51 | /// # Ok(()) 52 | /// # } 53 | /// ``` 54 | #[derive(Debug, Clone, Copy, Default)] 55 | pub struct If; 56 | 57 | impl NativeFn for If { 58 | fn evaluate( 59 | &self, 60 | mut args: Vec>, 61 | ctx: &mut CallContext<'_, T>, 62 | ) -> EvalResult { 63 | ctx.check_args_count(&args, 3)?; 64 | let else_val = args.pop().unwrap().extra; 65 | let then_val = args.pop().unwrap().extra; 66 | 67 | if let Value::Bool(condition) = &args[0].extra { 68 | Ok(if *condition { then_val } else { else_val }) 69 | } else { 70 | let err = ErrorKind::native("`if` requires first arg to be boolean"); 71 | Err(ctx 72 | .call_site_error(err) 73 | .with_location(&args[0], AuxErrorInfo::InvalidArg)) 74 | } 75 | } 76 | } 77 | 78 | /// Loop function that evaluates the provided closure while a certain condition is true. 79 | /// Returns the loop state afterwards. 80 | /// 81 | /// # Type 82 | /// 83 | /// (using [`arithmetic-typing`](https://docs.rs/arithmetic-typing/) notation) 84 | /// 85 | /// ```text 86 | /// ('T, ('T) -> Bool, ('T) -> 'T) -> 'T 87 | /// ``` 88 | /// 89 | /// # Examples 90 | /// 91 | /// ``` 92 | /// # use arithmetic_parser::grammars::{F32Grammar, Parse, Untyped}; 93 | /// # use arithmetic_eval::{fns, Environment, ExecutableModule, Value}; 94 | /// # fn main() -> anyhow::Result<()> { 95 | /// let program = " 96 | /// factorial = |x| { 97 | /// (_, acc) = while( 98 | /// (x, 1), 99 | /// |(i, _)| i >= 1, 100 | /// |(i, acc)| (i - 1, acc * i), 101 | /// ); 102 | /// acc 103 | /// }; 104 | /// factorial(5) == 120 && factorial(10) == 3628800 105 | /// "; 106 | /// let program = Untyped::::parse_statements(program)?; 107 | /// let module = ExecutableModule::new("test_while", &program)?; 108 | /// 109 | /// let mut env = Environment::new(); 110 | /// env.insert_native_fn("while", fns::While); 111 | /// assert_eq!(module.with_env(&env)?.run()?, Value::Bool(true)); 112 | /// # Ok(()) 113 | /// # } 114 | /// ``` 115 | #[derive(Debug, Clone, Copy, Default)] 116 | pub struct While; 117 | 118 | impl NativeFn for While { 119 | fn evaluate( 120 | &self, 121 | mut args: Vec>, 122 | ctx: &mut CallContext<'_, T>, 123 | ) -> EvalResult { 124 | ctx.check_args_count(&args, 3)?; 125 | 126 | let step_fn = extract_fn( 127 | ctx, 128 | args.pop().unwrap(), 129 | "`while` requires third arg to be a step function", 130 | )?; 131 | let condition_fn = extract_fn( 132 | ctx, 133 | args.pop().unwrap(), 134 | "`while` requires second arg to be a condition function", 135 | )?; 136 | let mut state = args.pop().unwrap(); 137 | let state_span = state.copy_with_extra(()); 138 | 139 | loop { 140 | let condition_value = condition_fn.evaluate(vec![state.clone()], ctx)?; 141 | match condition_value { 142 | Value::Bool(true) => { 143 | let new_state = step_fn.evaluate(vec![state], ctx)?; 144 | state = state_span.copy_with_extra(new_state); 145 | } 146 | Value::Bool(false) => break Ok(state.extra), 147 | _ => { 148 | let err = 149 | ErrorKind::native("`while` requires condition function to return booleans"); 150 | return Err(ctx.call_site_error(err)); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /eval/src/fns/std.rs: -------------------------------------------------------------------------------- 1 | //! Functions that require the Rust standard library. 2 | 3 | use std::fmt; 4 | 5 | use crate::{exec::ModuleId, CallContext, EvalResult, NativeFn, SpannedValue, Value}; 6 | 7 | /// Acts similarly to the `dbg!` macro, outputting the argument(s) to stderr and returning 8 | /// them. If a single argument is provided, it's returned as-is; otherwise, the arguments 9 | /// are wrapped into a tuple. 10 | /// 11 | /// # Examples 12 | /// 13 | /// ``` 14 | /// # use arithmetic_parser::grammars::{F32Grammar, Parse, Untyped}; 15 | /// # use arithmetic_eval::{fns, Environment, ExecutableModule, Value}; 16 | /// # fn main() -> anyhow::Result<()> { 17 | /// let program = "dbg(1 + 2) > 2.5"; 18 | /// let program = Untyped::::parse_statements(program)?; 19 | /// let module = ExecutableModule::new("test_dbg", &program)?; 20 | /// 21 | /// let mut env = Environment::new(); 22 | /// env.insert_native_fn("dbg", fns::Dbg); 23 | /// let value = module.with_env(&env)?.run()?; 24 | /// // Should output `[test_assert:1:5] 1 + 2 = 3` to stderr. 25 | /// assert_eq!(value, Value::Bool(true)); 26 | /// # Ok(()) 27 | /// # } 28 | /// ``` 29 | #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 30 | #[derive(Debug, Clone, Copy, Default)] 31 | pub struct Dbg; 32 | 33 | impl Dbg { 34 | fn print_value(module_id: &dyn ModuleId, value: &SpannedValue) { 35 | eprintln!( 36 | "[{module}:{line}:{col}] {val}", 37 | module = module_id, 38 | line = value.location_line(), 39 | col = value.get_column(), 40 | val = value.extra 41 | ); 42 | } 43 | } 44 | 45 | impl NativeFn for Dbg { 46 | fn evaluate( 47 | &self, 48 | mut args: Vec>, 49 | ctx: &mut CallContext<'_, T>, 50 | ) -> EvalResult { 51 | let module_id = ctx.call_location().module_id(); 52 | for arg in &args { 53 | Self::print_value(module_id, arg); 54 | } 55 | 56 | Ok(if args.len() == 1 { 57 | args.pop().unwrap().extra 58 | } else { 59 | Value::Tuple(args.into_iter().map(|spanned| spanned.extra).collect()) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /eval/src/values/tuple.rs: -------------------------------------------------------------------------------- 1 | //! `Tuple` and tightly related types. 2 | 3 | use core::{fmt, iter::FromIterator, ops}; 4 | 5 | use crate::{ 6 | alloc::{vec, Vec}, 7 | Value, 8 | }; 9 | 10 | /// Tuple of zero or more values. 11 | /// 12 | /// A tuple is similar to a [`Vec`] with [`Value`] elements and can be converted from / to it. 13 | /// It is possible to iterate over elements, index them, etc. 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// # use arithmetic_eval::{Tuple, Value}; 19 | /// let mut tuple = Tuple::::default(); 20 | /// tuple.push(Value::Prim(3)); 21 | /// tuple.push(Value::Prim(5)); 22 | /// assert_eq!(tuple.len(), 2); 23 | /// assert_eq!(tuple[1], Value::Prim(5)); 24 | /// assert!(tuple.iter().all(|val| !val.is_void())); 25 | /// 26 | /// // `Tuple` implements `FromIterator` / `Extend`. 27 | /// let mut other_tuple: Tuple = (0..=2).map(Value::Prim).collect(); 28 | /// other_tuple.extend(tuple); 29 | /// assert_eq!(other_tuple.len(), 5); 30 | /// ``` 31 | #[derive(Debug, Clone, PartialEq)] 32 | pub struct Tuple { 33 | elements: Vec>, 34 | } 35 | 36 | impl Default for Tuple { 37 | fn default() -> Self { 38 | Self::void() 39 | } 40 | } 41 | 42 | impl From> for Value { 43 | fn from(tuple: Tuple) -> Self { 44 | Self::Tuple(tuple) 45 | } 46 | } 47 | 48 | impl From>> for Tuple { 49 | fn from(elements: Vec>) -> Self { 50 | Self { elements } 51 | } 52 | } 53 | 54 | impl From> for Vec> { 55 | fn from(tuple: Tuple) -> Self { 56 | tuple.elements 57 | } 58 | } 59 | 60 | impl fmt::Display for Tuple { 61 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 62 | write!(formatter, "(")?; 63 | for (i, element) in self.iter().enumerate() { 64 | fmt::Display::fmt(element, formatter)?; 65 | if i + 1 < self.len() { 66 | formatter.write_str(", ")?; 67 | } else if self.len() == 1 { 68 | formatter.write_str(",")?; // terminal ',' to distinguish 1-element tuples 69 | } 70 | } 71 | write!(formatter, ")") 72 | } 73 | } 74 | 75 | impl Tuple { 76 | /// Creates a new empty tuple (aka a void value). 77 | pub const fn void() -> Self { 78 | Self { 79 | elements: Vec::new(), 80 | } 81 | } 82 | 83 | /// Returns the number of elements in this tuple. 84 | pub fn len(&self) -> usize { 85 | self.elements.len() 86 | } 87 | 88 | /// Checks if this tuple is empty (has no elements). 89 | pub fn is_empty(&self) -> bool { 90 | self.elements.is_empty() 91 | } 92 | 93 | /// Iterates over the elements in this tuple. 94 | pub fn iter(&self) -> impl Iterator> + '_ { 95 | self.elements.iter() 96 | } 97 | 98 | /// Pushes a value to the end of this tuple. 99 | pub fn push(&mut self, value: impl Into>) { 100 | self.elements.push(value.into()); 101 | } 102 | } 103 | 104 | impl ops::Index for Tuple { 105 | type Output = Value; 106 | 107 | fn index(&self, index: usize) -> &Self::Output { 108 | &self.elements[index] 109 | } 110 | } 111 | 112 | impl IntoIterator for Tuple { 113 | type Item = Value; 114 | /// Iterator type should be considered an implementation detail. 115 | type IntoIter = vec::IntoIter>; 116 | 117 | fn into_iter(self) -> Self::IntoIter { 118 | self.elements.into_iter() 119 | } 120 | } 121 | 122 | impl<'r, T> IntoIterator for &'r Tuple { 123 | type Item = &'r Value; 124 | /// Iterator type should be considered an implementation detail. 125 | type IntoIter = core::slice::Iter<'r, Value>; 126 | 127 | fn into_iter(self) -> Self::IntoIter { 128 | self.elements.iter() 129 | } 130 | } 131 | 132 | impl FromIterator for Tuple 133 | where 134 | V: Into>, 135 | { 136 | fn from_iter>(iter: I) -> Self { 137 | Self { 138 | elements: iter.into_iter().map(Into::into).collect(), 139 | } 140 | } 141 | } 142 | 143 | impl Extend for Tuple 144 | where 145 | V: Into>, 146 | { 147 | fn extend>(&mut self, iter: I) { 148 | let new_elements = iter.into_iter().map(Into::into); 149 | self.elements.extend(new_elements); 150 | } 151 | } 152 | 153 | #[cfg(test)] 154 | mod tests { 155 | use super::*; 156 | 157 | #[test] 158 | fn tuple_to_string() { 159 | let mut tuple = Tuple::::default(); 160 | assert_eq!(tuple.to_string(), "()"); 161 | 162 | tuple.push(Value::Prim(3.0)); 163 | assert_eq!(tuple.to_string(), "(3,)"); 164 | 165 | tuple.push(Value::Prim(4.0)); 166 | assert_eq!(tuple.to_string(), "(3, 4)"); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /eval/tests/check_readme.rs: -------------------------------------------------------------------------------- 1 | //! Tests that the README code samples actually work. 2 | 3 | use std::fs; 4 | 5 | use arithmetic_eval::{ 6 | env::{Assertions, Environment, Prelude}, 7 | fns, ExecutableModule, Value, 8 | }; 9 | use arithmetic_parser::grammars::{F32Grammar, Parse, Untyped}; 10 | use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd}; 11 | use rand::{thread_rng, Rng}; 12 | 13 | fn read_file(path: &str) -> String { 14 | fs::read_to_string(path).unwrap_or_else(|err| panic!("Cannot read file {path}: {err}")) 15 | } 16 | 17 | fn check_sample(code_sample: &str) { 18 | let program = Untyped::::parse_statements(code_sample).unwrap(); 19 | let module = ExecutableModule::new("test", &program).unwrap(); 20 | 21 | let mut env = Environment::::new(); 22 | env.extend(Prelude::iter().chain(Assertions::iter())); 23 | env.insert("INF", Value::Prim(f32::INFINITY)) 24 | .insert_native_fn("array", fns::Array) 25 | .insert_native_fn("assert_close", fns::AssertClose::new(1e-4)) 26 | .insert_wrapped_fn("sqrt", f32::sqrt) 27 | .insert_wrapped_fn("rand_num", |min: f32, max: f32| { 28 | thread_rng().gen_range(min..max) 29 | }); 30 | 31 | module.with_env(&env).unwrap().run().unwrap(); 32 | } 33 | 34 | #[test] 35 | fn code_samples_in_readme_are_valid() { 36 | let readme = read_file("README.md"); 37 | 38 | let parser = Parser::new(&readme); 39 | let mut code: Option = None; 40 | for event in parser { 41 | match event { 42 | Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) 43 | if lang.as_ref() == "text" => 44 | { 45 | assert!(code.is_none(), "Embedded code samples"); 46 | code = Some(String::with_capacity(1_024)); 47 | } 48 | Event::End(TagEnd::CodeBlock) => { 49 | if let Some(code_sample) = code.take() { 50 | assert!(!code_sample.is_empty()); 51 | check_sample(&code_sample); 52 | } 53 | } 54 | Event::Text(text) => { 55 | if let Some(code) = &mut code { 56 | code.push_str(text.as_ref()); 57 | } 58 | } 59 | _ => { /* Do nothing */ } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /eval/tests/integration/custom_cmp.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how to use custom comparison functions. 2 | 3 | use arithmetic_eval::{ 4 | arith::{ArithmeticExt, StdArithmetic}, 5 | env::{Assertions, Prelude}, 6 | Environment, ExecutableModule, 7 | }; 8 | use arithmetic_parser::grammars::{NumGrammar, Parse, Untyped}; 9 | use num_complex::Complex64; 10 | 11 | type ComplexGrammar = NumGrammar; 12 | 13 | fn compile_module(program: &str) -> ExecutableModule { 14 | let block = Untyped::::parse_statements(program).unwrap(); 15 | ExecutableModule::new("custom_cmp", &block).unwrap() 16 | } 17 | 18 | #[test] 19 | fn no_comparisons() { 20 | const PROGRAM: &str = " 21 | // Without comparisons, all comparison ops will return `false`. 22 | assert(!(1 < -1 || 1 <= -1 || 1 > -1 || 1 >= -1)); 23 | assert(!(-1 + 2i < 1 + i)); 24 | "; 25 | let module = compile_module(PROGRAM); 26 | 27 | let mut env = Environment::with_arithmetic(StdArithmetic.without_comparisons()); 28 | env.extend(Prelude::iter().chain(Assertions::iter())); 29 | module.with_env(&env).unwrap().run().unwrap(); 30 | } 31 | 32 | #[test] 33 | fn custom_cmp_function() { 34 | //! Defines comparisons by the real part of the number. 35 | 36 | const PROGRAM: &str = " 37 | // The defined arithmetic compares numbers by their real part. 38 | assert(1 > -1); 39 | assert(-1 + 2i < 1 + i); 40 | 41 | // This function will capture the original comparison function. 42 | is_positive = |x| x > 0; 43 | assert(is_positive(1)); 44 | assert(!is_positive(-1)); 45 | assert(!is_positive(0)); 46 | "; 47 | let module = compile_module(PROGRAM); 48 | 49 | let arithmetic = 50 | StdArithmetic.with_comparison(|x: &Complex64, y: &Complex64| x.re.partial_cmp(&y.re)); 51 | let mut env = Environment::with_arithmetic(arithmetic); 52 | env.extend(Prelude::iter().chain(Assertions::iter())); 53 | module.with_env(&env).unwrap().run().unwrap(); 54 | } 55 | 56 | #[test] 57 | fn partial_cmp_function() { 58 | //! Defines comparisons on real numbers, leaving numbers with imaginary parts non-comparable. 59 | 60 | const PROGRAM: &str = " 61 | // Real numbers can be compared. 62 | assert(-1 < 1 && 2 > 1); 63 | // Numbers with an imaginary part are not comparable. 64 | assert(!(-1 < i || -1 <= i || -1 > i || -1 >= i)); 65 | assert(!(2i > 3 || 2i <= 3)); 66 | "; 67 | let module = compile_module(PROGRAM); 68 | 69 | let arithmetic = StdArithmetic.with_comparison(|x: &Complex64, y: &Complex64| { 70 | if x.im == 0.0 && y.im == 0.0 { 71 | x.re.partial_cmp(&y.re) 72 | } else { 73 | None 74 | } 75 | }); 76 | let mut env = Environment::with_arithmetic(arithmetic); 77 | env.extend(Prelude::iter().chain(Assertions::iter())); 78 | module.with_env(&env).unwrap().run().unwrap(); 79 | } 80 | -------------------------------------------------------------------------------- /eval/tests/integration/hof.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how to define high-order native functions. 2 | 3 | use arithmetic_eval::{ 4 | fns, CallContext, Environment, ErrorKind, EvalResult, ExecutableModule, Function, NativeFn, 5 | SpannedValue, Value, 6 | }; 7 | use arithmetic_parser::grammars::{F32Grammar, Parse, Untyped}; 8 | 9 | /// Function that applies the `inner` function the specified amount of times to the result of 10 | /// the previous execution. 11 | #[derive(Debug, Clone)] 12 | struct Repeated { 13 | inner: Function, 14 | times: usize, 15 | } 16 | 17 | impl NativeFn for Repeated { 18 | fn evaluate<'a>( 19 | &self, 20 | mut args: Vec>, 21 | context: &mut CallContext<'_, T>, 22 | ) -> EvalResult { 23 | if args.len() != 1 { 24 | let err = ErrorKind::native("Should be called with single argument"); 25 | return Err(context.call_site_error(err)); 26 | } 27 | let mut arg = args.pop().unwrap(); 28 | for _ in 0..self.times { 29 | let result = self.inner.evaluate(vec![arg], context)?; 30 | arg = context.apply_call_location(result); 31 | } 32 | Ok(arg.extra) 33 | } 34 | } 35 | 36 | fn repeat(function: Function, times: f32) -> Result, String> { 37 | if times <= 0.0 { 38 | Err("`times` should be positive".to_owned()) 39 | } else { 40 | let function = Repeated { 41 | inner: function, 42 | times: times as usize, 43 | }; 44 | Ok(Function::native(function)) 45 | } 46 | } 47 | 48 | fn eager_repeat( 49 | context: &mut CallContext<'_, f32>, 50 | function: Function, 51 | times: f32, 52 | mut arg: Value, 53 | ) -> EvalResult { 54 | if times <= 0.0 { 55 | Err(context.call_site_error(ErrorKind::native("`times` should be positive"))) 56 | } else { 57 | for _ in 0..times as usize { 58 | arg = function.evaluate(vec![context.apply_call_location(arg)], context)?; 59 | } 60 | Ok(arg) 61 | } 62 | } 63 | 64 | #[test] 65 | fn repeated_function() -> anyhow::Result<()> { 66 | let program = " 67 | fn = |x| 2 * x + 1; 68 | repeated = repeat(fn, 3); 69 | // 2 * 1 + 1 = 3 -> 2 * 3 + 1 = 7 -> 2 * 7 + 1 = 15 70 | assert_eq(repeated(1), 15); 71 | // -1 is the immovable point of the transform 72 | assert_eq(repeated(-1), -1); 73 | "; 74 | let program = Untyped::::parse_statements(program)?; 75 | let program = ExecutableModule::new("repeat", &program)?; 76 | 77 | let mut env = Environment::new(); 78 | env.insert_wrapped_fn("repeat", repeat) 79 | .insert_native_fn("assert_eq", fns::AssertEq); 80 | program.with_env(&env)?.run()?; 81 | Ok(()) 82 | } 83 | 84 | #[test] 85 | fn eager_repeated_function() -> anyhow::Result<()> { 86 | let program = " 87 | fn = |x| 2 * x + 1; 88 | // 2 * 1 + 1 = 3 -> 2 * 3 + 1 = 7 -> 2 * 7 + 1 = 15 89 | assert_eq(repeat(fn, 3, 1), 15); 90 | // -1 is the immovable point of the transform 91 | assert_eq(repeat(fn, 3, -1), -1); 92 | "; 93 | let program = Untyped::::parse_statements(program)?; 94 | let program = ExecutableModule::new("repeat", &program)?; 95 | 96 | let mut env = Environment::new(); 97 | env.insert_wrapped_fn("repeat", eager_repeat) 98 | .insert_native_fn("assert_eq", fns::AssertEq); 99 | program.with_env(&env)?.run()?; 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /eval/tests/version_match.rs: -------------------------------------------------------------------------------- 1 | use version_sync::{assert_html_root_url_updated, assert_markdown_deps_updated}; 2 | 3 | #[test] 4 | fn readme_is_in_sync() { 5 | assert_markdown_deps_updated!("README.md"); 6 | } 7 | 8 | #[test] 9 | fn html_root_url_is_in_sync() { 10 | assert_html_root_url_updated!("src/lib.rs"); 11 | } 12 | -------------------------------------------------------------------------------- /parser/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project (the `arithmetic-parser` crate) will be 4 | documented in this file. The project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | 8 | ## 0.4.0-beta.1 - 2024-09-22 9 | 10 | ### Added 11 | 12 | - Make crate no-std-compatible and check compatibility in CI. (#101) 13 | 14 | - Enable parsing block expressions in the name position for field access / method calls; 15 | e.g., `xs.{Array.len}` or `xs.{Array.map}(|x| x > 0)`. (#117) 16 | 17 | ### Changed 18 | 19 | - Update `nom` parser dependency to v7 and `nom_locate` to v4. (#99, #100) 20 | 21 | - Bump minimum supported Rust version to 1.70 and switch to 2021 Rust edition. (#107, #108, #112) 22 | 23 | - Remove `MaybeSpanned` type in favor of `Location`. (#121) 24 | 25 | - Change `Grammar` and `Parse` traits to use a generic associated `Type` instead of trait lifetimes. (#123) 26 | 27 | ### Removed 28 | 29 | - Remove `StripCode` and `StripResultExt` traits as obsolete. (#121) 30 | 31 | - Remove lifetime generic from `Error`. (#123) 32 | 33 | ## 0.3.0 - 2021-05-24 34 | 35 | ### Added 36 | 37 | - Parse type annotations for varargs and tuples with a middle, such as 38 | `|...xs: T, y| { /* ... */ }` or `(head, ...tail: T)`. (#72) 39 | 40 | - Make `with_span` parsing combinator public. (#77) 41 | 42 | - Add `Expr::TypeCast` for cast expressions, such as `x as Bool`. (#83) 43 | 44 | - Add `Expr::FieldAccess` for field access expressions, such as 45 | `point.x` or `xs.0`. (#84) 46 | 47 | - Add `Expr::Object` for creating objects, i.e. aggregate data structures 48 | with heterogeneous named fields (known in Rust as structs). The `Object` 49 | syntax is similar to struct initialization in Rust or object creation 50 | in JS / TS. (#85, #87) 51 | 52 | - Add object destructuring via `Lvalue::Object`. (#86) 53 | 54 | - Allow mocking type parsing if the list of type annotations is statically known. (#88) 55 | 56 | ### Changed 57 | 58 | - Make `Grammar` and `Parse` traits parametric on the lifetime of input. 59 | This allows to have type annotations dependent on this lifetime as well. (#77) 60 | 61 | - Re-license the crate to MIT or Apache-2.0. (#87) 62 | 63 | ## 0.2.0 - 2020-12-05 64 | 65 | *(All changes are relative compared to [the 0.2.0-beta.1 release](#020-beta1---2020-10-04))* 66 | 67 | ### Added 68 | 69 | - Add infrastructure for stripping code fragments (such as `Code` enum and 70 | `StripCode` trait). This allows breaking lifetime dependency between code 71 | and the outcome of its parsing. (#26) 72 | 73 | - Make `GrammarExt` methods more generic: they now accept inputs convertible 74 | to an `InputSpan`, such as `&str`. (#32) 75 | 76 | - Allow switching Boolean expressions off. (#36) 77 | 78 | - Add parsers for integer numbers. (#40) 79 | 80 | - Add parsers for big integers from the `num-bigint` crate. (#46) 81 | 82 | ### Changed 83 | 84 | - Use homegrown `LocatedSpan` instead of one from `nom_locate` crate. 85 | See the type docs for reasoning. (#26) 86 | 87 | - Make most enums and structs with public fields non-exhaustive (e.g., `Expr`, 88 | `Statement`, `Lvalue`). (#26) 89 | 90 | - Rework errors (#32): 91 | 92 | - Rename error types: `Error` to `ErrorKind`, `SpannedError` to `Error`. 93 | - Use `Error` as the main error type instead of a `Spanned<_>` wrapper. 94 | - Implement `std::error::Error` for `Error`. 95 | 96 | - Use `//` and `/* .. */` comments instead of `#` ones. (#36) 97 | 98 | - Use the `OpPriority` enum to encode op priorities instead of integers. (#36) 99 | 100 | - Split `Grammar` into several traits (#38): 101 | 102 | - `ParseLiteral` responsible for parsing literals 103 | - `Grammar: ParseLiteral` for a complete set of parsers (literals + type annotations) 104 | - `Parse` (renamed from `GrammarExt`) to contain parsing features and parse `Block`s 105 | - Add helper wrappers `Typed` and `Untyped` to assist in composing parsing functionality. 106 | 107 | - Export `ParseLiteral`, `Grammar` and `Parse` from the `grammars` module. (#38) 108 | 109 | - Update dependencies. (#39) 110 | 111 | - Use the `bitflags` crate for parser `Features`. (#50) 112 | 113 | ### Fixed 114 | 115 | - Fix parsing of expressions like `1.abs()` for standard grammars. Previously, 116 | the parser consumed the `.` char as a part of the number literal, which led 117 | to a parser error. (#33) 118 | 119 | - Fix relative priority of unary ops and method calls, so that `-1.abs()` 120 | is correctly parsed as `-(1.abs())`, not as `(-1).abs()`. (#33) 121 | 122 | - Disallow using literals as function names. Thus, an expression like `1(2, x)` 123 | is no longer valid. (#33) 124 | 125 | - Disallow chained comparisons, such as `x < y < z`. (#36) 126 | 127 | - Make `&&` have higher priority than `||`, as in Rust. (#36) 128 | 129 | ## 0.2.0-beta.1 - 2020-10-04 130 | 131 | ### Added 132 | 133 | - Implement an optional grammar feature: order comparisons, that is, 134 | `>`, `<`, `>=` and `<=` binary operations. (#23) 135 | 136 | ### Changed 137 | 138 | - Update dependencies re-exported through the public interfaces, such as 139 | `nom_locate`. (#22) 140 | 141 | ## 0.1.0 - 2020-05-31 142 | 143 | The initial release of `arithmetic-parser`. 144 | -------------------------------------------------------------------------------- /parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arithmetic-parser" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "README.md" 10 | description = "Parser for arithmetic expressions with flexible literals and type annotations." 11 | categories = ["parser-implementations", "mathematics", "no-std"] 12 | keywords = ["parser", "arithmetic", "scripting", "language"] 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | 17 | [dependencies] 18 | # Public dependencies (present in the public API). 19 | anyhow.workspace = true 20 | bitflags.workspace = true 21 | nom.workspace = true 22 | nom_locate.workspace = true 23 | num-bigint = { workspace = true, optional = true } 24 | num-complex = { workspace = true, optional = true } 25 | num-traits.workspace = true 26 | 27 | [dev-dependencies] 28 | assert_matches.workspace = true 29 | hex.workspace = true 30 | pulldown-cmark.workspace = true 31 | version-sync.workspace = true 32 | 33 | [features] 34 | default = ["std"] 35 | # Enables support of types from `std`, such as the `Error` trait. 36 | std = ["anyhow/std"] 37 | 38 | [[example]] 39 | name = "complex_c" 40 | path = "examples/complex_c.rs" 41 | required-features = ["num-complex"] 42 | -------------------------------------------------------------------------------- /parser/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /parser/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /parser/README.md: -------------------------------------------------------------------------------- 1 | # Flexible Arithmetic Parser 2 | 3 | [![Build Status](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml) 4 | [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/arithmetic-parser#license) 5 | ![rust 1.70+ required](https://img.shields.io/badge/rust-1.70+-blue.svg) 6 | ![no_std supported](https://img.shields.io/badge/no__std-tested-green.svg) 7 | 8 | **Links:** [![Docs.rs](https://img.shields.io/docsrs/arithmetic-parser)](https://docs.rs/arithmetic-parser/) 9 | [![crate docs (master)](https://img.shields.io/badge/master-yellow.svg?label=docs)](https://slowli.github.io/arithmetic-parser/arithmetic_parser/) 10 | 11 | A versatile parser for arithmetic expressions which allows customizing literal definitions, 12 | type annotations and several other aspects of parsing. 13 | 14 | ## Usage 15 | 16 | Add this to your `Crate.toml`: 17 | 18 | ```toml 19 | [dependencies] 20 | arithmetic-parser = "0.4.0-beta.1" 21 | ``` 22 | 23 | The parser is overall similar to Rust. It supports variables, literals, comments, 24 | arithmetic and boolean operations, parentheses, function calls, tuples and tuple destructuring, 25 | function definitions, blocks, methods, and type annotations. 26 | In other words, the parser forms a foundation of a minimalistic scripting language, 27 | while leaving certain aspects up to user (most of all, specification of literals). 28 | 29 | See the crate docs for more details on the supported syntax features. 30 | 31 | ### Code sample 32 | 33 | Here is an example of code parsed with the grammar with real-valued literals 34 | and the only supported type `Num`: 35 | 36 | ```text 37 | // This is a comment. 38 | x = 1 + 2.5 * 3 + sin(a^3 / b^2); 39 | 40 | // Function declarations have syntax similar to Rust closures. 41 | some_function = |x| { 42 | r = min(rand(), 0.5); 43 | r * x 44 | }; 45 | 46 | // Objects are similar to JavaScript, except they require 47 | // a preceding hash `#`, like in Rhai (https://rhai.rs/). 48 | other_function = |a, b: Num| #{ sum: a + b, diff: a - b }; 49 | 50 | // Object destructuring is supported as well. 51 | { sum, diff: Num } = other_function( 52 | x, 53 | // Blocks are supported and have a similar syntax to Rust. 54 | some_function({ x = x - 0.5; x }), 55 | ); 56 | 57 | // Tuples have syntax similar to Rust (besides spread syntax 58 | // in destructuring, which is similar to one in JavaScript). 59 | (x, ...tail) = (1, 2).map(some_function); 60 | ``` 61 | 62 | ## Implementation details 63 | 64 | The parser is based on the [`nom`](https://docs.rs/nom/) crate. The core trait of the library, 65 | `Grammar`, is designed in such a way that switching optional features 66 | should not induce run-time overhead; the unused parsing code paths should be removed during 67 | compilation. 68 | 69 | ## See also 70 | 71 | - [`arithmetic-eval`] is a simple interpreter that could be used on parsed ASTs. 72 | - [`arithmetic-typing`] is a type checker / inference tool for parsed ASTs. 73 | 74 | ## License 75 | 76 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) 77 | or [MIT license](LICENSE-MIT) at your option. 78 | 79 | Unless you explicitly state otherwise, any contribution intentionally submitted 80 | for inclusion in `arithmetic-parser` by you, as defined in the Apache-2.0 license, 81 | shall be dual licensed as above, without any additional terms or conditions. 82 | 83 | [`arithmetic-eval`]: https://crates.io/crates/arithmetic-eval 84 | [`arithmetic-typing`]: https://crates.io/crates/arithmetic-typing 85 | -------------------------------------------------------------------------------- /parser/src/ast/lvalue.rs: -------------------------------------------------------------------------------- 1 | //! Lvalues for arithmetic expressions. 2 | 3 | use core::fmt; 4 | 5 | use crate::{alloc::Vec, spans::Spanned}; 6 | 7 | /// Length of an assigned lvalue. 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 9 | #[non_exhaustive] 10 | pub enum LvalueLen { 11 | /// Exact length. 12 | Exact(usize), 13 | /// Minimum length. 14 | AtLeast(usize), 15 | } 16 | 17 | impl LvalueLen { 18 | /// Checks if this length matches the provided length of the rvalue. 19 | pub fn matches(self, value: usize) -> bool { 20 | match self { 21 | Self::Exact(len) => value == len, 22 | Self::AtLeast(len) => value >= len, 23 | } 24 | } 25 | } 26 | 27 | impl fmt::Display for LvalueLen { 28 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | match self { 30 | Self::Exact(len) => write!(formatter, "{len}"), 31 | Self::AtLeast(len) => write!(formatter, "at least {len}"), 32 | } 33 | } 34 | } 35 | 36 | impl From for LvalueLen { 37 | fn from(value: usize) -> Self { 38 | Self::Exact(value) 39 | } 40 | } 41 | 42 | /// Tuple destructuring, such as `(a, b, ..., c)`. 43 | #[derive(Debug, Clone, PartialEq)] 44 | pub struct Destructure<'a, T> { 45 | /// Start part of the destructuring, e.g, `a` and `b` in `(a, b, ..., c)`. 46 | pub start: Vec>, 47 | /// Middle part of the destructuring, e.g., `rest` in `(a, b, ...rest, _)`. 48 | pub middle: Option>>, 49 | /// End part of the destructuring, e.g., `c` in `(a, b, ..., c)`. 50 | pub end: Vec>, 51 | } 52 | 53 | impl Destructure<'_, T> { 54 | /// Returns the length of destructured elements. 55 | pub fn len(&self) -> LvalueLen { 56 | if self.middle.is_some() { 57 | LvalueLen::AtLeast(self.start.len() + self.end.len()) 58 | } else { 59 | LvalueLen::Exact(self.start.len()) 60 | } 61 | } 62 | 63 | /// Checks if the destructuring is empty. 64 | pub fn is_empty(&self) -> bool { 65 | self.start.is_empty() 66 | } 67 | } 68 | 69 | /// Rest syntax, such as `...rest` in `(a, ...rest, b)`. 70 | #[derive(Debug, Clone, PartialEq)] 71 | pub enum DestructureRest<'a, T> { 72 | /// Unnamed rest syntax, i.e., `...`. 73 | Unnamed, 74 | /// Named rest syntax, e.g., `...rest`. 75 | Named { 76 | /// Variable span, e.g., `rest`. 77 | variable: Spanned<'a>, 78 | /// Type annotation of the value. 79 | ty: Option>, 80 | }, 81 | } 82 | 83 | impl<'a, T> DestructureRest<'a, T> { 84 | /// Tries to convert this rest declaration into an lvalue. Return `None` if the rest declaration 85 | /// is unnamed. 86 | pub fn to_lvalue(&self) -> Option> { 87 | match self { 88 | Self::Named { variable, .. } => { 89 | Some(variable.copy_with_extra(Lvalue::Variable { ty: None })) 90 | } 91 | Self::Unnamed => None, 92 | } 93 | } 94 | } 95 | 96 | /// Object destructuring, such as `{ x, y: new_y }`. 97 | #[derive(Debug, Clone, PartialEq)] 98 | #[non_exhaustive] 99 | pub struct ObjectDestructure<'a, T> { 100 | /// Fields mentioned in the destructuring. 101 | pub fields: Vec>, 102 | } 103 | 104 | /// Single field in [`ObjectDestructure`], such as `x` and `y: new_y` in `{ x, y: new_y }`. 105 | /// 106 | /// In addition to the "ordinary" `field: lvalue` syntax for a field with binding, 107 | /// an alternative one is supported: `field -> lvalue`. This makes the case 108 | /// of a field with type annotation easier to recognize (for humans); `field -> lvalue: Type` is 109 | /// arguably more readable than `field: lvalue: Type` (although the latter is still valid syntax). 110 | #[derive(Debug, Clone, PartialEq)] 111 | pub struct ObjectDestructureField<'a, T> { 112 | /// Field name, such as `xs` in `xs: (x, ...tail)`. 113 | pub field_name: Spanned<'a>, 114 | /// Binding for the field, such as `(x, ...tail)` in `xs: (x, ...tail)`. 115 | pub binding: Option>, 116 | } 117 | 118 | /// Assignable value. 119 | #[derive(Debug, Clone, PartialEq)] 120 | #[non_exhaustive] 121 | pub enum Lvalue<'a, T> { 122 | /// Simple variable, e.g., `x`. 123 | Variable { 124 | /// Type annotation of the value. 125 | ty: Option>, 126 | }, 127 | /// Tuple destructuring, e.g., `(x, y)`. 128 | Tuple(Destructure<'a, T>), 129 | /// Object destructuring, e.g., `{ x, y }`. 130 | Object(ObjectDestructure<'a, T>), 131 | } 132 | 133 | impl Lvalue<'_, T> { 134 | /// Returns type of this lvalue. 135 | pub fn ty(&self) -> LvalueType { 136 | match self { 137 | Self::Variable { .. } => LvalueType::Variable, 138 | Self::Tuple(_) => LvalueType::Tuple, 139 | Self::Object(_) => LvalueType::Object, 140 | } 141 | } 142 | } 143 | 144 | /// [`Lvalue`] with the associated code span. 145 | pub type SpannedLvalue<'a, T> = Spanned<'a, Lvalue<'a, T>>; 146 | 147 | /// Type of an [`Lvalue`]. 148 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 149 | #[non_exhaustive] 150 | pub enum LvalueType { 151 | /// Simple variable, e.g., `x`. 152 | Variable, 153 | /// Tuple destructuring, e.g., `(x, y)`. 154 | Tuple, 155 | /// Object destructuring, e.g., `{ x, y }`. 156 | Object, 157 | } 158 | 159 | impl fmt::Display for LvalueType { 160 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 161 | formatter.write_str(match self { 162 | Self::Variable => "simple variable", 163 | Self::Tuple => "tuple destructuring", 164 | Self::Object => "object destructuring", 165 | }) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /parser/src/parser/helpers.rs: -------------------------------------------------------------------------------- 1 | //! Passing helpers. 2 | 3 | use nom::{ 4 | branch::alt, 5 | bytes::{ 6 | complete::{tag, take_until, take_while, take_while1, take_while_m_n}, 7 | streaming, 8 | }, 9 | character::complete::char as tag_char, 10 | combinator::{cut, not, peek, recognize}, 11 | error::context, 12 | multi::many0, 13 | sequence::{delimited, preceded}, 14 | }; 15 | 16 | use crate::{grammars::Features, BinaryOp, Context, InputSpan, NomResult, Spanned, UnaryOp}; 17 | 18 | pub(super) trait GrammarType { 19 | const COMPLETE: bool; 20 | } 21 | 22 | #[derive(Debug)] 23 | pub(super) struct Complete(()); 24 | 25 | impl GrammarType for Complete { 26 | const COMPLETE: bool = true; 27 | } 28 | 29 | #[derive(Debug)] 30 | pub(super) struct Streaming(()); 31 | 32 | impl GrammarType for Streaming { 33 | const COMPLETE: bool = false; 34 | } 35 | 36 | impl UnaryOp { 37 | pub(super) fn from_span(span: Spanned<'_, char>) -> Spanned<'_, Self> { 38 | match span.extra { 39 | '-' => span.copy_with_extra(UnaryOp::Neg), 40 | '!' => span.copy_with_extra(UnaryOp::Not), 41 | _ => unreachable!(), 42 | } 43 | } 44 | 45 | pub(super) fn try_from_byte(byte: u8) -> Option { 46 | match byte { 47 | b'-' => Some(Self::Neg), 48 | b'!' => Some(Self::Not), 49 | _ => None, 50 | } 51 | } 52 | } 53 | 54 | impl BinaryOp { 55 | pub(super) fn from_span(span: InputSpan<'_>) -> Spanned<'_, Self> { 56 | Spanned::new( 57 | span, 58 | match *span.fragment() { 59 | "+" => Self::Add, 60 | "-" => Self::Sub, 61 | "*" => Self::Mul, 62 | "/" => Self::Div, 63 | "^" => Self::Power, 64 | "==" => Self::Eq, 65 | "!=" => Self::NotEq, 66 | "&&" => Self::And, 67 | "||" => Self::Or, 68 | ">" => Self::Gt, 69 | "<" => Self::Lt, 70 | ">=" => Self::Ge, 71 | "<=" => Self::Le, 72 | _ => unreachable!(), 73 | }, 74 | ) 75 | } 76 | 77 | pub(super) fn is_supported(self, features: Features) -> bool { 78 | match self { 79 | Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Power => true, 80 | Self::Eq | Self::NotEq | Self::And | Self::Or => { 81 | features.contains(Features::BOOLEAN_OPS_BASIC) 82 | } 83 | Self::Gt | Self::Lt | Self::Ge | Self::Le => features.contains(Features::BOOLEAN_OPS), 84 | } 85 | } 86 | } 87 | 88 | /// Whitespace and comments. 89 | pub(super) fn ws(input: InputSpan<'_>) -> NomResult<'_, InputSpan<'_>> { 90 | fn narrow_ws(input: InputSpan<'_>) -> NomResult<'_, InputSpan<'_>> { 91 | if T::COMPLETE { 92 | take_while1(|c: char| c.is_ascii_whitespace())(input) 93 | } else { 94 | streaming::take_while1(|c: char| c.is_ascii_whitespace())(input) 95 | } 96 | } 97 | 98 | fn long_comment_body(input: InputSpan<'_>) -> NomResult<'_, InputSpan<'_>> { 99 | if T::COMPLETE { 100 | context(Context::Comment.to_str(), cut(take_until("*/")))(input) 101 | } else { 102 | streaming::take_until("*/")(input) 103 | } 104 | } 105 | 106 | let comment = preceded(tag("//"), take_while(|c: char| c != '\n')); 107 | let long_comment = delimited(tag("/*"), long_comment_body::, tag("*/")); 108 | let ws_line = alt((narrow_ws::, comment, long_comment)); 109 | recognize(many0(ws_line))(input) 110 | } 111 | 112 | pub(super) fn mandatory_ws(input: InputSpan<'_>) -> NomResult<'_, InputSpan<'_>> { 113 | let not_ident_char = peek(not(take_while_m_n(1, 1, |c: char| { 114 | c.is_ascii_alphanumeric() || c == '_' 115 | }))); 116 | preceded(not_ident_char, ws::)(input) 117 | } 118 | 119 | /// Variable name, like `a_foo` or `Bar`. 120 | pub(super) fn var_name(input: InputSpan<'_>) -> NomResult<'_, InputSpan<'_>> { 121 | context( 122 | Context::Var.to_str(), 123 | preceded( 124 | peek(take_while_m_n(1, 1, |c: char| { 125 | c.is_ascii_alphabetic() || c == '_' 126 | })), 127 | take_while1(|c: char| c.is_ascii_alphanumeric() || c == '_'), 128 | ), 129 | )(input) 130 | } 131 | 132 | /// Checks if the provided string is a valid variable name. 133 | pub fn is_valid_variable_name(name: &str) -> bool { 134 | if name.is_empty() || !name.is_ascii() { 135 | return false; 136 | } 137 | 138 | match var_name(InputSpan::new(name)) { 139 | Ok((rest, _)) => rest.fragment().is_empty(), 140 | Err(_) => false, 141 | } 142 | } 143 | 144 | pub(super) fn comma_sep(input: InputSpan<'_>) -> NomResult<'_, char> { 145 | delimited(ws::, tag_char(','), ws::)(input) 146 | } 147 | -------------------------------------------------------------------------------- /parser/src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | //! Parsers implemented with the help of `nom`. 2 | 3 | use nom::{ 4 | branch::alt, 5 | bytes::complete::tag, 6 | character::complete::char as tag_char, 7 | combinator::{cut, map, not, opt, peek}, 8 | multi::many0, 9 | sequence::{delimited, preceded, terminated, tuple}, 10 | Err as NomErr, 11 | }; 12 | 13 | pub use self::helpers::is_valid_variable_name; 14 | use self::{ 15 | expr::expr, 16 | helpers::{ws, Complete, GrammarType, Streaming}, 17 | lvalue::{destructure, lvalue}, 18 | }; 19 | use crate::{ 20 | alloc::{vec, Box}, 21 | grammars::Parse, 22 | spans::with_span, 23 | Block, Error, ErrorKind, FnDefinition, InputSpan, NomResult, SpannedStatement, Statement, 24 | }; 25 | 26 | mod expr; 27 | mod helpers; 28 | mod lvalue; 29 | #[cfg(test)] 30 | mod tests; 31 | 32 | #[allow(clippy::option_if_let_else)] 33 | fn statement(input: InputSpan<'_>) -> NomResult<'_, SpannedStatement<'_, T::Base>> 34 | where 35 | T: Parse, 36 | Ty: GrammarType, 37 | { 38 | let assignment = tuple((tag("="), peek(not(tag_char('='))))); 39 | let assignment_parser = tuple(( 40 | opt(terminated( 41 | lvalue::, 42 | delimited(ws::, assignment, ws::), 43 | )), 44 | expr::, 45 | )); 46 | 47 | with_span(map(assignment_parser, |(lvalue, rvalue)| { 48 | // Clippy lint is triggered here. `rvalue` cannot be moved into both branches, so it's a false positive. 49 | if let Some(lvalue) = lvalue { 50 | Statement::Assignment { 51 | lhs: lvalue, 52 | rhs: Box::new(rvalue), 53 | } 54 | } else { 55 | Statement::Expr(rvalue) 56 | } 57 | }))(input) 58 | } 59 | 60 | /// Parses a complete list of statements. 61 | pub(crate) fn statements(input_span: InputSpan<'_>) -> Result, Error> 62 | where 63 | T: Parse, 64 | { 65 | if !input_span.fragment().is_ascii() { 66 | return Err(Error::new(input_span, ErrorKind::NonAsciiInput)); 67 | } 68 | statements_inner::(input_span) 69 | } 70 | 71 | /// Parses a potentially incomplete list of statements. 72 | pub(crate) fn streaming_statements( 73 | input_span: InputSpan<'_>, 74 | ) -> Result, Error> 75 | where 76 | T: Parse, 77 | { 78 | if !input_span.fragment().is_ascii() { 79 | return Err(Error::new(input_span, ErrorKind::NonAsciiInput)); 80 | } 81 | 82 | statements_inner::(input_span) 83 | .or_else(|_| statements_inner::(input_span)) 84 | } 85 | 86 | fn statements_inner(input_span: InputSpan<'_>) -> Result, Error> 87 | where 88 | T: Parse, 89 | Ty: GrammarType, 90 | { 91 | delimited(ws::, separated_statements::, ws::)(input_span) 92 | .map_err(|e| match e { 93 | NomErr::Failure(e) | NomErr::Error(e) => e, 94 | NomErr::Incomplete(_) => ErrorKind::Incomplete.with_span(&input_span.into()), 95 | }) 96 | .and_then(|(remaining, statements)| { 97 | if remaining.fragment().is_empty() { 98 | Ok(statements) 99 | } else { 100 | Err(ErrorKind::Leftovers.with_span(&remaining.into())) 101 | } 102 | }) 103 | } 104 | 105 | fn separated_statement(input: InputSpan<'_>) -> NomResult<'_, SpannedStatement<'_, T::Base>> 106 | where 107 | T: Parse, 108 | Ty: GrammarType, 109 | { 110 | terminated(statement::, preceded(ws::, tag_char(';')))(input) 111 | } 112 | 113 | /// List of statements separated by semicolons. 114 | fn separated_statements(input: InputSpan<'_>) -> NomResult<'_, Block<'_, T::Base>> 115 | where 116 | T: Parse, 117 | Ty: GrammarType, 118 | { 119 | map( 120 | tuple(( 121 | many0(terminated(separated_statement::, ws::)), 122 | opt(expr::), 123 | )), 124 | |(statements, return_value)| Block { 125 | statements, 126 | return_value: return_value.map(Box::new), 127 | }, 128 | )(input) 129 | } 130 | 131 | /// Block of statements, e.g., `{ x = 3; x + y }`. 132 | fn block(input: InputSpan<'_>) -> NomResult<'_, Block<'_, T::Base>> 133 | where 134 | T: Parse, 135 | Ty: GrammarType, 136 | { 137 | preceded( 138 | terminated(tag_char('{'), ws::), 139 | cut(terminated( 140 | separated_statements::, 141 | preceded(ws::, tag_char('}')), 142 | )), 143 | )(input) 144 | } 145 | 146 | /// Function definition, e.g., `|x, y: Sc| { x + y }`. 147 | fn fn_def(input: InputSpan<'_>) -> NomResult<'_, FnDefinition<'_, T::Base>> 148 | where 149 | T: Parse, 150 | Ty: GrammarType, 151 | { 152 | let body_parser = alt(( 153 | block::, 154 | map(expr::, |spanned| Block { 155 | statements: vec![], 156 | return_value: Some(Box::new(spanned)), 157 | }), 158 | )); 159 | 160 | let args_parser = preceded( 161 | terminated(tag_char('|'), ws::), 162 | cut(terminated( 163 | destructure::, 164 | preceded(ws::, tag_char('|')), 165 | )), 166 | ); 167 | 168 | let parser = tuple((with_span(args_parser), cut(preceded(ws::, body_parser)))); 169 | map(parser, |(args, body)| FnDefinition { args, body })(input) 170 | } 171 | -------------------------------------------------------------------------------- /parser/tests/check_readme.rs: -------------------------------------------------------------------------------- 1 | //! Tests that the README code sample actually parses. 2 | 3 | use std::fs; 4 | 5 | use arithmetic_parser::grammars::{F64Grammar, MockTypes, Parse, WithMockedTypes}; 6 | use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd}; 7 | 8 | struct MockedTypesList; 9 | 10 | impl MockTypes for MockedTypesList { 11 | const MOCKED_TYPES: &'static [&'static str] = &["Num"]; 12 | } 13 | 14 | type Grammar = WithMockedTypes; 15 | 16 | fn read_file(path: &str) -> String { 17 | fs::read_to_string(path).unwrap_or_else(|err| panic!("Cannot read file {path}: {err}")) 18 | } 19 | 20 | fn check_sample(code_sample: &str) { 21 | Grammar::parse_statements(code_sample).unwrap(); 22 | } 23 | 24 | #[test] 25 | fn code_sample_in_readme_is_parsed() { 26 | let readme = read_file("README.md"); 27 | 28 | let parser = Parser::new(&readme); 29 | let mut code: Option = None; 30 | for event in parser { 31 | match event { 32 | Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) 33 | if lang.as_ref() == "text" => 34 | { 35 | assert!(code.is_none(), "Embedded code samples"); 36 | code = Some(String::with_capacity(1_024)); 37 | } 38 | Event::End(TagEnd::CodeBlock) => { 39 | if let Some(code_sample) = code.take() { 40 | assert!(!code_sample.is_empty()); 41 | check_sample(&code_sample); 42 | } 43 | } 44 | Event::Text(text) => { 45 | if let Some(code) = &mut code { 46 | code.push_str(text.as_ref()); 47 | } 48 | } 49 | _ => { /* Do nothing */ } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /parser/tests/version_match.rs: -------------------------------------------------------------------------------- 1 | use version_sync::{assert_html_root_url_updated, assert_markdown_deps_updated}; 2 | 3 | #[test] 4 | fn readme_is_in_sync() { 5 | assert_markdown_deps_updated!("README.md"); 6 | } 7 | 8 | #[test] 9 | fn html_root_url_is_in_sync() { 10 | assert_html_root_url_updated!("src/lib.rs"); 11 | } 12 | -------------------------------------------------------------------------------- /typing/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project (the `arithmetic-typing` crate) will be 4 | documented in this file. The project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | 8 | ### Changed 9 | 10 | - Update `hashbrown` dependency to 0.15. 11 | 12 | ## 0.4.0-beta.1 - 2024-09-22 13 | 14 | ### Added 15 | 16 | - Support block expressions in the name position for method calls, e.g., `xs.{Array.map}(|x| x > 0)`. (#117) 17 | 18 | - Support no-std compilation mode. 19 | 20 | ### Changed 21 | 22 | - Bump minimum supported Rust version to 1.70 and switch to 2021 Rust edition. (#107, #108, #112) 23 | 24 | - Remove `Object::just()` constructor in favor of more general `From<[_; N]>` implementation. (#117) 25 | 26 | - Rename `ErrorLocation` to `ErrorPathFragment` and its getter in `Error` from `location()` to `path()` 27 | in order to distinguish it from `Location` from the parser crate. (#124) 28 | 29 | ### Removed 30 | 31 | - Remove lifetime generic from `Error` and related types. (#124) 32 | 33 | ### Fixed 34 | 35 | - Fix false positive during recursive type check for native parameterized functions. 36 | Previously, an assignment such as `reduce = fold;` (with `fold` being 37 | a native parametric function) or importing an object / tuple with functional fields 38 | led to such an error. (#100, #105) 39 | 40 | - Fix handling recursive type constraints, such as `|obj| (obj.len)(obj)`. Previously, 41 | such constraints led to stack overflow. (#105) 42 | 43 | ## 0.3.0 - 2021-05-24 44 | 45 | The initial release of the `arithmetic-typing` crate. 46 | -------------------------------------------------------------------------------- /typing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arithmetic-typing" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "README.md" 10 | description = "Hindley-Milner type inference for arithmetic expressions." 11 | categories = ["mathematics", "no-std"] 12 | keywords = ["typing", "type-system", "arithmetic", "scripting", "language"] 13 | 14 | [dependencies] 15 | arithmetic-parser.workspace = true 16 | 17 | anyhow.workspace = true 18 | nom.workspace = true 19 | num-traits.workspace = true 20 | 21 | # Optional dependencies 22 | hashbrown = { workspace = true, optional = true } 23 | 24 | [dev-dependencies] 25 | assert_matches.workspace = true 26 | hex.workspace = true 27 | pulldown-cmark.workspace = true 28 | version-sync.workspace = true 29 | 30 | [features] 31 | default = ["std"] 32 | # Enables support of types from `std`, such as the `Error` trait. 33 | std = ["anyhow/std", "arithmetic-parser/std"] 34 | -------------------------------------------------------------------------------- /typing/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /typing/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /typing/README.md: -------------------------------------------------------------------------------- 1 | # Type Inference for Arithmetic Grammars 2 | 3 | [![Build Status](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/slowli/arithmetic-parser/actions/workflows/ci.yml) 4 | [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/arithmetic-parser#license) 5 | ![rust 1.70+ required](https://img.shields.io/badge/rust-1.70+-blue.svg) 6 | ![no_std supported](https://img.shields.io/badge/no__std-tested-green.svg) 7 | 8 | **Links:** [![Docs on docs.rs](https://img.shields.io/docsrs/arithmetic-typing)](https://docs.rs/arithmetic-typing/) 9 | [![crate docs (master)](https://img.shields.io/badge/master-yellow.svg?label=docs)](https://slowli.github.io/arithmetic-parser/arithmetic_typing/) 10 | 11 | Hindley–Milner type inference for arithmetic expressions parsed 12 | by the [`arithmetic-parser`] crate. 13 | 14 | This crate allows parsing type annotations as a part of grammars, and inferring / 15 | checking types for ASTs produced by `arithmetic-parser`. 16 | Type inference is *partially* compatible with the interpreter from [`arithmetic-eval`]; 17 | if the inference algorithm succeeds on a certain expression / statement / block, 18 | it will execute successfully, but not all successfully executing items pass type inference. 19 | 20 | ## Usage 21 | 22 | Add this to your `Crate.toml`: 23 | 24 | ```toml 25 | [dependencies] 26 | arithmetic-typing = "0.4.0-beta.1" 27 | ``` 28 | 29 | ### Quick overview 30 | 31 | The type system supports all major constructions from [`arithmetic-parser`], 32 | such as tuples, objects, and functional types. Functions and arithmetic operations 33 | can place constraints on involved types, which are similar to Rust traits 34 | (except *much* more limited). There is an equivalent for dynamic typing / trait objects 35 | as well. Finally, the `any` type can be used to circumvent type system limitations. 36 | 37 | The type system is generic with respect to primitive types. This allows customizing 38 | processing of arithmetic ops and constraints, quite similar to `Arithmetic`s 39 | in the [`arithmetic-eval`] crate. 40 | 41 | For simple scripts, type inference may be successful without any annotations. 42 | In the examples below, the only annotation is added to *test* type inference, 43 | rather than to drive it: 44 | 45 | ```text 46 | minmax: ([Num; N]) -> { max: Num, min: Num } = 47 | |xs| xs.fold(#{ min: INF, max: -INF }, |acc, x| #{ 48 | min: if(x < acc.min, x, acc.min), 49 | max: if(x > acc.max, x, acc.max), 50 | }); 51 | assert_eq((3, 7, 2, 4).minmax().min, 2); 52 | assert_eq((5, -4, 6, 9, 1).minmax(), #{ min: -4, max: 9 }); 53 | ``` 54 | 55 | ```text 56 | INF_PT = #{ x: INF, y: INF }; 57 | 58 | min_point: ([{ x: Num, y: Num }; N]) -> { x: Num, y: Num } = 59 | |points| points 60 | .map(|pt| (pt, pt.x * pt.x + pt.y * pt.y)) 61 | .fold( 62 | #{ min_r: INF, pt: INF_PT }, 63 | |acc, (pt, r)| if(r < acc.min_r, #{ min_r: r, pt }, acc), 64 | ) 65 | .pt; 66 | 67 | assert_eq( 68 | array(10, |x| #{ x, y: 10 - x }).min_point(), 69 | #{ x: 5, y: 5 } 70 | ); 71 | ``` 72 | 73 | Please see the crate docs and [examples](examples) for info on type notation 74 | and more examples of usage. 75 | 76 | ## Missing or incomplete features 77 | 78 | - Sum / tagged union types 79 | - Type constraints beyond simplest ones 80 | - Specifying type vars in type annotations (beyond simplest cases) 81 | - Type aliases 82 | 83 | ## See also 84 | 85 | - [`arithmetic-eval`] is a simple interpreter that could be used on ASTs 86 | consumed by this crate. 87 | 88 | ## License 89 | 90 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) 91 | or [MIT license](LICENSE-MIT) at your option. 92 | 93 | Unless you explicitly state otherwise, any contribution intentionally submitted 94 | for inclusion in `arithmetic-typing` by you, as defined in the Apache-2.0 license, 95 | shall be dual licensed as above, without any additional terms or conditions. 96 | 97 | [`arithmetic-parser`]: https://crates.io/crates/arithmetic-parser 98 | [`arithmetic-eval`]: https://crates.io/crates/arithmetic-eval 99 | -------------------------------------------------------------------------------- /typing/examples/strings.rs: -------------------------------------------------------------------------------- 1 | //! A somewhat contrived arithmetic that parses string literals and only allows to add them 2 | //! and compare strings. 3 | 4 | use std::{fmt, str::FromStr}; 5 | 6 | use arithmetic_parser::{ 7 | grammars::{Parse, ParseLiteral}, 8 | BinaryOp, InputSpan, NomResult, 9 | }; 10 | use arithmetic_typing::{ 11 | arith::*, 12 | defs::Assertions, 13 | error::{ErrorPathFragment, OpErrors}, 14 | Annotated, PrimitiveType, Type, TypeEnvironment, 15 | }; 16 | 17 | /// Primitive type: string or boolean. 18 | #[derive(Debug, Clone, Copy, PartialEq)] 19 | enum StrType { 20 | Str, 21 | Bool, 22 | } 23 | 24 | impl fmt::Display for StrType { 25 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | formatter.write_str(match self { 27 | Self::Str => "Str", 28 | Self::Bool => "Bool", 29 | }) 30 | } 31 | } 32 | 33 | impl FromStr for StrType { 34 | type Err = anyhow::Error; 35 | 36 | fn from_str(s: &str) -> Result { 37 | match s { 38 | "Str" => Ok(Self::Str), 39 | "Bool" => Ok(Self::Bool), 40 | _ => Err(anyhow::anyhow!("Expected `Str` or `Bool`")), 41 | } 42 | } 43 | } 44 | 45 | impl PrimitiveType for StrType {} 46 | 47 | impl WithBoolean for StrType { 48 | const BOOL: Self = Self::Bool; 49 | } 50 | 51 | impl LinearType for StrType { 52 | fn is_linear(&self) -> bool { 53 | matches!(self, Self::Str) 54 | } 55 | } 56 | 57 | /// Grammar parsing strings as literals. 58 | #[derive(Debug, Clone, Copy)] 59 | struct StrGrammar; 60 | 61 | impl ParseLiteral for StrGrammar { 62 | type Lit = String; 63 | 64 | /// Parses an ASCII string like `"Hello, world!"`. 65 | fn parse_literal(input: InputSpan<'_>) -> NomResult<'_, Self::Lit> { 66 | use nom::{ 67 | branch::alt, 68 | bytes::complete::{escaped_transform, is_not}, 69 | character::complete::char as tag_char, 70 | combinator::{cut, map, opt}, 71 | sequence::{preceded, terminated}, 72 | }; 73 | 74 | let parser = escaped_transform( 75 | is_not("\\\"\n"), 76 | '\\', 77 | alt(( 78 | map(tag_char('\\'), |_| "\\"), 79 | map(tag_char('"'), |_| "\""), 80 | map(tag_char('n'), |_| "\n"), 81 | )), 82 | ); 83 | map( 84 | preceded(tag_char('"'), cut(terminated(opt(parser), tag_char('"')))), 85 | Option::unwrap_or_default, 86 | )(input) 87 | } 88 | } 89 | 90 | #[derive(Debug, Clone, Copy)] 91 | struct StrArithmetic; 92 | 93 | impl MapPrimitiveType for StrArithmetic { 94 | type Prim = StrType; 95 | 96 | fn type_of_literal(&self, _lit: &String) -> Self::Prim { 97 | StrType::Str 98 | } 99 | } 100 | 101 | impl TypeArithmetic for StrArithmetic { 102 | fn process_unary_op( 103 | &self, 104 | substitutions: &mut Substitutions, 105 | context: &UnaryOpContext, 106 | errors: OpErrors<'_, StrType>, 107 | ) -> Type { 108 | BoolArithmetic.process_unary_op(substitutions, context, errors) 109 | } 110 | 111 | fn process_binary_op( 112 | &self, 113 | substitutions: &mut Substitutions, 114 | context: &BinaryOpContext, 115 | mut errors: OpErrors<'_, StrType>, 116 | ) -> Type { 117 | const OP_SETTINGS: OpConstraintSettings<'static, StrType> = OpConstraintSettings { 118 | lin: &Linearity, 119 | ops: &Ops, 120 | }; 121 | 122 | match context.op { 123 | BinaryOp::Add => { 124 | NumArithmetic::unify_binary_op(substitutions, context, errors, OP_SETTINGS) 125 | } 126 | 127 | BinaryOp::Gt | BinaryOp::Lt | BinaryOp::Ge | BinaryOp::Le => { 128 | let lhs_ty = &context.lhs; 129 | let rhs_ty = &context.rhs; 130 | 131 | substitutions.unify( 132 | &Type::Prim(StrType::Str), 133 | lhs_ty, 134 | errors.join_path(ErrorPathFragment::Lhs), 135 | ); 136 | substitutions.unify( 137 | &Type::Prim(StrType::Str), 138 | rhs_ty, 139 | errors.join_path(ErrorPathFragment::Rhs), 140 | ); 141 | Type::BOOL 142 | } 143 | 144 | _ => BoolArithmetic.process_binary_op(substitutions, context, errors), 145 | } 146 | } 147 | } 148 | 149 | type Parser = Annotated; 150 | 151 | fn main() -> anyhow::Result<()> { 152 | let code = r#" 153 | x = "foo" + "bar"; 154 | // Spreading logic is reused from `NumArithmetic` and just works. 155 | y = "foo" + ("bar", "quux"); 156 | // Boolean logic works as well. 157 | assert("bar" != "baz"); 158 | assert("foo" > "bar" && "foo" <= "quux"); 159 | "#; 160 | let ast = Parser::parse_statements(code)?; 161 | 162 | let mut env = TypeEnvironment::::new(); 163 | env.insert("assert", Assertions::Assert); 164 | env.process_with_arithmetic(&StrArithmetic, &ast)?; 165 | assert_eq!(env["x"], Type::Prim(StrType::Str)); 166 | assert_eq!(env["y"].to_string(), "(Str, Str)"); 167 | 168 | let bogus_code = r#""foo" - "bar""#; 169 | let bogus_ast = Parser::parse_statements(bogus_code)?; 170 | let err = env 171 | .process_with_arithmetic(&StrArithmetic, &bogus_ast) 172 | .unwrap_err(); 173 | assert_eq!(err.to_string(), "1:1: Unsupported binary op: subtraction"); 174 | 175 | Ok(()) 176 | } 177 | -------------------------------------------------------------------------------- /typing/tests/check_readme.rs: -------------------------------------------------------------------------------- 1 | //! Tests that the README code samples actually work. 2 | 3 | use std::fs; 4 | 5 | use arithmetic_parser::grammars::{F32Grammar, Parse}; 6 | use arithmetic_typing::{ 7 | arith::{Num, NumArithmetic}, 8 | defs::{Assertions, Prelude}, 9 | Annotated, Type, TypeEnvironment, 10 | }; 11 | use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd}; 12 | 13 | type Grammar = Annotated; 14 | 15 | fn read_file(path: &str) -> String { 16 | fs::read_to_string(path).unwrap_or_else(|err| panic!("Cannot read file {}: {}", path, err)) 17 | } 18 | 19 | fn check_sample(code_sample: &str) { 20 | let program = Grammar::parse_statements(code_sample).unwrap(); 21 | 22 | let mut env: TypeEnvironment = Prelude::iter().chain(Assertions::iter()).collect(); 23 | env.insert("INF", Type::NUM) 24 | .insert("array", Prelude::array(Num::Num)); 25 | env.process_with_arithmetic(&NumArithmetic::with_comparisons(), &program) 26 | .unwrap(); 27 | } 28 | 29 | #[test] 30 | fn code_samples_in_readme_are_valid() { 31 | let readme = read_file("README.md"); 32 | 33 | let parser = Parser::new(&readme); 34 | let mut code: Option = None; 35 | for event in parser { 36 | match event { 37 | Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) 38 | if lang.as_ref() == "text" => 39 | { 40 | assert!(code.is_none(), "Embedded code samples"); 41 | code = Some(String::with_capacity(1_024)); 42 | } 43 | Event::End(TagEnd::CodeBlock) => { 44 | if let Some(code_sample) = code.take() { 45 | assert!(!code_sample.is_empty()); 46 | check_sample(&code_sample); 47 | } 48 | } 49 | Event::Text(text) => { 50 | if let Some(code) = &mut code { 51 | code.push_str(text.as_ref()); 52 | } 53 | } 54 | _ => { /* Do nothing */ } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /typing/tests/integration/examples/dsa.script: -------------------------------------------------------------------------------- 1 | ../../../../eval/examples/dsa.script -------------------------------------------------------------------------------- /typing/tests/integration/examples/elgamal.script: -------------------------------------------------------------------------------- 1 | ../../../../eval/examples/elgamal.script -------------------------------------------------------------------------------- /typing/tests/integration/examples/quick_sort.script: -------------------------------------------------------------------------------- 1 | //! Version of the quicksort sample from the eval README 2 | //! with a couple of necessary type annotations. 3 | 4 | sort = defer(|quick_sort: (_) -> [Num]| { 5 | |xs| { 6 | if(xs as [Num] == (), || () as [Num], || { 7 | (pivot, ...rest) = xs as any; 8 | lesser_part = rest.filter(|x| x < pivot).quick_sort(); 9 | greater_part = rest.filter(|x| x >= pivot).quick_sort(); 10 | lesser_part.push(pivot).merge(greater_part) 11 | })() 12 | } 13 | }); 14 | 15 | assert_eq((1, 7, -3, 2, -1, 4, 2).sort(), (-3, -1, 1, 2, 2, 4, 7)); 16 | 17 | xs = array(1000, |_| rand_num(0, 100)).sort(); 18 | { sorted } = xs.fold( 19 | #{ prev: -1, sorted: true }, 20 | |{ prev, sorted }, x| #{ 21 | prev: x, 22 | sorted: sorted && prev <= x 23 | }, 24 | ); 25 | assert(sorted); 26 | -------------------------------------------------------------------------------- /typing/tests/integration/examples/rfold.script: -------------------------------------------------------------------------------- 1 | ../../../../eval/examples/rfold.script -------------------------------------------------------------------------------- /typing/tests/integration/examples/schnorr.script: -------------------------------------------------------------------------------- 1 | ../../../../eval/examples/schnorr.script -------------------------------------------------------------------------------- /typing/tests/integration/main.rs: -------------------------------------------------------------------------------- 1 | //! Hub for integration tests. 2 | 3 | use std::fmt; 4 | 5 | use arithmetic_parser::grammars::NumGrammar; 6 | use arithmetic_typing::{ 7 | arith::{Constraint, Num, ObjectSafeConstraint, Substitutions}, 8 | error::{Error, ErrorKind, Errors, OpErrors}, 9 | visit::Visit, 10 | Annotated, DynConstraints, Function, PrimitiveType, Type, UnknownLen, 11 | }; 12 | 13 | mod annotations; 14 | mod basics; 15 | mod errors; 16 | mod examples; 17 | mod length_eqs; 18 | mod object; 19 | 20 | type F32Grammar = Annotated>; 21 | 22 | trait ErrorsExt { 23 | fn single(self) -> Error; 24 | } 25 | 26 | impl ErrorsExt for Errors { 27 | fn single(self) -> Error { 28 | if self.len() == 1 { 29 | self.into_iter().next().unwrap() 30 | } else { 31 | panic!("Expected 1 error, got {self:?}"); 32 | } 33 | } 34 | } 35 | 36 | /// Constraint for types that can be hashed. 37 | #[derive(Debug, Clone, Copy)] 38 | struct Hashed; 39 | 40 | impl fmt::Display for Hashed { 41 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | formatter.write_str("Hash") 43 | } 44 | } 45 | 46 | impl Constraint for Hashed { 47 | fn visitor<'r>( 48 | &self, 49 | substitutions: &'r mut Substitutions, 50 | errors: OpErrors<'r, Prim>, 51 | ) -> Box + 'r> { 52 | use arithmetic_typing::arith::StructConstraint; 53 | 54 | StructConstraint::new(*self, |_| true).visitor(substitutions, errors) 55 | } 56 | 57 | fn clone_boxed(&self) -> Box> { 58 | Box::new(*self) 59 | } 60 | } 61 | 62 | impl ObjectSafeConstraint for Hashed {} 63 | 64 | fn assert_incompatible_types( 65 | err: &ErrorKind, 66 | first: &Type, 67 | second: &Type, 68 | ) { 69 | let ErrorKind::TypeMismatch(x, y) = err else { 70 | panic!("Unexpected error type: {err:?}"); 71 | }; 72 | assert!( 73 | (x == first && y == second) || (x == second && y == first), 74 | "Unexpected incompatible types: {:?}, expected: {:?}", 75 | (x, y), 76 | (first, second) 77 | ); 78 | } 79 | 80 | fn hash_fn_type() -> Function { 81 | Function::builder() 82 | .with_varargs(DynConstraints::just(Hashed), UnknownLen::param(0)) 83 | .returning(Type::NUM) 84 | } 85 | 86 | #[test] 87 | fn hash_fn_type_display() { 88 | assert_eq!(hash_fn_type().to_string(), "(...[dyn Hash; N]) -> Num"); 89 | } 90 | 91 | /// `zip` function signature. 92 | fn zip_fn_type() -> Function { 93 | Function::builder() 94 | .with_arg(Type::param(0).repeat(UnknownLen::param(0))) 95 | .with_arg(Type::param(1).repeat(UnknownLen::param(0))) 96 | .returning(Type::slice( 97 | (Type::param(0), Type::param(1)), 98 | UnknownLen::param(0), 99 | )) 100 | .with_static_lengths(&[0]) 101 | .into() 102 | } 103 | 104 | #[test] 105 | fn zip_fn_type_display() { 106 | let zip_fn_string = zip_fn_type().to_string(); 107 | assert_eq!( 108 | zip_fn_string, 109 | "for (['T; N], ['U; N]) -> [('T, 'U); N]" 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /typing/tests/version_match.rs: -------------------------------------------------------------------------------- 1 | use version_sync::{assert_html_root_url_updated, assert_markdown_deps_updated}; 2 | 3 | #[test] 4 | fn readme_is_in_sync() { 5 | assert_markdown_deps_updated!("README.md"); 6 | } 7 | 8 | #[test] 9 | fn html_root_url_is_in_sync() { 10 | assert_html_root_url_updated!("src/lib.rs"); 11 | } 12 | --------------------------------------------------------------------------------