├── .github └── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── cas-attrs ├── Cargo.toml ├── README.md └── src │ ├── builtin.rs │ ├── error_kind.rs │ └── lib.rs ├── cas-compiler ├── Cargo.toml ├── README.md └── src │ ├── error.rs │ ├── expr │ ├── assign.rs │ ├── binary.rs │ ├── block.rs │ ├── branch.rs │ ├── break_expr.rs │ ├── call.rs │ ├── continue_expr.rs │ ├── if_expr.rs │ ├── index.rs │ ├── literal.rs │ ├── loops.rs │ ├── mod.rs │ ├── product.rs │ ├── range.rs │ ├── return_expr.rs │ ├── stmt.rs │ ├── sum.rs │ └── unary.rs │ ├── instruction.rs │ ├── item.rs │ ├── lib.rs │ └── sym_table.rs ├── cas-compute ├── Cargo.toml ├── README.md └── src │ ├── approx.rs │ ├── consts.rs │ ├── funcs │ ├── angle.rs │ ├── combinatoric.rs │ ├── complex.rs │ ├── miscellaneous.rs │ ├── mod.rs │ ├── power.rs │ ├── print.rs │ ├── probability.rs │ ├── round.rs │ ├── sequence.rs │ └── trigonometry.rs │ ├── lib.rs │ ├── numerical │ ├── builtin │ │ ├── error │ │ │ ├── check.rs │ │ │ ├── func_specific.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── error │ │ └── kind.rs │ ├── fmt │ │ ├── complex.rs │ │ ├── float.rs │ │ ├── integer.rs │ │ └── mod.rs │ ├── func.rs │ ├── mod.rs │ ├── trig_mode.rs │ └── value.rs │ ├── primitive.rs │ └── symbolic │ ├── expr │ ├── iter.rs │ └── mod.rs │ ├── mod.rs │ ├── simplify │ ├── fraction.rs │ ├── mod.rs │ ├── rules │ │ ├── add.rs │ │ ├── distribute.rs │ │ ├── imaginary.rs │ │ ├── mod.rs │ │ ├── multiply.rs │ │ ├── power.rs │ │ ├── root.rs │ │ └── trigonometry │ │ │ ├── consts.rs │ │ │ ├── mod.rs │ │ │ └── table.rs │ └── step.rs │ └── step_collector.rs ├── cas-error ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── cas-graph ├── Cargo.toml ├── README.md ├── img │ ├── erf-output.png │ ├── output.png │ └── parabola.png └── src │ ├── graph │ ├── analyzed.rs │ ├── eval.rs │ ├── mod.rs │ ├── opts.rs │ └── point.rs │ ├── lib.rs │ └── text_align.rs ├── cas-parser ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── parser │ ├── ast │ │ ├── assign.rs │ │ ├── binary.rs │ │ ├── block.rs │ │ ├── branch.rs │ │ ├── call.rs │ │ ├── expr.rs │ │ ├── for_expr.rs │ │ ├── helper │ │ │ ├── delimited.rs │ │ │ ├── mod.rs │ │ │ └── surrounded.rs │ │ ├── if_expr.rs │ │ ├── index.rs │ │ ├── literal.rs │ │ ├── loop_expr.rs │ │ ├── mod.rs │ │ ├── paren.rs │ │ ├── product.rs │ │ ├── range.rs │ │ ├── return_expr.rs │ │ ├── stmt.rs │ │ ├── sum.rs │ │ ├── unary.rs │ │ └── while_expr.rs │ ├── error.rs │ ├── fmt.rs │ ├── garbage.rs │ ├── iter.rs │ ├── keyword.rs │ ├── mod.rs │ └── token │ │ ├── mod.rs │ │ ├── op.rs │ │ └── pair.rs │ └── tokenizer │ ├── mod.rs │ └── token.rs ├── cas-rs ├── Cargo.toml └── src │ ├── error.rs │ └── main.rs ├── cas-unit-convert ├── Cargo.toml ├── README.md ├── examples │ └── convert-unit-repl │ │ ├── error.rs │ │ └── main.rs └── src │ ├── convert.rs │ ├── lib.rs │ └── unit.rs ├── cas-vm ├── Cargo.toml ├── README.md └── src │ ├── error.rs │ ├── frame.rs │ ├── instruction.rs │ ├── lib.rs │ └── register.rs └── examples ├── bad_lcm.calc ├── convert_binary.calc ├── counter.calc ├── environment_capture.calc ├── factorial.calc ├── function_scope.calc ├── higher_order_function.calc ├── if_branching.calc ├── manual_abs.calc ├── map_list.calc ├── memoized_fib.calc ├── ncr.calc ├── prime_notation.calc └── resolving_calls.calc /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report an issue or bug with cas-rs. 3 | title: '[Bug report title]' 4 | labels: 5 | - bug 6 | body: 7 | - type: textarea 8 | id: description 9 | attributes: 10 | label: Description 11 | description: >- 12 | Describe the issue in detail. Did something break? Is something wrong or 13 | confusing? 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: expected 18 | attributes: 19 | label: Expected behavior 20 | description: >- 21 | Describe what you expected to happen. What should the correct behavior 22 | be? 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: reproduce 27 | attributes: 28 | label: Steps to reproduce 29 | description: >- 30 | Show us what steps you took to encounter the issue. Be as specific as 31 | possible. 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: error 36 | attributes: 37 | label: Error logs 38 | description: If your issue caused a crash, provide a copy of the error logs. 39 | render: rs 40 | - type: textarea 41 | id: additional-info 42 | attributes: 43 | label: Additional information 44 | description: Include any additional information or context you'd like here. 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: GitHub Discussions 4 | url: https://github.com/ElectrifyPro/cas-rs/discussions 5 | about: Ask any general questions about cas-rs or contributing. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Propose a new feature or enhancement for cas-rs. 3 | title: '[Feature request]' 4 | labels: 5 | - enhancement 6 | body: 7 | - type: textarea 8 | id: description 9 | attributes: 10 | label: Description 11 | description: >- 12 | Describe the problem you want `cas-rs` to solve and the motivation for 13 | solving it. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: potential-solution 18 | attributes: 19 | label: Describe your solution 20 | description: If you have a solution in mind, please describe it. 21 | - type: textarea 22 | id: alternatives 23 | attributes: 24 | label: Describe alternatives you've considered 25 | description: Have you considered any other methods of solving the problem? 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | tags 4 | .vimspector.json 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | Thank you for considering contributing to `cas-rs`! There's a couple of ways you 4 | can contribute, described below. I greatly appreciate any and all contributions 5 | to `cas-rs`! 6 | 7 | If you have any questions about contributing outside of what is covered here, 8 | feel free to ask a question using 9 | [GitHub discussions](https://github.com/ElectrifyPro/cas-rs/discussions). 10 | 11 | ## Reporting issues, requesting features 12 | 13 | Use the [GitHub issue tracker](https://github.com/ElectrifyPro/cas-rs/issues). 14 | 15 | When reporting an issue, please include as much detail as possible, including a 16 | _**minimal example** that reproduces the issue_. A minimal example is the 17 | smallest possible version of your code / configuration that still reproduces the 18 | issue you're experiencing. Including one makes it significantly easier for 19 | others to narrow down where in code the issue might be. 20 | 21 | If you're requesting a feature, please include a detailed description of the 22 | feature you have in mind, and a rationale for why you want or need it. 23 | 24 | ## General questions or help 25 | 26 | Use [GitHub discussions](https://github.com/ElectrifyPro/cas-rs/discussions) for 27 | general questions about `cas-rs` or contributing. 28 | 29 | ## Contributing code 30 | 31 | Open a [pull request](https://github.com/ElectrifyPro/cas-rs/pulls) with your 32 | changes. 33 | 34 | You may want to discuss your proposed changes in an 35 | [issue](#reporting-issues-requesting-features) before opening a pull request. If 36 | you're working on a large change, it's a good idea to get feedback on your 37 | approach before you spend a lot of time on it. 38 | 39 | When contributing new features, you should include appropriate tests and 40 | documentation for your changes. Tests will help make sure that your new feature 41 | is working as intended, and also ensure that future changes from other 42 | developers won't break your feature. Documentation is intended to help users of 43 | the library understand how to use your new feature. 44 | 45 | Tests should cover all new functionality (and probably pass), and documentation 46 | should be clear and concise. If you made particular design decisions that you 47 | think others should be aware of, please include those in documentation as well. 48 | If a bug is fixed, include tests that reproduces the bug and now pass after your 49 | fix is applied. 50 | 51 | ### Branching 52 | 53 | There are two main 54 | [branches](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches) 55 | in `cas-rs`: 56 | 57 | - `main`: This is the stable branch. Anyone should be able to clone this branch 58 | and build a fully functional version of the library. You will most likely base 59 | your work on this branch. 60 | - `dev`: This is the development branch. This is where new features are being 61 | actively developed. Currently, this branch is significantly different from 62 | `main`. 63 | 64 | Create a separate branch for your work based on the `main` branch so that you 65 | won't come into conflict if `main` changes while you're working. When you're 66 | ready to submit your changes, open a pull request against the `main` branch. Set 67 | the base branch to `main`, and the compare branch to your feature branch, 68 | indicating that you want to merge your changes into `main`. 69 | 70 | ### Code style 71 | 72 | Currently, there is no strict style guide, though this may change in the future. 73 | You should try to match the existing style of the code you're working on, and 74 | default to using Rust style conventions. 75 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "cas-attrs", 4 | "cas-compute", 5 | "cas-error", 6 | "cas-graph", 7 | "cas-parser", 8 | "cas-rs", 9 | "cas-unit-convert", 10 | "cas-vm", 11 | ] 12 | resolver = "2" 13 | 14 | [profile.release] 15 | codegen-units = 1 16 | lto = true 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ElectrifyPro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cas-rs 2 | 3 | `cas-rs` is an opinionated computer algebra system written in Rust, originally made for use with the Discord bot [CalcBot](https://discord.com/application-directory/674457690646249472/). It is currently in a very early stage of development. 4 | 5 | See below for a guide on the REPL and some code examples. 6 | 7 | # Features 8 | 9 | - Simplification of algebraic expressions, learn more [here](cas-compute/README.md) 10 | - Graphing calculator, learn more [here](cas-graph/README.md) 11 | - Robust expression parser and evaluator with very human-friendly output 12 | - Arbitrary precision arithmetic 13 | - Real and complex numbers 14 | - Radix notation (e.g. `2'1010` = 10) 15 | - 60+ built-in functions, covering many expected use cases ([see here](https://github.com/ElectrifyPro/cas-rs/blob/dev/cas-compute/src/funcs/mod.rs)) 16 | - Powerful formatting options, including LaTeX code output 17 | - Builtin REPL 18 | - And more! 19 | 20 | # Quick start 21 | 22 | Below are some routine examples of how `cas-rs` can be used. 23 | 24 | ## REPL 25 | 26 | `cas-rs` comes with a builtin scripting language called "CalcScript" along with a REPL to help you try out the library. CalcScript is a compiled, mostly imperative, expression-oriented language, and attempts to keep syntax and visual noise minimal, while still readable. See the [`examples/`](examples) directory for examples of basic programs written in CalcScript. 27 | 28 | To install the REPL, run the following command (Rust needs to be installed): 29 | 30 | ```sh 31 | cargo install cas-rs --locked 32 | ``` 33 | 34 | You can then run the REPL with: 35 | 36 | ```sh 37 | cas-rs 38 | ``` 39 | 40 | Which will immediately drop you into a REPL session where you can run CalcScript code: 41 | 42 | ```text 43 | > x = (1 + sqrt(5))/2 44 | 1.61803398874989484820458683436563811772030917980576286213544862270526046281890244970720720418939113748475408807538689175212663386222353693179318006077 45 | > x == phi 46 | true 47 | ``` 48 | 49 | To learn more about CalcScript and example REPL usage, see the [`cas-parser`](cas-parser/README.md) crate. 50 | 51 | ## General expression evaluation 52 | 53 | `cas-rs` can also be used as a library to evaluate mathematical expressions and run CalcScript code. Use [`cas-parser`](cas-parser/README.md) to parse an expression, and [`cas-vm`](cas-vm/README.md) to compile and execute it. Here is an example of evaluating the expression `x^2 + 5x + 6` where `x = 2`: 54 | 55 | ```rust 56 | use cas_parser::parser::Parser; 57 | use cas_vm::Vm; 58 | 59 | let mut parser = Parser::new("x = 2; x^2 + 5x + 6"); 60 | let stmts = parser.try_parse_full_many().unwrap(); 61 | 62 | let result = Vm::compile_program(stmts) 63 | .unwrap() 64 | .run() 65 | .unwrap(); 66 | 67 | assert_eq!(result, 20.into()); 68 | ``` 69 | 70 | Learn more about this in the [`cas-vm`](cas-vm/README.md) crate. 71 | 72 | ## Algebraic simplification 73 | 74 | `cas-rs` includes a rudimentary algebraic simplifier that can simplify expressions using a variety of rules. Try it out with [`cas-compute`](cas-compute/README.md): 75 | 76 | ```rust 77 | use cas_compute::symbolic::simplify; 78 | use cas_parser::parser::{ast::Expr, Parser}; 79 | 80 | let mut parser = Parser::new("6x + 11x + 4y - 2y"); 81 | let expr = parser.try_parse_full::().unwrap(); 82 | println!("Original: {}", expr); // 6x+11x+4y-2y 83 | 84 | let simplified = simplify(&expr.into()); 85 | println!("Simplified: {}", simplified); // 2 * y + 17 * x 86 | ``` 87 | 88 | ```rust 89 | 90 | ## Graphing calculator 91 | 92 | A customizable graphing calculator is provided with the `cas-graph` crate. You can create a graph of multiple functions and points, customize the appearance of the viewport, functions, and points, and render the graph to a PNG file (or any format supported by the [`cairo`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/cairo/) crate. 93 | 94 | ```rust 95 | use cas_graph::Graph; 96 | use std::fs::File; 97 | 98 | fn main() -> Result<(), Box> { 99 | let surface = Graph::default() 100 | .try_add_expr("x^2 + 5x + 6").unwrap() 101 | .add_point((-5.0, 6.0)) 102 | .add_point((-4.0, 2.0)) 103 | .add_point((-3.0, 0.0)) 104 | .add_point((-2.0, 0.0)) 105 | .add_point((-1.0, 2.0)) 106 | .center_on_points() 107 | .draw()?; 108 | 109 | let mut file = File::create("parabola.png")?; 110 | surface.write_to_png(&mut file)?; 111 | 112 | Ok(()) 113 | } 114 | ``` 115 | 116 | Output (note: colors were randomly chosen; random color selection is not 117 | included in the example code): 118 | 119 | 120 | 121 | # Acknowledgements 122 | 123 | - Arbitrary precision arithmetic is implemented using the [`rug`](https://gitlab.com/tspiteri/rug) crate. 124 | - The design of `cas-parser` is heavily inspired by the Rust compiler, [`syn`](https://github.com/dtolnay/syn), and [`ariadne`](https://github.com/zesterer/ariadne). 125 | -------------------------------------------------------------------------------- /cas-attrs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas-attrs" 3 | description = "Derive macros for cas-error's ErrorKind and cas-compute's Builtin traits" 4 | version = "0.2.0" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/ElectrifyPro/cas-rs" 8 | keywords = ["derive", "proc-macro", "cas", "calcscript"] 9 | categories = ["mathematics"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dev-dependencies] 17 | ariadne = "0.2.0" 18 | cas-compute = { path = "../cas-compute" } 19 | cas-error = { path = "../cas-error" } 20 | 21 | [dependencies] 22 | quote = "^1.0" 23 | syn = { version = "^2.0", features = ["full", "derive", "extra-traits"] } 24 | proc-macro2 = "1.0.66" 25 | -------------------------------------------------------------------------------- /cas-attrs/README.md: -------------------------------------------------------------------------------- 1 | Derive macros for the `cas-rs` ecosystem of crates. 2 | 3 | You almost certainly do not need this crate to use `cas-rs`. It's only used 4 | internally by the `cas-rs` ecosystem to derive trait implementations for their 5 | types. 6 | -------------------------------------------------------------------------------- /cas-compiler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas-compiler" 3 | description = "Bytecode compiler for CalcScript programs" 4 | version = "0.2.0" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/ElectrifyPro/cas-rs" 8 | keywords = ["calcscript", "codegen", "math-language", "bytecode", "evaluation"] 9 | categories = ["compilers", "mathematics"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | ariadne = "0.2.0" 15 | cas-attrs = { version = "0.2.0", path = "../cas-attrs" } 16 | cas-compute = { version = "0.2.0", path = "../cas-compute" } 17 | cas-error = { version = "0.2.0", path = "../cas-error" } 18 | cas-parser = { version = "0.2.0", path = "../cas-parser" } 19 | -------------------------------------------------------------------------------- /cas-compiler/src/error.rs: -------------------------------------------------------------------------------- 1 | use ariadne::Fmt; 2 | use cas_attrs::ErrorKind; 3 | use cas_error::EXPR; 4 | 5 | /// Tried to override a builtin constant. 6 | #[derive(Debug, Clone, ErrorKind, PartialEq)] 7 | #[error( 8 | message = format!("cannot override builtin constant: `{}`", self.name), 9 | labels = ["this variable"], 10 | help = "choose a different name for this variable", 11 | note = "builtin constants include: `i`, `e`, `phi`, `pi`, or `tau`", 12 | // TODO: ariadne does not allow multiple notes 13 | // note = "consider using `let` to shadow the constant", 14 | )] 15 | pub struct OverrideBuiltinConstant { 16 | /// The name of the variable that was attempted to be overridden. 17 | pub name: String, 18 | } 19 | 20 | /// Tried to override a builtin function. 21 | #[derive(Debug, Clone, ErrorKind, PartialEq)] 22 | #[error( 23 | message = format!("cannot override builtin function: `{}`", self.name), 24 | labels = ["this function"], 25 | help = "choose a different name for this function", 26 | )] 27 | pub struct OverrideBuiltinFunction { 28 | /// The name of the function that was attempted to be overridden. 29 | pub name: String, 30 | } 31 | 32 | /// The variable is undefined. 33 | #[derive(Debug, Clone, ErrorKind, PartialEq)] 34 | #[error( 35 | message = format!("unknown variable: `{}`", self.name), 36 | labels = ["this variable"], 37 | help = format!("to define it, type: {} = {}", (&self.name).fg(EXPR), "".fg(EXPR)), 38 | )] 39 | pub struct UnknownVariable { 40 | /// The name of the variable that was undefined. 41 | pub name: String, 42 | } 43 | 44 | /// The function is undefined. 45 | #[derive(Debug, Clone, ErrorKind, PartialEq)] 46 | #[error( 47 | message = format!("unknown function: `{}`", self.name), 48 | labels = ["this function"], 49 | help = if self.suggestions.is_empty() { 50 | "see the documentation for a list of available functions".to_string() 51 | } else if self.suggestions.len() == 1 { 52 | format!("did you mean the `{}` function?", (&*self.suggestions[0]).fg(EXPR)) 53 | } else { 54 | format!( 55 | "did you mean one of these functions? {}", 56 | self.suggestions 57 | .iter() 58 | .map(|s| format!("`{}`", s.fg(EXPR))) 59 | .collect::>() 60 | .join(", ") 61 | ) 62 | } 63 | )] 64 | pub struct UnknownFunction { 65 | /// The name of the function that was undefined. 66 | pub name: String, 67 | 68 | /// A list of similarly named functions, if any. 69 | pub suggestions: Vec, 70 | } 71 | 72 | /// Too many arguments were given to a function call. 73 | #[derive(Debug, Clone, ErrorKind, PartialEq)] 74 | #[error( 75 | message = format!("too many arguments given to the `{}` function", self.name), 76 | labels = ["this function call", "", "these argument(s) are extraneous"], 77 | help = format!( 78 | "the `{}` function takes {} argument(s); there are {} argument(s) provided here", 79 | (&self.name).fg(EXPR), 80 | self.expected, 81 | self.given 82 | ), 83 | note = format!("function signature: `{}({})`", self.name, self.signature), 84 | )] 85 | pub struct TooManyArguments { 86 | /// The name of the function that was called. 87 | pub name: String, 88 | 89 | /// The number of arguments that were expected. 90 | pub expected: usize, 91 | 92 | /// The number of arguments that were given. 93 | pub given: usize, 94 | 95 | /// The signature of the function, not including the function name. 96 | pub signature: String, 97 | } 98 | 99 | /// An argument to a function call is missing. 100 | #[derive(Debug, Clone, ErrorKind, PartialEq)] 101 | #[error( 102 | message = match &*self.indices { 103 | &[index] => format!("missing required argument #{} for the `{}` function", index + 1, self.name), 104 | _ => format!( 105 | "missing required arguments {} for the `{}` function", 106 | self.indices 107 | .iter() 108 | .map(|i| format!("#{}", i + 1)) 109 | .collect::>() 110 | .join(", "), 111 | self.name 112 | ), 113 | }, 114 | labels = ["this function call", ""], 115 | help = format!( 116 | "the `{}` function takes {} argument(s); there are {} argument(s) provided here", 117 | (&self.name).fg(EXPR), 118 | self.expected, 119 | self.given 120 | ), 121 | note = format!("function signature: `{}({})`", self.name, self.signature), 122 | )] 123 | pub struct MissingArgument { 124 | /// The name of the function that was called. 125 | pub name: String, 126 | 127 | /// The indices of the missing arguments. 128 | pub indices: Vec, 129 | 130 | /// The number of arguments that were expected. 131 | pub expected: usize, 132 | 133 | /// The number of arguments that were given. 134 | pub given: usize, 135 | 136 | /// The signature of the function, not including the function name. 137 | pub signature: String, 138 | } 139 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/binary.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use cas_parser::parser::ast::binary::Binary; 3 | use crate::{Compile, Compiler, InstructionKind}; 4 | 5 | impl Compile for Binary { 6 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 7 | self.lhs.compile(compiler)?; 8 | self.rhs.compile(compiler)?; 9 | compiler.add_instr_with_spans( 10 | InstructionKind::Binary(self.op.kind), 11 | vec![self.lhs.span(), self.op.span.clone(), self.rhs.span()], 12 | ); 13 | Ok(()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/block.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use cas_parser::parser::ast::Block; 3 | use crate::{Compile, Compiler}; 4 | use super::compile_stmts; 5 | 6 | impl Compile for Block { 7 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 8 | compiler.new_scope(|compiler| compile_stmts(&self.stmts, compiler))?; 9 | Ok(()) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/branch.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use cas_parser::parser::ast::branch::{Of, Then}; 3 | use crate::{Compile, Compiler}; 4 | 5 | impl Compile for Then { 6 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 7 | self.expr.compile(compiler) 8 | } 9 | } 10 | 11 | impl Compile for Of { 12 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 13 | self.expr.compile(compiler) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/break_expr.rs: -------------------------------------------------------------------------------- 1 | use cas_compute::numerical::value::Value; 2 | use cas_error::Error; 3 | use cas_parser::parser::ast::loop_expr::Break; 4 | use crate::{Compile, Compiler, InstructionKind}; 5 | 6 | impl Compile for Break { 7 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 8 | if let Some(value) = &self.value { 9 | value.compile(compiler)?; 10 | } else { 11 | compiler.add_instr(InstructionKind::LoadConst(Value::Unit)); 12 | } 13 | 14 | let loop_end = compiler.state.loop_end.unwrap(); 15 | compiler.add_instr(InstructionKind::Jump(loop_end)); 16 | Ok(()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/call.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use cas_parser::parser::ast::call::Call; 3 | use crate::{Compile, Compiler, InstructionKind}; 4 | 5 | impl Compile for Call { 6 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 7 | self.args.iter().try_for_each(|arg| arg.compile(compiler))?; 8 | 9 | // TODO: allow calling any receiever when it is supported 10 | // load the function 11 | let symbol_id = compiler.resolve_symbol(&self.name)?; 12 | compiler.add_instr_with_spans( 13 | InstructionKind::LoadVar(symbol_id), 14 | vec![self.name.span.clone()], 15 | ); 16 | 17 | let spans = self.outer_span() 18 | .into_iter() 19 | .chain(self.args.iter().map(|arg| arg.span())) 20 | .collect(); 21 | 22 | if self.derivatives > 0 { 23 | // compute derivative of function 24 | compiler.add_instr_with_spans( 25 | InstructionKind::CallDerivative(self.derivatives, self.args.len()), 26 | spans, 27 | ); 28 | } else { 29 | // call the function 30 | compiler.add_instr_with_spans(InstructionKind::Call(self.args.len()), spans); 31 | } 32 | 33 | Ok(()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/continue_expr.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use cas_parser::parser::ast::loop_expr::Continue; 3 | use crate::{Compile, Compiler, InstructionKind}; 4 | 5 | impl Compile for Continue { 6 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 7 | let loop_start = compiler.state.loop_start.unwrap(); 8 | compiler.add_instr(InstructionKind::Jump(loop_start)); 9 | Ok(()) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/if_expr.rs: -------------------------------------------------------------------------------- 1 | use cas_compute::numerical::value::Value; 2 | use cas_error::Error; 3 | use cas_parser::parser::ast::if_expr::If; 4 | use crate::{Compile, Compiler, InstructionKind}; 5 | 6 | impl Compile for If { 7 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 8 | self.condition.compile(compiler)?; 9 | 10 | let else_start = compiler.new_unassociated_label(); 11 | compiler.add_instr_with_spans( 12 | InstructionKind::JumpIfFalse(else_start), 13 | // for error if condition doesn't evaluate to boolean 14 | vec![self.condition.span(), self.then_expr.span()], 15 | ); 16 | 17 | self.then_expr.compile(compiler)?; 18 | 19 | let if_end = compiler.new_unassociated_label(); 20 | compiler.add_instr(InstructionKind::Jump(if_end)); 21 | 22 | compiler.set_end_label(else_start); 23 | if let Some(else_expr) = &self.else_expr { 24 | else_expr.compile(compiler)?; 25 | } else { 26 | compiler.add_instr(InstructionKind::LoadConst(Value::Unit)); 27 | } 28 | 29 | compiler.set_end_label(if_end); 30 | 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/index.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use cas_parser::parser::ast::index::Index; 3 | use crate::{Compile, Compiler, InstructionKind}; 4 | 5 | impl Compile for Index { 6 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 7 | // load the (hopefully) list value onto the stack 8 | self.target.compile(compiler)?; 9 | 10 | // load the index value onto the stack 11 | self.index.compile(compiler)?; 12 | 13 | // load the value at the index onto the stack 14 | compiler.add_instr_with_spans( 15 | InstructionKind::LoadIndexed, 16 | vec![self.target.span(), self.index.span()], 17 | ); 18 | Ok(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/literal.rs: -------------------------------------------------------------------------------- 1 | use cas_compute::{ 2 | numerical::value::Value, 3 | primitive::{float_from_str, from_str_radix, int_from_str}, 4 | }; 5 | use cas_error::Error; 6 | use cas_parser::parser::ast::literal::Literal; 7 | use crate::{Compile, Compiler, InstructionKind}; 8 | 9 | impl Compile for Literal { 10 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 11 | match self { 12 | Literal::Integer(int) => compiler.add_instr(InstructionKind::LoadConst(Value::Integer(int_from_str(&int.value)))), 13 | Literal::Float(float) => compiler.add_instr(InstructionKind::LoadConst(Value::Float(float_from_str(&float.value)))), 14 | Literal::Radix(radix) => compiler.add_instr(InstructionKind::LoadConst(Value::Integer(from_str_radix(radix.value.as_str(), radix.base)))), 15 | Literal::Boolean(boolean) => compiler.add_instr(InstructionKind::LoadConst(Value::Boolean(boolean.value))), 16 | Literal::Symbol(sym) => { 17 | let symbol = compiler.resolve_symbol(sym)?; 18 | compiler.add_instr_with_spans( 19 | InstructionKind::LoadVar(symbol), 20 | vec![sym.span.clone()], 21 | ); 22 | }, 23 | Literal::Unit(_) => compiler.add_instr(InstructionKind::LoadConst(Value::Unit)), 24 | Literal::List(list) => { 25 | // compile the elements of the list 26 | for element in list.values.iter() { 27 | element.compile(compiler)?; 28 | } 29 | 30 | // create a list with the number of elements on the stack 31 | compiler.add_instr(InstructionKind::CreateList(list.values.len())); 32 | }, 33 | Literal::ListRepeat(list_repeat) => { 34 | // compile the element to repeat 35 | list_repeat.value.compile(compiler)?; 36 | 37 | // compile the number of times to repeat the element 38 | list_repeat.count.compile(compiler)?; 39 | 40 | // create a list with the number of elements on the stack 41 | compiler.add_instr_with_spans( 42 | InstructionKind::CreateListRepeat, 43 | vec![list_repeat.count.span()], 44 | ); 45 | }, 46 | }; 47 | 48 | Ok(()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/mod.rs: -------------------------------------------------------------------------------- 1 | mod assign; 2 | mod binary; 3 | mod block; 4 | mod branch; 5 | mod break_expr; 6 | mod call; 7 | mod continue_expr; 8 | mod if_expr; 9 | mod index; 10 | mod literal; 11 | mod loops; 12 | mod product; 13 | mod range; 14 | mod return_expr; 15 | mod stmt; 16 | mod sum; 17 | mod unary; 18 | 19 | pub use stmt::compile_stmts; 20 | 21 | use cas_error::Error; 22 | use cas_parser::parser::ast::expr::Expr; 23 | use crate::{Compile, Compiler}; 24 | 25 | impl Compile for Expr { 26 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 27 | match self { 28 | Expr::Literal(literal) => literal.compile(compiler), 29 | Expr::Paren(paren) => paren.expr.compile(compiler), 30 | Expr::Block(block) => block.compile(compiler), 31 | Expr::Sum(sum) => sum.compile(compiler), 32 | Expr::Product(product) => product.compile(compiler), 33 | Expr::If(if_expr) => if_expr.compile(compiler), 34 | Expr::Loop(loop_expr) => loop_expr.compile(compiler), 35 | Expr::While(while_expr) => while_expr.compile(compiler), 36 | Expr::For(for_expr) => for_expr.compile(compiler), 37 | Expr::Then(then_expr) => then_expr.compile(compiler), 38 | Expr::Of(of_expr) => of_expr.compile(compiler), 39 | Expr::Break(break_expr) => break_expr.compile(compiler), 40 | Expr::Continue(continue_expr) => continue_expr.compile(compiler), 41 | Expr::Return(return_expr) => return_expr.compile(compiler), 42 | Expr::Call(call) => call.compile(compiler), 43 | Expr::Index(index) => index.compile(compiler), 44 | Expr::Unary(unary) => unary.compile(compiler), 45 | Expr::Binary(binary) => binary.compile(compiler), 46 | Expr::Assign(assign) => assign.compile(compiler), 47 | Expr::Range(range) => range.compile(compiler), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/product.rs: -------------------------------------------------------------------------------- 1 | use cas_compute::{numerical::value::Value, primitive::int}; 2 | use cas_error::Error; 3 | use cas_parser::parser::{ast::{Product, RangeKind}, token::op::BinOpKind}; 4 | use crate::{item::Symbol, Compile, Compiler, InstructionKind}; 5 | 6 | impl Compile for Product { 7 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 8 | // ``` 9 | // product n in 1..10 of n 10 | // ``` 11 | // 12 | // equivalent to: 13 | // 14 | // ``` 15 | // { 16 | // out = 1 17 | // for n in 1..10 { 18 | // out *= n 19 | // } 20 | // out 21 | // } 22 | // ``` 23 | // 24 | // but there is **no** control flow 25 | 26 | compiler.new_scope(|compiler| { 27 | // initialize the product to 1 28 | compiler.add_instr(InstructionKind::LoadConst(Value::Integer(int(1)))); 29 | 30 | // compile range end up here so that the index variable isn't in scope, then insert it 31 | // down at the condition 32 | let chunk = compiler.new_chunk_get(|compiler| { 33 | self.range.end.compile(compiler) 34 | })?; 35 | 36 | // assign: initialize index in range 37 | self.range.start.compile(compiler)?; 38 | let symbol_id = compiler.add_symbol(&self.variable)?; 39 | compiler.add_instr(InstructionKind::AssignVar(symbol_id)); 40 | 41 | // condition: continue summing while the variable is in the range: 42 | // `symbol_id < self.range.end` 43 | let condition_start = compiler.new_end_label(); 44 | compiler.add_instr(InstructionKind::LoadVar(Symbol::User(symbol_id))); 45 | compiler.add_chunk_instrs(chunk); 46 | match self.range.kind { 47 | RangeKind::HalfOpen => compiler.add_instr(InstructionKind::Binary(BinOpKind::Less)), 48 | RangeKind::Closed => compiler.add_instr(InstructionKind::Binary(BinOpKind::LessEq)), 49 | } 50 | 51 | let loop_end = compiler.new_unassociated_label(); 52 | compiler.add_instr(InstructionKind::JumpIfFalse(loop_end)); 53 | 54 | // body: compute body, multiply it to cummulative product 55 | self.body.compile(compiler)?; 56 | compiler.add_instr(InstructionKind::Binary(BinOpKind::Mul)); 57 | 58 | // increment index 59 | compiler.add_instr(InstructionKind::LoadVar(Symbol::User(symbol_id))); 60 | compiler.add_instr(InstructionKind::LoadConst(Value::Integer(int(1)))); 61 | compiler.add_instr(InstructionKind::Binary(BinOpKind::Add)); 62 | compiler.add_instr(InstructionKind::AssignVar(symbol_id)); 63 | 64 | compiler.add_instr(InstructionKind::Jump(condition_start)); 65 | 66 | compiler.set_end_label(loop_end); 67 | Ok(()) 68 | })?; 69 | Ok(()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/range.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use cas_parser::parser::ast::range::Range; 3 | use crate::{Compile, Compiler, InstructionKind}; 4 | 5 | impl Compile for Range { 6 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 7 | self.start.compile(compiler)?; 8 | self.end.compile(compiler)?; 9 | compiler.add_instr(InstructionKind::CreateRange(self.kind)); 10 | Ok(()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/return_expr.rs: -------------------------------------------------------------------------------- 1 | use cas_compute::numerical::value::Value; 2 | use cas_error::Error; 3 | use cas_parser::parser::ast::return_expr::Return; 4 | use crate::{Compile, Compiler, InstructionKind}; 5 | 6 | impl Compile for Return { 7 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 8 | if let Some(expr) = &self.value { 9 | expr.compile(compiler)?; 10 | } else { 11 | compiler.add_instr(InstructionKind::LoadConst(Value::Unit)); 12 | } 13 | compiler.add_instr(InstructionKind::Return); 14 | Ok(()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/stmt.rs: -------------------------------------------------------------------------------- 1 | use cas_compute::numerical::value::Value; 2 | use cas_error::Error; 3 | use cas_parser::parser::ast::stmt::Stmt; 4 | use crate::{Compile, Compiler, InstructionKind}; 5 | 6 | /// Helper function to compile multiple statements. 7 | pub fn compile_stmts(stmts: &[Stmt], compiler: &mut Compiler) -> Result<(), Error> { 8 | let Some((last, stmts)) = stmts.split_last() else { 9 | // nothing to compile 10 | compiler.add_instr(InstructionKind::LoadConst(Value::Unit)); 11 | return Ok(()); 12 | }; 13 | 14 | compiler.with_state(|state| { 15 | state.last_stmt = false; 16 | }, |compiler| { 17 | stmts.iter() 18 | .try_for_each(|stmt| stmt.compile(compiler)) 19 | })?; 20 | 21 | compiler.with_state(|state| { 22 | state.last_stmt = true; 23 | }, |compiler| { 24 | last.compile(compiler) 25 | }) 26 | } 27 | 28 | impl Compile for Stmt { 29 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 30 | self.expr.compile(compiler)?; 31 | if self.semicolon.is_some() { 32 | compiler.add_instr(InstructionKind::Drop); 33 | compiler.add_instr(InstructionKind::LoadConst(Value::Unit)); 34 | } 35 | if !compiler.state.last_stmt { 36 | compiler.add_instr(InstructionKind::Drop); 37 | } 38 | Ok(()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/sum.rs: -------------------------------------------------------------------------------- 1 | use cas_compute::{numerical::value::Value, primitive::int}; 2 | use cas_error::Error; 3 | use cas_parser::parser::{ast::{RangeKind, Sum}, token::op::BinOpKind}; 4 | use crate::{item::Symbol, Compile, Compiler, InstructionKind}; 5 | 6 | impl Compile for Sum { 7 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 8 | // ``` 9 | // sum n in 1..10 of n 10 | // ``` 11 | // 12 | // equivalent to: 13 | // 14 | // ``` 15 | // { 16 | // out = 0 17 | // for n in 1..10 { 18 | // out += n 19 | // } 20 | // out 21 | // } 22 | // ``` 23 | // 24 | // but there is **no** control flow 25 | 26 | compiler.new_scope(|compiler| { 27 | // initialize the sum to 0 28 | compiler.add_instr(InstructionKind::LoadConst(Value::Integer(int(0)))); 29 | 30 | // compile range end up here so that the index variable isn't in scope, then insert it 31 | // down at the condition 32 | let chunk = compiler.new_chunk_get(|compiler| { 33 | self.range.end.compile(compiler) 34 | })?; 35 | 36 | // assign: initialize index in range 37 | self.range.start.compile(compiler)?; 38 | let symbol_id = compiler.add_symbol(&self.variable)?; 39 | compiler.add_instr(InstructionKind::AssignVar(symbol_id)); 40 | 41 | // condition: continue summing while the variable is in the range: 42 | // `symbol_id < self.range.end` 43 | let condition_start = compiler.new_end_label(); 44 | compiler.add_instr(InstructionKind::LoadVar(Symbol::User(symbol_id))); 45 | compiler.add_chunk_instrs(chunk); 46 | match self.range.kind { 47 | RangeKind::HalfOpen => compiler.add_instr(InstructionKind::Binary(BinOpKind::Less)), 48 | RangeKind::Closed => compiler.add_instr(InstructionKind::Binary(BinOpKind::LessEq)), 49 | } 50 | 51 | let loop_end = compiler.new_unassociated_label(); 52 | compiler.add_instr(InstructionKind::JumpIfFalse(loop_end)); 53 | 54 | // body: compute body, add it to cummulative sum 55 | self.body.compile(compiler)?; 56 | compiler.add_instr(InstructionKind::Binary(BinOpKind::Add)); 57 | 58 | // increment index 59 | compiler.add_instr(InstructionKind::LoadVar(Symbol::User(symbol_id))); 60 | compiler.add_instr(InstructionKind::LoadConst(Value::Integer(int(1)))); 61 | compiler.add_instr(InstructionKind::Binary(BinOpKind::Add)); 62 | compiler.add_instr(InstructionKind::AssignVar(symbol_id)); 63 | 64 | compiler.add_instr(InstructionKind::Jump(condition_start)); 65 | 66 | compiler.set_end_label(loop_end); 67 | Ok(()) 68 | })?; 69 | Ok(()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cas-compiler/src/expr/unary.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use cas_parser::parser::ast::unary::Unary; 3 | use crate::{Compile, Compiler, InstructionKind}; 4 | 5 | impl Compile for Unary { 6 | fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> { 7 | self.operand.compile(compiler)?; 8 | compiler.add_instr_with_spans( 9 | InstructionKind::Unary(self.op.kind), 10 | vec![self.operand.span(), self.op.span.clone()], 11 | ); 12 | Ok(()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cas-compute/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas-compute" 3 | description = "Tools for evaluation of CalcScript expressions" 4 | version = "0.2.0" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/ElectrifyPro/cas-rs" 8 | keywords = ["calcscript", "calculator", "math", "algebra", "evaluation"] 9 | categories = ["mathematics", "data-structures"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [features] 14 | default = ["numerical"] 15 | numerical = [] 16 | mysql = ["mysql_common", "flate2"] 17 | serde = ["dep:serde", "dep:serde_repr", "rug/serde", "cas-parser/serde"] 18 | 19 | [dev-dependencies] 20 | assert_float_eq = "1.1.3" 21 | pretty_assertions = "1.3.0" 22 | 23 | [dependencies.mysql_common] 24 | version = "0.30.6" 25 | default-features = false 26 | features = ["derive"] 27 | optional = true 28 | 29 | [dependencies.serde] 30 | version = "1.0.188" 31 | features = ["derive", "rc"] 32 | optional = true 33 | 34 | [dependencies] 35 | ariadne = "0.2.0" 36 | cas-attrs = { version = "0.2.0", path = "../cas-attrs" } 37 | cas-error = { version = "0.2.0", path = "../cas-error" } 38 | cas-parser = { version = "0.2.0", path = "../cas-parser" } 39 | flate2 = { version = "1.0.27", optional = true } 40 | levenshtein = "1.0.5" 41 | once_cell = "1.18.0" 42 | rand = "0.8.5" 43 | rug = "1.22.0" 44 | serde_repr = { version = "0.1.6", optional = true } 45 | -------------------------------------------------------------------------------- /cas-compute/README.md: -------------------------------------------------------------------------------- 1 | Tools for evaluation of CalcScript expressions. 2 | 3 | This crate provides tools to help evaluate CalcScript code. It is split into two 4 | main submodules, `funcs` and `symbolic`, which provide implementations of useful 5 | mathematical functions, and symbolic evaluation respectively. Currently, 6 | symbolic manipulation is limited to simplification, but this will be extended in 7 | the future. 8 | 9 | # Usage 10 | 11 | ## Function implementations 12 | 13 | The `funcs` module statically implements a variety of algorithms and functions 14 | used in CalcScript, such as trigonometric, combinatoric, and probability 15 | distribution functions. The functions use the arbitrary-precision types from 16 | the [`rug`] crate to ensure that they can handle high-precision calculations. 17 | 18 | To use a function, import it from the `funcs` module and call it with the 19 | appropriate arguments. You may need to convert the arguments to the expected 20 | [`rug`] these conversions. 21 | 22 | ```rust 23 | use cas_compute::funcs::miscellaneous::{Factorial, Gamma}; 24 | use cas_compute::numerical::value::Value; 25 | use cas_compute::primitive::{complex, float}; 26 | 27 | let z = complex((8.0, 0.0)); // 8 + 0i 28 | // let z = complex(8); // equivalent 29 | let gamma_z = Gamma::eval_static(z); 30 | 31 | // Factorial supports floats, so it returns a `Value` for it to support both 32 | // floating-point and integer return types 33 | let factorial_i = Factorial::eval_static(float(7)); // 7! = 5040 34 | 35 | // the effect is that we need to wrap `gamma_z` in a `Value` to compare it with 36 | // factorial_i, or extract a `rug::Complex` from `factorial_i` to compare it 37 | // with `gamma_z` 38 | // 39 | // this will most likely be improved in the future 40 | 41 | assert_eq!(Value::Complex(gamma_z).coerce_integer(), factorial_i); 42 | ``` 43 | 44 | ## Numerical helpers 45 | 46 | The `numerical` module provides helpers used in other `cas-rs` libraries, mainly 47 | the [`Value`] type and [`Value`] formatting. Since version 0.2.0, it no longer 48 | provides numerical evaluation; instead, that functionality was moved to `cas-vm` 49 | as CalcScript became a compiled language. 50 | 51 | ## Simplification 52 | 53 | The `symbolic` module currently provides [`simplify`](symbolic::simplify()) to 54 | simplify expressions symbolically. 55 | 56 | See its [module-level documentation](symbolic) for more information. 57 | 58 | ```rust 59 | use cas_compute::primitive::int; 60 | use cas_compute::symbolic::{expr::{SymExpr, Primary}, simplify}; 61 | use cas_parser::parser::{ast::Expr, Parser}; 62 | 63 | let mut parser = Parser::new("x + x + x"); 64 | let ast_expr = parser.try_parse_full::().unwrap(); 65 | 66 | let simplified = simplify(&ast_expr.into()); 67 | 68 | // `x + x + x = 3x` 69 | assert_eq!(simplified, SymExpr::Mul(vec![ 70 | SymExpr::Primary(Primary::Integer(int(3))), 71 | SymExpr::Primary(Primary::Symbol("x".to_string())), 72 | ])); 73 | ``` 74 | 75 | # Feature flags 76 | 77 | - `mysql`: Derives [`mysql_common`] traits for various types provided by this 78 | crate. 79 | - `serde`: Derives [`Serialize`] and [`Deserialize`] for various types provided 80 | by this crate. 81 | 82 | [`rug`]: https://gitlab.com/tspiteri/rug 83 | [`Value`]: https://docs.rs/cas-compute/latest/cas_compute/numerical/value/enum.Value.html 84 | [`mysql_common`]: https://docs.rs/mysql-common/latest/mysql_common/ 85 | [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html 86 | [`Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html 87 | -------------------------------------------------------------------------------- /cas-compute/src/approx.rs: -------------------------------------------------------------------------------- 1 | use rug::{Float, Integer, Rational}; 2 | use std::cmp::Ordering; 3 | use super::primitive::float; 4 | 5 | /// Computes the [`Rational`] from the continued fraction form of a float. 6 | fn rational_from_continued_fraction(continued_fraction_form: &[Integer]) -> Rational { 7 | let mut rational = Rational::new(); 8 | for (i, integer) in continued_fraction_form.iter().rev().enumerate() { 9 | if i == 0 { 10 | if integer.cmp0() == Ordering::Equal { 11 | continue; 12 | } 13 | rational += Rational::from(Rational::ONE / integer); 14 | } else { 15 | rational = (rational + integer).recip(); 16 | } 17 | } 18 | 19 | if rational.cmp0() == Ordering::Equal { 20 | rational 21 | } else { 22 | rational.recip() 23 | } 24 | } 25 | 26 | /// Approximates the given float as a rational fraction. 27 | /// 28 | /// This function applies the continued fraction algorithm to the given float until the error is 29 | /// less than `1e-60`. 30 | /// 31 | /// We don't use [`Float::to_rational`] because it can produce bad / useless results due to 32 | /// floating point arithmetic errors. Rather, we use the continued fraction algorithm to compute 33 | /// the rational approximation ourselves. 34 | /// 35 | /// See 36 | /// [Wikipedia](https://en.wikipedia.org/wiki/Continued_fraction#Calculating_continued_fraction_representations) 37 | /// for more information. 38 | pub fn approximate_rational(n: &Float) -> Rational { 39 | let orig = n; 40 | 41 | let mut continued_fraction_form = Vec::new(); 42 | let mut n = n.clone(); 43 | loop { 44 | let (integer, fractional) = n.trunc_fract(float(0)); 45 | continued_fraction_form.push(integer.to_integer().unwrap()); 46 | 47 | // check how close we are to the original number 48 | let rational = rational_from_continued_fraction(&continued_fraction_form); 49 | let error = float(orig - rational).abs(); 50 | 51 | if fractional.is_zero() || error < 1e-60 { 52 | break; 53 | } 54 | 55 | n = fractional.recip(); 56 | } 57 | 58 | rational_from_continued_fraction(&continued_fraction_form) 59 | } 60 | -------------------------------------------------------------------------------- /cas-compute/src/consts.rs: -------------------------------------------------------------------------------- 1 | //! Builtin constants used throughout `cas-rs`. 2 | 3 | use once_cell::sync::Lazy; 4 | use rug::{Complex, Float}; 5 | use std::{collections::HashSet, sync::OnceLock}; 6 | use super::primitive::{complex, float}; 7 | 8 | pub static ZERO: Lazy = Lazy::new(|| float(0)); 9 | 10 | pub static ONE: Lazy = Lazy::new(|| float(1)); 11 | 12 | pub static ONE_HALF: Lazy = Lazy::new(|| float(1) / float(2)); 13 | 14 | pub static TWO: Lazy = Lazy::new(|| float(2)); 15 | 16 | pub static TEN: Lazy = Lazy::new(|| float(10)); 17 | 18 | /// The imaginary unit. 19 | pub static I: Lazy = Lazy::new(|| complex((0, 1))); 20 | 21 | /// Euler's number. 22 | pub static E: Lazy = Lazy::new(|| float(1).exp()); 23 | 24 | /// The golden ratio. 25 | pub static PHI: Lazy = Lazy::new(|| (float(1) + float(5).sqrt()) / float(2)); 26 | 27 | pub static PI: Lazy = Lazy::new(|| float(-1).acos()); 28 | 29 | pub static TAU: Lazy = Lazy::new(|| float(2) * &*PI); 30 | 31 | /// Returns a set of all constants provided in a `cas-rs` script. 32 | #[cfg(feature = "numerical")] 33 | pub fn all() -> &'static HashSet<&'static str> { 34 | // NOTE: this is a set and not a map; as a map, we would have to store a `Value` in order to 35 | // support both `Float` and `Complex` constants 36 | // however, at the moment, `Value` is not `Sync` (because of `Value::List`), so we cannot store 37 | // it in a `OnceLock`. this will probably change in the future 38 | static CONSTS: OnceLock> = OnceLock::new(); 39 | CONSTS.get_or_init(|| { 40 | HashSet::from(["i", "e", "phi", "pi", "tau"]) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /cas-compute/src/funcs/angle.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for converting between degrees and radians. 2 | 3 | use cas_attrs::builtin; 4 | use crate::consts::{PI, TAU}; 5 | use rug::Float; 6 | 7 | /// Converts the given value from degrees to radians. 8 | #[derive(Debug)] 9 | pub struct Dtr; 10 | 11 | #[cfg_attr(feature = "numerical", builtin)] 12 | impl Dtr { 13 | pub fn eval_static(n: Float) -> Float { 14 | n * &*PI / 180.0 15 | } 16 | } 17 | 18 | /// Converts the given value from radians to degrees. 19 | #[derive(Debug)] 20 | pub struct Rtd; 21 | 22 | #[cfg_attr(feature = "numerical", builtin)] 23 | impl Rtd { 24 | pub fn eval_static(n: Float) -> Float { 25 | n * 180.0 / &*PI 26 | } 27 | } 28 | 29 | /// Computes the amount of angle needed to traverse a specified fraction of a circle. 30 | /// 31 | /// For example, `circle(0.25)` returns `PI / 2`, since a rotation of `PI / 2` radians is needed to 32 | /// traverse a quarter of a circle. 33 | #[derive(Debug)] 34 | pub struct Circle; 35 | 36 | #[cfg_attr(feature = "numerical", builtin)] 37 | impl Circle { 38 | pub fn eval_static(n: Float) -> Float { 39 | n * &*TAU 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cas-compute/src/funcs/combinatoric.rs: -------------------------------------------------------------------------------- 1 | //! Counting functions. 2 | 3 | use cas_attrs::builtin; 4 | use crate::{funcs::miscellaneous::partial_factorial, primitive::int}; 5 | use rug::Integer; 6 | 7 | /// Combinations function. 8 | /// 9 | /// The returned value can be interepeted in a number of ways: 10 | /// 11 | /// - Returns the number of ways to choose `k` (`r`) items from `n` items, where the order of the 12 | /// items does not matter. 13 | /// - Returns the coefficient of the `x^k` term in the polynomial expansion of `(x + 1)^n`, or the 14 | /// coefficient of the `x^k * y^(n - k)` term in the polynomial expansion of `(x + y)^n`. 15 | /// - Returns the number in row `n` and column `k` of Pascal's triangle. 16 | #[derive(Debug)] 17 | pub struct Ncr; 18 | 19 | #[cfg_attr(feature = "numerical", builtin)] 20 | impl Ncr { 21 | pub fn eval_static(n: Integer, k: Integer) -> Integer { 22 | if k > n { 23 | // TODO: what if k > n, return an error 24 | return Integer::from(0); 25 | } 26 | 27 | let sub = int(&n - &k); 28 | if k > sub { 29 | partial_factorial(n, k) / partial_factorial(sub, int(1)) 30 | } else { 31 | partial_factorial(n, sub) / partial_factorial(k, int(1)) 32 | } 33 | } 34 | } 35 | 36 | /// Permutations function. Returns the number of ways to choose `k` (`r`) items from `n` items, 37 | /// where the order of the items does matter. 38 | #[derive(Debug)] 39 | pub struct Npr; 40 | 41 | #[cfg_attr(feature = "numerical", builtin)] 42 | impl Npr { 43 | pub fn eval_static(n: Integer, k: Integer) -> Integer { 44 | // TODO: report error 45 | 46 | let sub = &n - k; 47 | partial_factorial(n, sub) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cas-compute/src/funcs/complex.rs: -------------------------------------------------------------------------------- 1 | //! Useful functions for complex numbers. 2 | 3 | use cas_attrs::builtin; 4 | use rug::{Complex, Float}; 5 | 6 | /// Returns the real part of the given complex number. 7 | #[derive(Debug)] 8 | pub struct Re; 9 | 10 | #[cfg_attr(feature = "numerical", builtin)] 11 | impl Re { 12 | pub fn eval_static(n: Complex) -> Float { 13 | n.into_real_imag().0 14 | } 15 | } 16 | 17 | /// Returns the imaginary part of the given complex number. 18 | #[derive(Debug)] 19 | pub struct Im; 20 | 21 | #[cfg_attr(feature = "numerical", builtin)] 22 | impl Im { 23 | pub fn eval_static(n: Complex) -> Float { 24 | n.into_real_imag().1 25 | } 26 | } 27 | 28 | /// Returns the argument of the given complex number, in radians. 29 | #[derive(Debug)] 30 | pub struct Arg; 31 | 32 | #[cfg_attr(feature = "numerical", builtin(radian = output))] 33 | impl Arg { 34 | pub fn eval_static(n: Complex) -> Float { 35 | n.arg().into_real_imag().0 // `n`.arg() returns Complex interestingly 36 | } 37 | } 38 | 39 | /// Returns the complex conjugate of the given complex number. 40 | #[derive(Debug)] 41 | pub struct Conj; 42 | 43 | #[cfg_attr(feature = "numerical", builtin)] 44 | impl Conj { 45 | pub fn eval_static(n: Complex) -> Complex { 46 | n.conj() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cas-compute/src/funcs/mod.rs: -------------------------------------------------------------------------------- 1 | //! All built-in functions provided by the numerical and symbolic libraries. 2 | //! 3 | //! Each function is implemented as a unit `struct` with an associated `eval_static` method. This 4 | //! method can be used to evaluate the function in Rust code if the types of the arguments are 5 | //! known at compile time. 6 | //! 7 | //! If the `numerical` feature is enabled, the [`Builtin`] trait (provided by the feature) is also 8 | //! implemented for each function, enabling the function to be evaluated with arbitrary arguments 9 | //! at runtime after type checking. See the `cas-vm` crate for more information on runtime 10 | //! evaluation. 11 | //! 12 | //! # Example 13 | //! 14 | //! ``` 15 | //! use cas_compute::consts::PI; 16 | //! use cas_compute::funcs::trigonometry::Sin; 17 | //! use cas_compute::primitive::complex; 18 | //! 19 | //! // evaluate sin(pi / 2) using `eval_static` 20 | //! let result = Sin::eval_static(complex(&*PI) / 2.0); 21 | //! println!("sin(pi / 2) = {}", result); 22 | //! 23 | //! // see `cas-vm` for runtime evaluation of this example 24 | //! ``` 25 | 26 | pub mod angle; 27 | pub mod complex; 28 | pub mod combinatoric; 29 | pub mod miscellaneous; 30 | pub mod power; 31 | pub mod print; 32 | pub mod probability; // TODO: add poison distribution 33 | pub mod round; 34 | pub mod sequence; 35 | pub mod trigonometry; 36 | 37 | #[cfg(feature = "numerical")] 38 | use crate::numerical::builtin::Builtin; 39 | 40 | #[cfg(feature = "numerical")] 41 | use std::collections::HashMap; 42 | 43 | use std::sync::OnceLock; 44 | 45 | /// Returns a list of all builtin functions that can be numerically evaluated. 46 | #[cfg(feature = "numerical")] 47 | pub fn all() -> &'static HashMap<&'static str, Box> { 48 | use angle::*; 49 | use complex::*; 50 | use combinatoric::*; 51 | use miscellaneous::*; 52 | use power::*; 53 | use print::*; 54 | use probability::*; 55 | use round::*; 56 | use sequence::*; 57 | use trigonometry::*; 58 | 59 | static FUNCTIONS: OnceLock>> = OnceLock::new(); 60 | 61 | macro_rules! build { 62 | ($($name:literal $upname:ident),* $(,)?) => { 63 | [ 64 | $( 65 | ($name, Box::new($upname) as Box), 66 | )* 67 | ] 68 | .into_iter() 69 | .collect() 70 | }; 71 | } 72 | 73 | FUNCTIONS.get_or_init(|| 74 | build! { 75 | "print" Print, 76 | "sin" Sin, 77 | "cos" Cos, 78 | "tan" Tan, 79 | "csc" Csc, 80 | "sec" Sec, 81 | "cot" Cot, 82 | "asin" Asin, 83 | "acos" Acos, 84 | "atan" Atan, 85 | "atan2" Atan2, 86 | "acsc" Acsc, 87 | "asec" Asec, 88 | "acot" Acot, 89 | "sinh" Sinh, 90 | "cosh" Cosh, 91 | "tanh" Tanh, 92 | "csch" Csch, 93 | "sech" Sech, 94 | "coth" Coth, 95 | "asinh" Asinh, 96 | "acosh" Acosh, 97 | "atanh" Atanh, 98 | "acsch" Acsch, 99 | "asech" Asech, 100 | "acoth" Acoth, 101 | "dtr" Dtr, 102 | "rad" Dtr, // intentional alias for dtr 103 | "rtd" Rtd, 104 | "deg" Rtd, // intentional alias for rtd 105 | "circle" Circle, 106 | "exp" Exp, 107 | "ln" Ln, 108 | "log" Log, 109 | "scientific" Scientific, 110 | "pow" Pow, 111 | "sqrt" Sqrt, 112 | "cbrt" Cbrt, 113 | "root" Root, 114 | "hypot" Hypot, 115 | "re" Re, 116 | "im" Im, 117 | "arg" Arg, 118 | "conj" Conj, 119 | "fib" Fib, 120 | "ncr" Ncr, 121 | "npr" Npr, 122 | "erf" Erf, 123 | "erfc" Erfc, 124 | "inverf" Inverf, 125 | "normpdf" Normpdf, 126 | "normcdf" Normcdf, 127 | "invnorm" Invnorm, 128 | "geompdf" Geompdf, 129 | "geomcdf" Geomcdf, 130 | "binompdf" Binompdf, 131 | "binomcdf" Binomcdf, 132 | "poisspdf" Poisspdf, 133 | "poisscdf" Poisscdf, 134 | "siground" Siground, 135 | "round" Round, 136 | "ceil" Ceil, 137 | "floor" Floor, 138 | "trunc" Trunc, 139 | "abs" Abs, 140 | "bool" Bool, 141 | "rand" Rand, 142 | "factorial" Factorial, 143 | "gamma" Gamma, 144 | "lerp" Lerp, 145 | "invlerp" Invlerp, 146 | "min" Min, 147 | "max" Max, 148 | "clamp" Clamp, 149 | "gcf" Gcf, 150 | "lcm" Lcm, 151 | "sign" Sign, 152 | "size" Size, 153 | } 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /cas-compute/src/funcs/power.rs: -------------------------------------------------------------------------------- 1 | //! Functions related to powers, exponentiation, and roots. 2 | 3 | use cas_attrs::builtin; 4 | use crate::consts::{I, TAU, TEN}; 5 | use crate::primitive::{complex, float}; 6 | use rug::{ops::Pow as _, Complex, Float}; 7 | 8 | /// Builds a function whose `rug` implementation has the same name as the given function. 9 | macro_rules! simple { 10 | ($($name:ident $upname:ident; $doc:literal),* $(,)?) => { 11 | $( 12 | #[doc = $doc] 13 | #[derive(Debug)] 14 | pub struct $upname; 15 | 16 | #[cfg_attr(feature = "numerical", builtin)] 17 | impl $upname { 18 | pub fn eval_static(n: Complex) -> Complex { 19 | n.$name() 20 | } 21 | } 22 | )* 23 | }; 24 | } 25 | 26 | /// Returns `arg[0] * 10 ^ arg[1]`. This provides a convenient way to express numbers in scientific 27 | /// notation. 28 | #[derive(Debug)] 29 | pub struct Scientific; 30 | 31 | #[cfg_attr(feature = "numerical", builtin)] 32 | impl Scientific { 33 | pub fn eval_static(a: Complex, b: Complex) -> Complex { 34 | a * complex(&*TEN).pow(b) 35 | } 36 | } 37 | 38 | /// The logarithm function to an arbitrary base, `log(x, base = 10)`. 39 | #[derive(Debug)] 40 | pub struct Log; 41 | 42 | #[cfg_attr(feature = "numerical", builtin)] 43 | impl Log { 44 | pub fn eval_static(n: Complex, base: Option) -> Complex { 45 | let base = base.unwrap_or(complex(&*TEN)); 46 | n.ln() / base.ln() 47 | } 48 | } 49 | 50 | /// Basic power function, `pow(x, y)`. 51 | #[derive(Debug)] 52 | pub struct Pow; 53 | 54 | #[cfg_attr(feature = "numerical", builtin)] 55 | impl Pow { 56 | pub fn eval_static(x: Complex, y: Complex) -> Complex { 57 | x.pow(y) 58 | } 59 | } 60 | 61 | /// The cube root function, `cbrt(x)`. 62 | /// 63 | /// This function returns the principal cube root of `x`. 64 | #[derive(Debug)] 65 | pub struct Cbrt; 66 | 67 | #[cfg_attr(feature = "numerical", builtin)] 68 | impl Cbrt { 69 | pub fn eval_static(n: Complex) -> Complex { 70 | let one_third = float(3.0).recip(); 71 | if n.real().is_sign_positive() { 72 | n.pow(one_third) 73 | } else { 74 | // alternate form for negative numbers which chooses the branch closest to the real axis 75 | let (abs, arg) = ( 76 | complex(n.abs_ref()).into_real_imag().0, 77 | n.arg().into_real_imag().0, 78 | ); 79 | let lhs = abs.cbrt(); 80 | let rhs = complex(one_third * (arg + &*TAU) * &*I).exp(); 81 | lhs * rhs 82 | } 83 | } 84 | } 85 | 86 | /// Returns the `n`th root of `x`. 87 | #[derive(Debug)] 88 | pub struct Root; 89 | 90 | #[cfg_attr(feature = "numerical", builtin)] 91 | impl Root { 92 | pub fn eval_static(x: Complex, n: Complex) -> Complex { 93 | x.pow(n.recip()) 94 | } 95 | } 96 | 97 | /// Returns the hypothenuse of a right triangle with sides `a` and `b`. 98 | #[derive(Debug)] 99 | pub struct Hypot; 100 | 101 | #[cfg_attr(feature = "numerical", builtin)] 102 | impl Hypot { 103 | pub fn eval_static(a: Float, b: Float) -> Float { 104 | a.hypot(&b) 105 | } 106 | } 107 | 108 | simple! { 109 | exp Exp; "The exponential function, `e ^ x`.", 110 | ln Ln; "The natural logarithm, `ln(x)`.", 111 | sqrt Sqrt; "The square root function, `sqrt(x)`.", 112 | } 113 | -------------------------------------------------------------------------------- /cas-compute/src/funcs/print.rs: -------------------------------------------------------------------------------- 1 | use cas_attrs::builtin; 2 | use crate::numerical::value::Value; 3 | 4 | /// Prints the given value to stdout with a trailing newline. 5 | #[derive(Debug)] 6 | pub struct Print; 7 | 8 | #[cfg_attr(feature = "numerical", builtin)] 9 | impl Print { 10 | pub fn eval_static(v: &Value) { 11 | println!("{}", v); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cas-compute/src/funcs/round.rs: -------------------------------------------------------------------------------- 1 | //! Rounding functions. 2 | 3 | use cas_attrs::builtin; 4 | use crate::primitive::{complex, float}; 5 | use rug::{ops::Pow, Complex, Float, Integer}; 6 | 7 | /// Build a rounding function. 8 | macro_rules! build_rounding { 9 | ($($name:ident $upname:ident; $doc:literal),* $(,)?) => { 10 | $( 11 | #[doc = $doc] 12 | #[derive(Debug)] 13 | pub struct $upname; 14 | 15 | #[cfg_attr(feature = "numerical", builtin)] 16 | impl $upname { 17 | pub fn eval_static(n: Complex, s: Option) -> Complex { 18 | fn inner(n: Float, s: &Float) -> Float { 19 | (n / s).$name() * s 20 | } 21 | 22 | let (real, imag) = n.into_real_imag(); 23 | let s = s.unwrap_or_else(|| float(1)); 24 | complex((inner(real, &s), inner(imag, &s))) 25 | } 26 | } 27 | )* 28 | }; 29 | } 30 | 31 | /// Rounds a number to a given number of significant digits. 32 | #[derive(Debug)] 33 | pub struct Siground; 34 | 35 | #[cfg_attr(feature = "numerical", builtin)] 36 | impl Siground { 37 | pub fn eval_static(n: Complex, d: Integer) -> Complex { 38 | fn inner(n: Float, d: &Integer) -> Float { 39 | if n.is_zero() { 40 | return n; 41 | } 42 | 43 | let num_digits = float(n.abs_ref()).log10().ceil().to_integer().unwrap(); 44 | let power = d - num_digits; 45 | 46 | let magnitude = float(10).pow(&power); 47 | let shifted = (n * &magnitude).round(); 48 | shifted / magnitude 49 | } 50 | 51 | let (real, imag) = n.into_real_imag(); 52 | complex((inner(real, &d), inner(imag, &d))) 53 | } 54 | } 55 | 56 | build_rounding! { 57 | round Round; "Round a number to the nearest integer.", 58 | ceil Ceil; "Round a number up to the nearest integer.", 59 | floor Floor; "Round a number down to the nearest integer.", 60 | trunc Trunc; "Rounds a number towards zero.", 61 | } 62 | -------------------------------------------------------------------------------- /cas-compute/src/funcs/sequence.rs: -------------------------------------------------------------------------------- 1 | //! Functions to determine the specified term of particular sequences. 2 | 3 | use cas_attrs::builtin; 4 | use crate::primitive::int; 5 | use rug::Integer; 6 | 7 | /// Returns the `n`th term of the Fibonacci sequence, using a fast doubling algorithm with `O(log 8 | /// n)` complexity, extended to support negative indices. 9 | /// 10 | /// The implementation considers `fib(1) = fib(2) = 1`. 11 | #[derive(Debug)] 12 | pub struct Fib; 13 | 14 | #[cfg_attr(feature = "numerical", builtin)] 15 | impl Fib { 16 | pub fn eval_static(n: Integer) -> Integer { 17 | let is_negative = n < 0; 18 | let is_even = n.is_even(); 19 | 20 | let mut stack = vec![n.abs()]; 21 | while let Some(last) = stack.last() { 22 | if *last > 1 { 23 | stack.push(int(last) / 2); 24 | } else { 25 | break; 26 | } 27 | } 28 | 29 | let (mut a, mut b) = (int(0), int(1)); 30 | while let Some(next) = stack.pop() { 31 | let c = (int(2) * &b - &a) * &a; 32 | let d = int(&a * &a) + int(&b * &b); 33 | if next.is_even() { 34 | (a, b) = (c, d); 35 | } else { 36 | (b, a) = (c + &d, d); 37 | } 38 | } 39 | 40 | if is_negative && is_even { 41 | -a 42 | } else { 43 | a 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cas-compute/src/funcs/trigonometry.rs: -------------------------------------------------------------------------------- 1 | //! General trigonometric and hyperbolic trigonometric functions. 2 | 3 | use cas_attrs::builtin; 4 | use rug::{Complex, Float}; 5 | 6 | /// Build a trigonometric function that takes in a single radian input argument. 7 | macro_rules! build_radian_input { 8 | ($($name:ident $upname:ident; $func:expr),* $(,)?) => { 9 | $( 10 | #[derive(Debug)] 11 | pub struct $upname; 12 | 13 | #[cfg_attr(feature = "numerical", builtin(radian = input))] 14 | impl $upname { 15 | pub fn eval_static(n: Complex) -> Complex { 16 | // NOTE: the closure call is contained within the macro, so we allow 17 | // the clippy::redundant_closure_call lint 18 | #[allow(clippy::redundant_closure_call)] 19 | ($func)(n) 20 | } 21 | } 22 | )* 23 | }; 24 | } 25 | 26 | /// Build a trigonometric function that takes in a single input argument and returns a radian 27 | /// value. 28 | macro_rules! build_radian_output { 29 | ($($name:ident $upname:ident; $func:expr),* $(,)?) => { 30 | $( 31 | #[derive(Debug)] 32 | pub struct $upname; 33 | 34 | #[cfg_attr(feature = "numerical", builtin(radian = output))] 35 | impl $upname { 36 | pub fn eval_static(n: Complex) -> Complex { 37 | #[allow(clippy::redundant_closure_call)] 38 | ($func)(n) 39 | } 40 | } 41 | )* 42 | }; 43 | } 44 | 45 | /// Build a hyperbolic trigonometric function. 46 | macro_rules! build_hyperbolic { 47 | ($($name:ident $upname:ident; $func:expr),* $(,)?) => { 48 | $( 49 | #[derive(Debug)] 50 | pub struct $upname; 51 | 52 | #[cfg_attr(feature = "numerical", builtin)] 53 | impl $upname { 54 | pub fn eval_static(n: Complex) -> Complex { 55 | #[allow(clippy::redundant_closure_call)] 56 | ($func)(n) 57 | } 58 | } 59 | )* 60 | }; 61 | } 62 | 63 | #[cfg(feature = "numerical")] 64 | build_radian_input! { 65 | sin Sin; Complex::sin, 66 | cos Cos; Complex::cos, 67 | tan Tan; Complex::tan, 68 | csc Csc; |n: Complex| n.sin().recip(), // csc is implemented for Float but not Complex 69 | sec Sec; |n: Complex| n.cos().recip(), 70 | cot Cot; |n: Complex| n.tan().recip(), 71 | } 72 | 73 | #[cfg(feature = "numerical")] 74 | build_radian_output! { 75 | asin Asin; Complex::asin, 76 | acos Acos; Complex::acos, 77 | atan Atan; Complex::atan, 78 | acsc Acsc; |n: Complex| n.recip().asin(), // similar to csc, etc. 79 | asec Asec; |n: Complex| n.recip().acos(), 80 | acot Acot; |n: Complex| n.recip().atan(), 81 | } 82 | 83 | #[cfg(feature = "numerical")] 84 | build_hyperbolic! { 85 | sinh Sinh; Complex::sinh, 86 | cosh Cosh; Complex::cosh, 87 | tanh Tanh; Complex::tanh, 88 | csch Csch; |n: Complex| n.sinh().recip(), 89 | sech Sech; |n: Complex| n.cosh().recip(), 90 | coth Coth; |n: Complex| n.tanh().recip(), 91 | asinh Asinh; Complex::asinh, 92 | acosh Acosh; Complex::acosh, 93 | atanh Atanh; Complex::atanh, 94 | acsch Acsch; |n: Complex| n.recip().asinh(), 95 | asech Asech; |n: Complex| n.recip().acosh(), 96 | acoth Acoth; |n: Complex| n.recip().atanh(), 97 | } 98 | 99 | #[derive(Debug)] 100 | pub struct Atan2; 101 | 102 | #[cfg_attr(feature = "numerical", builtin(radian = output))] 103 | impl Atan2 { 104 | pub fn eval_static(y: Float, x: &Float) -> Float { 105 | y.atan2(x) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cas-compute/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | pub mod approx; 4 | pub mod consts; 5 | pub mod funcs; 6 | pub mod numerical; 7 | pub mod primitive; 8 | pub mod symbolic; 9 | -------------------------------------------------------------------------------- /cas-compute/src/numerical/builtin/error/check.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | /// Too many arguments were given to a function call. 4 | #[derive(Debug)] 5 | pub struct TooManyArguments { 6 | /// The name of the function that was called. 7 | pub name: &'static str, 8 | 9 | /// The number of arguments that were expected. 10 | pub expected: usize, 11 | 12 | /// The number of arguments that were given. 13 | pub given: usize, 14 | 15 | /// The signature of the function. 16 | pub signature: &'static str, 17 | } 18 | 19 | /// An argument to a function call is missing. 20 | #[derive(Debug)] 21 | pub struct MissingArgument { 22 | /// The name of the function that was called. 23 | pub name: &'static str, 24 | 25 | /// The indices of the missing arguments. 26 | pub indices: Range, 27 | 28 | /// The number of arguments that were expected. 29 | pub expected: usize, 30 | 31 | /// The number of arguments that were given. 32 | pub given: usize, 33 | 34 | /// The signature of the function. 35 | pub signature: &'static str, 36 | } 37 | 38 | /// An argument to a function call has the wrong type. 39 | #[derive(Debug)] 40 | pub struct TypeMismatch { 41 | /// The name of the function that was called. 42 | pub name: &'static str, 43 | 44 | /// The index of the argument that was mismatched. 45 | pub index: usize, 46 | 47 | /// The type of the argument that was expected. 48 | pub expected: &'static str, 49 | 50 | /// The type of the argument that was given. 51 | pub given: &'static str, 52 | 53 | /// The signature of the function, not including the function name. 54 | pub signature: &'static str, 55 | } 56 | -------------------------------------------------------------------------------- /cas-compute/src/numerical/builtin/error/func_specific.rs: -------------------------------------------------------------------------------- 1 | use cas_attrs::ErrorKind; 2 | use cas_error::ErrorKind; 3 | use cas_parser::parser::ast::call::Call; 4 | use crate::numerical::builtin::BuiltinError; 5 | use std::ops::Range; 6 | 7 | /// Represents an error specific to a builtin function. 8 | #[derive(Debug)] 9 | pub enum FunctionSpecific { 10 | /// Errors for the `ncr` and `npr` builtin function. 11 | Ncpr(NcprError), 12 | } 13 | 14 | impl FunctionSpecific { 15 | /// Get the spans for the error. 16 | pub fn spans(&self, call: &Call) -> Vec> { 17 | match self { 18 | FunctionSpecific::Ncpr(e) => e.spans(call), 19 | } 20 | } 21 | 22 | /// Convert the [`FunctionSpecific`] into an [`ErrorKind`]. 23 | pub fn into_kind(self) -> Box { 24 | match self { 25 | FunctionSpecific::Ncpr(e) => Box::new(e), 26 | } 27 | } 28 | } 29 | 30 | /// Errors for the `ncr` and `npr` builtin function. 31 | #[derive(Debug, Clone, Copy, ErrorKind, PartialEq, Eq)] 32 | #[error( 33 | message = format!("incorrect arguments for the `{}` function", self.function_name), 34 | labels = if self.error == NcprErrorKind::NLessThanK { 35 | [ 36 | "this function call", 37 | "", 38 | "(1) argument `n`...", 39 | "(2) ...must be less than or equal to argument `k`", 40 | ].iter() 41 | } else { 42 | [ 43 | "this function call", 44 | "", 45 | "argument `k` must be positive", 46 | ].iter() 47 | } 48 | )] 49 | pub struct NcprError { 50 | /// The specific function name. 51 | pub function_name: &'static str, 52 | 53 | /// The error that occurred. 54 | pub error: NcprErrorKind, 55 | } 56 | 57 | impl NcprError { 58 | pub fn new(function_name: &'static str, error: NcprErrorKind) -> Self { 59 | Self { function_name, error } 60 | } 61 | } 62 | 63 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 64 | pub enum NcprErrorKind { 65 | /// The first argument is less than the second. 66 | NLessThanK, 67 | 68 | /// One or both of the arguments is negative. 69 | NegativeArgs, 70 | } 71 | 72 | impl NcprError { 73 | fn spans(&self, call: &Call) -> Vec> { 74 | let mut this_function_call = call.outer_span().to_vec(); 75 | match self.error { 76 | NcprErrorKind::NLessThanK => { 77 | this_function_call.extend(call.args.iter().map(|arg| arg.span())); 78 | }, 79 | NcprErrorKind::NegativeArgs => { 80 | this_function_call.push(call.args[1].span()); 81 | }, 82 | }; 83 | this_function_call 84 | } 85 | } 86 | 87 | impl From for BuiltinError { 88 | fn from(e: NcprError) -> Self { 89 | BuiltinError::FunctionSpecific(FunctionSpecific::Ncpr(e)) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cas-compute/src/numerical/builtin/error/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod check; 2 | pub mod func_specific; 3 | 4 | pub use check::{MissingArgument, TooManyArguments, TypeMismatch}; 5 | pub use func_specific::FunctionSpecific; 6 | 7 | /// Represents an error that can occur while evaluating a builtin function. 8 | #[derive(Debug)] 9 | pub enum BuiltinError { 10 | /// The function was called with too many arguments. 11 | TooManyArguments(TooManyArguments), 12 | 13 | /// An argument to the function call is missing. 14 | MissingArgument(MissingArgument), 15 | 16 | /// The function was called with a mismatched argument type. 17 | TypeMismatch(TypeMismatch), 18 | 19 | /// A function specific error. 20 | FunctionSpecific(FunctionSpecific), 21 | } 22 | -------------------------------------------------------------------------------- /cas-compute/src/numerical/builtin/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | 3 | use error::BuiltinError; 4 | use super::{trig_mode::TrigMode, value::Value}; 5 | 6 | /// A function parameter to a builtin function. 7 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 8 | pub struct BuiltinParam { 9 | /// The name of the parameter. 10 | pub name: &'static str, 11 | 12 | /// Whether the parameter is required or optional. 13 | pub kind: ParamKind, 14 | 15 | /// The typename of the parameter. 16 | pub typename: Option<&'static str>, 17 | } 18 | 19 | /// The kind of the function parameter; either required or optional. 20 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 21 | pub enum ParamKind { 22 | Required, 23 | Optional, 24 | } 25 | 26 | /// A trait implemented by all builtin functions. 27 | pub trait Builtin: std::fmt::Debug + Send + Sync { 28 | /// Returns the name of the function. 29 | // NOTE: this is a `&self` method and not an associated constant to make the trait object-safe 30 | fn name(&self) -> &'static str; 31 | 32 | /// The function's signature, indicating all parameters, whether they are required or optional, 33 | /// and the expected typenames. 34 | fn sig(&self) -> &'static [BuiltinParam]; 35 | 36 | /// The function's signature as a string, used for error messages. 37 | fn sig_str(&self) -> &'static str; 38 | 39 | /// Evaluates the function. 40 | fn eval(&self, trig_mode: TrigMode, args: Vec) -> Result; 41 | } 42 | 43 | impl Builtin for &'static dyn Builtin { 44 | fn name(&self) -> &'static str { 45 | (**self).name() 46 | } 47 | 48 | fn sig(&self) -> &'static [BuiltinParam] { 49 | (**self).sig() 50 | } 51 | 52 | fn sig_str(&self) -> &'static str { 53 | (**self).sig_str() 54 | } 55 | 56 | fn eval(&self, trig_mode: TrigMode, args: Vec) -> Result { 57 | (**self).eval(trig_mode, args) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cas-compute/src/numerical/fmt/complex.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions to format complex numbers. 2 | 3 | use rug::{Complex, Float}; 4 | use std::fmt::Formatter; 5 | use super::{float, FormatOptions, NumberFormat}; 6 | 7 | /// Helper function to format the imaginary part of the complex number, with or without 8 | /// parentheses. 9 | fn fmt_helper(f: &mut Formatter<'_>, n: &Float, options: FormatOptions) -> std::fmt::Result { 10 | if n == &1 { 11 | return write!(f, "i"); 12 | } else if n == &-1 { 13 | // NOTE: this path is impossible in the `fmt` function, which makes the input to this 14 | // function positive 15 | return write!(f, "-i"); 16 | } 17 | 18 | // if we need to format this part in scientific notation, we need to add parentheses 19 | // around it to avoid ambiguity 20 | // we need parentheses if the user specifies scientific notation, or if 21 | // `NumberFormat::Auto` decides it's time 22 | if options.number == NumberFormat::Scientific 23 | || options.number == NumberFormat::Auto && float::should_use_scientific(n) 24 | { 25 | write!(f, "(")?; 26 | float::fmt_scientific(f, n, options)?; 27 | write!(f, ")")?; 28 | } else { 29 | float::fmt(f, n, options)?; 30 | } 31 | 32 | write!(f, "i") 33 | } 34 | 35 | /// Formats a complex number. 36 | pub fn fmt(f: &mut Formatter<'_>, c: &Complex, options: FormatOptions) -> std::fmt::Result { 37 | let (re, im) = (c.real(), c.imag()); 38 | 39 | // for the standard notation, real part comes first, then imaginary 40 | // four possible combinations: 41 | // 1. real and imaginary exist (i.e. are non-zero) 42 | // 2. only real exists 43 | // 3. only imaginary exists 44 | // 4. neither real nor imaginary exist (i.e. zero) 45 | match (re.is_zero(), im.is_zero()) { 46 | (false, false) => { 47 | // write real part 48 | float::fmt(f, re, options)?; 49 | 50 | // write imaginary part 51 | if im.is_sign_positive() { 52 | write!(f, " + ")?; 53 | fmt_helper(f, im, options)?; 54 | } else { 55 | write!(f, " - ")?; 56 | fmt_helper(f, &im.as_neg(), options)?; 57 | } 58 | 59 | Ok(()) 60 | }, 61 | (false, true) => float::fmt(f, re, options), 62 | (true, false) => fmt_helper(f, im, options), 63 | (true, true) => write!(f, "0"), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cas-compute/src/numerical/func.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use super::{builtin::Builtin, value::Value}; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 6 | 7 | #[cfg(feature = "serde")] 8 | fn serialize_builtin(builtin: &dyn Builtin, serializer: S) -> Result 9 | where 10 | S: Serializer, 11 | { 12 | serializer.serialize_str(builtin.name()) 13 | } 14 | 15 | #[cfg(feature = "serde")] 16 | fn deserialize_builtin<'de, 'a, D>(deserializer: D) -> Result<&'a dyn Builtin, D::Error> 17 | where 18 | D: Deserializer<'de>, 19 | { 20 | let name = String::deserialize(deserializer)?; 21 | let functions = crate::funcs::all(); 22 | functions.get(name.as_str()) 23 | .map(|f| &**f) 24 | .ok_or_else(|| serde::de::Error::custom("unknown function")) 25 | } 26 | 27 | /// A function. 28 | /// 29 | /// Functions are treated as values just like any other value in `cas-rs`; they can be stored 30 | /// in variables, passed as arguments to other functions, and returned from functions. 31 | #[derive(Debug, Clone)] 32 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 33 | pub enum Function { 34 | /// A user-defined function. 35 | /// 36 | /// The inner value is a `usize` that represents the index of the function's chunk. 37 | User(User), 38 | 39 | /// A built-in function. 40 | #[cfg_attr(feature = "serde", serde( 41 | serialize_with = "serialize_builtin", 42 | deserialize_with = "deserialize_builtin" 43 | ))] 44 | Builtin(&'static dyn Builtin), 45 | } 46 | 47 | /// A user-defined function. 48 | #[derive(Debug, Clone, PartialEq)] 49 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 50 | pub struct User { 51 | /// The index of the function's chunk. 52 | /// 53 | /// TODO: comparing index is not enough to compare two functions 54 | pub index: usize, 55 | 56 | /// The variables captured by the function from the environment. 57 | /// 58 | /// This is determined at compile time. 59 | pub captures: HashSet, 60 | 61 | /// The values of the variables in the function's environment at the time of the function's 62 | /// creation. 63 | /// 64 | /// This is determined at runtime. 65 | pub environment: HashMap, 66 | } 67 | 68 | impl User { 69 | /// Creates a new user-defined function with the given chunk index an captured variables. 70 | pub fn new(index: usize, captures: HashSet) -> Self { 71 | Self { index, captures, environment: HashMap::new() } 72 | } 73 | } 74 | 75 | /// Manual implementation of [`PartialEq`] to support `dyn Builtin` by comparing pointers. 76 | impl PartialEq for Function { 77 | fn eq(&self, other: &Self) -> bool { 78 | match (self, other) { 79 | (Self::User(a), Self::User(b)) => a == b, 80 | (Self::Builtin(a), Self::Builtin(b)) => std::ptr::eq(*a, *b), 81 | _ => false, 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /cas-compute/src/numerical/mod.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for numerical evaluation of CalcScript expressions. 2 | //! 3 | //! If you are looking to evaluate CalcScript expressions, see the related 4 | //! [`cas-vm`](https://crates.io/crates/cas-vm) crate instead. Since version `cas-compute` 0.2.0, 5 | //! this crate no longer provides this functionality. 6 | //! 7 | //! # Usage 8 | //! 9 | //! This crate contains tools to help you evaluate CalcScript expressions. It is meant to be used 10 | //! in conjunction with [`cas-vm`], a virtual machine that executes CalcScript code, to tweak 11 | //! evaluation and extract results. 12 | //! 13 | //! When interacting with [`cas-vm`], you'll mostly come into contact with the [`Value`] type, 14 | //! which packages any value representable in the CalcScript language, including integers, floats, 15 | //! complex numbers, lists, functions, and more. [`Value`] contains a number of convenience methods 16 | //! to manipulate and convert values between types, as well as formatting them for display. 17 | //! 18 | //! [`cas-vm`]: https://crates.io/crates/cas-vm 19 | 20 | #![cfg(feature = "numerical")] 21 | 22 | pub mod builtin; 23 | pub mod fmt; 24 | pub mod func; 25 | pub mod trig_mode; 26 | pub mod value; 27 | 28 | pub use value::Value; 29 | -------------------------------------------------------------------------------- /cas-compute/src/numerical/trig_mode.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "mysql")] 2 | use mysql_common::prelude::FromValue; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde_repr::{Deserialize_repr, Serialize_repr}; 6 | 7 | /// The trigonometric mode of a context. This will affect the evaluation of input to trigonometric 8 | /// functions, and output from trigonometric functions. 9 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] 10 | #[cfg_attr(feature = "mysql", derive(FromValue))] 11 | #[cfg_attr(feature = "mysql", mysql(is_integer))] 12 | #[cfg_attr(feature = "serde", derive(Serialize_repr, Deserialize_repr))] 13 | #[repr(u8)] 14 | pub enum TrigMode { 15 | /// Use radians. 16 | #[default] 17 | Radians, 18 | 19 | /// Use degrees. 20 | Degrees, 21 | } 22 | 23 | impl std::fmt::Display for TrigMode { 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 | match self { 26 | TrigMode::Radians => write!(f, "radians"), 27 | TrigMode::Degrees => write!(f, "degrees"), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cas-compute/src/primitive.rs: -------------------------------------------------------------------------------- 1 | //! Functions to construct [`Integer`]s, [`Float`]s, and [`Complex`] numbers from various types. 2 | 3 | use cas_parser::parser::ast::literal::DIGITS; 4 | use rug::{ops::Pow, Assign, Complex, Float, Integer}; 5 | 6 | /// The number of digits of precision to use when computing values. 7 | pub const PRECISION: u32 = 1 << 9; 8 | 9 | /// Creates an [`Integer`] with the given value. 10 | pub fn int(n: T) -> Integer 11 | where 12 | Integer: From, 13 | { 14 | Integer::from(n) 15 | } 16 | 17 | /// Creates an [`Integer`] from a [`Float`] by truncating the fractional part. 18 | pub fn int_from_float(f: Float) -> Integer { 19 | // TODO: can panic if NaN 20 | f.trunc().to_integer().unwrap() 21 | } 22 | 23 | /// Creates an [`Integer`] from a string slice. 24 | pub fn int_from_str(s: &str) -> Integer { 25 | Integer::from_str_radix(s, 10).unwrap() 26 | } 27 | 28 | /// Creates a [`Float`] with the given value. 29 | pub fn float(n: T) -> Float 30 | where 31 | Float: Assign, 32 | { 33 | Float::with_val(PRECISION, n) 34 | } 35 | 36 | /// Creates a [`Float`] from a string slice. 37 | pub fn float_from_str(s: &str) -> Float { 38 | Float::with_val(PRECISION, Float::parse(s).unwrap()) 39 | } 40 | 41 | /// Parses a number from a string, with the given radix. The radix must be between 2 and 64, 42 | /// inclusive. 43 | /// 44 | /// TODO: Replace panics with errors 45 | pub fn from_str_radix(s: &str, radix: u8) -> Integer { 46 | let mut result = int(0); 47 | let allowed_digits = &DIGITS[..radix as usize]; 48 | 49 | let radix = int(radix); 50 | for (i, c) in s.chars().rev().enumerate() { 51 | let digit = int(allowed_digits.iter().position(|&d| d == c).unwrap()); 52 | result += digit * int((&radix).pow(i as u32)); 53 | } 54 | 55 | result 56 | } 57 | 58 | /// Creates a [`Complex`] with the given value. 59 | pub fn complex(n: T) -> Complex 60 | where 61 | Complex: Assign, 62 | { 63 | Complex::with_val(PRECISION, n) 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn radix_eval() { 72 | let expected = 1072.0; 73 | let numbers = [ 74 | (2, "10000110000"), 75 | (8, "2060"), 76 | (25, "1hm"), 77 | (32, "11g"), 78 | (47, "mC"), 79 | ]; 80 | 81 | for (radix, number) in numbers.iter() { 82 | assert_eq!(from_str_radix(number, *radix), expected); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /cas-compute/src/symbolic/expr/iter.rs: -------------------------------------------------------------------------------- 1 | use super::SymExpr; 2 | 3 | /// An iterator that iteratively traverses the tree of expressions in left-to-right post-order 4 | /// (i.e. depth-first). 5 | /// 6 | /// This iterator is created by [`Expr::post_order_iter`]. 7 | pub struct ExprIter<'a> { 8 | stack: Vec<&'a SymExpr>, 9 | last_visited: Option<&'a SymExpr>, 10 | } 11 | 12 | impl<'a> ExprIter<'a> { 13 | /// Creates a new iterator that traverses the tree of expressions in left-to-right post-order 14 | /// (i.e. depth-first). 15 | pub fn new(expr: &'a SymExpr) -> Self { 16 | Self { 17 | stack: vec![expr], 18 | last_visited: None, 19 | } 20 | } 21 | 22 | /// Pops the current expression in the stack and marks it as the last visited expression. 23 | fn visit(&mut self) -> Option<&'a SymExpr> { 24 | self.last_visited = Some(self.stack.pop()?); 25 | self.last_visited 26 | } 27 | 28 | /// Returns true if the given expression matches the last visited expression. 29 | fn is_last_visited(&self, expr: &'a SymExpr) -> bool { 30 | match self.last_visited { 31 | Some(last_visited) => std::ptr::eq(last_visited, expr), 32 | None => false, 33 | } 34 | } 35 | } 36 | 37 | impl<'a> Iterator for ExprIter<'a> { 38 | type Item = &'a SymExpr; 39 | 40 | fn next(&mut self) -> Option { 41 | loop { 42 | let expr = self.stack.last()?; 43 | match expr { 44 | SymExpr::Primary(_) => return self.visit(), 45 | SymExpr::Add(terms) => { 46 | if terms.is_empty() || self.is_last_visited(terms.last().unwrap()) { 47 | return self.visit(); 48 | } 49 | for term in terms.iter().rev() { 50 | self.stack.push(term); 51 | } 52 | }, 53 | SymExpr::Mul(factors) => { 54 | if factors.is_empty() || self.is_last_visited(factors.last().unwrap()) { 55 | return self.visit(); 56 | } 57 | for factor in factors.iter().rev() { 58 | self.stack.push(factor); 59 | } 60 | }, 61 | SymExpr::Exp(lhs, rhs) => { 62 | if self.is_last_visited(rhs) { 63 | return self.visit(); 64 | } 65 | self.stack.push(rhs); 66 | self.stack.push(lhs); 67 | }, 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cas-compute/src/symbolic/mod.rs: -------------------------------------------------------------------------------- 1 | //! Algebraic manipulation of expressions. 2 | //! 3 | //! # Expression representation 4 | //! 5 | //! Algebraic expressions in this module are represented as a tree of [`SymExpr`] nodes. It's 6 | //! similar to the [`cas_parser::parser::ast::Expr`] nodes produced by [`cas_parser`], with the 7 | //! main difference being that [`SymExpr`] nodes **flatten** out the tree structure. 8 | //! 9 | //! For example, the expression `x + (y + z)` would be represented internally as a single 10 | //! [`SymExpr::Add`] node with _three_ children, `x`, `y`, and `z`, where as the 11 | //! [`cas_parser::parser::ast::Expr`] node would have two children, `x` and `(y + z)`. 12 | //! 13 | //! This is done to make it easier to perform algebraic manipulations on the expression. A common 14 | //! step in simplifying an expression is to combine "like terms", that is, to combine terms that 15 | //! share the same factors (e.g. `x + x = 2x`). This is much easier to do when the terms in 16 | //! question are all at the same level in the tree. 17 | //! 18 | //! If you have a [`cas_parser::parser::ast::Expr`], you can convert it to an [`SymExpr`] using 19 | //! the [`From`] trait. It should be noted that conversion to [`SymExpr`] is lossy, as 20 | //! [`SymExpr`] does not store span information and is free to rearrange the terms and / or 21 | //! factors during conversion, however the resulting expression will be semantically equivalent to 22 | //! the original. 23 | //! 24 | //! ``` 25 | //! use cas_compute::symbolic::expr::{Primary, SymExpr}; 26 | //! use cas_parser::parser::{ast::Expr, Parser}; 27 | //! 28 | //! let mut parser = Parser::new("x + (y + z)"); 29 | //! let ast_expr = parser.try_parse_full::().unwrap(); 30 | //! 31 | //! let expr: SymExpr = ast_expr.into(); 32 | //! assert_eq!(expr, SymExpr::Add(vec![ 33 | //! SymExpr::Primary(Primary::Symbol("x".to_string())), 34 | //! SymExpr::Primary(Primary::Symbol("y".to_string())), 35 | //! SymExpr::Primary(Primary::Symbol("z".to_string())), 36 | //! ])); 37 | //! ``` 38 | //! 39 | //! # Simplification 40 | //! 41 | //! A primary use case for algebraic manipulation is to reduce expressions to some canonical form. 42 | //! This is done with the [`simplify()`] function, which accepts an expression and returns a 43 | //! "simplified" version of it. 44 | //! 45 | //! The definition of "simplified" is, of course, somewhat subjective. We define an expression to 46 | //! be simplified if it has the lowest _complexity_ in the set of all expressions **semantically 47 | //! equivalent** to it, where complexity is roughly defined as the number of nodes in the 48 | //! expression tree. For example, `x + x` is not simplified, because it can be reduced to one term 49 | //! `2x`, which has lower complexity. 50 | //! 51 | //! Simplification is done by applying a set of simplification rules to the expression in multiple 52 | //! passes. Each rule is simply a function that accepts an expression and returns [`Option`]; 53 | //! if the rule is applicable to the expression, the rule is applied and the result is returned. 54 | //! 55 | //! The current set of rules is defined in [`simplify::rules`], and covers things like combining 56 | //! like terms / factors, distributing multiplication over addition, basic power rules, and more. 57 | //! 58 | //! ``` 59 | //! use cas_compute::primitive::int; 60 | //! use cas_compute::symbolic::{expr::{Primary, SymExpr}, simplify}; 61 | //! use cas_parser::parser::{ast::Expr, Parser}; 62 | //! 63 | //! let mut parser = Parser::new("x + x + x"); 64 | //! let ast_expr = parser.try_parse_full::().unwrap(); 65 | //! let simplified = simplify(&ast_expr.into()); 66 | //! 67 | //! // `x + x + x = 3x` 68 | //! assert_eq!(simplified, SymExpr::Mul(vec![ 69 | //! SymExpr::Primary(Primary::Integer(int(3))), 70 | //! SymExpr::Primary(Primary::Symbol("x".to_string())), 71 | //! ])); 72 | //! ``` 73 | //! 74 | //! For more information, see the [`mod@simplify`] module. 75 | 76 | pub mod expr; 77 | pub mod simplify; 78 | pub mod step_collector; 79 | 80 | pub use expr::SymExpr; 81 | pub use simplify::{simplify, simplify_with, simplify_with_steps}; 82 | pub use step_collector::StepCollector; 83 | -------------------------------------------------------------------------------- /cas-compute/src/symbolic/simplify/rules/distribute.rs: -------------------------------------------------------------------------------- 1 | //! Simplification rules related to the distributive property. 2 | 3 | use crate::symbolic::{ 4 | expr::SymExpr, 5 | simplify::{rules::{do_multiply, do_power}, step::Step}, 6 | step_collector::StepCollector, 7 | }; 8 | 9 | /// `a*(b+c) = a*b + a*c` 10 | pub fn distributive_property(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 11 | let opt = do_multiply(expr, |factors| { 12 | // find the first `Expr::Add`, and distribute every other factor over it 13 | let mut factors_to_distribute = factors.to_vec(); 14 | let add_factor_terms = { 15 | let idx = factors_to_distribute.iter() 16 | .position(|factor| matches!(factor, SymExpr::Add(_))); 17 | if let Some(idx) = idx { 18 | if let SymExpr::Add(terms) = factors_to_distribute.swap_remove(idx) { 19 | terms 20 | } else { 21 | unreachable!() 22 | } 23 | } else { 24 | return None; 25 | } 26 | }; 27 | 28 | let new_terms = add_factor_terms.into_iter() 29 | .map(|term| { 30 | SymExpr::Mul(factors_to_distribute.clone()) * term 31 | }) 32 | .collect::>(); 33 | Some(SymExpr::Add(new_terms)) 34 | })?; 35 | 36 | // keep the step collection logic outside of the closure to make it implement `Fn` 37 | step_collector.push(Step::DistributiveProperty); 38 | Some(opt) 39 | } 40 | 41 | /// `(a*b)^c = a^c * b^c` 42 | pub fn distribute_power(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 43 | let opt = do_power(expr, |lhs, rhs| { 44 | if let SymExpr::Mul(factors) = lhs { 45 | let new_factors = factors.iter() 46 | .map(|factor| SymExpr::Exp( 47 | Box::new(factor.clone()), 48 | Box::new(rhs.clone()), 49 | )) 50 | .collect::>(); 51 | 52 | return Some(SymExpr::Mul(new_factors)); 53 | } 54 | 55 | None 56 | })?; 57 | 58 | step_collector.push(Step::DistributePower); 59 | Some(opt) 60 | } 61 | 62 | /// Applies all distribution rules. 63 | /// 64 | /// The distributive property may or may not reduce the complexity of the expression, since it can 65 | /// introduce additional operations. However, it may be necessary for future rules to apply. 66 | pub fn all(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 67 | distributive_property(expr, step_collector) 68 | .or_else(|| distribute_power(expr, step_collector)) 69 | } 70 | -------------------------------------------------------------------------------- /cas-compute/src/symbolic/simplify/rules/imaginary.rs: -------------------------------------------------------------------------------- 1 | //! Simplification rules for expressions involving the imaginary unit. 2 | 3 | use crate::primitive::int; 4 | use crate::symbolic::{ 5 | expr::{SymExpr, Primary}, 6 | simplify::{rules::do_power, step::Step}, 7 | step_collector::StepCollector, 8 | }; 9 | 10 | // i^.. 11 | // 0 1 2 3 12 | // 1 i -1 -i 13 | // 14 | // 4 5 6 7 15 | // 1 i -1 -i 16 | 17 | /// `i^(4n) = 1` 18 | /// 19 | /// `i^0` can be handled by `power_zero`, but this rule is more general. 20 | pub fn i_pow_0(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 21 | let opt = do_power(expr, |lhs, rhs| { 22 | if lhs.as_symbol()? == "i" && int(rhs.as_integer()? % 4).is_zero() { 23 | Some(SymExpr::Primary(Primary::Integer(int(1)))) 24 | } else { 25 | None 26 | } 27 | })?; 28 | 29 | step_collector.push(Step::I0); 30 | Some(opt) 31 | } 32 | 33 | /// `i^(4n+1) = i` 34 | pub fn i_pow_1(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 35 | let opt = do_power(expr, |lhs, rhs| { 36 | if lhs.as_symbol()? == "i" && int(rhs.as_integer()? % 4) == 1 { 37 | Some(SymExpr::Primary(Primary::Symbol("i".to_string()))) 38 | } else { 39 | None 40 | } 41 | })?; 42 | 43 | step_collector.push(Step::I1); 44 | Some(opt) 45 | } 46 | 47 | /// `i^(4n+2) = -1` 48 | pub fn i_pow_2(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 49 | let opt = do_power(expr, |lhs, rhs| { 50 | if lhs.as_symbol()? == "i" && int(rhs.as_integer()? % 4) == 2 { 51 | Some(SymExpr::Primary(Primary::Integer(int(-1)))) 52 | } else { 53 | None 54 | } 55 | })?; 56 | 57 | step_collector.push(Step::I2); 58 | Some(opt) 59 | } 60 | 61 | /// `i^(4n+3) = -i` 62 | pub fn i_pow_3(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 63 | let opt = do_power(expr, |lhs, rhs| { 64 | if lhs.as_symbol()? == "i" && int(rhs.as_integer()? % 4) == 3 { 65 | Some(-SymExpr::Primary(Primary::Symbol("i".to_string()))) 66 | } else { 67 | None 68 | } 69 | })?; 70 | 71 | step_collector.push(Step::I3); 72 | Some(opt) 73 | } 74 | 75 | /// Applies all imaginary unit rules. 76 | /// 77 | /// All imaginary unit rules will reduce the complexity of the expression. 78 | pub fn all(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 79 | i_pow_0(expr, step_collector) 80 | .or_else(|| i_pow_1(expr, step_collector)) 81 | .or_else(|| i_pow_2(expr, step_collector)) 82 | .or_else(|| i_pow_3(expr, step_collector)) 83 | } 84 | -------------------------------------------------------------------------------- /cas-compute/src/symbolic/simplify/rules/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of many simplification rules. 2 | //! 3 | //! Each rule in this module is a function that takes the expression to simplify as an argument, 4 | //! and returns `Some(expr)` with the simplified expression if the rule applies, or `None` if the 5 | //! rule does not apply. 6 | 7 | pub mod add; 8 | pub mod distribute; 9 | pub mod imaginary; 10 | pub mod multiply; 11 | pub mod power; 12 | pub mod root; 13 | pub mod trigonometry; 14 | 15 | use crate::symbolic::step_collector::StepCollector; 16 | use super::{SymExpr, Primary, step::Step}; 17 | 18 | /// If the expression is a function call with the given function name, calls the given 19 | /// transformation function with the arguments. 20 | /// 21 | /// Returns `Some(expr)` with the transformed expression if a transformation was applied. 22 | pub(crate) fn do_call( 23 | expr: &SymExpr, 24 | name: &str, 25 | f: impl Copy + Fn(&[SymExpr]) -> Option, 26 | ) -> Option { 27 | if let SymExpr::Primary(Primary::Call(target_name, args)) = expr { 28 | if target_name == name { 29 | return f(args); 30 | } 31 | } 32 | 33 | None 34 | } 35 | 36 | /// If the expression is an add expression, calls the given transformation function with the terms. 37 | /// 38 | /// Returns `Some(expr)` with the transformed expression if a transformation was applied. 39 | pub(crate) fn do_add(expr: &SymExpr, f: impl Copy + Fn(&[SymExpr]) -> Option) -> Option { 40 | if let SymExpr::Add(terms) = expr { 41 | f(terms) 42 | } else { 43 | None 44 | } 45 | } 46 | 47 | /// If the expression is a multiplication expression, calls the given transformation function with 48 | /// the factors. 49 | /// 50 | /// Returns `Some(expr)` with the transformed expression if a transformation was applied. 51 | pub(crate) fn do_multiply(expr: &SymExpr, f: impl Copy + Fn(&[SymExpr]) -> Option) -> Option { 52 | if let SymExpr::Mul(factors) = expr { 53 | f(factors) 54 | } else { 55 | None 56 | } 57 | } 58 | 59 | /// If the expression is a power expression, calls the given transformation function with the left 60 | /// and right-hand-side of the power. 61 | /// 62 | /// Returns `Some(expr)` with the transformed expression if a transformation was applied. 63 | pub(crate) fn do_power(expr: &SymExpr, f: impl Copy + Fn(&SymExpr, &SymExpr) -> Option) -> Option { 64 | if let SymExpr::Exp(lhs, rhs) = expr { 65 | f(lhs, rhs) 66 | } else { 67 | None 68 | } 69 | } 70 | 71 | /// Applies all rules. 72 | pub fn all(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 73 | add::all(expr, step_collector) 74 | .or_else(|| multiply::all(expr, step_collector)) 75 | .or_else(|| power::all(expr, step_collector)) 76 | .or_else(|| distribute::all(expr, step_collector)) 77 | .or_else(|| imaginary::all(expr, step_collector)) 78 | .or_else(|| trigonometry::all(expr, step_collector)) 79 | .or_else(|| root::all(expr, step_collector)) 80 | } 81 | -------------------------------------------------------------------------------- /cas-compute/src/symbolic/simplify/rules/power.rs: -------------------------------------------------------------------------------- 1 | //! Simplification rules for power expressions. 2 | 3 | use crate::primitive::int; 4 | use crate::symbolic::{ 5 | expr::{SymExpr, Primary}, 6 | simplify::{rules::do_power, step::Step}, 7 | step_collector::StepCollector, 8 | }; 9 | use rug::ops::Pow; 10 | 11 | /// `a^0 = 1` 12 | /// 13 | /// `0^0` is defined as `1` by this rule, though it may be undefined in other mathematical 14 | /// contexts. 15 | pub fn power_zero(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 16 | let opt = do_power(expr, |_, rhs| { 17 | if rhs.as_integer()?.is_zero() { 18 | Some(SymExpr::Primary(Primary::Integer(int(1)))) 19 | } else { 20 | None 21 | } 22 | })?; 23 | 24 | // keep the step collection logic outside of the closure to make it implement `Fn` 25 | step_collector.push(Step::PowerZero); 26 | Some(opt) 27 | } 28 | 29 | /// `0^a = 0` 30 | /// 31 | /// `0^0` is handled by the [`power_zero`] rule. 32 | pub fn power_zero_left(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 33 | let opt = do_power(expr, |lhs, _| { 34 | if lhs.as_integer()?.is_zero() { 35 | Some(SymExpr::Primary(Primary::Integer(int(0)))) 36 | } else { 37 | None 38 | } 39 | })?; 40 | 41 | step_collector.push(Step::PowerZeroLeft); 42 | Some(opt) 43 | } 44 | 45 | /// `1^a = 1` 46 | pub fn power_one_left(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 47 | let opt = do_power(expr, |lhs, _| { 48 | if lhs.as_integer()? == &1 { 49 | Some(SymExpr::Primary(Primary::Integer(int(1)))) 50 | } else { 51 | None 52 | } 53 | })?; 54 | 55 | step_collector.push(Step::PowerOneLeft); 56 | Some(opt) 57 | } 58 | 59 | /// `a^1 = a` 60 | pub fn power_one(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 61 | let opt = do_power(expr, |lhs, rhs| { 62 | if rhs.as_integer()? == &1 { 63 | Some(lhs.clone()) 64 | } else { 65 | None 66 | } 67 | })?; 68 | 69 | step_collector.push(Step::PowerOne); 70 | Some(opt) 71 | } 72 | 73 | /// `(a^b)^c = a^(b*c)` 74 | pub fn power_power(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 75 | let opt = do_power(expr, |lhs, rhs| { 76 | if let SymExpr::Exp(base, exponent) = lhs { 77 | return Some(SymExpr::Exp( 78 | Box::new(*base.clone()), 79 | Box::new(*exponent.clone() * rhs.clone()), 80 | )); 81 | } 82 | 83 | None 84 | })?; 85 | 86 | step_collector.push(Step::PowerPower); 87 | Some(opt) 88 | } 89 | 90 | /// Simplifies integer powers. 91 | pub fn integer(expr: &SymExpr, _: &mut dyn StepCollector) -> Option { 92 | do_power(expr, |lhs, rhs| { 93 | if let Some(lhs) = lhs.as_integer() { 94 | if let Some(rhs) = rhs.as_integer() { 95 | return Some(SymExpr::Primary(Primary::Integer(lhs.pow(rhs.to_u32()?).into()))); 96 | } 97 | } 98 | 99 | None 100 | }) 101 | } 102 | 103 | /// Applies all power rules. 104 | /// 105 | /// All power rules will reduce the complexity of the expression. 106 | pub fn all(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 107 | power_zero(expr, step_collector) 108 | .or_else(|| power_zero_left(expr, step_collector)) 109 | .or_else(|| power_one_left(expr, step_collector)) 110 | .or_else(|| power_one(expr, step_collector)) 111 | .or_else(|| power_power(expr, step_collector)) 112 | .or_else(|| integer(expr, step_collector)) 113 | } 114 | -------------------------------------------------------------------------------- /cas-compute/src/symbolic/simplify/rules/trigonometry/mod.rs: -------------------------------------------------------------------------------- 1 | //! Simplification rules for trigonometric functions. 2 | 3 | mod consts; 4 | mod table; 5 | 6 | use crate::primitive::int; 7 | use crate::symbolic::{ 8 | expr::{SymExpr, Primary}, 9 | simplify::{ 10 | fraction::{extract_explicit_frac, make_fraction}, 11 | rules::do_call, 12 | step::Step, 13 | self, 14 | }, 15 | step_collector::StepCollector, 16 | }; 17 | use std::collections::HashMap; 18 | 19 | /// Apply normalization and simplify the given trigonometric expression using the provided lookup 20 | /// table. 21 | fn simplify_trig(arg: SymExpr, table: &HashMap<&SymExpr, table::TrigOut>) -> Option { 22 | // example: compute sin(pi/6) 23 | // compute normalized fraction: (pi/6) / (2pi) = 1/12 24 | let mut expr = { 25 | let two_pi = SymExpr::Primary(Primary::Integer(int(2))) * SymExpr::Primary(Primary::Symbol("pi".to_string())); 26 | let raw = make_fraction(arg, two_pi); 27 | simplify::simplify(&raw) 28 | }; 29 | 30 | // expect the result to be a fraction 31 | let (numerator, denominator) = extract_explicit_frac(&mut expr)?; 32 | 33 | // turn the fraction into a normalized `Expr` 34 | let fraction = { 35 | if numerator.is_zero() { 36 | SymExpr::Primary(Primary::Integer(int(0))) 37 | } else if denominator == 1 { 38 | SymExpr::Primary(Primary::Integer(numerator)) 39 | } else { 40 | // the fraction is the normalized angle from 0 to 1, but can be outside that range 41 | // get the fraction in the range 0 to 1 by computing `numerator % denominator` 42 | 43 | // positive modulo (to handle negative numerators) 44 | let numerator = (numerator % &denominator + &denominator) % &denominator; 45 | make_fraction( 46 | SymExpr::Primary(Primary::Integer(numerator)), 47 | SymExpr::Primary(Primary::Integer(denominator)), 48 | ) 49 | } 50 | }; 51 | 52 | // use the provided table to get the output 53 | table.get(&fraction) 54 | .map(|out| { 55 | if out.neg { 56 | -out.output.clone() 57 | } else { 58 | out.output.clone() 59 | } 60 | }) 61 | } 62 | 63 | /// `sin(x)` 64 | pub fn sin(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 65 | let opt = do_call(expr, "sin", |args| { 66 | simplify_trig(args.first().cloned()?, &table::SIN_TABLE) 67 | })?; 68 | 69 | // keep the step collection logic outside of the closure to make it implement `Fn` 70 | step_collector.push(Step::Sin); 71 | Some(opt) 72 | } 73 | 74 | /// `cos(x)` 75 | pub fn cos(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 76 | let opt = do_call(expr, "cos", |args| { 77 | simplify_trig(args.first().cloned()?, &table::COS_TABLE) 78 | })?; 79 | 80 | step_collector.push(Step::Cos); 81 | Some(opt) 82 | } 83 | 84 | /// `tan(x)` 85 | pub fn tan(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 86 | let opt = do_call(expr, "tan", |args| { 87 | simplify_trig(args.first().cloned()?, &table::TAN_TABLE) 88 | })?; 89 | 90 | step_collector.push(Step::Tan); 91 | Some(opt) 92 | } 93 | 94 | /// Applies all trigonometric rules. 95 | /// 96 | /// All trigonometric rules will reduce the complexity of the expression. 97 | pub fn all(expr: &SymExpr, step_collector: &mut dyn StepCollector) -> Option { 98 | sin(expr, step_collector) 99 | .or_else(|| cos(expr, step_collector)) 100 | .or_else(|| tan(expr, step_collector)) 101 | } 102 | -------------------------------------------------------------------------------- /cas-compute/src/symbolic/simplify/step.rs: -------------------------------------------------------------------------------- 1 | /// Possible simplification steps. 2 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 3 | pub enum Step { 4 | /// `0+a = a` 5 | /// `a+0 = a` 6 | AddZero, 7 | 8 | /// `0*a = 0` 9 | /// `a*0 = 0` 10 | MultiplyZero, 11 | 12 | /// `1*a = a` 13 | /// `a*1 = a` 14 | MultiplyOne, 15 | 16 | /// `3/12 = 1/4` 17 | /// `12/3 = 4` 18 | ReduceFraction, 19 | 20 | /// `a+a = 2a` 21 | /// `a+a+a = 3a` 22 | /// `2a+3a = 5a` 23 | /// etc. 24 | CombineLikeTerms, 25 | 26 | /// `a*a = a^2` 27 | /// `a*a*a = a^3` 28 | /// `a^2*a^3 = a^5` 29 | /// etc. 30 | CombineLikeFactors, 31 | 32 | /// `a^0 = 1` 33 | PowerZero, 34 | 35 | /// `0^a = 0` 36 | PowerZeroLeft, 37 | 38 | /// `1^a = 1` 39 | PowerOneLeft, 40 | 41 | /// `a^1 = a` 42 | PowerOne, 43 | 44 | /// `a^b^c = a^(b*c)` 45 | PowerPower, 46 | 47 | /// `a*(b+c) = a*b + a*c` 48 | DistributiveProperty, 49 | 50 | /// `(a*b)^c = a^c*b^c` 51 | DistributePower, 52 | 53 | /// `i^(4n) = 1` 54 | I0, 55 | 56 | /// `i^(4n+1) = i` 57 | I1, 58 | 59 | /// `i^(4n+2) = -1` 60 | I2, 61 | 62 | /// `i^(4n+3) = -i` 63 | I3, 64 | 65 | /// `sin(x)` identity 66 | Sin, 67 | 68 | /// `cos(x)` identity 69 | Cos, 70 | 71 | /// `tan(x)` identity 72 | Tan, 73 | 74 | /// `sqrt(x^2) = x`, `x >= 0` 75 | /// `cbrt(x^3) = x` 76 | /// `root(x^y, y) = x` 77 | Root, 78 | } 79 | -------------------------------------------------------------------------------- /cas-compute/src/symbolic/step_collector.rs: -------------------------------------------------------------------------------- 1 | /// A type that collects the steps of an algorithm. 2 | /// 3 | /// [`StepCollector`] is also implemented for the unit type `()`. This is useful when you don't 4 | /// want to know the steps taken by an algorithm, which can enable the solver to use more 5 | /// efficient, less-human-friendly algorithms. 6 | pub trait StepCollector { 7 | /// Adds a step to the collector. 8 | fn push(&mut self, step: S); 9 | } 10 | 11 | impl StepCollector for () { 12 | #[inline] 13 | fn push(&mut self, _: S) {} 14 | } 15 | 16 | impl StepCollector for Vec { 17 | #[inline] 18 | fn push(&mut self, step: S) { 19 | self.push(step); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cas-error/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas-error" 3 | description = "Error type for generic errors in CalcScript" 4 | version = "0.2.0" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/ElectrifyPro/cas-rs" 8 | keywords = ["calcscript", "error"] 9 | categories = ["mathematics"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dev-dependencies] 14 | cas-attrs = { version = "0.2.0", path = "../cas-attrs" } 15 | strip-ansi-escapes = "0.2.1" 16 | 17 | [dependencies] 18 | ariadne = "0.2.0" 19 | -------------------------------------------------------------------------------- /cas-error/README.md: -------------------------------------------------------------------------------- 1 | Contains the common [`ErrorKind`] trait implemented by all errors in the 2 | `cas-rs` ecosystem. 3 | 4 | All errors that implement this trait can be output to `stderr` in a 5 | user-friendly way with the [`Error::report_to_stderr`] method. The 6 | [`ErrorKind`] dervie macro is used to implement this trait with a given error 7 | message, help message, note, and labels to apply to spans in the error report. 8 | 9 | # Example 10 | 11 | Here we create a custom error type and derive the [`ErrorKind`] trait for it. 12 | Expressions in the error message are evaluated at runtime and have access to 13 | `&self`, allowing you to use the fields of the error type in the message. 14 | 15 | ```rust 16 | use cas_attrs::ErrorKind; 17 | use cas_error::Error; 18 | 19 | /// Tried to override a builtin constant. 20 | #[derive(Debug, Clone, ErrorKind, PartialEq)] 21 | #[error( 22 | message = format!("cannot override builtin constant: `{}`", self.name), 23 | labels = ["this variable"], 24 | help = "choose a different name for this variable", 25 | note = "builtin constants include: `i`, `e`, `phi`, `pi`, or `tau`", 26 | )] 27 | pub struct OverrideBuiltinConstant { 28 | /// The name of the variable that was attempted to be overridden. 29 | pub name: String, 30 | } 31 | 32 | let error = Error::new( 33 | vec![0..2], // span pointing to `pi` in the source 34 | OverrideBuiltinConstant { 35 | name: "pi".to_string(), 36 | }, 37 | ); 38 | 39 | // print the error to stderr with colors 40 | error.report_to_stderr("src_id", "pi = 3").unwrap(); 41 | 42 | // or manually grab the error text (note: there is some trailing whitespace) 43 | let error_str = { 44 | let mut bytes = vec![]; 45 | error.report_to(&mut bytes, "src_id", "pi = 3").unwrap(); 46 | strip_ansi_escapes::strip_str(String::from_utf8_lossy(&bytes)) 47 | }; 48 | assert_eq!( 49 | error_str, 50 | "Error: cannot override builtin constant: `pi` 51 | ╭─[src_id:1:1] 52 | │ 53 | 1 │ pi = 3 54 | │ ─┬ 55 | │ ╰── this variable 56 | │ 57 | │ Help: choose a different name for this variable 58 | │ 59 | │ Note: builtin constants include: `i`, `e`, `phi`, `pi`, or `tau` 60 | ───╯ 61 | " 62 | ); 63 | ``` 64 | 65 | [`Error::report_to_stderr`]: https://docs.rs/cas-error/latest/cas_error/struct.Error.html#method.report_to_stderr 66 | -------------------------------------------------------------------------------- /cas-error/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use ariadne::{Color, Report, Source}; 4 | use std::{fmt::Debug, io::Write, ops::Range}; 5 | 6 | /// The color to use to highlight expressions. 7 | pub const EXPR: Color = Color::RGB(52, 235, 152); 8 | 9 | /// Represents any kind of error that can occur during some operation. 10 | pub trait ErrorKind: Debug + Send { 11 | /// Builds the report for this error. 12 | fn build_report<'a>( 13 | &self, 14 | src_id: &'a str, 15 | spans: &[Range], 16 | ) -> Report<(&'a str, Range)>; 17 | } 18 | 19 | /// An error associated with regions of source code that can be highlighted. 20 | #[derive(Debug)] 21 | pub struct Error { 22 | /// The regions of the source code that this error originated from. 23 | pub spans: Vec>, 24 | 25 | /// The kind of error that occurred. 26 | pub kind: Box, 27 | } 28 | 29 | impl Error { 30 | /// Creates a new error with the given spans and kind. 31 | pub fn new(spans: Vec>, kind: impl ErrorKind + 'static) -> Self { 32 | Self { spans, kind: Box::new(kind) } 33 | } 34 | 35 | /// Build a report from this error kind. 36 | pub fn build_report<'a>(&self, src_id: &'a str) -> Report<(&'a str, Range)> { 37 | self.kind.build_report(src_id, &self.spans) 38 | } 39 | 40 | /// Writes the error to stderr. 41 | /// 42 | /// The `ariadne` crate's [`Report`] type does not have a `Display` implementation, so we can 43 | /// only use its `eprint` method to print to stderr. 44 | /// 45 | /// [`Report`]: ariadne::Report 46 | pub fn report_to_stderr(&self, src_id: &str, input: &str) -> std::io::Result<()> { 47 | self.report_to(std::io::stderr(), src_id, input) 48 | } 49 | 50 | /// Writes the error to an output stream. 51 | pub fn report_to( 52 | &self, 53 | writer: W, 54 | src_id: &str, 55 | input: &str, 56 | ) -> std::io::Result<()> { 57 | let report = self.build_report(src_id); 58 | report.write((src_id, Source::from(input)), writer) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cas-graph/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas-graph" 3 | description = "A customizable graphing calculator for CalcScript" 4 | version = "0.2.0" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/ElectrifyPro/cas-rs" 8 | keywords = ["calcscript", "graphing", "calculator", "customizable"] 9 | categories = ["mathematics", "visualization"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | cairo-rs = { version = "0.18.2", features = ["png"] } 15 | cas-compiler = { version = "0.2.0", path = "../cas-compiler" } 16 | cas-compute = { version = "0.2.0", path = "../cas-compute" } 17 | cas-error = { version = "0.2.0", path = "../cas-error" } 18 | cas-parser = { version = "0.2.0", path = "../cas-parser" } 19 | cas-vm = { version = "0.2.0", path = "../cas-vm" } 20 | rayon = "1.7.0" 21 | -------------------------------------------------------------------------------- /cas-graph/README.md: -------------------------------------------------------------------------------- 1 | A customizable graphing calculator for CalcScript. 2 | 3 | # Usage 4 | 5 | Creating a graph is as simple as creating a [`Graph`] object, adding expressions 6 | and points with [`Graph::add_expr`] / [`Graph::try_add_expr`] and 7 | [`Graph::add_point`], and finally calling [`Graph::draw`]. The result can be 8 | written to a file in PNG and SVG format. 9 | 10 | See the [module-level documentation](graph) for more information. 11 | 12 | # Example 13 | 14 | Graph the expressions `x = erf(y)` and `y = erf(x)` with the viewport centered 15 | on the points `(0, 8.1)`, `(1.2, 6.2)`, `(2.3, 4.3)`, `(3.4, 2.4)`, and `(4.5, 16 | 0.5)`, then write the result to `erf-output.png`: 17 | 18 | ```rust,no_run 19 | use cas_graph::Graph; 20 | use std::fs::File; 21 | 22 | fn main() -> Result<(), Box> { 23 | let surface = Graph::default() 24 | .try_add_expr("erf(y)").unwrap() 25 | .try_add_expr("erf(x)").unwrap() 26 | .add_point((0.0, 8.1)) 27 | .add_point((1.2, 6.2)) 28 | .add_point((2.3, 4.3)) 29 | .add_point((3.4, 2.4)) 30 | .add_point((4.5, 0.5)) 31 | .center_on_points() 32 | .draw()?; 33 | 34 | let mut file = File::create("erf-output.png")?; 35 | surface.write_to_png(&mut file)?; 36 | 37 | Ok(()) 38 | } 39 | ``` 40 | 41 | Output (note: colors were randomly chosen; random color selection is not 42 | included in the example code): 43 | 44 | 45 | -------------------------------------------------------------------------------- /cas-graph/img/erf-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElectrifyPro/cas-rs/02a253416c2069b91975a8a652e3ad296d8073fb/cas-graph/img/erf-output.png -------------------------------------------------------------------------------- /cas-graph/img/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElectrifyPro/cas-rs/02a253416c2069b91975a8a652e3ad296d8073fb/cas-graph/img/output.png -------------------------------------------------------------------------------- /cas-graph/img/parabola.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElectrifyPro/cas-rs/02a253416c2069b91975a8a652e3ad296d8073fb/cas-graph/img/parabola.png -------------------------------------------------------------------------------- /cas-graph/src/graph/analyzed.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use cas_parser::parser::{ 3 | ast::{binary::Binary, expr::Expr, literal::Literal}, 4 | token::op::{BinOp, BinOpKind}, 5 | Parser, 6 | }; 7 | use std::collections::HashSet; 8 | 9 | /// Predicts and extracts the independent variable from the given expression. 10 | pub fn predict_independent(expr: Expr) -> (Variable, Expr) { 11 | let expr = if let Expr::Binary(Binary { 12 | lhs, 13 | op: BinOp { 14 | kind: BinOpKind::Eq, 15 | implicit, 16 | span: op_span, 17 | }, 18 | rhs, 19 | span, 20 | }) = expr { 21 | // variable by itself on left-hand-side: y=..., x=..., etc. 22 | // could indicate: y=x^2, x=y^2, etc. 23 | if let Expr::Literal(Literal::Symbol(sym)) = lhs.as_ref() { 24 | return match sym.name.as_str() { 25 | "x" => (Variable::Y, *rhs), 26 | "y" => (Variable::X, *rhs), 27 | "theta" => (Variable::Theta, *rhs), 28 | _ => todo!("unknown special variable"), 29 | }; 30 | } else if let Expr::Literal(Literal::Symbol(sym)) = rhs.as_ref() { 31 | return match sym.name.as_str() { 32 | "x" => (Variable::Y, *lhs), 33 | "y" => (Variable::X, *lhs), 34 | "theta" => (Variable::Theta, *lhs), 35 | _ => todo!("unknown special variable"), 36 | }; 37 | } 38 | 39 | // recreate the binary to allow fallling back to basic symbol counting 40 | Expr::Binary(Binary { 41 | lhs, 42 | op: BinOp { 43 | kind: BinOpKind::Eq, 44 | implicit, 45 | span: op_span, 46 | }, 47 | rhs, 48 | span, 49 | }) 50 | } else { 51 | expr 52 | }; 53 | 54 | let vars = expr.post_order_iter() 55 | .filter_map(|node| match node { 56 | Expr::Literal(Literal::Symbol(sym)) => match sym.name.as_str() { 57 | "x" => Some(Variable::X), 58 | "y" => Some(Variable::Y), 59 | "theta" => Some(Variable::Theta), 60 | _ => None, 61 | }, 62 | _ => None, 63 | }) 64 | .collect::>(); 65 | 66 | if vars.len() > 1 { 67 | todo!("multiple special variables") 68 | } else if let Some(var) = vars.into_iter().next() { 69 | (var, expr) 70 | } else { 71 | todo!("no special variables") 72 | } 73 | } 74 | 75 | /// Special variable names that are used in expressions to be graphed. 76 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 77 | pub enum Variable { 78 | X, 79 | Y, 80 | Theta, 81 | } 82 | 83 | impl Variable { 84 | /// Returns the string slice representation of the variable. 85 | pub fn as_str(self) -> &'static str { 86 | match self { 87 | Self::X => "x", 88 | Self::Y => "y", 89 | Self::Theta => "theta", 90 | } 91 | } 92 | } 93 | 94 | /// An expression that has been analyzed and ready to be drawn. 95 | #[derive(Clone, Debug)] 96 | pub struct AnalyzedExpr { 97 | /// The expression to draw. 98 | pub expr: Expr, 99 | 100 | /// The independent variable. 101 | pub independent: Variable, 102 | 103 | /// The color of the expression, given as an RGB tuple with each value in the range 0.0 to 1.0. 104 | /// 105 | /// The default color is a solid red. 106 | pub color: (f64, f64, f64), 107 | } 108 | 109 | impl AnalyzedExpr { 110 | /// Analyze the given expression and return a new [`AnalyzedExpr`]. 111 | pub fn new(expr: Expr) -> Self { 112 | let (independent, expr) = predict_independent(expr); 113 | Self { 114 | expr, 115 | independent, 116 | color: (1.0, 0.0, 0.0), 117 | } 118 | } 119 | 120 | /// Parses and analyzes the given expression and returns a new [`AnalyzedExpr`]. 121 | pub fn parse(expr: &str) -> Result> { 122 | Parser::new(expr) 123 | .try_parse_full() 124 | .map(Self::new) 125 | } 126 | 127 | /// Sets the color of the expression. 128 | /// 129 | /// Returns the expression itself to allow chaining. 130 | pub fn with_color(mut self, color: (f64, f64, f64)) -> Self { 131 | self.color = color; 132 | self 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /cas-graph/src/graph/point.rs: -------------------------------------------------------------------------------- 1 | /// A pair of `(x, y)` values in **graph** units. 2 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 3 | pub struct GraphPoint(pub T, pub T); 4 | 5 | impl From<(T, T)> for GraphPoint { 6 | fn from((x, y): (T, T)) -> GraphPoint { 7 | GraphPoint(x, y) 8 | } 9 | } 10 | 11 | impl GraphPoint 12 | where 13 | T: Into + Copy, 14 | { 15 | /// Returns the distance between two points. 16 | pub fn distance(self, other: GraphPoint) -> f64 { 17 | (self.0.into() - other.0.into()).hypot(self.1.into() - other.1.into()) 18 | } 19 | } 20 | 21 | /// A pair of `(x, y)` values in **canvas** units. 22 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 23 | pub struct CanvasPoint(pub T, pub T); 24 | 25 | impl From<(T, T)> for CanvasPoint { 26 | fn from((x, y): (T, T)) -> CanvasPoint { 27 | CanvasPoint(x, y) 28 | } 29 | } 30 | 31 | /// A point to draw in a graph with style information. 32 | /// 33 | /// The default point color is red. 34 | #[derive(Clone, Debug, PartialEq)] 35 | pub struct Point { 36 | /// The coordinates of the point to draw. 37 | pub coordinates: GraphPoint, 38 | 39 | /// The color of the point, given as an RGB tuple with each value in the 40 | /// range 0.0 to 1.0. 41 | /// 42 | /// The default color is a solid red. 43 | pub color: (f64, f64, f64), 44 | 45 | /// The label of the point. If omitted, the point will be labeled with its 46 | /// coordinates, to a maximum of 3 decimal places for each coordinate. 47 | pub label: Option, 48 | } 49 | 50 | impl Default for Point { 51 | fn default() -> Point { 52 | Point { 53 | coordinates: (0.0, 0.0).into(), 54 | color: (1.0, 0.0, 0.0), 55 | label: None, 56 | } 57 | } 58 | } 59 | 60 | impl Point 61 | where 62 | T: Into + Copy, 63 | Point: Default, 64 | { 65 | /// Creates a new point with the given coordinates. 66 | pub fn new(coordinates: impl Into>) -> Point { 67 | Point { 68 | coordinates: coordinates.into(), 69 | ..Default::default() 70 | } 71 | } 72 | 73 | /// Sets the color of the point. 74 | /// 75 | /// Returns the point itself to allow chaining. 76 | pub fn with_color(mut self, color: (f64, f64, f64)) -> Point { 77 | self.color = color; 78 | self 79 | } 80 | 81 | /// Sets the label of the point. 82 | /// 83 | /// Returns the point itself to allow chaining. 84 | pub fn with_label(mut self, label: String) -> Point { 85 | self.label = Some(label); 86 | self 87 | } 88 | } 89 | 90 | impl From<(T, T)> for Point 91 | where 92 | Point: Default, 93 | { 94 | fn from((x, y): (T, T)) -> Point { 95 | Point { 96 | coordinates: GraphPoint(x, y), 97 | ..Default::default() 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /cas-graph/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | pub mod graph; 4 | mod text_align; 5 | 6 | pub use graph::{ 7 | analyzed::AnalyzedExpr, 8 | CanvasPoint, Graph, GraphOptions, GraphPoint, Point, 9 | }; 10 | -------------------------------------------------------------------------------- /cas-graph/src/text_align.rs: -------------------------------------------------------------------------------- 1 | use cairo::{Context, Error, TextExtents}; 2 | 3 | /// A trait to add the `show_text_align` to the [`Context`] type. 4 | pub trait ShowTextAlign { 5 | /// Shows the given text at the given `(x, y)` position, with the given alignment. 6 | /// 7 | /// Draw text to the given context with the given alignment point. 8 | /// 9 | /// By default, text is rendered with the bottom left corner of the text at the given `(x, y)` 10 | /// point. This function allows you to specify an alignment point, which is a pair of `(x, y)` 11 | /// values, each between `0.0` and `1.0`, indicating the horizontal and vertical alignment of the 12 | /// text, respectively. 13 | /// 14 | /// For example, `(0.0, 0.0)` will align the bottom left corner of the text to the given `(x, y)` 15 | /// point, `(0.5, 0.5)` will center the text, `(1.0, 1.0)` will align the top right corner of the 16 | /// text to the given `(x, y)` point, etc. 17 | fn show_text_align( 18 | &self, 19 | text: &str, 20 | point: (f64, f64), 21 | align: (f64, f64), 22 | ) -> Result; 23 | 24 | /// Shows the given text at the given `(x, y)` position, with the given alignment, using the 25 | /// provided [`TextExtents`] to calculate the alignment. 26 | /// 27 | /// This is useful if you have already calculated the [`TextExtents`] for the text you want to 28 | /// draw, and want to avoid calculating them again. 29 | fn show_text_align_with_extents( 30 | &self, 31 | text: &str, 32 | point: (f64, f64), 33 | align: (f64, f64), 34 | extents: &TextExtents, 35 | ) -> Result<(), Error>; 36 | } 37 | 38 | impl ShowTextAlign for Context { 39 | fn show_text_align( 40 | &self, 41 | text: &str, 42 | (x, y): (f64, f64), 43 | align: (f64, f64), 44 | ) -> Result { 45 | let extents = self.text_extents(text)?; 46 | self.show_text_align_with_extents(text, (x, y), align, &extents)?; 47 | Ok(extents) 48 | } 49 | 50 | fn show_text_align_with_extents( 51 | &self, 52 | text: &str, 53 | (x, y): (f64, f64), 54 | align: (f64, f64), 55 | extents: &TextExtents, 56 | ) -> Result<(), Error> { 57 | let x = x - extents.width() * align.0; 58 | let y = y + extents.height() * align.1; 59 | self.move_to(x, y); 60 | self.show_text(text) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cas-parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas-parser" 3 | description = "Parser for the CalcScript language" 4 | version = "0.2.0" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/ElectrifyPro/cas-rs" 8 | keywords = ["calcscript", "parser", "lexer", "calculator"] 9 | categories = ["mathematics", "parser-implementations"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [features] 14 | serde = ["dep:serde"] 15 | 16 | [dev-dependencies] 17 | pretty_assertions = "1.3.0" 18 | 19 | [dependencies.serde] 20 | version = "1.0.188" 21 | features = ["derive"] 22 | optional = true 23 | 24 | [dependencies] 25 | ariadne = "0.2.0" 26 | cas-attrs = { version = "0.2.0", path = "../cas-attrs" } 27 | cas-error = { version = "0.2.0", path = "../cas-error" } 28 | logos = "0.14" 29 | -------------------------------------------------------------------------------- /cas-parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | #![allow(clippy::single_range_in_vec_init)] // for reporting one or more spans to errors 4 | 5 | pub mod tokenizer; 6 | pub mod parser; 7 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/block.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::stmt::Stmt, 4 | error::UnclosedParenthesis, 5 | fmt::Latex, 6 | garbage::Garbage, 7 | token::{CloseCurly, OpenCurly}, 8 | Parse, 9 | Parser, 10 | }; 11 | use std::{fmt, ops::Range}; 12 | 13 | #[cfg(feature = "serde")] 14 | use serde::{Deserialize, Serialize}; 15 | 16 | /// A blocked expression. A [`Block`] can contain multiple expressions in the form of statements. 17 | /// The last statement in the block is the return value of the block. 18 | #[derive(Debug, Clone, PartialEq, Eq)] 19 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 20 | pub struct Block { 21 | /// The inner statements. 22 | pub stmts: Vec, 23 | 24 | /// The region of the source code that this [`Block`] was parsed from. 25 | pub span: Range, 26 | } 27 | 28 | impl Block { 29 | /// Returns the span of the [`Block`]. 30 | pub fn span(&self) -> Range { 31 | self.span.clone() 32 | } 33 | } 34 | 35 | impl<'source> Parse<'source> for Block { 36 | fn std_parse( 37 | input: &mut Parser<'source>, 38 | recoverable_errors: &mut Vec 39 | ) -> Result> { 40 | let open_curly = input.try_parse::().forward_errors(recoverable_errors)?; 41 | let mut stmts = Vec::new(); 42 | while let Ok(stmt) = input.try_parse().forward_errors(recoverable_errors) { 43 | stmts.push(stmt); 44 | } 45 | let close_curly = input.try_parse::() 46 | .forward_errors(recoverable_errors) 47 | .unwrap_or_else(|_| { 48 | recoverable_errors.push(Error::new( 49 | vec![open_curly.span.clone()], 50 | UnclosedParenthesis { opening: true }, 51 | )); 52 | 53 | // fake a close paren for recovery purposes 54 | Garbage::garbage() 55 | }); 56 | Ok(Self { 57 | stmts, 58 | span: open_curly.span.start..close_curly.span.end, 59 | }) 60 | } 61 | } 62 | 63 | impl std::fmt::Display for Block { 64 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 65 | write!(f, "{{")?; 66 | for stmt in &self.stmts { 67 | stmt.fmt(f)?; 68 | } 69 | write!(f, "}}") 70 | } 71 | } 72 | 73 | impl Latex for Block { 74 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 75 | write!(f, "{{")?; 76 | for stmt in &self.stmts { 77 | stmt.fmt_latex(f)?; 78 | } 79 | write!(f, "}}") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/branch.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::expr::Expr, 4 | error::{OfOutsideSumProduct, ThenOutsideIfWhileFor}, 5 | fmt::Latex, 6 | keyword::{Of as OfToken, Then as ThenToken}, 7 | Parse, 8 | Parser, 9 | }; 10 | use std::{fmt, ops::Range}; 11 | 12 | #[cfg(feature = "serde")] 13 | use serde::{Deserialize, Serialize}; 14 | 15 | /// A `then` expression, such as `then x += 1`. 16 | /// 17 | /// A `then` expression simply evaluates the expression that follows the `then` keyword, and 18 | /// returns its result. It is only valid in the context of `if` and `while` expressions, and is 19 | /// used to concisely write one-line `if` and `while` expressions, such as `if x < 10 then x += 1`. 20 | #[derive(Debug, Clone, PartialEq, Eq)] 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | pub struct Then { 23 | /// The expression to evaluate. 24 | pub expr: Box, 25 | 26 | /// The region of the source code that this expression was parsed from. 27 | pub span: Range, 28 | 29 | /// The span of the `then` keyword. 30 | pub then_span: Range, 31 | } 32 | 33 | impl Then { 34 | /// Returns the span of the `then` expression. 35 | pub fn span(&self) -> Range { 36 | self.span.clone() 37 | } 38 | } 39 | 40 | impl<'source> Parse<'source> for Then { 41 | fn std_parse( 42 | input: &mut Parser<'source>, 43 | recoverable_errors: &mut Vec 44 | ) -> Result> { 45 | let then_token = input.try_parse::().forward_errors(recoverable_errors)?; 46 | 47 | if !input.state.allow_then { 48 | recoverable_errors.push(Error::new( 49 | vec![then_token.span.clone()], 50 | ThenOutsideIfWhileFor, 51 | )); 52 | } 53 | 54 | // this is to catch stuff like `if true then then then then then 1` 55 | let body = input.try_parse_with_state::<_, Expr>(|input| { 56 | input.allow_then = false; 57 | }).forward_errors(recoverable_errors)?; 58 | let span = then_token.span.start..body.span().end; 59 | 60 | Ok(Self { 61 | expr: Box::new(body), 62 | span, 63 | then_span: then_token.span, 64 | }) 65 | } 66 | } 67 | 68 | impl std::fmt::Display for Then { 69 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 70 | write!(f, "then {}", self.expr) 71 | } 72 | } 73 | 74 | impl Latex for Then { 75 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 76 | write!(f, "\\text{{ then }}")?; 77 | self.expr.fmt_latex(f)?; 78 | Ok(()) 79 | } 80 | } 81 | 82 | /// An `of` expression, such as `of x`. 83 | /// 84 | /// An `of` expression's body is repeatedly evaluated during summation or product expressions, 85 | /// with a specific variable in the `of` expression taking on each value in the range of the 86 | /// summation or product. It is only valid in the context of `sum` and `product` expressions, and 87 | /// is used to concisely write one-line `sum` and `product` expressions, such as `sum x in 1..10 of 88 | /// x`. 89 | #[derive(Debug, Clone, PartialEq, Eq)] 90 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 91 | pub struct Of { 92 | /// The expression to evaluate. 93 | pub expr: Box, 94 | 95 | /// The region of the source code that this expression was parsed from. 96 | pub span: Range, 97 | 98 | /// The span of the `of` keyword. 99 | pub of_span: Range, 100 | } 101 | 102 | impl Of { 103 | /// Returns the span of the `of` expression. 104 | pub fn span(&self) -> Range { 105 | self.span.clone() 106 | } 107 | } 108 | 109 | impl<'source> Parse<'source> for Of { 110 | fn std_parse( 111 | input: &mut Parser<'source>, 112 | recoverable_errors: &mut Vec 113 | ) -> Result> { 114 | let of_token = input.try_parse::().forward_errors(recoverable_errors)?; 115 | 116 | if !input.state.allow_of { 117 | recoverable_errors.push(Error::new( 118 | vec![of_token.span.clone()], 119 | OfOutsideSumProduct, 120 | )); 121 | } 122 | 123 | // this is to catch stuff like `sum x in 1..10 of of of of of 1` 124 | let body = input.try_parse_with_state::<_, Expr>(|input| { 125 | input.allow_of = false; 126 | }).forward_errors(recoverable_errors)?; 127 | let span = of_token.span.start..body.span().end; 128 | 129 | Ok(Self { 130 | expr: Box::new(body), 131 | span, 132 | of_span: of_token.span, 133 | }) 134 | } 135 | } 136 | 137 | impl std::fmt::Display for Of { 138 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 139 | write!(f, "of {}", self.expr) 140 | } 141 | } 142 | 143 | impl Latex for Of { 144 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 145 | write!(f, "\\text{{ of }}")?; 146 | self.expr.fmt_latex(f)?; 147 | Ok(()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/for_expr.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::{ 4 | expr::Expr, 5 | literal::LitSym, 6 | range::Range as RangeExpr, 7 | }, 8 | fmt::Latex, 9 | keyword::{In as InToken, For as ForToken}, 10 | Parse, 11 | Parser, 12 | }; 13 | use std::{fmt, ops::Range}; 14 | 15 | #[cfg(feature = "serde")] 16 | use serde::{Deserialize, Serialize}; 17 | 18 | /// A `for` loop expression, such as `for i in 0..10 then print(i)`. The loop body is executed for 19 | /// each value in the specified range, with the variable taking on the current value. 20 | #[derive(Debug, Clone, PartialEq, Eq)] 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | pub struct For { 23 | /// The variable name representing each value in the range. 24 | pub variable: LitSym, 25 | 26 | /// The range of values that the variable will take on. 27 | pub range: RangeExpr, 28 | 29 | /// The body of the summation. 30 | pub body: Box, 31 | 32 | /// The region of the source code that this `for` expression was parsed from. 33 | pub span: Range, 34 | } 35 | 36 | impl For { 37 | /// Returns the span of the `for` expression. 38 | pub fn span(&self) -> Range { 39 | self.span.clone() 40 | } 41 | } 42 | 43 | impl<'source> Parse<'source> for For { 44 | fn std_parse( 45 | input: &mut Parser<'source>, 46 | recoverable_errors: &mut Vec 47 | ) -> Result> { 48 | let for_token = input.try_parse::().forward_errors(recoverable_errors)?; 49 | let variable = input.try_parse::().forward_errors(recoverable_errors)?; 50 | input.try_parse::().forward_errors(recoverable_errors)?; 51 | let range = input.try_parse::().forward_errors(recoverable_errors)?; 52 | let body = input.try_parse_with_state::<_, Expr>(|state| { 53 | state.allow_then = true; 54 | state.allow_loop_control = true; 55 | }).forward_errors(recoverable_errors)?; 56 | let span = for_token.span.start..body.span().end; 57 | 58 | Ok(Self { 59 | variable, 60 | range, 61 | body: Box::new(body), 62 | span, 63 | }) 64 | } 65 | } 66 | 67 | impl std::fmt::Display for For { 68 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 69 | write!(f, "for {} in {} {}", self.variable, self.range, self.body) 70 | } 71 | } 72 | 73 | impl Latex for For { 74 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 75 | write!( 76 | f, 77 | "\\forall {} \\in {} \\text{{ do }} {}", 78 | self.variable, 79 | self.range, 80 | self.body 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/helper/delimited.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{Parse, Parser}; 3 | use std::marker::PhantomData; 4 | 5 | /// Represents zero or more values that are delimited by a certain token. 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub struct Delimited { 8 | /// The values. 9 | pub values: Vec, 10 | 11 | /// Marker type to allow using `D` as a type parameter. 12 | delimiter: PhantomData, 13 | } 14 | 15 | impl<'source, D, T> Parse<'source> for Delimited 16 | where 17 | D: Parse<'source> + std::fmt::Debug, 18 | T: Parse<'source> + std::fmt::Debug, 19 | { 20 | fn std_parse( 21 | input: &mut Parser<'source>, 22 | recoverable_errors: &mut Vec 23 | ) -> Result> { 24 | let mut values = Vec::new(); 25 | 26 | loop { 27 | // manually catch unrecoverable errors so we can parse zero values 28 | let Ok(value) = input.try_parse().forward_errors(recoverable_errors) else { 29 | break; 30 | }; 31 | values.push(value); 32 | 33 | if input.try_parse::().forward_errors(recoverable_errors).is_err() { 34 | break; 35 | } 36 | } 37 | 38 | Ok(Self { values, delimiter: PhantomData }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/helper/mod.rs: -------------------------------------------------------------------------------- 1 | //! Helper `struct`s for parsing AST nodes. 2 | 3 | pub mod delimited; 4 | pub mod surrounded; 5 | 6 | pub use delimited::Delimited; 7 | pub use surrounded::Surrounded; 8 | 9 | /// Type alias for a comma-separated list of values, surrounded by parentheses. 10 | pub type ParenDelimited<'source, T> = surrounded::Surrounded< 11 | 'source, 12 | crate::parser::token::OpenParen<'source>, 13 | delimited::Delimited, T>, 14 | >; 15 | 16 | /// Type alias for a value surrounded by square brackets. 17 | pub type Square<'source, T> = surrounded::Surrounded<'source, crate::parser::token::OpenSquare<'source>, T>; 18 | 19 | /// Type alias for a comma-separated list of values, surrounded by square brackets. 20 | pub type SquareDelimited<'source, T> = Square<'source, delimited::Delimited, T>>; 21 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/helper/surrounded.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | token::pair::Pair, 4 | Parse, 5 | Parser, 6 | }; 7 | use std::marker::PhantomData; 8 | 9 | /// Represents a value that is surrounded by two tokens. 10 | #[derive(Debug, Clone, PartialEq, Eq)] 11 | pub struct Surrounded<'source, P, T> 12 | where 13 | P: Pair, 14 | { 15 | /// The opening token that was parsed. 16 | pub open: P::Open<'source>, 17 | 18 | /// The value. 19 | pub value: T, 20 | 21 | /// The closing token that was parsed. 22 | pub close: P::Close<'source>, 23 | 24 | /// Marker type to allow using `P` as a type parameter. 25 | pair: PhantomData

, 26 | } 27 | 28 | impl<'source, P, T> Parse<'source> for Surrounded<'source, P, T> 29 | where 30 | P: Parse<'source> + Pair, 31 | T: Parse<'source>, 32 | { 33 | fn std_parse( 34 | input: &mut Parser<'source>, 35 | recoverable_errors: &mut Vec 36 | ) -> Result> { 37 | let open = input.try_parse().forward_errors(recoverable_errors)?; 38 | 39 | // clone the input so we can scan forward without affecting the original input 40 | let mut input_ahead = input.clone(); 41 | 42 | // scan forward for the corresponding end token 43 | // if we don't find it, do not attempt to parse the inner value 44 | let mut depth = 1; 45 | while depth > 0 { 46 | let token = input_ahead.next_token() 47 | .map_err(|eof| vec![eof])?; 48 | 49 | if token.kind == P::OPEN { 50 | depth += 1; 51 | } else if token.kind == P::CLOSE { 52 | depth -= 1; 53 | } 54 | 55 | if depth == 0 { 56 | break; 57 | } 58 | } 59 | 60 | // exiting the loop means that there is indeed a corresponding end token 61 | let value = input.try_parse().forward_errors(recoverable_errors)?; 62 | 63 | // if this fails, there's probably extraneous tokens between the value and the end token 64 | let close = input.try_parse().forward_errors(recoverable_errors)?; 65 | 66 | Ok(Self { open, value, close, pair: PhantomData }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/if_expr.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::expr::{Atom, Expr}, 4 | error::MissingIfBranch, 5 | fmt::Latex, 6 | garbage::Garbage, 7 | keyword::{Else, If as IfToken}, 8 | Parse, 9 | Parser, 10 | }; 11 | use std::{fmt, ops::Range}; 12 | 13 | #[cfg(feature = "serde")] 14 | use serde::{Deserialize, Serialize}; 15 | 16 | /// An `if` expression, such as `if true 1 else 2`. 17 | #[derive(Debug, Clone, PartialEq, Eq)] 18 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 19 | pub struct If { 20 | /// The condition of the `if` expression. 21 | pub condition: Box, 22 | 23 | /// The expression to evaluate if the condition is true. 24 | pub then_expr: Box, 25 | 26 | /// The expression to evaluate if the condition is false. 27 | pub else_expr: Option>, 28 | 29 | /// The region of the source code that this literal was parsed from. 30 | pub span: Range, 31 | 32 | /// The span of the `if` keyword. 33 | pub if_span: Range, 34 | 35 | /// The span of the `else` keyword. 36 | pub else_span: Option>, 37 | } 38 | 39 | impl If { 40 | /// Returns the span of the `if` expression. 41 | pub fn span(&self) -> Range { 42 | self.span.clone() 43 | } 44 | } 45 | 46 | impl<'source> Parse<'source> for If { 47 | fn std_parse( 48 | input: &mut Parser<'source>, 49 | recoverable_errors: &mut Vec 50 | ) -> Result> { 51 | let if_token = input.try_parse::().forward_errors(recoverable_errors)?; 52 | let condition = input.try_parse().forward_errors(recoverable_errors)?; 53 | let then_expr = input.try_parse_with_state::<_, Atom>(|input| { 54 | input.allow_then = true; 55 | }) 56 | .map(Expr::from) 57 | .forward_errors(recoverable_errors)?; 58 | let (else_token, else_expr) = 'else_branch: { 59 | let Ok(else_token) = input.try_parse::().forward_errors(recoverable_errors) else { 60 | break 'else_branch (None, None); 61 | }; 62 | input.try_parse::() 63 | .forward_errors(recoverable_errors) 64 | .map(|expr| (Some(else_token), Some(expr))) 65 | .unwrap_or_else(|_| { 66 | recoverable_errors.push(Error::new( 67 | vec![if_token.span.clone(), input.span()], 68 | MissingIfBranch { 69 | keyword: "else", 70 | }, 71 | )); 72 | Garbage::garbage() 73 | }) 74 | }; 75 | let span = if let Some(else_expr) = &else_expr { 76 | if_token.span.start..else_expr.span().end 77 | } else { 78 | if_token.span.start..then_expr.span().end 79 | }; 80 | 81 | Ok(Self { 82 | condition: Box::new(condition), 83 | then_expr: Box::new(then_expr), 84 | else_expr: else_expr.map(Box::new), 85 | span, 86 | if_span: if_token.span, 87 | else_span: else_token.map(|token| token.span), 88 | }) 89 | } 90 | } 91 | 92 | impl std::fmt::Display for If { 93 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 94 | write!(f, "if ")?; 95 | self.condition.fmt(f)?; 96 | self.then_expr.fmt(f)?; 97 | if let Some(else_expr) = &self.else_expr { 98 | write!(f, " else ")?; 99 | else_expr.fmt(f)?; 100 | } 101 | Ok(()) 102 | } 103 | } 104 | 105 | impl Latex for If { 106 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 107 | write!(f, "\\text{{if }}")?; 108 | self.condition.fmt_latex(f)?; 109 | self.then_expr.fmt_latex(f)?; 110 | if let Some(else_expr) = &self.else_expr { 111 | write!(f, "\\text{{ else }}")?; 112 | else_expr.fmt_latex(f)?; 113 | } 114 | Ok(()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/index.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::{expr::{Expr, Primary}, helper::Square}, 4 | fmt::Latex, 5 | Parser, 6 | }; 7 | use std::{fmt, ops::Range}; 8 | 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | /// List indexing, such as `list[0]`. 13 | /// 14 | /// [`Parse`]: crate::parser::Parse 15 | #[derive(Debug, Clone, PartialEq, Eq)] 16 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 17 | pub struct Index { 18 | /// The expression being indexed. 19 | pub target: Box, 20 | 21 | /// The amount to index by. 22 | pub index: Box, 23 | 24 | /// The region of the source code that this function call was parsed from. 25 | pub span: Range, 26 | 27 | /// The span of the brackets that surround the index expression. 28 | pub bracket_span: Range, 29 | } 30 | 31 | impl Index { 32 | /// Returns the span of the index expression. 33 | pub fn span(&self) -> Range { 34 | self.span.clone() 35 | } 36 | 37 | /// Returns a set of two spans, where the first is the span of the expression being indexed 38 | /// (with the opening bracket) and the second is the span of the closing bracket. 39 | pub fn outer_span(&self) -> [Range; 2] { 40 | [ 41 | self.target.span().start..self.bracket_span.start + 1, 42 | self.bracket_span.end - 1..self.bracket_span.end, 43 | ] 44 | } 45 | 46 | /// Attempts to parse an [`Index`], where the initial target has already been parsed. 47 | /// 48 | /// Besides the returned [`Primary`], the return value also includes a boolean that indicates 49 | /// if the expression was changed due to successfully parsing a [`Index`]. This function can 50 | /// return even if no [`Index`], which occurs when we determine that we shouldn't have taken the 51 | /// [`Index`] path. The boolean is used to let the caller know that this is was the case. 52 | /// 53 | /// This is similar to what we had to do with [`Binary`]. 54 | /// 55 | /// [`Binary`]: crate::parser::ast::binary::Binary 56 | pub fn parse_or_lower( 57 | input: &mut Parser, 58 | recoverable_errors: &mut Vec, 59 | mut target: Primary, 60 | ) -> (Primary, bool) { 61 | let mut changed = false; 62 | 63 | // iteratively search for nested index expressions 64 | while let Ok(surrounded) = input.try_parse::>().forward_errors(recoverable_errors) { 65 | let span = target.span().start..surrounded.close.span.end; 66 | target = Primary::Index(Self { 67 | target: Box::new(target.into()), 68 | index: Box::new(surrounded.value), 69 | span, 70 | bracket_span: surrounded.open.span.start..surrounded.close.span.end, 71 | }); 72 | changed = true; 73 | } 74 | 75 | (target, changed) 76 | } 77 | } 78 | 79 | impl std::fmt::Display for Index { 80 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 81 | write!(f, "{}[{}]", self.target, self.index) 82 | } 83 | } 84 | 85 | impl Latex for Index { 86 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 87 | write!(f, "{}_{{{}}}", self.target, self.index) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod assign; 2 | pub mod binary; 3 | pub mod block; 4 | pub mod branch; 5 | pub mod call; 6 | pub mod expr; 7 | pub mod for_expr; 8 | pub mod helper; 9 | pub mod index; 10 | pub mod if_expr; 11 | pub mod literal; 12 | pub mod loop_expr; 13 | pub mod paren; 14 | pub mod product; 15 | pub mod range; 16 | pub mod return_expr; 17 | pub mod stmt; 18 | pub mod sum; 19 | pub mod unary; 20 | pub mod while_expr; 21 | 22 | pub use assign::{Assign, AssignTarget, FuncHeader, Param}; 23 | pub use binary::Binary; 24 | pub use block::Block; 25 | pub use branch::Then; 26 | pub use call::Call; 27 | pub use expr::{Expr, Primary}; 28 | pub use for_expr::For; 29 | pub use if_expr::If; 30 | pub use index::Index; 31 | pub use literal::{Literal, LitFloat, LitInt, LitList, LitListRepeat, LitRadix, LitSym, LitUnit}; 32 | pub use loop_expr::Loop; 33 | pub use paren::Paren; 34 | pub use product::Product; 35 | pub use range::{Range, RangeKind}; 36 | pub use return_expr::Return; 37 | pub use stmt::Stmt; 38 | pub use sum::Sum; 39 | pub use unary::Unary; 40 | pub use while_expr::While; 41 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/paren.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::expr::Expr, 4 | error::UnclosedParenthesis, 5 | fmt::Latex, 6 | garbage::Garbage, 7 | token::{CloseParen, OpenParen}, 8 | Parse, 9 | Parser, 10 | }; 11 | use std::{fmt, ops::Range}; 12 | 13 | #[cfg(feature = "serde")] 14 | use serde::{Deserialize, Serialize}; 15 | 16 | /// A parenthesized expression. A [`Paren`] can only contain a single expression. 17 | #[derive(Debug, Clone, PartialEq, Eq)] 18 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 19 | pub struct Paren { 20 | /// The inner expression. 21 | pub expr: Box, 22 | 23 | /// The region of the source code that this [`Paren`] was parsed from. 24 | pub span: Range, 25 | } 26 | 27 | impl Paren { 28 | /// Returns the span of the parenthesized expression. 29 | pub fn span(&self) -> Range { 30 | self.span.clone() 31 | } 32 | 33 | /// Returns the innermost expression in the parenthesized expression. 34 | pub fn innermost(&self) -> &Expr { 35 | let mut inner = &self.expr; 36 | while let Expr::Paren(paren) = inner.as_ref() { 37 | inner = &paren.expr; 38 | } 39 | inner 40 | } 41 | 42 | /// Returns the innermost expression in the parenthesized expression, consuming the [`Paren`]. 43 | pub fn into_innermost(self) -> Expr { 44 | let mut inner = self.expr; 45 | while let Expr::Paren(paren) = *inner { 46 | inner = paren.expr; 47 | } 48 | *inner 49 | } 50 | } 51 | 52 | impl<'source> Parse<'source> for Paren { 53 | fn std_parse( 54 | input: &mut Parser<'source>, 55 | recoverable_errors: &mut Vec 56 | ) -> Result> { 57 | let open_paren = input.try_parse::().forward_errors(recoverable_errors)?; 58 | let expr = input.try_parse().forward_errors(recoverable_errors)?; 59 | let close_paren = input.try_parse::() 60 | .forward_errors(recoverable_errors) 61 | .unwrap_or_else(|_| { 62 | recoverable_errors.push(Error::new( 63 | vec![open_paren.span.clone()], 64 | UnclosedParenthesis { opening: true }, 65 | )); 66 | 67 | // fake a close paren for recovery purposes 68 | Garbage::garbage() 69 | }); 70 | Ok(Self { 71 | expr: Box::new(expr), 72 | span: open_paren.span.start..close_paren.span.end, 73 | }) 74 | } 75 | } 76 | 77 | impl std::fmt::Display for Paren { 78 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 79 | write!(f, "(")?; 80 | self.expr.fmt(f)?; 81 | write!(f, ")") 82 | } 83 | } 84 | 85 | impl Latex for Paren { 86 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 87 | write!(f, "\\left(")?; 88 | self.expr.fmt_latex(f)?; 89 | write!(f, "\\right)") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/product.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::{ 4 | expr::Expr, 5 | literal::LitSym, 6 | range::Range as RangeExpr, 7 | }, 8 | fmt::Latex, 9 | keyword::{In as InToken, Product as ProductToken}, 10 | Parse, 11 | Parser, 12 | }; 13 | use std::{fmt, ops::Range}; 14 | 15 | #[cfg(feature = "serde")] 16 | use serde::{Deserialize, Serialize}; 17 | 18 | /// A product expression, such as `product n in 1..10 of n`. 19 | /// 20 | /// A product expression is a shortcut for a loop that represents an accumulative product. The 21 | /// final expression is multiplied over the specified range, with a specific variable name taking 22 | /// on each value in the range. The above example is equivalent to the following code: 23 | /// 24 | /// ```calcscript 25 | /// out = 1 26 | /// for n in 1..10 { 27 | /// out *= n 28 | /// } 29 | /// out 30 | /// ``` 31 | #[derive(Debug, Clone, PartialEq, Eq)] 32 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 33 | pub struct Product { 34 | /// The variable name representing each value in the range. 35 | pub variable: LitSym, 36 | 37 | /// The range of values that the variable will take on. 38 | pub range: RangeExpr, 39 | 40 | /// The body of the product. 41 | pub body: Box, 42 | 43 | /// The region of the source code that this `product` expression was parsed from. 44 | pub span: Range, 45 | } 46 | 47 | impl Product { 48 | /// Returns the span of the `product` expression. 49 | pub fn span(&self) -> Range { 50 | self.span.clone() 51 | } 52 | } 53 | 54 | impl<'source> Parse<'source> for Product { 55 | fn std_parse( 56 | input: &mut Parser<'source>, 57 | recoverable_errors: &mut Vec 58 | ) -> Result> { 59 | let product_token = input.try_parse::().forward_errors(recoverable_errors)?; 60 | let variable = input.try_parse::().forward_errors(recoverable_errors)?; 61 | input.try_parse::().forward_errors(recoverable_errors)?; 62 | let range = input.try_parse::().forward_errors(recoverable_errors)?; 63 | let body = input.try_parse_with_state::<_, Expr>(|state| { 64 | state.allow_of = true; 65 | }).forward_errors(recoverable_errors)?; 66 | let span = product_token.span.start..body.span().end; 67 | 68 | Ok(Self { 69 | variable, 70 | range, 71 | body: Box::new(body), 72 | span, 73 | }) 74 | } 75 | } 76 | 77 | impl std::fmt::Display for Product { 78 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 79 | write!(f, "product {} in {} {}", self.variable, self.range, self.body) 80 | } 81 | } 82 | 83 | impl Latex for Product { 84 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 85 | write!( 86 | f, 87 | "\\prod_{{{}={}}}^{{{}}} {}", 88 | self.variable, 89 | self.range.start, 90 | self.range.end, 91 | self.body, 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/range.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ast::expr::Expr, fmt::Latex}; 2 | use std::fmt; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// Whether a range is inclusive or exclusive. 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 9 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 10 | pub enum RangeKind { 11 | /// A half-open range, `[start, end)`. 12 | HalfOpen, 13 | 14 | /// A closed range, `[start, end]`. 15 | Closed, 16 | } 17 | 18 | /// A range expression, written as `start..end` for a half-open range (`[start, end)`), or 19 | /// `start..=end` for a closed range (`[start, end]`). 20 | #[derive(Debug, Clone, PartialEq, Eq)] 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | pub struct Range { 23 | /// The start of the range. 24 | pub start: Box, 25 | 26 | /// The end of the range. 27 | pub end: Box, 28 | 29 | /// Whether the range is inclusive or exclusive. 30 | pub kind: RangeKind, 31 | 32 | /// The region of the source code that this literal was parsed from. 33 | pub span: std::ops::Range, 34 | } 35 | 36 | impl Range { 37 | /// Returns the span of the `range` expression. 38 | pub fn span(&self) -> std::ops::Range { 39 | self.span.clone() 40 | } 41 | } 42 | 43 | impl std::fmt::Display for Range { 44 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 45 | match self.kind { 46 | RangeKind::HalfOpen => write!(f, "{} .. {}", self.start, self.end), 47 | RangeKind::Closed => write!(f, "{} ..= {}", self.start, self.end), 48 | } 49 | } 50 | } 51 | 52 | impl Latex for Range { 53 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 54 | match self.kind { 55 | RangeKind::HalfOpen => write!(f, "\\left[{}, {}\\right)", self.start, self.end), 56 | RangeKind::Closed => write!(f, "\\left[{}, {}\\right]", self.start, self.end), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/return_expr.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::expr::Expr, 4 | error::ReturnOutsideFunction, 5 | fmt::Latex, 6 | keyword::Return as ReturnToken, 7 | Parse, 8 | Parser, 9 | }; 10 | use std::{fmt, ops::Range}; 11 | 12 | #[cfg(feature = "serde")] 13 | use serde::{Deserialize, Serialize}; 14 | 15 | /// A `return` expression, used to return a value from a function. 16 | #[derive(Debug, Clone, PartialEq, Eq)] 17 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 18 | pub struct Return { 19 | /// The value to return from the function. 20 | pub value: Option>, 21 | 22 | /// The region of the source code that this expression was parsed from. 23 | pub span: Range, 24 | 25 | /// The span of the `return` keyword. 26 | pub return_span: Range, 27 | } 28 | 29 | impl Return { 30 | /// Returns the span of the `return` expression. 31 | pub fn span(&self) -> Range { 32 | self.span.clone() 33 | } 34 | } 35 | 36 | impl<'source> Parse<'source> for Return { 37 | fn std_parse( 38 | input: &mut Parser<'source>, 39 | recoverable_errors: &mut Vec 40 | ) -> Result> { 41 | let return_token = input.try_parse::().forward_errors(recoverable_errors)?; 42 | let value = input.try_parse_with_state::<_, Expr>(|state| { 43 | state.expr_end_at_eol = true; 44 | }).forward_errors(recoverable_errors).ok(); 45 | let span = if let Some(value) = &value { 46 | return_token.span.start..value.span().end 47 | } else { 48 | return_token.span.clone() 49 | }; 50 | 51 | // `return` expressions can only be used inside functions 52 | if !input.state.allow_return { 53 | recoverable_errors.push(Error::new( 54 | vec![return_token.span.clone()], 55 | ReturnOutsideFunction, 56 | )); 57 | } 58 | 59 | Ok(Self { 60 | value: value.map(Box::new), 61 | span, 62 | return_span: return_token.span, 63 | }) 64 | } 65 | } 66 | 67 | impl std::fmt::Display for Return { 68 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 69 | write!(f, "return")?; 70 | if let Some(value) = &self.value { 71 | write!(f, " {}", value)?; 72 | } 73 | Ok(()) 74 | } 75 | } 76 | 77 | impl Latex for Return { 78 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 79 | write!(f, "\\text{{return }}")?; 80 | if let Some(value) = &self.value { 81 | value.fmt_latex(f)?; 82 | } 83 | Ok(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/stmt.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::expr::Expr, 4 | fmt::Latex, 5 | token::Semicolon, 6 | Parse, 7 | Parser, 8 | }; 9 | use std::{fmt, ops::Range}; 10 | 11 | #[cfg(feature = "serde")] 12 | use serde::{Deserialize, Serialize}; 13 | 14 | /// Represents a statement in CalcScript. 15 | /// 16 | /// A statement is the building block of all CalcScript programs. A complete program is a sequence 17 | /// of one or more statements that are executed in order, and returns the value of the last 18 | /// statement. A statement's return value can be discarded by adding a semicolon at the end of the 19 | /// statement. 20 | #[derive(Debug, Clone, PartialEq, Eq)] 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | pub struct Stmt { 23 | /// The expression of a statement. 24 | pub expr: Expr, 25 | 26 | /// The span of the semicolon that terminates the statement, if any. 27 | /// 28 | /// When this is [`None`], the statement is an expression, and will return the value of the 29 | /// expression. Otherwise, the expression is evaluated for side effects, and the statement 30 | /// returns the unit type `()`. 31 | pub semicolon: Option>, 32 | 33 | /// The region of the source code that this statement was parsed from. 34 | pub span: Range, 35 | } 36 | 37 | impl Stmt { 38 | /// Returns the span of the statement. 39 | pub fn span(&self) -> Range { 40 | self.span.clone() 41 | } 42 | } 43 | 44 | impl<'source> Parse<'source> for Stmt { 45 | fn std_parse( 46 | input: &mut Parser<'source>, 47 | recoverable_errors: &mut Vec 48 | ) -> Result> { 49 | let expr = input.try_parse::().forward_errors(recoverable_errors)?; 50 | let semicolon = if let Ok(semi) = input.try_parse::().forward_errors(recoverable_errors) { 51 | Some(semi.span.clone()) 52 | } else { 53 | None 54 | }; 55 | let stmt_span = if let Some(semicolon) = &semicolon { 56 | expr.span().start..semicolon.end 57 | } else { 58 | expr.span() 59 | }; 60 | 61 | Ok(Stmt { 62 | expr, 63 | semicolon, 64 | span: stmt_span, 65 | }) 66 | } 67 | } 68 | 69 | impl std::fmt::Display for Stmt { 70 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 71 | write!(f, "{}", self.expr)?; 72 | if self.semicolon.is_some() { 73 | write!(f, ";")?; 74 | } 75 | Ok(()) 76 | } 77 | } 78 | 79 | impl Latex for Stmt { 80 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 81 | self.expr.fmt_latex(f)?; 82 | if self.semicolon.is_some() { 83 | write!(f, ";")?; 84 | } 85 | Ok(()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/sum.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::{ 4 | expr::Expr, 5 | literal::LitSym, 6 | range::Range as RangeExpr, 7 | }, 8 | fmt::Latex, 9 | keyword::{In as InToken, Sum as SumToken}, 10 | Parse, 11 | Parser, 12 | }; 13 | use std::{fmt, ops::Range}; 14 | 15 | #[cfg(feature = "serde")] 16 | use serde::{Deserialize, Serialize}; 17 | 18 | /// A sum expression, such as `sum n in 1..10 of n`. 19 | /// 20 | /// A sum expression is a shortcut for a loop that represents a summation. The final expression is 21 | /// summed over the specified range, with a specific variable name taking on each value in the 22 | /// range. The above example is equivalent to the following code: 23 | /// 24 | /// ```calcscript 25 | /// out = 0 26 | /// for n in 1..10 { 27 | /// out += n 28 | /// } 29 | /// out 30 | /// ``` 31 | #[derive(Debug, Clone, PartialEq, Eq)] 32 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 33 | pub struct Sum { 34 | /// The variable name representing each value in the range. 35 | pub variable: LitSym, 36 | 37 | /// The range of values that the variable will take on. 38 | pub range: RangeExpr, 39 | 40 | /// The body of the summation. 41 | pub body: Box, 42 | 43 | /// The region of the source code that this `sum` expression was parsed from. 44 | pub span: Range, 45 | } 46 | 47 | impl Sum { 48 | /// Returns the span of the `sum` expression. 49 | pub fn span(&self) -> Range { 50 | self.span.clone() 51 | } 52 | } 53 | 54 | impl<'source> Parse<'source> for Sum { 55 | fn std_parse( 56 | input: &mut Parser<'source>, 57 | recoverable_errors: &mut Vec 58 | ) -> Result> { 59 | let sum_token = input.try_parse::().forward_errors(recoverable_errors)?; 60 | let variable = input.try_parse::().forward_errors(recoverable_errors)?; 61 | input.try_parse::().forward_errors(recoverable_errors)?; 62 | let range = input.try_parse::().forward_errors(recoverable_errors)?; 63 | let body = input.try_parse_with_state::<_, Expr>(|state| { 64 | state.allow_of = true; 65 | }).forward_errors(recoverable_errors)?; 66 | let span = sum_token.span.start..body.span().end; 67 | 68 | Ok(Self { 69 | variable, 70 | range, 71 | body: Box::new(body), 72 | span, 73 | }) 74 | } 75 | } 76 | 77 | impl std::fmt::Display for Sum { 78 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 79 | write!(f, "sum {} in {} {}", self.variable, self.range, self.body) 80 | } 81 | } 82 | 83 | impl Latex for Sum { 84 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 85 | write!( 86 | f, 87 | "\\sum_{{{}={}}}^{{{}}} {}", 88 | self.variable, 89 | self.range.start, 90 | self.range.end, 91 | self.body, 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /cas-parser/src/parser/ast/while_expr.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::parser::{ 3 | ast::expr::Expr, 4 | fmt::Latex, 5 | keyword::While as WhileToken, 6 | Parse, 7 | Parser, 8 | }; 9 | use std::{fmt, ops::Range}; 10 | 11 | #[cfg(feature = "serde")] 12 | use serde::{Deserialize, Serialize}; 13 | 14 | /// A `while` loop expression, such as `while x < 10 then x += 1`. The loop body is executed 15 | /// repeatedly as long as the outer condition is true. 16 | #[derive(Debug, Clone, PartialEq, Eq)] 17 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 18 | pub struct While { 19 | /// The condition that must be true for the loop body to be executed. 20 | pub condition: Box, 21 | 22 | /// The body of the loop. 23 | pub body: Box, 24 | 25 | /// The region of the source code that this expression was parsed from. 26 | pub span: Range, 27 | 28 | /// The span of the `while` keyword. 29 | pub while_span: Range, 30 | } 31 | 32 | impl While { 33 | /// Returns the span of the `while` loop expression. 34 | pub fn span(&self) -> Range { 35 | self.span.clone() 36 | } 37 | } 38 | 39 | impl<'source> Parse<'source> for While { 40 | fn std_parse( 41 | input: &mut Parser<'source>, 42 | recoverable_errors: &mut Vec 43 | ) -> Result> { 44 | let while_token = input.try_parse::().forward_errors(recoverable_errors)?; 45 | let condition = input.try_parse::().forward_errors(recoverable_errors)?; 46 | let then_body = input.try_parse_with_state::<_, Expr>(|state| { 47 | state.allow_then = true; 48 | state.allow_loop_control = true; 49 | }).forward_errors(recoverable_errors)?; 50 | let span = while_token.span.start..then_body.span().end; 51 | 52 | Ok(Self { 53 | condition: Box::new(condition), 54 | body: Box::new(then_body), 55 | span, 56 | while_span: while_token.span, 57 | }) 58 | } 59 | } 60 | 61 | impl std::fmt::Display for While { 62 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 63 | write!(f, "while {} {}", self.condition, self.body) 64 | } 65 | } 66 | 67 | impl Latex for While { 68 | fn fmt_latex(&self, f: &mut fmt::Formatter) -> fmt::Result { 69 | write!(f, "\\text{{while }}")?; 70 | self.condition.fmt_latex(f)?; 71 | self.body.fmt_latex(f)?; 72 | Ok(()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cas-parser/src/parser/fmt.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | use super::{ast::expr::Expr, token::op::BinOpKind}; 3 | 4 | /// A trait for types that can be formatted as LaTeX. 5 | pub trait Latex { 6 | /// Format the value as LaTeX. 7 | fn fmt_latex(&self, f: &mut Formatter) -> Result; 8 | 9 | /// Wraps the value in a [`LatexFormatter`], which implements [`Display`]. 10 | fn as_display(&self) -> LatexFormatter<'_, Self> { 11 | LatexFormatter(self) 12 | } 13 | } 14 | 15 | /// A wrapper type that implements [`Display`] for any type that implements [`Latex`]. 16 | pub struct LatexFormatter<'a, T: ?Sized>(&'a T); 17 | 18 | impl Display for LatexFormatter<'_, T> 19 | where 20 | T: Latex, 21 | { 22 | fn fmt(&self, f: &mut Formatter) -> Result { 23 | self.0.fmt_latex(f) 24 | } 25 | } 26 | 27 | /// Helper to format powers. 28 | pub fn fmt_pow(f: &mut Formatter, left: Option<&Expr>, right: Option<&Expr>) -> Result { 29 | if let Some(left) = left { 30 | let left = left.innermost(); 31 | let mut insert_with_paren = || { 32 | write!(f, "\\left(")?; 33 | left.fmt_latex(f)?; 34 | write!(f, "\\right)") 35 | }; 36 | 37 | // all of these are separate match arms instead of a single match arm with multiple 38 | // patterns, because apparently that can't be parsed correctly 39 | match left { 40 | Expr::Unary(unary) 41 | if unary.op.precedence() <= BinOpKind::Exp.precedence() => insert_with_paren(), 42 | // NOTE: exp is the highest precedence binary operator, so this check is not necessary, 43 | // but is just here for completeness 44 | Expr::Binary(binary) 45 | if binary.op.precedence() <= BinOpKind::Exp.precedence() => insert_with_paren(), 46 | Expr::Call(call) if call.name.name == "pow" => insert_with_paren(), 47 | _ => left.fmt_latex(f), 48 | }? 49 | } 50 | write!(f, "^{{")?; 51 | if let Some(right) = right { 52 | right.innermost().fmt_latex(f)?; 53 | } 54 | write!(f, "}}") 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use super::*; 60 | 61 | use crate::parser::Parser; 62 | 63 | #[test] 64 | fn fmt_display() { 65 | let mut parser = Parser::new("3x + 6"); 66 | let expr = parser.try_parse_full::().unwrap(); 67 | let fmt = format!("{}", expr); 68 | 69 | assert_eq!(fmt, "3x+6"); 70 | } 71 | 72 | #[test] 73 | fn fmt_display_2() { 74 | let mut parser = Parser::new("f(x) = x^2 + 5x + 6"); 75 | let expr = parser.try_parse_full::().unwrap(); 76 | let fmt = format!("{}", expr); 77 | 78 | assert_eq!(fmt, "f(x) = x^2+5x+6"); 79 | } 80 | 81 | #[test] 82 | fn fmt_display_3() { 83 | let mut parser = Parser::new("x^(3(x + 6))^9"); 84 | let expr = parser.try_parse_full::().unwrap(); 85 | let fmt = format!("{}", expr); 86 | 87 | assert_eq!(fmt, "x^(3(x+6))^9"); 88 | } 89 | 90 | #[test] 91 | fn fmt_latex() { 92 | let mut parser = Parser::new("sqrt(3x)^2"); 93 | let expr = parser.try_parse_full::().unwrap(); 94 | let fmt = format!("{}", expr.as_display()); 95 | 96 | assert_eq!(fmt, "\\sqrt{3x}^{2}"); 97 | } 98 | 99 | #[test] 100 | fn fmt_latex_2() { 101 | let mut parser = Parser::new("f(x) = 1/x + 5/x^2 + 6/x^3"); 102 | let expr = parser.try_parse_full::().unwrap(); 103 | let fmt = format!("{}", expr.as_display()); 104 | 105 | assert_eq!(fmt, "\\mathrm{ f } \\left(x\\right) = \\frac{1}{x}+\\frac{5}{x^{2}}+\\frac{6}{x^{3}}"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cas-parser/src/parser/garbage.rs: -------------------------------------------------------------------------------- 1 | use super::ast::{assign::AssignTarget, expr::Expr, literal::{LitSym, Literal}, paren::Paren}; 2 | 3 | /// A trait for producing garbage values, useful for recovering from parsing errors. 4 | /// 5 | /// We could've implemented [`Default`] on types instead, but garbage values are not useful to the 6 | /// end user, and we don't want to encourage its use due to [`Default`] being implemented. 7 | pub(crate) trait Garbage { 8 | /// Produces a garbage value. 9 | fn garbage() -> Self; 10 | } 11 | 12 | /// Implements [`Garbage`] for tuples. 13 | macro_rules! garbage_tuple { 14 | ($($ty:ident),*) => { 15 | impl<$($ty: Garbage),*> Garbage for ($($ty,)*) { 16 | fn garbage() -> Self { 17 | ($($ty::garbage(),)*) 18 | } 19 | } 20 | }; 21 | } 22 | 23 | garbage_tuple!(A, B); 24 | 25 | impl Garbage for Result { 26 | fn garbage() -> Self { 27 | Ok(T::garbage()) 28 | } 29 | } 30 | 31 | impl Garbage for Option { 32 | fn garbage() -> Self { 33 | Some(T::garbage()) 34 | } 35 | } 36 | 37 | impl Garbage for AssignTarget { 38 | fn garbage() -> Self { 39 | AssignTarget::Symbol(LitSym::garbage()) 40 | } 41 | } 42 | 43 | impl Garbage for Expr { 44 | fn garbage() -> Self { 45 | Expr::Literal(Literal::Symbol(LitSym::garbage())) 46 | } 47 | } 48 | 49 | impl Garbage for LitSym { 50 | fn garbage() -> Self { 51 | Self { name: String::new(), span: 0..0 } 52 | } 53 | } 54 | 55 | impl Garbage for Paren { 56 | fn garbage() -> Self { 57 | Self { expr: Box::new(Expr::garbage()), span: 0..0 } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cas-parser/src/parser/keyword.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | use crate::{ 3 | parser::{error::UnexpectedToken, garbage::Garbage, Parser, Parse}, 4 | tokenizer::TokenKind, 5 | }; 6 | use std::ops::Range; 7 | 8 | /// Generates a unit struct for each keyword, as well as a simple [`Parse`] implementation for each 9 | /// keyword. This enables the parser to use and request keywords as a type, which is much more 10 | /// ergonomic than using a string. 11 | macro_rules! keywords { 12 | ($(($name:ident, $lexeme:tt))*) => { 13 | $( 14 | #[derive(Clone, Debug, PartialEq)] 15 | pub struct $name<'source> { 16 | pub lexeme: &'source str, 17 | pub span: Range, 18 | } 19 | 20 | impl<'source> Parse<'source> for $name<'source> { 21 | fn std_parse( 22 | input: &mut Parser<'source>, 23 | _: &mut Vec 24 | ) -> Result> { 25 | let token = input.next_token().map_err(|e| vec![e])?; 26 | 27 | if token.kind == TokenKind::Keyword { 28 | if token.lexeme != stringify!($lexeme) { 29 | // return Err(vec![Error::new(vec![token.span], UnexpectedToken { 30 | // expected: &[stringify!($lexeme)], 31 | // found: token.kind, 32 | // })]); 33 | // TODO 34 | return Err(vec![Error::new(vec![token.span], UnexpectedToken { 35 | expected: &[TokenKind::Keyword], 36 | found: token.kind, 37 | })]); 38 | } 39 | Ok(Self { 40 | lexeme: token.lexeme, 41 | span: token.span, 42 | }) 43 | } else { 44 | Err(vec![Error::new(vec![token.span], UnexpectedToken { 45 | expected: &[TokenKind::Keyword], 46 | found: token.kind, 47 | })]) 48 | } 49 | } 50 | } 51 | 52 | impl<'source> Garbage for $name<'source> { 53 | fn garbage() -> Self { 54 | Self { lexeme: "", span: 0..0 } 55 | } 56 | } 57 | )* 58 | }; 59 | } 60 | 61 | keywords!( 62 | (Let, let) 63 | (If, if) 64 | (Then, then) 65 | (Else, else) 66 | (For, for) 67 | (Sum, sum) 68 | (Product, product) 69 | (In, in) 70 | (Of, of) 71 | (Loop, loop) 72 | (While, while) 73 | (Break, break) 74 | (Continue, continue) 75 | (Return, return) 76 | ); 77 | -------------------------------------------------------------------------------- /cas-parser/src/parser/token/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod op; 2 | pub mod pair; 3 | 4 | use cas_error::Error; 5 | use crate::{ 6 | parser::{error::UnexpectedToken, garbage::Garbage, Parser, Parse}, 7 | tokenizer::TokenKind, 8 | }; 9 | use std::ops::Range; 10 | 11 | /// Generates a unit struct for each token kind, as well as a simple [`Parse`] implementation for 12 | /// each token kind. This enables the parser to use and request token kinds as a type, which is 13 | /// much more ergonomic than using a string. 14 | macro_rules! token_kinds { 15 | ($($name:ident)*) => { 16 | $( 17 | #[derive(Clone, Debug, PartialEq)] 18 | pub struct $name<'source> { 19 | pub lexeme: &'source str, 20 | pub span: Range, 21 | } 22 | 23 | impl<'source> Parse<'source> for $name<'source> { 24 | fn std_parse( 25 | input: &mut Parser<'source>, 26 | _: &mut Vec 27 | ) -> Result> { 28 | let token = input.next_token().map_err(|e| vec![e])?; 29 | 30 | if token.kind == TokenKind::$name { 31 | Ok(Self { 32 | lexeme: token.lexeme, 33 | span: token.span, 34 | }) 35 | } else { 36 | Err(vec![Error::new(vec![token.span], UnexpectedToken { 37 | expected: &[TokenKind::$name], 38 | found: token.kind, 39 | })]) 40 | } 41 | } 42 | } 43 | 44 | impl<'source> Garbage for $name<'source> { 45 | fn garbage() -> Self { 46 | Self { lexeme: "", span: 0..0 } 47 | } 48 | } 49 | )* 50 | }; 51 | } 52 | 53 | token_kinds!( 54 | NewLine 55 | Whitespace 56 | Eq 57 | NotEq 58 | ApproxEq 59 | ApproxNotEq 60 | Add 61 | Sub 62 | Mul 63 | Div 64 | Mod 65 | Exp 66 | Greater 67 | GreaterEq 68 | Less 69 | LessEq 70 | Not 71 | Factorial 72 | And 73 | Or 74 | BitAnd 75 | BitOr 76 | BitNot 77 | BitRight 78 | BitLeft 79 | Assign 80 | AddAssign 81 | SubAssign 82 | MulAssign 83 | DivAssign 84 | ModAssign 85 | ExpAssign 86 | AndAssign 87 | OrAssign 88 | BitAndAssign 89 | BitOrAssign 90 | BitRightAssign 91 | BitLeftAssign 92 | Bin 93 | Oct 94 | Hex 95 | Name 96 | Keyword 97 | Comma 98 | OpenParen 99 | CloseParen 100 | OpenCurly 101 | CloseCurly 102 | OpenSquare 103 | CloseSquare 104 | Quote 105 | Semicolon 106 | Int 107 | Boolean 108 | Dot 109 | RangeHalfOpen 110 | RangeClosed 111 | Symbol 112 | ); 113 | -------------------------------------------------------------------------------- /cas-parser/src/parser/token/pair.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | parser::{ 3 | token::{ 4 | CloseParen, 5 | OpenParen, 6 | CloseCurly, 7 | OpenCurly, 8 | CloseSquare, 9 | OpenSquare, 10 | }, 11 | Parse, 12 | }, 13 | tokenizer::TokenKind, 14 | }; 15 | 16 | /// A trait for token kinds that have a logical pairing with another token kind. Only tokens that 17 | /// implement [`Pair`] can be used with the [`Surrounded`] helper. 18 | /// 19 | /// This includes tokens like parentheses, square brackets, and curly braces. 20 | /// 21 | /// [`Surrounded`]: crate::parser::ast::helper::surrounded::Surrounded 22 | pub trait Pair { 23 | /// The type of the opening token, defined in the [`token`] module. 24 | /// 25 | /// [`token`]: crate::parser::token 26 | type Open<'source>: Parse<'source>; 27 | 28 | /// The type of the closing token, defined in the [`token`] module. 29 | /// 30 | /// [`token`]: crate::parser::token 31 | type Close<'source>: Parse<'source>; 32 | 33 | /// The corresponding opening [`TokenKind`]. 34 | const OPEN: TokenKind; 35 | 36 | /// The corresponding closing [`TokenKind`]. 37 | const CLOSE: TokenKind; 38 | } 39 | 40 | /// Generates implementations of [`Pair`] for each pair of token kinds. 41 | macro_rules! pairs { 42 | ($($open:ident $close:ident),* $(,)?) => { 43 | $( 44 | impl Pair for $open<'_> { 45 | type Open<'source> = $open<'source>; 46 | type Close<'source> = $close<'source>; 47 | const OPEN: TokenKind = TokenKind::$open; 48 | const CLOSE: TokenKind = TokenKind::$close; 49 | } 50 | 51 | impl Pair for $close<'_> { 52 | type Open<'source> = $open<'source>; 53 | type Close<'source> = $close<'source>; 54 | const OPEN: TokenKind = TokenKind::$open; 55 | const CLOSE: TokenKind = TokenKind::$close; 56 | } 57 | )* 58 | } 59 | } 60 | 61 | pairs!( 62 | OpenParen CloseParen, 63 | OpenCurly CloseCurly, 64 | OpenSquare CloseSquare, 65 | ); 66 | -------------------------------------------------------------------------------- /cas-parser/src/tokenizer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token; 2 | 3 | use logos::{Lexer, Logos}; 4 | pub use token::{Token, TokenKind}; 5 | 6 | /// Returns an iterator over the token kinds produced by the tokenizer. 7 | pub fn tokenize(input: &str) -> Lexer { 8 | TokenKind::lexer(input) 9 | } 10 | 11 | /// Returns an owned array containing all of the tokens produced by the tokenizer. This allows us 12 | /// to backtrack in case of an error. 13 | pub fn tokenize_complete(input: &str) -> Box<[Token]> { 14 | let mut lexer = tokenize(input); 15 | let mut tokens = Vec::new(); 16 | 17 | while let Some(Ok(kind)) = lexer.next() { 18 | tokens.push(Token { 19 | span: lexer.span(), 20 | kind, 21 | lexeme: lexer.slice(), 22 | }); 23 | } 24 | 25 | tokens.into_boxed_slice() 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | 32 | /// Compares the tokens produced by the tokenizer to the raw expected tokens. 33 | fn compare_tokens<'source, const N: usize>(input: &'source str, expected: [(TokenKind, &'source str); N]) { 34 | let mut lexer = tokenize(input); 35 | 36 | for (expected_kind, expected_lexeme) in expected.into_iter() { 37 | assert_eq!(lexer.next(), Some(Ok(expected_kind))); 38 | assert_eq!(lexer.slice(), expected_lexeme); 39 | } 40 | 41 | assert_eq!(lexer.next(), None); 42 | } 43 | 44 | #[test] 45 | fn basic_expr() { 46 | compare_tokens( 47 | "1 + 2", 48 | [ 49 | (TokenKind::Int, "1"), 50 | (TokenKind::Whitespace, " "), 51 | (TokenKind::Add, "+"), 52 | (TokenKind::Whitespace, " "), 53 | (TokenKind::Int, "2"), 54 | ], 55 | ); 56 | } 57 | 58 | #[test] 59 | fn complex_expr() { 60 | compare_tokens( 61 | "3 x - 0xff + 0b101 * $", 62 | [ 63 | (TokenKind::Int, "3"), 64 | (TokenKind::Whitespace, " "), 65 | (TokenKind::Name, "x"), 66 | (TokenKind::Whitespace, " "), 67 | (TokenKind::Sub, "-"), 68 | (TokenKind::Whitespace, " "), 69 | (TokenKind::Hex, "0x"), 70 | (TokenKind::Name, "ff"), 71 | (TokenKind::Whitespace, " "), 72 | (TokenKind::Add, "+"), 73 | (TokenKind::Whitespace, " "), 74 | (TokenKind::Bin, "0b"), 75 | (TokenKind::Int, "101"), 76 | (TokenKind::Whitespace, " "), 77 | (TokenKind::Mul, "*"), 78 | (TokenKind::Whitespace, " "), 79 | (TokenKind::Symbol, "$"), 80 | ], 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cas-parser/src/tokenizer/token.rs: -------------------------------------------------------------------------------- 1 | use logos::Logos; 2 | use std::ops::Range; 3 | 4 | /// The different kinds of tokens that can be produced by the tokenizer. 5 | #[derive(Logos, Clone, Copy, Debug, PartialEq, Eq)] 6 | pub enum TokenKind { 7 | #[regex(r"[\n\r]+")] 8 | NewLine, 9 | 10 | #[regex(r"[ \t]+")] 11 | Whitespace, 12 | 13 | #[regex(r"//.*")] 14 | Comment, 15 | 16 | #[token("==")] 17 | Eq, 18 | 19 | #[token("!=")] 20 | NotEq, 21 | 22 | #[token("~==")] 23 | ApproxEq, 24 | 25 | #[token("~!=")] 26 | ApproxNotEq, 27 | 28 | #[token("+")] 29 | Add, 30 | 31 | #[token("-")] 32 | Sub, 33 | 34 | #[token("*")] 35 | Mul, 36 | 37 | #[token("/")] 38 | Div, 39 | 40 | #[token("%")] 41 | Mod, 42 | 43 | #[token("^")] 44 | Exp, 45 | 46 | #[token(">")] 47 | Greater, 48 | 49 | #[token(">=")] 50 | GreaterEq, 51 | 52 | #[token("<")] 53 | Less, 54 | 55 | #[token("<=")] 56 | LessEq, 57 | 58 | #[token("not")] 59 | Not, 60 | 61 | #[token("!")] 62 | Factorial, 63 | 64 | #[token("&&")] 65 | And, 66 | 67 | #[token("||")] 68 | Or, 69 | 70 | #[token("&")] 71 | BitAnd, 72 | 73 | #[token("|")] 74 | BitOr, 75 | 76 | #[token("~")] 77 | BitNot, 78 | 79 | #[token(">>")] 80 | BitRight, 81 | 82 | #[token("<<")] 83 | BitLeft, 84 | 85 | #[token("=")] 86 | Assign, 87 | 88 | #[token("+=")] 89 | AddAssign, 90 | 91 | #[token("-=")] 92 | SubAssign, 93 | 94 | #[token("*=")] 95 | MulAssign, 96 | 97 | #[token("/=")] 98 | DivAssign, 99 | 100 | #[token("%=")] 101 | ModAssign, 102 | 103 | #[token("^=")] 104 | ExpAssign, 105 | 106 | #[token("&&=")] 107 | AndAssign, 108 | 109 | #[token("||=")] 110 | OrAssign, 111 | 112 | #[token("&=")] 113 | BitAndAssign, 114 | 115 | #[token("|=")] 116 | BitOrAssign, 117 | 118 | #[token(">>=")] 119 | BitRightAssign, 120 | 121 | #[token("<<=")] 122 | BitLeftAssign, 123 | 124 | #[token("0b")] 125 | Bin, 126 | 127 | #[token("0o")] 128 | Oct, 129 | 130 | #[token("0x")] 131 | Hex, 132 | 133 | #[regex(r"[a-zA-Z_]+|atan2")] // TODO: includes horrible hard-coded test for atan2 134 | Name, 135 | 136 | #[regex(r"let|if|then|else|for|sum|product|in|of|loop|while|break|continue|return")] 137 | Keyword, 138 | 139 | #[token(",")] 140 | Comma, 141 | 142 | #[token("(")] 143 | OpenParen, 144 | 145 | #[token(")")] 146 | CloseParen, 147 | 148 | #[token("{")] 149 | OpenCurly, 150 | 151 | #[token("}")] 152 | CloseCurly, 153 | 154 | #[token("[")] 155 | OpenSquare, 156 | 157 | #[token("]")] 158 | CloseSquare, 159 | 160 | #[token("'")] 161 | Quote, 162 | 163 | #[token(";")] 164 | Semicolon, 165 | 166 | #[regex(r"\d+")] 167 | Int, 168 | 169 | #[regex(r"(true|false)")] 170 | Boolean, 171 | 172 | #[token(".")] 173 | Dot, 174 | 175 | #[token("..")] 176 | RangeHalfOpen, 177 | 178 | #[token("..=")] 179 | RangeClosed, 180 | 181 | #[regex(r".", priority = 0)] 182 | Symbol, 183 | } 184 | 185 | impl TokenKind { 186 | /// Returns true if the token represents a token that should be ignored by the parser. 187 | pub fn is_ignore(self) -> bool { 188 | matches!(self, TokenKind::Whitespace | TokenKind::NewLine | TokenKind::Comment) 189 | } 190 | 191 | /// Returns true if the token represents significant whitespace. 192 | pub fn is_significant_whitespace(self) -> bool { 193 | matches!(self, TokenKind::NewLine) 194 | } 195 | } 196 | 197 | /// A token produced by the tokenizer. 198 | #[derive(Debug, Clone, PartialEq)] 199 | pub struct Token<'source> { 200 | /// The region of the source code that this token originated from. 201 | pub span: Range, 202 | 203 | /// The kind of token. 204 | pub kind: TokenKind, 205 | 206 | /// The raw lexeme that was parsed into this token. 207 | pub lexeme: &'source str, 208 | } 209 | 210 | impl Token<'_> { 211 | /// Returns true if the token represents a token that should be ignored by the parser. 212 | pub fn is_ignore(&self) -> bool { 213 | self.kind.is_ignore() 214 | } 215 | 216 | /// Returns true if the token represents significant whitespace, i.e., the token is a newline. 217 | pub fn is_significant_whitespace(&self) -> bool { 218 | self.kind.is_significant_whitespace() 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /cas-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas-rs" 3 | description = "A CalcScript executor and REPL." 4 | version = "0.2.0" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/ElectrifyPro/cas-rs" 8 | categories = ["mathematics", "compilers", "command-line-utilities"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dev-dependencies] 13 | ariadne = "0.2.0" 14 | 15 | [dependencies] 16 | cas-compute = { version = "0.2.0", path = "../cas-compute", default-features = false, features = ["numerical"] } 17 | cas-error = { version = "0.2.0", path = "../cas-error" } 18 | cas-parser = { version = "0.2.0", path = "../cas-parser" } 19 | cas-vm = { version = "0.2.0", path = "../cas-vm" } 20 | rustyline = "13.0.0" 21 | -------------------------------------------------------------------------------- /cas-rs/src/error.rs: -------------------------------------------------------------------------------- 1 | use cas_error::Error; 2 | 3 | /// Utility enum to package one or multiple errors. 4 | pub enum ReplError { 5 | /// Multiple errors that can occur during parsing. 6 | Many(Vec), 7 | 8 | /// Single error that can occur during compilation or evaluation. 9 | One(Error), 10 | } 11 | 12 | impl ReplError { 13 | /// Report the errors in this [`ReplError`] to stderr. 14 | /// 15 | /// The `ariadne` crate's [`Report`] type actually does not have a `Display` implementation, so 16 | /// we can only use its `eprint` method to print to stderr. 17 | /// 18 | /// [`Report`]: https://docs.rs/ariadne/latest/ariadne/struct.Report.html 19 | pub fn report_to_stderr(&self, src_id: &str, input: &str) { 20 | match self { 21 | Self::Many(errs) => errs.iter().for_each(|err| err.report_to_stderr(src_id, input).unwrap()), 22 | Self::One(err) => err.report_to_stderr(src_id, input).unwrap(), 23 | } 24 | } 25 | } 26 | 27 | impl From> for ReplError { 28 | fn from(errs: Vec) -> Self { 29 | Self::Many(errs) 30 | } 31 | } 32 | 33 | impl From for ReplError { 34 | fn from(err: Error) -> Self { 35 | Self::One(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cas-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | 3 | use cas_compute::numerical::{ 4 | fmt::{FormatOptions, FormatOptionsBuilder, NumberFormat, Scientific, Separator}, 5 | value::Value, 6 | }; 7 | use error::ReplError; 8 | use cas_parser::parser::Parser; 9 | use cas_vm::{ReplVm, Vm}; 10 | use rustyline::{error::ReadlineError, DefaultEditor}; 11 | use std::{fs::File, io::{self, BufReader, IsTerminal, Read}}; 12 | 13 | /// Executes the given input string in a new VM, returning the results of the execution. 14 | #[inline] 15 | fn execute(input: &str) -> Result { 16 | let ast = Parser::new(input).try_parse_full_many()?; 17 | let mut vm = Vm::compile_program(ast)?; 18 | let value = vm.run()?; 19 | Ok(value) 20 | } 21 | 22 | /// Executes the given input string in a new VM, printing the results. 23 | fn execute_and_print(source_name: &str, input: &str, fmt: FormatOptions) { 24 | output(source_name, input, execute(input), fmt); 25 | } 26 | 27 | /// Executes the given input string in the REPL VM, returning the results of the execution. 28 | #[inline] 29 | fn repl_execute(input: &str, vm: &mut ReplVm) -> Result { 30 | let ast = Parser::new(input).try_parse_full_many()?; 31 | let value = vm.execute(ast)?; 32 | Ok(value) 33 | } 34 | 35 | /// Executes the given input string in the REPL VM, printing the results. 36 | fn repl_execute_and_print(source_name: &str, input: &str, vm: &mut ReplVm, fmt: FormatOptions) { 37 | output(source_name, input, repl_execute(input, vm), fmt); 38 | } 39 | 40 | /// Prints the result of the execution. 41 | #[inline] 42 | fn output(source_name: &str, input: &str, res: Result, fmt: FormatOptions) { 43 | match res { 44 | Ok(Value::Unit) => (), // intentionally print nothing 45 | Ok(res) => println!("{}", res.fmt(fmt)), 46 | Err(err) => err.report_to_stderr(source_name, input), 47 | } 48 | } 49 | 50 | fn main() { 51 | let mut args = std::env::args(); 52 | args.next(); 53 | 54 | let fmt = FormatOptionsBuilder::new() 55 | .number(NumberFormat::Auto) 56 | .scientific(Scientific::Times) 57 | .precision(Some(150)) 58 | .separators(Separator::Never) 59 | .build(); 60 | 61 | if let Some(filename) = args.next() { 62 | // run source file 63 | let mut file = BufReader::new(File::open(&filename).unwrap()); 64 | let mut input = String::new(); 65 | file.read_to_string(&mut input).unwrap(); 66 | 67 | execute_and_print(&filename, &input, fmt); 68 | } else if !io::stdin().is_terminal() { 69 | // read source from stdin 70 | let mut input = String::new(); 71 | io::stdin().read_to_string(&mut input).unwrap(); 72 | 73 | execute_and_print("stdin", &input, fmt); 74 | } else { 75 | // run the repl / interactive mode 76 | let mut entry = 0; 77 | let mut rl = DefaultEditor::new().unwrap(); 78 | let mut vm = ReplVm::new(); 79 | 80 | fn process_line( 81 | entry: usize, 82 | rl: &mut DefaultEditor, 83 | vm: &mut ReplVm, 84 | fmt: FormatOptions, 85 | ) -> Result<(), ReadlineError> { 86 | let input = rl.readline("> ")?; 87 | if input.trim().is_empty() { 88 | return Ok(()); 89 | } 90 | 91 | rl.add_history_entry(&input)?; 92 | 93 | let source_name = format!("repl:{}", entry); 94 | repl_execute_and_print(&source_name, &input, vm, fmt); 95 | Ok(()) 96 | } 97 | 98 | loop { 99 | entry += 1; 100 | 101 | if let Err(err) = process_line(entry, &mut rl, &mut vm, fmt) { 102 | match err { 103 | ReadlineError::Eof | ReadlineError::Interrupted => (), 104 | _ => eprintln!("{}", err), 105 | } 106 | break; 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /cas-unit-convert/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas-unit-convert" 3 | description = "Unit conversion library for CalcScript" 4 | version = "0.2.0" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/ElectrifyPro/cas-rs" 8 | keywords = ["calcscript", "unit", "convert", "conversion"] 9 | categories = ["mathematics"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dev-dependencies] 14 | approx = "0.5.1" 15 | assert_float_eq = "1.1.3" 16 | rustyline = "13.0.0" 17 | -------------------------------------------------------------------------------- /cas-unit-convert/examples/convert-unit-repl/error.rs: -------------------------------------------------------------------------------- 1 | use cas_unit_convert::{unit::InvalidUnit, ConversionError}; 2 | use std::num::ParseFloatError; 3 | 4 | /// Utility enum to package any error that can occur during unit conversion. 5 | pub enum Error { 6 | /// Incorrect number of arguments. 7 | Args, 8 | 9 | /// Invalid float. 10 | Float(ParseFloatError), 11 | 12 | /// Invalid unit. 13 | Unit(InvalidUnit), 14 | 15 | /// No conversion between the two units. 16 | Conversion(ConversionError), 17 | } 18 | 19 | impl std::fmt::Display for Error { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | match self { 22 | Self::Args => write!(f, "Format: "), 23 | Self::Float(err) => write!(f, "{}", err), 24 | Self::Unit(err) => write!(f, "{}", err), 25 | Self::Conversion(err) => write!(f, "{}", err), 26 | } 27 | } 28 | } 29 | 30 | impl From> for Error { 31 | fn from(_: Vec<&str>) -> Self { 32 | Self::Args 33 | } 34 | } 35 | 36 | impl From for Error { 37 | fn from(error: ParseFloatError) -> Self { 38 | Self::Float(error) 39 | } 40 | } 41 | 42 | impl From for Error { 43 | fn from(error: InvalidUnit) -> Self { 44 | Self::Unit(error) 45 | } 46 | } 47 | 48 | impl From for Error { 49 | fn from(error: ConversionError) -> Self { 50 | Self::Conversion(error) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cas-unit-convert/examples/convert-unit-repl/main.rs: -------------------------------------------------------------------------------- 1 | //! Simple command line tool to convert between units of measurement. 2 | //! 3 | //! Type a conversion in the form: ` `, e.g. `1.5 m ft`. 4 | //! 5 | //! You can convert between derived units, i.e. volume to length^3, e.g. `500 mL cm^3`. 6 | 7 | mod error; 8 | 9 | use cas_unit_convert::*; 10 | use error::Error; 11 | use rustyline::{error::ReadlineError, DefaultEditor}; 12 | 13 | fn convert(input: &str) -> Result<(), Error> { 14 | let parts: [&str; 3] = input 15 | .split_whitespace() 16 | .collect::>() 17 | .try_into()?; 18 | let [value, from, to] = parts; 19 | 20 | let value = value.parse::()?; 21 | let from = CompoundUnit::try_from(from)?; 22 | let to = CompoundUnit::try_from(to)?; 23 | 24 | let m = Measurement::new(value, from.clone()); 25 | let m2 = m.convert(to.clone())?; 26 | println!("{} {} = {} {}", value, from, m2.value(), to); 27 | 28 | Ok(()) 29 | } 30 | 31 | fn main() { 32 | // run the repl / interactive mode 33 | let mut rl = DefaultEditor::new().unwrap(); 34 | 35 | fn process_line(rl: &mut DefaultEditor) -> Result<(), ReadlineError> { 36 | let input = rl.readline("> ")?; 37 | rl.add_history_entry(&input)?; 38 | if let Err(e) = convert(&input) { 39 | eprintln!("{}", e); 40 | } 41 | Ok(()) 42 | } 43 | 44 | loop { 45 | if let Err(err) = process_line(&mut rl) { 46 | match err { 47 | ReadlineError::Eof | ReadlineError::Interrupted => (), 48 | _ => eprintln!("{}", err), 49 | } 50 | break; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cas-unit-convert/src/convert.rs: -------------------------------------------------------------------------------- 1 | //! Provides the [`Convert`] trait, which is implemented by all [`Unit`]s. 2 | 3 | use super::unit::Unit; 4 | 5 | /// A trait implemented by all [`Unit`], providing the information needed to convert between them. 6 | pub trait Convert { 7 | /// The base unit of this unit. 8 | /// 9 | /// The base unit is the unit in which conversions to other units of the same quantity are 10 | /// defined. For example, the base unit of length is the meter, and the base unit of volume 11 | /// is the cubic meter. It isn't particularly important which unit is chosen as the base 12 | /// unit, as long as conversions between each unit and the base unit are correctly defined. 13 | /// 14 | /// In some cases, the choice of base unit may affect the precision of conversions. For 15 | /// example, metric and customary units often don't have exact conversions between them. If the 16 | /// base unit is chosen to be a metric unit, then conversions between customary units will 17 | /// be done by converting to the metric base unit, and then to the target customary unit, which 18 | /// is where the precision loss can occur. 19 | const BASE: Self; 20 | 21 | /// Returns the conversion factor from `&self` to [`Convert::BASE`], i.e. the value to multiply 22 | /// a quantity in this unit by, in order to get a quantity in [`Convert::BASE`]. If the `self` 23 | /// unit is the same as this unit, then this function should return `1.0`. 24 | /// 25 | /// For example, if [`Convert::BASE`] is the meter, the conversion factor for a centimeter 26 | /// would be `0.01`. 27 | fn conversion_factor(&self) -> f64; 28 | 29 | /// Defines the conversion factor from [`Convert::BASE`], to a base unit that [`Convert::BASE`] 30 | /// is derived from. Returns [`None`] if there is no conversion factor, meaning the two units 31 | /// are unrelated. 32 | /// 33 | /// For example, [`Volume`] is derived from [`Length`] units cubed. The base unit for 34 | /// [`Length`] is defined to be [`Length::Meter`]. So, this function should be manually 35 | /// implemented for [`Volume`], and it should return the value to multiply by to convert from 36 | /// `Volume::?` to cubic meters. 37 | /// 38 | /// [`Volume`]: super::Volume 39 | /// [`Length`]: super::Length 40 | /// [`Length::Meter`]: super::Length::Meter 41 | fn conversion_factor_to(&self, _: impl Into) -> Option { 42 | None 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cas-vm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas-vm" 3 | description = "Virtual machine that executes CalcScript bytecode" 4 | version = "0.2.0" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/ElectrifyPro/cas-rs" 8 | keywords = ["calcscript", "vm", "interpreter", "math", "scripting"] 9 | categories = ["mathematics", "compilers"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | ariadne = "0.2.0" 15 | cas-attrs = { version = "0.2.0", path = "../cas-attrs" } 16 | cas-compiler = { version = "0.2.0", path = "../cas-compiler" } 17 | cas-compute = { version = "0.2.0", path = "../cas-compute" } 18 | cas-error = { version = "0.2.0", path = "../cas-error" } 19 | cas-parser = { version = "0.2.0", path = "../cas-parser" } 20 | replace_with = "0.1.7" 21 | rug = "1.22.0" 22 | -------------------------------------------------------------------------------- /cas-vm/README.md: -------------------------------------------------------------------------------- 1 | Virtual machine for `cas-rs`. 2 | 3 | This crate provides [`Vm`], a virtual machine that executes bytecode 4 | instructions generated by the CalcScript [`Compiler`]. It maintains its own 5 | state and allows you to inspect and manipulate its variables over the course of 6 | program execution. 7 | 8 | # Usage 9 | 10 | To run a program, you can easily create a [`Vm`] by compiling a program with 11 | [`Vm::compile_program`], then calling [`Vm::run`] to execute it. It returns a 12 | [`Value`] that contains the return value of the program. 13 | 14 | ```rust 15 | use cas_compute::numerical::value::Value; 16 | use cas_parser::parser::Parser; 17 | use cas_vm::Vm; 18 | 19 | let source = "5sin(pi/2) * 6!"; 20 | let mut parser = Parser::new(source); 21 | let stmts = parser.try_parse_full_many().unwrap(); 22 | 23 | let mut vm = Vm::compile_program(stmts).unwrap(); 24 | 25 | use cas_compute::primitive::float; 26 | // 5sin(pi/2) * 6! 27 | // = 5 * 1 * 720 = 3600 28 | assert_eq!(vm.run().unwrap(), 3600.into()); 29 | ``` 30 | 31 | It is also possible to programatically manipulate the [`Vm`]'s variables, 32 | although this will take some work. You'll have to manually declare the variable 33 | in the compilation phase to obtain an index for it, and then use that index 34 | to set the variable in the [`Vm`]. Here's an example of how to do that: 35 | 36 | ```rust 37 | use cas_compiler::Compiler; 38 | use cas_compiler::expr::compile_stmts; 39 | use cas_parser::parser::ast::LitSym; 40 | use cas_parser::parser::Parser; 41 | use cas_vm::Vm; 42 | 43 | let source = "x^2 + 5x + 6"; 44 | let mut parser = Parser::new(source); 45 | let stmts = parser.try_parse_full_many().unwrap(); 46 | 47 | let mut compiler = Compiler::new(); 48 | 49 | // declare existence of `x` in the compiler (or a compilation error will occur) 50 | // while also obtaining an index for `x` in the compiler 51 | let x_id = compiler.add_symbol(&LitSym { 52 | name: String::from("x"), 53 | span: 0..0 54 | }).unwrap(); 55 | 56 | // `x` must be initialized before we execute the vm (or a runtime error will occur)! 57 | 58 | // helper function to compile the statements in place of `Compiler::compile_program` 59 | compile_stmts(&stmts, &mut compiler).unwrap(); 60 | 61 | // create the virtual machine from the compiler and run test cases 62 | let mut vm = Vm::from(compiler); 63 | 64 | let cases = [ 65 | // (x, expected) 66 | (2, 20), 67 | (5, 56), 68 | (9, 132), 69 | (14, 272), 70 | (35, 1406), 71 | (256, 66822), 72 | ]; 73 | 74 | for (x, expected) in cases.into_iter() { 75 | // set the value of `x` in the vm 76 | vm.variables.insert(x_id, x.into()); 77 | 78 | // ensure `x^2 + 5x + 6` is equal to `expected` 79 | let result = vm.run().unwrap(); 80 | assert_eq!(result, expected.into()); 81 | } 82 | ``` 83 | 84 | This process will likely be simplified in the future. 85 | -------------------------------------------------------------------------------- /cas-vm/src/frame.rs: -------------------------------------------------------------------------------- 1 | use cas_compute::numerical::value::Value; 2 | use std::collections::HashMap; 3 | 4 | /// A stack frame in a call stack. 5 | #[derive(Debug)] 6 | pub struct Frame { 7 | /// The instruction to return to when this frame is popped, given as a 2-tuple of the chunk 8 | /// index and instruction index. 9 | pub return_instruction: (usize, usize), 10 | 11 | /// The variables stored in this frame. 12 | pub variables: HashMap, 13 | 14 | /// Whether this frame is being used to evaluate the derivative of a function. 15 | pub derivative: bool, 16 | } 17 | 18 | impl Frame { 19 | /// Creates a new [`Frame`] with the given return instruction. 20 | pub fn new(return_instruction: (usize, usize)) -> Self { 21 | Self { 22 | return_instruction, 23 | variables: HashMap::new(), 24 | derivative: false, 25 | } 26 | } 27 | 28 | /// Sets the variables stored in the frame. 29 | pub fn with_variables(mut self, variables: HashMap) -> Self { 30 | self.variables = variables; 31 | self 32 | } 33 | 34 | /// Sets the derivative flag on the frame. 35 | pub fn with_derivative(mut self) -> Self { 36 | self.derivative = true; 37 | self 38 | } 39 | 40 | /// Adds a variable to the frame. 41 | pub fn add_variable(&mut self, index: usize, value: Value) { 42 | self.variables.insert(index, value); 43 | } 44 | 45 | /// Gets a variable from the frame. 46 | pub fn get_variable(&self, index: usize) -> Option<&Value> { 47 | self.variables.get(&index) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cas-vm/src/register.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | /// Register state of the virtual machine. 4 | #[derive(Clone, Debug, Default)] 5 | pub struct Registers { 6 | /// Spans indicating the call site of the function, used to report errors at the correct 7 | /// location. 8 | pub call_site_spans: Vec>, 9 | 10 | /// The name of the function being executed. 11 | pub fn_name: String, 12 | 13 | /// The signature of the function being executed. 14 | pub fn_signature: String, 15 | 16 | /// Counts the number of currently available arguments for the function during argument 17 | /// validation. 18 | /// 19 | /// When a function is called, its arguments are pushed onto the stack, and this counter is 20 | /// initalized to that number of arguments. The function header then executes and determines 21 | /// whether to push default arguments onto the stack, incrementing the counter if so. 22 | pub num_args: usize, 23 | 24 | /// The number of expected arguments for the function. 25 | pub num_params: usize, 26 | 27 | /// The number of default parameters in the function signature. 28 | pub num_default_params: usize, 29 | } 30 | -------------------------------------------------------------------------------- /examples/bad_lcm.calc: -------------------------------------------------------------------------------- 1 | bad_lcm(a, b) = { 2 | i = 0 3 | loop { 4 | i += 1 5 | if i % a == 0 && i % b == 0 { 6 | break i 7 | } 8 | } 9 | } 10 | 11 | bad_lcm(37, 41) 12 | -------------------------------------------------------------------------------- /examples/convert_binary.calc: -------------------------------------------------------------------------------- 1 | // Converts an integer into its 32-bit binary representation. The result is a 2 | // list of 32 elements, representing the bits of the number from most 3 | // significant to least significant. 4 | to_bin(n) = { 5 | n = round(n) 6 | out = [0; 32] 7 | 8 | if n >= 0 && n <= 2^32 - 1 { 9 | i = 31 10 | while n > 0 { 11 | out[i] = n % 2 12 | n = floor(n / 2) 13 | i -= 1 14 | if i < 0 break 15 | } 16 | out 17 | } 18 | } 19 | 20 | // Converts a 32-bit binary representation into an integer. 21 | from_bin(b) = sum i in 0..32 of b[i] * 2^(31 - i) 22 | 23 | tests = [ 24 | 2'0, // 0 25 | 2'100010, // 34 26 | 2'11111111, // 255 27 | 2'100111101, // 317 28 | 2'111001010, // 458 29 | 2'1000000000, // 512 30 | 2'1101110010111, // 7063 31 | ] 32 | 33 | results = [false; 7] 34 | 35 | for i in 0..7 { 36 | results[i] = from_bin(to_bin(tests[i])) == tests[i] 37 | } 38 | 39 | results 40 | -------------------------------------------------------------------------------- /examples/counter.calc: -------------------------------------------------------------------------------- 1 | // This function creates a counter that increments by 1 each time it is called. 2 | // It uses a closure to maintain the state of the counter. 3 | make_counter() = { 4 | count = [0] 5 | 6 | // Return the counting closure. The name doesn't matter (we chose "_"), but 7 | // all functions must have a name. 8 | _() = { 9 | // We can actually add to the list directly; CalcScript applies the + 10 | // operator to all elements in the list. In this case, there's just one 11 | // element, so it works like a normal number. 12 | count += 1 13 | count[0] 14 | } 15 | } 16 | 17 | // Each created counter has its own state, so we can create multiple counters 18 | // that work independently. 19 | counter_a = make_counter() 20 | print(counter_a()) // Output: 1 21 | print(counter_a()) // Output: 2 22 | print(counter_a()) // Output: 3 23 | print(counter_a()) // Output: 4 24 | 25 | counter_b = make_counter() 26 | print(counter_b()) // Output: 1 27 | print(counter_b()) // Output: 2 28 | print(counter_a()) // Output: 5 29 | print(counter_b()) // Output: 3 30 | -------------------------------------------------------------------------------- /examples/environment_capture.calc: -------------------------------------------------------------------------------- 1 | // Functions in CalcScript implement environment capture. This program 2 | // demonstrates how this works. 3 | 4 | x = 10 5 | 6 | // The function `f` captures the value of `x` at the time it is defined, which 7 | // is 10 in this case. Environment capture means that the value of `x` used 8 | // inside `f` will always be 10, even if we change `x` later below. 9 | f() = x * 2 10 | print(f()) // prints 20 11 | 12 | x = 55 13 | print(f()) // still prints 20 14 | 15 | [x, f()] // returns [55, 20] 16 | -------------------------------------------------------------------------------- /examples/factorial.calc: -------------------------------------------------------------------------------- 1 | fact(n) = product a in 1..=n of a 2 | fact(14) == 14! 3 | -------------------------------------------------------------------------------- /examples/function_scope.calc: -------------------------------------------------------------------------------- 1 | x = 14 2 | g() = { x = 9 } // this `x` is a new variable, not the `x` in the outer scope 3 | g() 4 | x 5 | -------------------------------------------------------------------------------- /examples/higher_order_function.calc: -------------------------------------------------------------------------------- 1 | divisor(d) = f(x) = x / d 2 | 3 | half = divisor(2) // = half(x) = x / 2 4 | third = divisor(3) // = third(x) = x / 3 5 | 6 | print(half(32)) // 32 / 2 = 16 7 | print(third(32)) // 32 / 3 = 10.666... 8 | 9 | print(half(40)) // 40 / 2 = 20 10 | print(third(40)) // 40 / 3 = 13.333... 11 | 12 | [half(32), third(32), half(40), third(40)] 13 | -------------------------------------------------------------------------------- /examples/if_branching.calc: -------------------------------------------------------------------------------- 1 | x = 5 2 | iseven(n) = n % 2 == 0 3 | if iseven(x) { 4 | x^2 + 5x + 6 5 | } else if bool(x) && false { 6 | exp(x) 7 | } else { 8 | log(x, 2) 9 | } 10 | -------------------------------------------------------------------------------- /examples/manual_abs.calc: -------------------------------------------------------------------------------- 1 | my_abs(n) = if n < 0 then -n else n 2 | my_abs(-4) 3 | -------------------------------------------------------------------------------- /examples/map_list.calc: -------------------------------------------------------------------------------- 1 | map(list, f) = { 2 | for n in 0..10 then list[n] = f(list[n]) 3 | list 4 | } 5 | 6 | list = [ 7 | [0, 0], 8 | [1, 1], 9 | [2, 2], 10 | [3, 3], 11 | [4, 4], 12 | [5, 5], 13 | [6, 6], 14 | [7, 7], 15 | [8, 8], 16 | [9, 9] 17 | ] 18 | map(map(list, f(x) = x[0]^2), sqrt) 19 | -------------------------------------------------------------------------------- /examples/memoized_fib.calc: -------------------------------------------------------------------------------- 1 | cache = [-1; 64] 2 | my_fib(n) = if n <= 1 then n else { 3 | if cache[n] != -1 return cache[n] 4 | cache[n] = my_fib(n - 1) + my_fib(n - 2) 5 | } 6 | 7 | my_fib(63) 8 | -------------------------------------------------------------------------------- /examples/ncr.calc: -------------------------------------------------------------------------------- 1 | my_ncr(n, k) = { 2 | partial_factorial(n, k) = { 3 | product a in k + 1..=n of a 4 | } 5 | 6 | if k > n return 7 | 8 | sub = n - k 9 | 10 | // ncr(n, k) = n! / (k! * (n - k)!) 11 | if k > sub { 12 | partial_factorial(n, k) / partial_factorial(sub, 1) 13 | } else { 14 | partial_factorial(n, sub) / partial_factorial(k, 1) 15 | } 16 | } 17 | 18 | // partial_factorial() is only available in my_ncr() 19 | // if you uncomment this line, a compilation error will occur 20 | // partial_factorial(5, 3) 21 | 22 | my_ncr(5, 3) == ncr(5, 3) 23 | -------------------------------------------------------------------------------- /examples/prime_notation.calc: -------------------------------------------------------------------------------- 1 | // This program demonstrates prime notation: a useful way to quickly approximate the derivative of a one-parameter function. 2 | 3 | f(x) = x^2 + 5x + 6 4 | g(x) = 2x + 5 // derivative of f(x) 5 | 6 | tests = [ 7 | 2, // f'(2) = 9 8 | 4, // f'(4) = 13 9 | 7, // f'(7) = 19 10 | 15, // f'(15) = 35 11 | 28, // f'(28) = 61 12 | 59, // f'(59) = 123 13 | 100, // f'(100) = 205 14 | ] 15 | 16 | result = [false; 15] 17 | 18 | for t in 0..7 { 19 | result[t] = g(tests[t]) ~== f'(tests[t]) // use ~== since prime notation is an approximation 20 | } 21 | 22 | // This also works for higher order derivatives and builtin functions. Here are 23 | // some lighter examples (all should output true): 24 | // NOTE: Oscillating functions, like `sin` and `cos`, will have more 25 | // significant error as you compute higher derivatives. The most accurate 26 | // approximation is the first derivative. 27 | 28 | result[7] = sin'(2) ~== cos(2) 29 | result[8] = cos'(2) ~== -sin(2) 30 | result[9] = exp'(2) ~== exp(2) 31 | result[10] = ln'(2) ~== 1/2 32 | result[11] = sqrt'(2) ~== 1/(2 * sqrt(2)) 33 | result[12] = cbrt'(2) ~== 1/(3 * cbrt(4)) 34 | result[13] = sin''''(2) ~== sin(2) 35 | 36 | // You might want to use a function that takes more than one parameter. For 37 | // example, the `log`arithm function accepts two parameters, the argument and 38 | // the base, in order to be as general as possible. However, because of this, 39 | // it cannot directly be used with prime notation. 40 | 41 | // A simple workaround is to partially apply the function, fixing the base to 42 | // a constant value: 43 | 44 | f(n) = log(n, 2) // log base 2 45 | 46 | // Which can then be used with prime notation: 47 | 48 | result[14] = f'(2) ~== 1/(2 * ln(2)) // log'(2, 2) = 1/(2 * ln(2)) 49 | 50 | result 51 | -------------------------------------------------------------------------------- /examples/resolving_calls.calc: -------------------------------------------------------------------------------- 1 | g() = 2 2 | 3 | f() = { 4 | f() = f() = () 5 | out = [false; 2]; 6 | 7 | out[0] = g() // 2 8 | g() = 3 9 | out[1] = g() // 3 10 | 11 | out 12 | } 13 | 14 | f() == [2, 3] 15 | --------------------------------------------------------------------------------